Kaydet (Commit) 1ff11304 authored tarafından Florian Apolloner's avatar Florian Apolloner

[1.7.x] Fixed #22680 -- I/O operation on closed file.

This patch is two-fold; first it ensure that Django does close everything in
request.FILES at the end of the request and secondly the storage system should
no longer close any files during save, it's up to the caller to handle that --
or let Django close the files at the end of the request.

Backport of e2efc896 from master.
üst de0e285b
...@@ -209,7 +209,6 @@ class FileSystemStorage(Storage): ...@@ -209,7 +209,6 @@ class FileSystemStorage(Storage):
# This file has a file path that we can move. # This file has a file path that we can move.
if hasattr(content, 'temporary_file_path'): if hasattr(content, 'temporary_file_path'):
file_move_safe(content.temporary_file_path(), full_path) file_move_safe(content.temporary_file_path(), full_path)
content.close()
# This is a normal uploadedfile that we can stream. # This is a normal uploadedfile that we can stream.
else: else:
...@@ -228,7 +227,6 @@ class FileSystemStorage(Storage): ...@@ -228,7 +227,6 @@ class FileSystemStorage(Storage):
_file = os.fdopen(fd, mode) _file = os.fdopen(fd, mode)
_file.write(chunk) _file.write(chunk)
finally: finally:
content.close()
locks.unlock(fd) locks.unlock(fd)
if _file is not None: if _file is not None:
_file.close() _file.close()
......
...@@ -96,9 +96,6 @@ class InMemoryUploadedFile(UploadedFile): ...@@ -96,9 +96,6 @@ class InMemoryUploadedFile(UploadedFile):
def open(self, mode=None): def open(self, mode=None):
self.file.seek(0) self.file.seek(0)
def close(self):
pass
def chunks(self, chunk_size=None): def chunks(self, chunk_size=None):
self.file.seek(0) self.file.seek(0)
yield self.read() yield self.read()
......
...@@ -207,6 +207,8 @@ class BaseHandler(object): ...@@ -207,6 +207,8 @@ class BaseHandler(object):
signals.got_request_exception.send(sender=self.__class__, request=request) signals.got_request_exception.send(sender=self.__class__, request=request)
response = self.handle_uncaught_exception(request, resolver, sys.exc_info()) response = self.handle_uncaught_exception(request, resolver, sys.exc_info())
response._closable_objects.append(request)
return response return response
def handle_uncaught_exception(self, request, resolver, exc_info): def handle_uncaught_exception(self, request, resolver, exc_info):
......
...@@ -228,6 +228,7 @@ class MultiPartParser(object): ...@@ -228,6 +228,7 @@ class MultiPartParser(object):
break break
except SkipFile: except SkipFile:
self._close_files()
# Just use up the rest of this file... # Just use up the rest of this file...
exhaust(field_stream) exhaust(field_stream)
else: else:
...@@ -237,6 +238,7 @@ class MultiPartParser(object): ...@@ -237,6 +238,7 @@ class MultiPartParser(object):
# If this is neither a FIELD or a FILE, just exhaust the stream. # If this is neither a FIELD or a FILE, just exhaust the stream.
exhaust(stream) exhaust(stream)
except StopUpload as e: except StopUpload as e:
self._close_files()
if not e.connection_reset: if not e.connection_reset:
exhaust(self._input_data) exhaust(self._input_data)
else: else:
...@@ -268,6 +270,14 @@ class MultiPartParser(object): ...@@ -268,6 +270,14 @@ class MultiPartParser(object):
"""Cleanup filename from Internet Explorer full paths.""" """Cleanup filename from Internet Explorer full paths."""
return filename and filename[filename.rfind("\\") + 1:].strip() return filename and filename[filename.rfind("\\") + 1:].strip()
def _close_files(self):
# Free up all file handles.
# FIXME: this currently assumes that upload handlers store the file as 'file'
# We should document that... (Maybe add handler.free_file to complement new_file)
for handler in self._upload_handlers:
if hasattr(handler, 'file'):
handler.file.close()
class LazyStream(six.Iterator): class LazyStream(six.Iterator):
""" """
......
...@@ -5,6 +5,7 @@ import os ...@@ -5,6 +5,7 @@ import os
import re import re
import sys import sys
from io import BytesIO from io import BytesIO
from itertools import chain
from pprint import pformat from pprint import pformat
from django.conf import settings from django.conf import settings
...@@ -245,6 +246,11 @@ class HttpRequest(object): ...@@ -245,6 +246,11 @@ class HttpRequest(object):
else: else:
self._post, self._files = QueryDict('', encoding=self._encoding), MultiValueDict() self._post, self._files = QueryDict('', encoding=self._encoding), MultiValueDict()
def close(self):
if hasattr(self, '_files'):
for f in chain.from_iterable(l[1] for l in self._files.lists()):
f.close()
# File-like and iterator interface. # File-like and iterator interface.
# #
# Expects self._stream to be set to an appropriate source of bytes by # Expects self._stream to be set to an appropriate source of bytes by
......
...@@ -504,6 +504,10 @@ File Uploads ...@@ -504,6 +504,10 @@ File Uploads
attribute is now optional. If it is omitted or given ``None`` or an empty attribute is now optional. If it is omitted or given ``None`` or an empty
string, a subdirectory won't be used for storing the uploaded files. string, a subdirectory won't be used for storing the uploaded files.
* Uploaded files are now explicitly closed before the response is delivered to
the client. Partially uploaded files are also closed as long as they are
named ``file`` in the upload handler.
Forms Forms
^^^^^ ^^^^^
......
...@@ -19,7 +19,8 @@ from django.core.cache import cache ...@@ -19,7 +19,8 @@ from django.core.cache import cache
from django.core.exceptions import SuspiciousOperation from django.core.exceptions import SuspiciousOperation
from django.core.files.base import File, ContentFile from django.core.files.base import File, ContentFile
from django.core.files.storage import FileSystemStorage, get_storage_class from django.core.files.storage import FileSystemStorage, get_storage_class
from django.core.files.uploadedfile import SimpleUploadedFile from django.core.files.uploadedfile import (InMemoryUploadedFile, SimpleUploadedFile,
TemporaryUploadedFile)
from django.test import LiveServerTestCase, SimpleTestCase from django.test import LiveServerTestCase, SimpleTestCase
from django.test import override_settings from django.test import override_settings
from django.utils import six from django.utils import six
...@@ -206,6 +207,23 @@ class FileStorageTests(unittest.TestCase): ...@@ -206,6 +207,23 @@ class FileStorageTests(unittest.TestCase):
self.storage.delete('path/to/test.file') self.storage.delete('path/to/test.file')
def test_save_doesnt_close(self):
with TemporaryUploadedFile('test', 'text/plain', 1, 'utf8') as file:
file.write(b'1')
file.seek(0)
self.assertFalse(file.closed)
self.storage.save('path/to/test.file', file)
self.assertFalse(file.closed)
self.assertFalse(file.file.closed)
file = InMemoryUploadedFile(six.StringIO('1'), '', 'test',
'text/plain', 1, 'utf8')
with file:
self.assertFalse(file.closed)
self.storage.save('path/to/test.file', file)
self.assertFalse(file.closed)
self.assertFalse(file.file.closed)
def test_file_path(self): def test_file_path(self):
""" """
File storage returns the full path of a file File storage returns the full path of a file
......
...@@ -327,6 +327,37 @@ class FileUploadTests(TestCase): ...@@ -327,6 +327,37 @@ class FileUploadTests(TestCase):
self.assertEqual(got.get('file1'), 1) self.assertEqual(got.get('file1'), 1)
self.assertEqual(got.get('file2'), 2) self.assertEqual(got.get('file2'), 2)
def test_fileuploads_closed_at_request_end(self):
file = tempfile.NamedTemporaryFile
with file() as f1, file() as f2a, file() as f2b:
response = self.client.post('/fd_closing/t/', {
'file': f1,
'file2': (f2a, f2b),
})
request = response.wsgi_request
# Check that the files got actually parsed.
self.assertTrue(hasattr(request, '_files'))
file = request._files['file']
self.assertTrue(file.closed)
files = request._files.getlist('file2')
self.assertTrue(files[0].closed)
self.assertTrue(files[1].closed)
def test_no_parsing_triggered_by_fd_closing(self):
file = tempfile.NamedTemporaryFile
with file() as f1, file() as f2a, file() as f2b:
response = self.client.post('/fd_closing/f/', {
'file': f1,
'file2': (f2a, f2b),
})
request = response.wsgi_request
# Check that the fd closing logic doesn't trigger parsing of the stream
self.assertFalse(hasattr(request, '_files'))
def test_file_error_blocking(self): def test_file_error_blocking(self):
""" """
The server should not block when there are upload errors (bug #8622). The server should not block when there are upload errors (bug #8622).
......
...@@ -15,4 +15,5 @@ urlpatterns = patterns('', ...@@ -15,4 +15,5 @@ urlpatterns = patterns('',
(r'^getlist_count/$', views.file_upload_getlist_count), (r'^getlist_count/$', views.file_upload_getlist_count),
(r'^upload_errors/$', views.file_upload_errors), (r'^upload_errors/$', views.file_upload_errors),
(r'^filename_case/$', views.file_upload_filename_case_view), (r'^filename_case/$', views.file_upload_filename_case_view),
(r'^fd_closing/(?P<access>t|f)/$', views.file_upload_fd_closing),
) )
...@@ -157,3 +157,9 @@ def file_upload_content_type_extra(request): ...@@ -157,3 +157,9 @@ def file_upload_content_type_extra(request):
(k, smart_str(v)) for k, v in uploadedfile.content_type_extra.items() (k, smart_str(v)) for k, v in uploadedfile.content_type_extra.items()
]) ])
return HttpResponse(json.dumps(params)) return HttpResponse(json.dumps(params))
def file_upload_fd_closing(request, access):
if access == 't':
request.FILES # Trigger file parsing.
return HttpResponse('')
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment