From f299b40fe63fcba28d64920562532fb3469fc95f Mon Sep 17 00:00:00 2001
From: Simon Lejeune <sle@openerp.com>
Date: Fri, 19 Sep 2014 11:13:55 +0200
Subject: [PATCH] [FIX] report: windows: lower level of file opening/closing,
 which to find wkhtmltpdf

* Use of 'which' to find the wkhtmltopdf binary (allow the win32 service to find it)
* Use of mkstemp and manual close of the file descriptors
---
 addons/report/models/report.py | 147 ++++++++++++++++++---------------
 1 file changed, 81 insertions(+), 66 deletions(-)

diff --git a/addons/report/models/report.py b/addons/report/models/report.py
index c9b4490d34f7..eb2261a00498 100644
--- a/addons/report/models/report.py
+++ b/addons/report/models/report.py
@@ -21,7 +21,7 @@
 
 from openerp import api
 from openerp.osv import osv
-from openerp.tools import config
+from openerp.tools import config, which
 from openerp.tools.translate import _
 from openerp.addons.web.http import request
 from openerp.tools.safe_eval import safe_eval as eval
@@ -32,31 +32,41 @@ import base64
 import logging
 import tempfile
 import lxml.html
-import cStringIO
+import os
 import subprocess
+from contextlib import closing
 from distutils.version import LooseVersion
 from functools import partial
-from os import name as OsName
 from pyPdf import PdfFileWriter, PdfFileReader
-from shutil import rmtree
 
 
 _logger = logging.getLogger(__name__)
 
 
