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:
parent
21b127bfbc
commit
11c4a4412b
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
~~~~~
|
~~~~~
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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.
|
||||||
|
|
Loading…
Reference in New Issue