fix(upload): preserve fields in multipart fallback
Build and Push Docker Container / build-and-push (push) Successful in 1m40s

- Recover regular multipart fields when the file part is not exposed through request.files.

- Preserve expires and note values for browser uploads that need body-based file recovery.

- Add parser coverage for file parts without filenames and recovered expiry fields.

- Extend upload route tests to assert recovered uploads keep expiry metadata.

- Bump NanoShare to 1.27.3.
This commit is contained in:
2026-09-10 00:18:57 +02:00
parent 21aca1b9d7
commit 3194e534e0
6 changed files with 171 additions and 4 deletions
+28
View File
@@ -0,0 +1,28 @@
from my_modules.upload_content import parse_multipart_upload_body, recover_file_from_multipart_body
def test_recovers_file_part_without_filename():
boundary = '----nanoshare-test-boundary'
body = (
f'--{boundary}\r\n'
'Content-Disposition: form-data; name="file"\r\n'
'Content-Type: application/octet-stream\r\n'
'\r\n'
).encode() + b'hello upload' + (
'\r\n'
f'--{boundary}\r\n'
'Content-Disposition: form-data; name="expires"\r\n'
'\r\n'
'7d\r\n'
f'--{boundary}--\r\n'
).encode()
uploaded = recover_file_from_multipart_body(body, boundary)
parts = parse_multipart_upload_body(body, boundary)
assert uploaded is not None
assert uploaded.filename == 'file'
assert uploaded.mimetype == 'application/octet-stream'
assert uploaded.stream.read() == b'hello upload'
assert parts.fields == {'expires': '7d'}
assert parts.file is not None
assert parts.file.stream.read() == b'hello upload'
+52 -1
View File
@@ -40,9 +40,15 @@ class AwaitableValue:
return get_value().__await__()
class FakeRequest:
def __init__(self, form, files):
def __init__(self, form, files, body=b'', boundary=None):
self.form = AwaitableValue(form)
self.files = AwaitableValue(files)
self.mimetype = 'multipart/form-data' if boundary else ''
self.mimetype_params = {'boundary': boundary} if boundary else {}
self._body = body
async def get_data(self, cache=True):
return self._body
class FakeConvex:
def __init__(self):
@@ -91,5 +97,50 @@ def test_upload_accepts_file_field_without_filename(monkeypatch):
assert await response.get_json() == {'ok': True}
assert app.convex.sent == [(b'hello', 'application/octet-stream')]
assert app.convex.files[0]['file_name'].endswith('.bin')
assert app.convex.files[0]['expires_at'] is not None
asyncio.run(run_test())
def test_upload_recovers_file_field_from_multipart_body(monkeypatch):
async def run_test():
upload = load_upload_module(monkeypatch)
app = Quart(__name__)
app.secret_key = 'test-secret'
app.convex = FakeConvex()
app.orphan_storage_registry = None
boundary = '----nanoshare-test-boundary'
body = (
f'--{boundary}\r\n'
'Content-Disposition: form-data; name="file"\r\n'
'Content-Type: application/octet-stream\r\n'
'\r\n'
).encode() + b'hello recovered' + (
'\r\n'
f'--{boundary}\r\n'
'Content-Disposition: form-data; name="expires"\r\n'
'\r\n'
'7d\r\n'
f'--{boundary}--\r\n'
).encode()
async with app.test_request_context('/api/upload', method='POST'):
session['user'] = {'sub': 'user_1'}
fake_request = FakeRequest(
form={'expires': '7d', 'note': 'test', 'text': ''},
files={},
body=body,
boundary=boundary,
)
monkeypatch.setattr(upload, 'request', fake_request)
response = await upload.api_upload()
assert await response.get_json() == {'ok': True}
assert app.convex.sent == [(b'hello recovered', 'application/octet-stream')]
assert app.convex.files[0]['file_name'].endswith('.bin')
assert app.convex.files[0]['expires_at'] is not None
assert app.convex.files[0]['note'] == 'test'
asyncio.run(run_test())