-"""Check the presence of wkhtmltopdf and return its version at OpnerERP start-up."""
+#--------------------------------------------------------------------------
+# Helpers
+#--------------------------------------------------------------------------
+def _get_wkhtmltopdf_bin():
+    defpath = os.environ.get('PATH', os.defpath).split(os.pathsep)
+    return which('wkhtmltopdf', path=os.pathsep.join(defpath))
+
+
+#--------------------------------------------------------------------------
+# Check the presence of Wkhtmltopdf and return its version at Odoo start-up
+#--------------------------------------------------------------------------
 wkhtmltopdf_state = 'install'
 try:
     process = subprocess.Popen(
-        ['wkhtmltopdf', '--version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE
+        [_get_wkhtmltopdf_bin(), '--version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE
     )
-except OSError:
-    _logger.info('You need wkhtmltopdf to print a pdf version of the reports.')
+except (OSError, IOError):
+    _logger.info('You need Wkhtmltopdf to print a pdf version of the reports.')
 else:
+    _logger.info('Will use the Wkhtmltopdf binary at %s' % _get_wkhtmltopdf_bin())
     out, err = process.communicate()
     version = re.search('([0-9.]+)', out).group(0)
     if LooseVersion(version) < LooseVersion('0.12.0'):
-        _logger.info('Upgrade wkhtmltopdf to (at least) 0.12.0')
+        _logger.info('Upgrade Wkhtmltopdf to (at least) 0.12.0')
         wkhtmltopdf_state = 'upgrade'
     else:
         wkhtmltopdf_state = 'ok'
@@ -353,9 +363,7 @@ class Report(osv.Model):
         :param save_in_attachment: dict of reports to save/load in/from the db
         :returns: Content of the pdf as a string
         """
-        command = ['wkhtmltopdf']
         command_args = []
-        tmp_dir = tempfile.mkdtemp(prefix='report.tmp.')
 
         # Passing the cookie to wkhtmltopdf in order to resolve internal links.
         try:
@@ -383,44 +391,45 @@ class Report(osv.Model):
 
         # Execute WKhtmltopdf
         pdfdocuments = []
-
-        # Workaround the NamedTemporaryFile limitation on Windows, which requires the file to
-        # be closed before accessing it by its name.
-        delete_tempfile = True
-        if OsName == 'nt':
-            delete_tempfile = False
+        temporary_files = []
 
         for index, reporthtml in enumerate(bodies):
             local_command_args = []
-            pdfreport = tempfile.NamedTemporaryFile(delete=delete_tempfile, suffix='.pdf', prefix='report.tmp.', dir=tmp_dir, mode='w+b')
+            pdfreport_fd, pdfreport_path = tempfile.mkstemp(suffix='.pdf', prefix='report.tmp.')
+            temporary_files.append(pdfreport_path)
 
             # Directly load the document if we already have it
             if save_in_attachment and save_in_attachment['loaded_documents'].get(reporthtml[0]):
-                pdfreport.write(save_in_attachment['loaded_documents'].get(reporthtml[0]))
-                pdfreport.seek(0)
-                pdfdocuments.append(pdfreport)
+                with closing(os.fdopen(pdfreport_fd, 'w')) as pdfreport:
+                    pdfreport.write(save_in_attachment['loaded_documents'][reporthtml[0]])
+                pdfdocuments.append(pdfreport_path)
                 continue
+            else:
+                os.close(pdfreport_fd)
 
             # Wkhtmltopdf handles header/footer as separate pages. Create them if necessary.
             if headers:
-                head_file = tempfile.NamedTemporaryFile(delete=delete_tempfile, suffix='.html', prefix='report.header.tmp.', dir=tmp_dir, mode='w+')
-                head_file.write(headers[index])
-                head_file.seek(0)
-                local_command_args.extend(['--header-html', head_file.name])
+                head_file_fd, head_file_path = tempfile.mkstemp(suffix='.html', prefix='report.header.tmp.')
+                temporary_files.append(head_file_path)
+                with closing(os.fdopen(head_file_fd, 'w')) as head_file:
+                    head_file.write(headers[index])
+                local_command_args.extend(['--header-html', head_file_path])
             if footers:
-                foot_file = tempfile.NamedTemporaryFile(delete=delete_tempfile, suffix='.html', prefix='report.footer.tmp.', dir=tmp_dir, mode='w+')
-                foot_file.write(footers[index])
-                foot_file.seek(0)
-                local_command_args.extend(['--footer-html', foot_file.name])
+                foot_file_fd, foot_file_path = tempfile.mkstemp(suffix='.html', prefix='report.footer.tmp.')
+                temporary_files.append(foot_file_path)
+                with closing(os.fdopen(foot_file_fd, 'w')) as foot_file:
+                    foot_file.write(footers[index])
+                local_command_args.extend(['--footer-html', foot_file_path])
 
             # Body stuff
-            content_file = tempfile.NamedTemporaryFile(delete=delete_tempfile, suffix='.html', prefix='report.body.tmp.', dir=tmp_dir, mode='w+')
-            content_file.write(reporthtml[1])
-            content_file.seek(0)
+            content_file_fd, content_file_path = tempfile.mkstemp(suffix='.html', prefix='report.body.tmp.')
+            temporary_files.append(content_file_path)
+            with closing(os.fdopen(content_file_fd, 'w')) as content_file:
+                content_file.write(reporthtml[1])
 
             try:
-                wkhtmltopdf = command + command_args + local_command_args
-                wkhtmltopdf += [content_file.name] + [pdfreport.name]
+                wkhtmltopdf = [_get_wkhtmltopdf_bin()] + command_args + local_command_args
+                wkhtmltopdf += [content_file_path] + [pdfreport_path]
 
                 process = subprocess.Popen(wkhtmltopdf, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
                 out, err = process.communicate()
@@ -432,37 +441,38 @@ class Report(osv.Model):
 
                 # Save the pdf in attachment if marked
                 if reporthtml[0] is not False and save_in_attachment.get(reporthtml[0]):
-                    attachment = {
-                        'name': save_in_attachment.get(reporthtml[0]),
-                        'datas': base64.encodestring(pdfreport.read()),
-                        'datas_fname': save_in_attachment.get(reporthtml[0]),
-                        'res_model': save_in_attachment.get('model'),
-                        'res_id': reporthtml[0],
-                    }
-                    self.pool['ir.attachment'].create(cr, uid, attachment)
+                    with open(pdfreport_path, 'rb') as pdfreport:
+                        attachment = {
+                            'name': save_in_attachment.get(reporthtml[0]),
+                            'datas': base64.encodestring(pdfreport.read()),
+                            'datas_fname': save_in_attachment.get(reporthtml[0]),
+                            'res_model': save_in_attachment.get('model'),
+                            'res_id': reporthtml[0],
+                        }
+                        self.pool['ir.attachment'].create(cr, uid, attachment)
                     _logger.info('The PDF document %s is now saved in the '
                                  'database' % attachment['name'])
 
-                pdfreport.seek(0)
-                pdfdocuments.append(pdfreport)
-
-                content_file.close()
-
-                if headers:
-                    head_file.close()
-                if footers:
-                    foot_file.close()
+                pdfdocuments.append(pdfreport_path)
             except:
                 raise
 
         # Return the entire document
         if len(pdfdocuments) == 1:
-            content = pdfdocuments[0].read()
-            pdfdocuments[0].close()
+            entire_report_path = pdfdocuments[0]
         else:
-            content = self._merge_pdf(pdfdocuments)
+            entire_report_path = self._merge_pdf(pdfdocuments)
+            temporary_files.append(entire_report_path)
 
-        rmtree(tmp_dir)
+        with open(entire_report_path, 'rb') as pdfdocument:
+            content = pdfdocument.read()
+
+        # Manual cleanup of the temporary files
+        for temporary_file in temporary_files:
+            try:
+                os.unlink(temporary_file)
+            except (OSError, IOError):
+                _logger.error('Error when trying to remove file %s' % temporary_file)
 
         return content
 
@@ -499,7 +509,7 @@ class Report(osv.Model):
         if specific_paperformat_args and specific_paperformat_args.get('data-report-dpi'):
             command_args.extend(['--dpi', str(specific_paperformat_args['data-report-dpi'])])
         elif paperformat.dpi:
-            if OsName == 'nt' and int(paperformat.dpi) <= 95:
+            if os.name == 'nt' and int(paperformat.dpi) <= 95:
                 _logger.info("Generating PDF on Windows platform require DPI >= 96. Using 96 instead.")
                 command_args.extend(['--dpi', '96'])
             else:
@@ -526,18 +536,23 @@ class Report(osv.Model):
     def _merge_pdf(self, documents):
         """Merge PDF files into one.
 
-        :param documents: list of pdf files
-        :returns: string containing the merged pdf
+        :param documents: list of path of pdf files
+        :returns: path of the merged pdf
         """
         writer = PdfFileWriter()
+        streams = []  # We have to close the streams *after* PdfFilWriter's call to write()
         for document in documents:
-            reader = PdfFileReader(file(document.name, "rb"))
+            pdfreport = file(document, 'rb')
+            streams.append(pdfreport)
+            reader = PdfFileReader(pdfreport)
             for page in range(0, reader.getNumPages()):
                 writer.addPage(reader.getPage(page))
-            document.close()
-        merged = cStringIO.StringIO()
-        writer.write(merged)
-        merged.seek(0)
-        content = merged.read()
-        merged.close()
-        return content
+
+        merged_file_fd, merged_file_path = tempfile.mkstemp(suffix='.html', prefix='report.merged.tmp.')
+        with closing(os.fdopen(merged_file_fd, 'w')) as merged_file:
+            writer.write(merged_file)
+
+        for stream in streams:
+            stream.close()
+
+        return merged_file_path
-- 
GitLab