Skip to content

Commit a16f5aa

Browse files
Copilotringabout
andcommitted
Add support for multiple file uploads with same name
Co-authored-by: ringabout <43030857+ringabout@users.noreply.github.com>
1 parent 342dbe5 commit a16f5aa

File tree

5 files changed

+156
-10
lines changed

5 files changed

+156
-10
lines changed

src/prologue/core/context.nim

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,10 +166,19 @@ func initUploadFile*(filename, body: string): UpLoadFile =
166166
UploadFile(filename: filename, body: body)
167167

168168
func getUploadFile*(ctx: Context, name: string): UpLoadFile {.inline.} =
169-
## Gets the UploadFile from request.
169+
## Gets the first UploadFile from request with the given name.
170+
## For retrieving multiple files with the same name, use `getUploadFiles` instead.
170171
let file = ctx.request.formParams[name]
171172
initUploadFile(filename = file.params.getOrDefault("filename"), body = file.body)
172173

174+
func getUploadFiles*(ctx: Context, name: string): seq[UpLoadFile] {.inline.} =
175+
## Gets all UploadFiles from request with the given name.
176+
## This is useful when multiple files are uploaded from a single input element with the `multiple` attribute.
177+
result = @[]
178+
if ctx.request.formParams.data.hasKey(name):
179+
for file in ctx.request.formParams.data[name]:
180+
result.add(initUploadFile(filename = file.params.getOrDefault("filename"), body = file.body))
181+
173182
proc save*(uploadFile: UpLoadFile, dir: string, filename = "") {.inline.} =
174183
## Saves the UploadFile to ``dir``.
175184
if not dirExists(dir):

src/prologue/core/form.nim

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ func parseFormPart*(body, contentType: string): FormPart =
6363
for line in head.splitLines:
6464
let header = line.parseHeader
6565
if header.key != "Content-Disposition":
66-
result.data[name].params[header.key] = header.value[0]
66+
if name.len > 0 and result.data.hasKey(name) and result.data[name].len > 0:
67+
result.data[name][^1].params[header.key] = header.value[0]
6768
continue
6869
pos = 0
6970
let
@@ -81,18 +82,20 @@ func parseFormPart*(body, contentType: string): FormPart =
8182
case formKey
8283
of "name":
8384
name = move(formValue)
84-
result.data[name] = (newStringTable(mode = modeCaseSensitive), "")
85+
if not result.data.hasKey(name):
86+
result.data[name] = @[]
87+
result.data[name].add((newStringTable(mode = modeCaseSensitive), ""))
8588
of "filename":
86-
result.data[name].params["filename"] = move(formValue)
89+
result.data[name][^1].params["filename"] = move(formValue)
8790
of "filename*":
88-
result.data[name].params["filenameStar"] = move(formValue)
91+
result.data[name][^1].params["filenameStar"] = move(formValue)
8992
else:
9093
discard
9194
inc(times)
9295
if times >= 3:
9396
break
9497

95-
result.data[name].body = tail
98+
result.data[name][^1].body = tail
9699

97100
func parseFormParams*(request: var Request, contentType: string) =
98101
## Parses get or post or query parameters.

src/prologue/core/types.nim

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,18 +40,24 @@ type
4040
Fault = "fault"
4141

4242
FormPart* = object
43-
data*: OrderedTableRef[string, tuple[params: StringTableRef, body: string]]
43+
data*: OrderedTableRef[string, seq[tuple[params: StringTableRef, body: string]]]
4444

4545

4646
func initFormPart*(): FormPart =
47-
FormPart(data: newOrderedTable[string, (StringTableRef, string)]())
47+
FormPart(data: newOrderedTable[string, seq[(StringTableRef, string)]]())
4848

4949
func `[]`*(formPart: FormPart, key: string): tuple[params: StringTableRef,
5050
body: string] {.inline.} =
51-
formPart.data[key]
51+
# Returns the first item for backward compatibility
52+
if formPart.data.hasKey(key) and formPart.data[key].len > 0:
53+
formPart.data[key][0]
54+
else:
55+
(newStringTable(mode = modeCaseSensitive), "")
5256

5357
proc `[]=`*(formPart: FormPart, key: string, body: string) {.inline.} =
54-
formPart.data[key] = (newStringTable(mode = modeCaseSensitive), body)
58+
if not formPart.data.hasKey(key):
59+
formPart.data[key] = @[]
60+
formPart.data[key].add((newStringTable(mode = modeCaseSensitive), body))
5561

5662
func tryParseInt(value: string, default: int): int {.inline.} =
5763
try:

tests/unit/tunit_core/tunit_context.nim

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,3 +301,89 @@ suite "getFormParams":
301301
#Then
302302
check param == ""
303303

