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>
This commit is contained in:
aryan 2020-02-20 00:23:48 +05:30 committed by Mariusz Felisiak
parent 21b127bfbc
commit 11c4a4412b
7 changed files with 71 additions and 4 deletions

View File

@ -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):
""" """

View File

@ -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)

View File

@ -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

View File

@ -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
~~~~~ ~~~~~

View File

@ -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:

View File

@ -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),
] ]

View File

@ -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.