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
"""
import os
from io import BytesIO
from django.conf import settings
@ -127,6 +127,13 @@ class FileUploadHandler:
"""
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):
"""
@ -147,6 +154,15 @@ class TemporaryFileUploadHandler(FileUploadHandler):
self.file.size = file_size
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):
"""

View File

@ -150,6 +150,8 @@ class MultiPartParser:
num_post_keys = 0
# To limit the amount of data read from the request.
read_size = None
# Whether a file upload is finished.
uploaded_file = True
try:
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.
self.handle_file_complete(old_field_name, counters)
old_field_name = None
uploaded_file = True
try:
disposition = meta_data['content-disposition'][1]
@ -225,6 +228,7 @@ class MultiPartParser:
content_length = None
counters = [0] * len(handlers)
uploaded_file = False
try:
for handler in handlers:
try:
@ -279,6 +283,9 @@ class MultiPartParser:
if not e.connection_reset:
exhaust(self._input_data)
else:
if not uploaded_file:
for handler in handlers:
handler.upload_interrupted()
# Make sure that the request data is all fed
exhaust(self._input_data)

View File

@ -212,6 +212,13 @@ attributes:
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)
Allows the handler to completely override the parsing of the raw

View File

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

View File

@ -6,12 +6,13 @@ import sys
import tempfile as sys_tempfile
import unittest
from io import BytesIO, StringIO
from unittest import mock
from urllib.parse import quote
from django.core.files import temp as tempfile
from django.core.files.uploadedfile import SimpleUploadedFile
from django.http.multipartparser import (
MultiPartParser, MultiPartParserError, parse_header,
FILE, MultiPartParser, MultiPartParserError, Parser, parse_header,
)
from django.test import SimpleTestCase, TestCase, client, override_settings
@ -443,6 +444,30 @@ class FileUploadTests(TestCase):
temp_path = response.json()['temp_path']
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):
file = tempfile.NamedTemporaryFile
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('upload_errors/', views.file_upload_errors),
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),
re_path(r'^fd_closing/(?P<access>t|f)/$', views.file_upload_fd_closing),
]

View File

@ -2,6 +2,7 @@ import hashlib
import os
from django.core.files.uploadedfile import UploadedFile
from django.core.files.uploadhandler import TemporaryFileUploadHandler
from django.http import HttpResponse, HttpResponseServerError, JsonResponse
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):
"""
Check the .getlist() function to ensure we receive the correct number of files.