304+
305+
306+
suite "getUploadFile":
307+
test "Given a single uploaded file, When getUploadFile is called, Then return the file":
308+
#Given
309+
var ctx = new Context
310+
ctx.request.formParams = initFormPart()
311+
let expectedFilename = "test.txt"
312+
let expectedBody = "file content"
313+
ctx.request.formParams.data["file"] = @[(newStringTable({"filename": expectedFilename}.newStringTable), expectedBody)]
314+
315+
#When
316+
let uploadFile = ctx.getUploadFile("file")
317+
318+
#Then
319+
check uploadFile.filename == expectedFilename
320+
check uploadFile.body == expectedBody
321+
322+
test "Given multiple uploaded files with same name, When getUploadFile is called, Then return the first file":
323+
#Given
324+
var ctx = new Context
325+
ctx.request.formParams = initFormPart()
326+
let expectedFilename = "test1.txt"
327+
let expectedBody = "first file content"
328+
ctx.request.formParams.data["files"] = @[
329+
(newStringTable({"filename": expectedFilename}.newStringTable), expectedBody),
330+
(newStringTable({"filename": "test2.txt"}.newStringTable), "second file content")
331+
]
332+
333+
#When
334+
let uploadFile = ctx.getUploadFile("files")
335+
336+
#Then
337+
check uploadFile.filename == expectedFilename
338+
check uploadFile.body == expectedBody
339+
340+
341+
suite "getUploadFiles":
342+
test "Given multiple uploaded files with same name, When getUploadFiles is called, Then return all files":
343+
#Given
344+
var ctx = new Context
345+
ctx.request.formParams = initFormPart()
346+
ctx.request.formParams.data["files"] = @[
347+
(newStringTable({"filename": "test1.txt"}.newStringTable), "first file content"),
348+
(newStringTable({"filename": "test2.txt"}.newStringTable), "second file content"),
349+
(newStringTable({"filename": "test3.txt"}.newStringTable), "third file content")
350+
]
351+
352+
#When
353+
let uploadFiles = ctx.getUploadFiles("files")
354+
355+
#Then
356+
check uploadFiles.len == 3
357+
check uploadFiles[0].filename == "test1.txt"
358+
check uploadFiles[0].body == "first file content"
359+
check uploadFiles[1].filename == "test2.txt"
360+
check uploadFiles[1].body == "second file content"
361+
check uploadFiles[2].filename == "test3.txt"
362+
check uploadFiles[2].body == "third file content"
363+
364+
test "Given a single uploaded file, When getUploadFiles is called, Then return a sequence with one file":
365+
#Given
366+
var ctx = new Context
367+
ctx.request.formParams = initFormPart()
368+
let expectedFilename = "test.txt"
369+
let expectedBody = "file content"
370+
ctx.request.formParams.data["file"] = @[(newStringTable({"filename": expectedFilename}.newStringTable), expectedBody)]
371+
372+
#When
373+
let uploadFiles = ctx.getUploadFiles("file")
374+
375+
#Then
376+
check uploadFiles.len == 1
377+
check uploadFiles[0].filename == expectedFilename
378+
check uploadFiles[0].body == expectedBody
379+
380+
test "Given no uploaded files, When getUploadFiles is called, Then return an empty sequence":
381+
#Given
382+
var ctx = new Context
383+
ctx.request.formParams = initFormPart()
384+
385+
#When
386+
let uploadFiles = ctx.getUploadFiles("nonexistent")
387+
388+
#Then
389+
check uploadFiles.len == 0

tests/unit/tunit_core/tunit_form.nim

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,45 @@ block:
5656
doAssert formPart.data.contains("myfile"), "myfile field should be present"
5757
doAssert formPart.data["myfile"].body == "Hello World"
5858
doAssert formPart.data["myfile"].params.getOrDefault("filename", "") == "test.txt"
59+
60+
block:
61+
# Test for multiple file inputs with the same name (issue: handle multiple file upload from a single input element)
62+
# This tests that multiple files uploaded with the same input name are all captured
63+
const testmime =
64+
"------WebKitFormBoundary7MA4YWxkTrZu0gW\13\10" &
65+
"Content-Disposition: form-data; name=\"files\"; filename=\"test1.txt\"\13\10" &
66+
"Content-Type: text/plain\13\10" &
67+
"\13\10" &
68+
"First file content\13\10" &
69+
"------WebKitFormBoundary7MA4YWxkTrZu0gW\13\10" &
70+
"Content-Disposition: form-data; name=\"files\"; filename=\"test2.txt\"\13\10" &
71+
"Content-Type: text/plain\13\10" &
72+
"\13\10" &
73+
"Second file content\13\10" &
74+
"------WebKitFormBoundary7MA4YWxkTrZu0gW\13\10" &
75+
"Content-Disposition: form-data; name=\"files\"; filename=\"test3.txt\"\13\10" &
76+
"Content-Type: text/plain\13\10" &
77+
"\13\10" &
78+
"Third file content\13\10" &
79+
"------WebKitFormBoundary7MA4YWxkTrZu0gW--\13\10"
80+
const contenttype = "multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW"
81+
let formPart = parseFormPart(testmime, contenttype)
82+
83+
# Verify that all three files with the same name are captured
84+
doAssert formPart.data.contains("files"), "files field should be present"
85+
doAssert formPart.data["files"].len == 3, "Should have 3 files"
86+
87+
# Verify first file
88+
doAssert formPart.data["files"][0].body == "First file content"
89+
doAssert formPart.data["files"][0].params.getOrDefault("filename", "") == "test1.txt"
90+
doAssert formPart.data["files"][0].params.getOrDefault("Content-Type", "") == "text/plain"
91+
92+
# Verify second file
93+
doAssert formPart.data["files"][1].body == "Second file content"
94+
doAssert formPart.data["files"][1].params.getOrDefault("filename", "") == "test2.txt"
95+
doAssert formPart.data["files"][1].params.getOrDefault("Content-Type", "") == "text/plain"
96+
97+
# Verify third file
98+
doAssert formPart.data["files"][2].body == "Third file content"
99+
doAssert formPart.data["files"][2].params.getOrDefault("filename", "") == "test3.txt"
100+
doAssert formPart.data["files"][2].params.getOrDefault("Content-Type", "") == "text/plain"

0 commit comments

Comments
 (0)