Browse Source

Fixed #30422 -- Made TemporaryFileUploadHandler handle interrupted uploads.

This patch allows upload handlers to handle interrupted uploads.

Co-Authored-By: Mariusz Felisiak <felisiak.mariusz@gmail.com>
aryan 5 years ago
parent
commit
11c4a4412b

+ 17 - 1
django/core/files/uploadhandler.py

@@ -1,7 +1,7 @@
 """
 """
 Base file upload handler classes, and the built-in concrete subclasses
 Base file upload handler classes, and the built-in concrete subclasses
 """
 """
-
+import os
 from io import BytesIO
 from io import BytesIO
 
 
 from django.conf import settings
 from django.conf import settings
@@ -127,6 +127,13 @@ class FileUploadHandler:
         """
         """
         pass
         pass
 
 
+    def upload_interrupted(self):
+        """
+        Signal that the upload was interrupted. Subclasses should perform
+        cleanup that is necessary for this handler.
+        """
+        pass
+
 
 
 class TemporaryFileUploadHandler(FileUploadHandler):
 class TemporaryFileUploadHandler(FileUploadHandler):
     """
     """
@@ -147,6 +154,15 @@ class TemporaryFileUploadHandler(FileUploadHandler):
         self.file.size = file_size
         self.file.size = file_size
         return self.file
         return self.file
 
 
+    def upload_interrupted(self):
+        if hasattr(self, 'file'):
+            temp_location = self.file.temporary_file_path()
+            try:
+                self.file.close()
+                os.remove(temp_location)
+            except FileNotFoundError:
+                pass
+
 
 
 class MemoryFileUploadHandler(FileUploadHandler):
 class MemoryFileUploadHandler(FileUploadHandler):
     """
     """

+ 7 - 0
django/http/multipartparser.py

@@ -150,6 +150,8 @@ class MultiPartParser:
         num_post_keys = 0
         num_post_keys = 0
         # To limit the amount of data read from the request.
         # To limit the amount of data read from the request.
         read_size = None
         read_size = None
+        # Whether a file upload is finished.
+        uploaded_file = True
 
 
         try:
         try:
             for item_type, meta_data, field_stream in Parser(stream, self._boundary):
             for item_type, meta_data, field_stream in Parser(stream, self._boundary):
@@ -159,6 +161,7 @@ class MultiPartParser:
                     # we hit the next boundary/part of the multipart content.
                     # we hit the next boundary/part of the multipart content.
                     self.handle_file_complete(old_field_name, counters)
                     self.handle_file_complete(old_field_name, counters)
                     old_field_name = None
                     old_field_name = None
+                    uploaded_file = True
 
 
                 try:
                 try:
                     disposition = meta_data['content-disposition'][1]
                     disposition = meta_data['content-disposition'][1]
@@ -225,6 +228,7 @@ class MultiPartParser:
                         content_length = None
                         content_length = None
 
 
                     counters = [0] * len(handlers)
                     counters = [0] * len(handlers)
+                    uploaded_file = False
                     try:
                     try:
                         for handler in handlers:
                         for handler in handlers:
                             try:
                             try:
@@ -279,6 +283,9 @@ class MultiPartParser:
             if not e.connection_reset:
             if not e.connection_reset:
                 exhaust(self._input_data)
                 exhaust(self._input_data)
         else:
         else:
+            if not uploaded_file:
+                for handler in handlers:
+                    handler.upload_interrupted()
             # Make sure that the request data is all fed
             # Make sure that the request data is all fed
             exhaust(self._input_data)
             exhaust(self._input_data)
 
 

+ 7 - 0
docs/ref/files/uploads.txt

@@ -212,6 +212,13 @@ attributes:
 
 
     Callback signaling that the entire upload (all files) has completed.
     Callback signaling that the entire upload (all files) has completed.
 
 
+.. method:: FileUploadHandler.upload_interrupted()
+
+    .. versionadded:: 3.2
+
+    Callback signaling that the upload was interrupted, e.g. when the user
+    closed their browser during file upload.
+
 .. method:: FileUploadHandler.handle_raw_input(input_data, META, content_length, boundary, encoding)
 .. method:: FileUploadHandler.handle_raw_input(input_data, META, content_length, boundary, encoding)
 
 
     Allows the handler to completely override the parsing of the raw
     Allows the handler to completely override the parsing of the raw

+ 3 - 2
docs/releases/3.2.txt

@@ -195,8 +195,9 @@ File Storage
 File Uploads
 File Uploads
 ~~~~~~~~~~~~
 ~~~~~~~~~~~~
 
 
