-
Notifications
You must be signed in to change notification settings - Fork 370
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[api] Improve normal file upload design for public APIs #3878
base: master
Are you sure you want to change the base?
Changes from all commits
8146d83
ff96a19
fc38f5c
93d1072
1f9d480
d112725
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,6 +21,7 @@ | |
import operator | ||
import mimetypes | ||
import posixpath | ||
from io import BytesIO as string_io | ||
|
||
from django.core.paginator import EmptyPage, Paginator | ||
from django.http import HttpResponse, HttpResponseNotModified, HttpResponseRedirect, StreamingHttpResponse | ||
|
@@ -46,6 +47,7 @@ | |
FILE_DOWNLOAD_CACHE_CONTROL, | ||
MAX_FILE_SIZE_UPLOAD_LIMIT, | ||
REDIRECT_DOWNLOAD, | ||
RESTRICT_FILE_EXTENSIONS, | ||
SHOW_DOWNLOAD_BUTTON, | ||
) | ||
from filebrowser.lib import xxd | ||
|
@@ -394,81 +396,42 @@ def upload_complete(request): | |
|
||
@api_error_handler | ||
def upload_file(request): | ||
""" | ||
A wrapper around the actual upload view function to clean up the temporary file afterwards if it fails. | ||
|
||
Returns JSON. | ||
""" | ||
pass | ||
# response = {} | ||
|
||
# try: | ||
# response = _upload_file(request) | ||
# except Exception as e: | ||
# LOG.exception('Upload operation failed.') | ||
|
||
# file = request.FILES.get('file') | ||
# if file and hasattr(file, 'remove'): # TODO: Call from proxyFS -- Check feasibility of this old comment | ||
# file.remove() | ||
|
||
# return HttpResponse(str(e).split('\n', 1)[0], status=500) # TODO: Check error message and status code | ||
|
||
# return JsonResponse(response) | ||
# Read request body first to prevent RawPostDataException later on which occurs when trying to access body after it has already been read | ||
body_data_bytes = string_io(request.body) | ||
|
||
|
||
def _upload_file(request): | ||
""" | ||
Handles file uploaded by HDFSfileUploadHandler. | ||
|
||
The uploaded file is stored in HDFS at its destination with a .tmp suffix. | ||
We just need to rename it to the destination path. | ||
""" | ||
uploaded_file = request.FILES['file'] | ||
dest_path = request.GET.get('dest') | ||
response = {} | ||
|
||
if MAX_FILE_SIZE_UPLOAD_LIMIT.get() >= 0 and uploaded_file.size > MAX_FILE_SIZE_UPLOAD_LIMIT.get(): | ||
return HttpResponse(f'File exceeds maximum allowed size of {MAX_FILE_SIZE_UPLOAD_LIMIT.get()} bytes.', status=500) | ||
dest_path = request.POST.get('destination_path') | ||
|
||
# Use form for now to triger the upload handler process by Django. | ||
# Might be a better solution now to try directly using handler in request.fs.upload() for all FS. | ||
# form = UploadAPIFileForm(request.POST, request.FILES) | ||
# Check if the file type is restricted | ||
_, file_type = os.path.splitext(uploaded_file.name) | ||
if RESTRICT_FILE_EXTENSIONS.get() and file_type.lower() in [ext.lower() for ext in RESTRICT_FILE_EXTENSIONS.get()]: | ||
return HttpResponse(f'File type "{file_type}" is not allowed. Please choose a file with a different extetypension.', status=400) | ||
|
||
if request.META.get('upload_failed'): | ||
raise Exception(request.META.get('upload_failed')) # TODO: Check error message and status code | ||
# Check if the file size exceeds the maximum allowed size | ||
max_size = MAX_FILE_SIZE_UPLOAD_LIMIT.get() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this the same for all file systems? |
||
if max_size >= 0 and uploaded_file.size >= max_size: | ||
return HttpResponse(f'File exceeds maximum allowed size of {max_size} bytes. Please upload a smaller file.', status=413) | ||
|
||
# if not form.is_valid(): | ||
# raise Exception(f"Error in upload form: {form.errors}") | ||
# Check if the destination path is a directory and the file name contains a path separator | ||
# This prevents directory traversal attacks | ||
if request.fs.isdir(dest_path) and posixpath.sep in uploaded_file.name: | ||
return HttpResponse(f'Invalid filename. Path separators are not allowed.', status=400) | ||
|
||
# Check if the file already exists at the destination path | ||
filepath = request.fs.join(dest_path, uploaded_file.name) | ||
|
||
if request.fs.isdir(dest_path) and posixpath.sep in uploaded_file.name: | ||
raise Exception(f'Upload failed: {posixpath.sep} is not allowed in the filename {uploaded_file.name}.') # TODO: status code | ||
if request.fs.exists(filepath): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think ideally there should be an option to override an existing file here if the user has already been informed and made that choice. E.g. in the case of an updated version of the file but at same file path. |
||
return HttpResponse(f'The file path {filepath} already exists.', status=409) | ||
|
||
try: | ||
request.fs.upload(file=uploaded_file, path=dest_path, username=request.user.username) | ||
except IOError as ex: | ||
already_exists = False | ||
try: | ||
already_exists = request.fs.exists(dest_path) | ||
except Exception: | ||
pass | ||
|
||
if already_exists: | ||
messsage = f'Upload failed: Destination {filepath} already exists.' | ||
else: | ||
messsage = f'Upload error: Copy to {filepath} failed: {str(ex)}' | ||
raise Exception(messsage) # TODO: Check error messages above and status code | ||
request.fs.upload_v1(request.META, input_data=body_data_bytes, destination=dest_path, username=request.user.username) | ||
except Exception as ex: | ||
return HttpResponse(f'Upload to {filepath} failed: {str(ex)}', status=500) | ||
|
||
# TODO: Check response fields below | ||
response.update( | ||
{ | ||
'path': filepath, | ||
'result': _massage_stats(request, stat_absolute_path(filepath, request.fs.stats(filepath))), | ||
} | ||
) | ||
response = { | ||
'uploaded_file_stats': _massage_stats(request, stat_absolute_path(filepath, request.fs.stats(filepath))), | ||
} | ||
|
||
return response | ||
return JsonResponse(response) | ||
|
||
|
||
@api_error_handler | ||
|
@@ -766,7 +729,6 @@ def bulk_op(request, op): | |
|
||
error_dict = {} | ||
for p in path_list: | ||
|
||
tmp_dict = bulk_dict | ||
if op in (copy, move): | ||
tmp_dict['source_path'] = p | ||
|
@@ -795,10 +757,12 @@ def _massage_stats(request, stats): | |
stats_dict = stats.to_json_dict() | ||
normalized_path = request.fs.normpath(stats_dict.get('path')) | ||
|
||
stats_dict.update({ | ||
'path': normalized_path, | ||
'type': filetype(stats.mode), | ||
'rwx': rwx(stats.mode, stats.aclBit), | ||
}) | ||
stats_dict.update( | ||
{ | ||
'path': normalized_path, | ||
'type': filetype(stats.mode), | ||
'rwx': rwx(stats.mode, stats.aclBit), | ||
} | ||
) | ||
|
||
return stats_dict |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: spelling extetypension
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
two questions