Add file-transfer module: multipart limits, filename handling, Range downloads, streaming under -Xmx64m, S3 presigned URLs

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01VRLbHYdXVEBzGVhYq3pz4q
This commit is contained in:
Claude
2026-10-17 15:41:24 +00:00
parent b9388053c4
commit 008211f2fa
25 changed files with 1072 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
limits: max-file-size=1MB, max-request-size=2MB
real HTTP, one 500 KB file -> status=201 body={"id":"d8d0b4b8-d530-42aa-b005-9e1b747449a7","originalFilename":"a.txt","displayName":"a.txt","size":512000,"declaredContentType":"text/plai...
real HTTP, one 1.5 MB file -> status=413 body={"detail":"The upload is larger than the configured limit (spring.servlet.multipart.max-file-size / max-request-size).","instance":"/files",...
real HTTP, 3 x 900 KB in one request -> status=413 body={"detail":"The upload is larger than the configured limit (spring.servlet.multipart.max-file-size / max-request-size).","instance":"/files",...
real HTTP, one 10 MB file, 10 attempts -> 7 x status=413, 3 x connection error
MockMvc, the same 1.5 MB file -> status=201
+4
View File
@@ -0,0 +1,4 @@
client sent filename=../../etc/cron.d/evil.txt Content-Type=image/png
server response: {"id":"4980219c-b92a-46d4-a5ab-b5d7636fb7f0","originalFilename":"../../etc/cron.d/evil.txt","displayName":"evil.txt","size":5,"declaredContentType":"image/png"}
root.resolve(originalFilename).normalize() stays inside the storage dir: false
client sent a Windows-style path; server response: {"id":"d436e57e-6855-4fff-b5ca-b9dd0fec25c2","originalFilename":"..\\..\\windows\\system32\\evil.dll","displayName":"evil.dll","size":1,"declaredContentType":"text/plain"}
+12
View File
@@ -0,0 +1,12 @@
GET /files/{id}
status=200 body=ABCDEFGHIJKLMNOPQRSTUVWXYZ
content-type: application/octet-stream
content-length: 26
content-disposition: attachment; filename="resume 2026.txt"; filename*=UTF-8''r%C3%A9sum%C3%A9%202026.txt
accept-ranges: bytes
x-content-type-options: nosniff
GET /files/{id} with Range: bytes=10-19
status=206 body=KLMNOPQRST
content-range: bytes 10-19/26
GET /files/{id} with Range: bytes=500-
status=416 content-range: bytes */26
+18
View File
@@ -0,0 +1,18 @@
PUT a URL signed for 1 second, sent after 2.2 seconds -> status=403 <Error><Code>AccessDenied</Code></Error>
POST /presign/upload {"contentType":"image/png"} -> status=200
method=PUT expiresInSeconds=300 headers={"content-type":"image/png"}
url path: http://<fake-s3>/uploads/4dfdf0af-141a-4d1b-9d35-3609a9385791
query: X-Amz-Algorithm=AWS4-HMAC-SHA256
query: X-Amz-Date=20261017T154013Z
query: X-Amz-SignedHeaders=content-type%3Bhost
query: X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20261017%2Fus-east-1%2Fs3%2Faws4_request
query: X-Amz-Expires=300
query: X-Amz-Signature=4e5c56c29980cf521e477119788a0616c392deac4686df7259957af9f1445203
PUT with the signed Content-Type (image/png) -> status=200
PUT the same URL again (presigned URLs are reusable) -> status=200
PUT with a different Content-Type (text/html) -> status=403 <Error><Code>SignatureDoesNotMatch</Code></Error>
PUT with no Content-Type header -> status=403 <Error><Code>SignatureDoesNotMatch</Code></Error>
PUT after changing the last character of the key -> status=403 <Error><Code>SignatureDoesNotMatch</Code></Error>
PUT after raising X-Amz-Expires to 7 days -> status=403 <Error><Code>SignatureDoesNotMatch</Code></Error>
GET presigned URL -> status=200 bytes match upload=true content-disposition=attachment; filename="holiday.png"
disallowed type: POST /presign/upload text/html -> status=400
+4
View File
@@ -0,0 +1,4 @@
JVM started with -Xmx64m
GET /buffered/200 (200 MB) -> status=500 downloaded=108 bytes in 0.273337s
GET /stream/200 (200 MB) -> status=200 downloaded=209715200 bytes in 0.337059s
GET /stream/1 afterwards -> status=200
+3
View File
@@ -0,0 +1,3 @@
JVM started with -Xmx64m, multipart limits removed, uploading a 150 MB file
multipart POST /files -> status=201 sent=157286616 bytes in 1.518657s
raw POST /files/raw -> status=201 sent=157286400 bytes in 1.850810s