-* ...
-
+* The new :meth:`FileUploadHandler.upload_interrupted()
+  <django.core.files.uploadhandler.FileUploadHandler.upload_interrupted>`
+  callback allows handling interrupted uploads.
 
 
 Forms
 Forms
 ~~~~~
 ~~~~~

+ 26 - 1
tests/file_uploads/tests.py

@@ -6,12 +6,13 @@ import sys
 import tempfile as sys_tempfile
 import tempfile as sys_tempfile
 import unittest
 import unittest
 from io import BytesIO, StringIO
 from io import BytesIO, StringIO
+from unittest import mock
 from urllib.parse import quote
 from urllib.parse import quote
 
 
 from django.core.files import temp as tempfile
 from django.core.files import temp as tempfile
 from django.core.files.uploadedfile import SimpleUploadedFile
 from django.core.files.uploadedfile import SimpleUploadedFile
 from django.http.multipartparser import (
 from django.http.multipartparser import (
-    MultiPartParser, MultiPartParserError, parse_header,
+    FILE, MultiPartParser, MultiPartParserError, Parser, parse_header,
 )
 )
 from django.test import SimpleTestCase, TestCase, client, override_settings
 from django.test import SimpleTestCase, TestCase, client, override_settings
 
 
@@ -443,6 +444,30 @@ class FileUploadTests(TestCase):
             temp_path = response.json()['temp_path']
             temp_path = response.json()['temp_path']
             self.assertIs(os.path.exists(temp_path), False)
             self.assertIs(os.path.exists(temp_path), False)
 
 
+    def test_upload_interrupted_temporary_file_handler(self):
+        # Simulate an interrupted upload by omitting the closing boundary.
+        class MockedParser(Parser):
+            def __iter__(self):
+                for item in super().__iter__():
+                    item_type, meta_data, field_stream = item
+                    yield item_type, meta_data, field_stream
+                    if item_type == FILE:
+                        return
+
+        with tempfile.NamedTemporaryFile() as temp_file:
+            temp_file.write(b'a')
+            temp_file.seek(0)
+            with mock.patch(
+                'django.http.multipartparser.Parser',
+                MockedParser,
+            ):
+                response = self.client.post(
+                    '/temp_file/upload_interrupted/',
+                    {'file': temp_file},
+                )
+            temp_path = response.json()['temp_path']
+            self.assertIs(os.path.exists(temp_path), False)
+
     def test_fileupload_getlist(self):
     def test_fileupload_getlist(self):
         file = tempfile.NamedTemporaryFile
         file = tempfile.NamedTemporaryFile
         with file() as file1, file() as file2, file() as file2a:
         with file() as file1, file() as file2, file() as file2a:

+ 1 - 0
tests/file_uploads/urls.py

@@ -14,6 +14,7 @@ urlpatterns = [
     path('getlist_count/', views.file_upload_getlist_count),
     path('getlist_count/', views.file_upload_getlist_count),
     path('upload_errors/', views.file_upload_errors),
     path('upload_errors/', views.file_upload_errors),
     path('temp_file/stop_upload/', views.file_stop_upload_temporary_file),
     path('temp_file/stop_upload/', views.file_stop_upload_temporary_file),
+    path('temp_file/upload_interrupted/', views.file_upload_interrupted_temporary_file),
     path('filename_case/', views.file_upload_filename_case_view),
     path('filename_case/', views.file_upload_filename_case_view),
     re_path(r'^fd_closing/(?P<access>t|f)/$', views.file_upload_fd_closing),
     re_path(r'^fd_closing/(?P<access>t|f)/$', views.file_upload_fd_closing),
 ]
 ]

+ 10 - 0
tests/file_uploads/views.py

@@ -2,6 +2,7 @@ import hashlib
 import os
 import os
 
 
 from django.core.files.uploadedfile import UploadedFile
 from django.core.files.uploadedfile import UploadedFile
+from django.core.files.uploadhandler import TemporaryFileUploadHandler
 from django.http import HttpResponse, HttpResponseServerError, JsonResponse
 from django.http import HttpResponse, HttpResponseServerError, JsonResponse
 
 
 from .models import FileModel
 from .models import FileModel
@@ -112,6 +113,15 @@ def file_stop_upload_temporary_file(request):
     )
     )
 
 
 
 
+def file_upload_interrupted_temporary_file(request):
+    request.upload_handlers.insert(0, TemporaryFileUploadHandler())
+    request.upload_handlers.pop(2)
+    request.FILES  # Trigger file parsing.
+    return JsonResponse(
+        {'temp_path': request.upload_handlers[0].file.temporary_file_path()},
+    )
+
+
 def file_upload_getlist_count(request):
 def file_upload_getlist_count(request):
     """
     """
     Check the .getlist() function to ensure we receive the correct number of files.
     Check the .getlist() function to ensure we receive the correct number of files.