Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds a new FileDockQuestion component that provides a simpler interface for multiple file uploads without a submit button. Files are uploaded individually to avoid HTTP requests exceeding 10 MB, replacing the previous approach where all files were uploaded together.
Changes:
- Added
FileDockQuestioncomponent with drag-and-drop functionality and individual file management - Implemented individual file upload to prevent large HTTP requests
- Added server-side file removal functionality with
removeFilesfunction - Modified file size validation limits (9.9 MB per file on frontend, 10 MB per request on backend, 100 MB total directory size)
Reviewed changes
Copilot reviewed 7 out of 8 changed files in this pull request and generated 17 comments.
Show a summary per file
| File | Description |
|---|---|
| components/Quiz/FileDockQuestion.tsx | New component for drag-and-drop file uploads without submit button |
| components/Quiz/Quiz.tsx | Integration logic to use FileDockQuestion for uploads questions when maxAttempts !== 1 |
| context/QuizContextProvider.tsx | Added file management functions (addFiles, removeFile, getLastAnswer) and REMOVEORADDFILES action |
| api/quiz.ts | Added postFileAnswer function to handle file answer recording in database |
| utils/zip.ts | Added removeFiles function for server-side file deletion, made hash24 private |
| app/api/upload-answer/route.ts | Modified to handle individual file uploads with cumulative size check (100 MB total) |
| i18n/dict.tsx | Added translations for file size errors and removal failures |
| next-env.d.ts | Updated generated Next.js types path (.next/types → .next/dev/types) |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| for(const file of addFiles) { | ||
| const formData = new FormData(); | ||
| formData.append("files", file); | ||
| formData.append("accessToken", user?.accessToken || ""); | ||
| formData.append("bookId", bookId.toString()); | ||
| formData.append("qId", | ||
| (await getQId(bookId, questionId)).toString()); | ||
| if (groupId) { | ||
| formData.append("groupId", groupId.toString()); | ||
| } | ||
| const res = await fetch("/api/upload-answer", { | ||
| method: "POST", | ||
| body: formData, | ||
| }); | ||
| if (!res.ok) { | ||
| quizReducer({type: "ERROR", value: {questionId}}); | ||
| return false; | ||
| } | ||
| } |
There was a problem hiding this comment.
The loop uploads files sequentially but doesn't handle partial failures well. If a file upload fails in the middle of the loop (line 320-322), the function returns immediately, leaving some files uploaded to the server but not recorded in the database. The uploaded files will remain on the server but be orphaned. Consider either: 1) tracking which files were successfully uploaded and passing only those to postFileAnswer, or 2) rolling back filesystem changes when upload fails, or 3) documenting that partial uploads may occur.
| ((await db.get(` | ||
| SELECT answer FROM answers | ||
| WHERE userId = ? AND bookId = ? AND groupId IS ? AND questionId = ? | ||
| ORDER BY createdAt DESC | ||
| LIMIT 1 | ||
| `, [userId, bookId, groupId, qId] | ||
| )) as { answer: string } | undefined)?.answer |
There was a problem hiding this comment.
Potential null pointer issue: If removeFiles is not true and there are no previous answers, the expression on line 136 will try to call .split(":") on undefined, resulting in a runtime error. The optional chaining stops at .answer but doesn't protect against undefined being passed to .split(). Add a null check or default value before calling .split(), such as: '?.answer?.split(":") || []' or '(... )?.answer ?? ""'.
| ((await db.get(` | |
| SELECT answer FROM answers | |
| WHERE userId = ? AND bookId = ? AND groupId IS ? AND questionId = ? | |
| ORDER BY createdAt DESC | |
| LIMIT 1 | |
| `, [userId, bookId, groupId, qId] | |
| )) as { answer: string } | undefined)?.answer | |
| ((((await db.get(` | |
| SELECT answer FROM answers | |
| WHERE userId = ? AND bookId = ? AND groupId IS ? AND questionId = ? | |
| ORDER BY createdAt DESC | |
| LIMIT 1 | |
| `, [userId, bookId, groupId, qId] | |
| )) as { answer: string } | undefined)?.answer ?? "") |
utils/zip.ts
Outdated
| try { | ||
| await fs.promises.rm(path.join(dir!, file), {force: true}); |
There was a problem hiding this comment.
Type safety issue: The variable 'file' is typed as 'string' in the parameter (line 85), but this function is called with file names from the removeFiles parameter which could contain path separators. The path.join on line 87 could create paths outside the intended directory if 'file' contains "../" sequences. This is a security vulnerability even though the file parameter is typed as string. Use path.basename(file) before joining to ensure the file stays within the directory.
| try { | |
| await fs.promises.rm(path.join(dir!, file), {force: true}); | |
| const safeFile = path.basename(file); | |
| try { | |
| await fs.promises.rm(path.join(dir!, safeFile), {force: true}); |
| const totalSize = files.reduce((acc, file) => acc + file.size, 0); | ||
| if (totalSize > 50 * 1024 * 1024) { | ||
|
|
||
| if (addFiles.some((file) => file.size > 9.9 * 1024 * 1024)) { |
There was a problem hiding this comment.
Inconsistent file size limits: The frontend checks if individual files are larger than 9.9 MB (line 276), but the backend API endpoint checks if the total size of files in a single request exceeds 10 MB (line 18 in app/api/upload-answer/route.ts). Since files are uploaded individually in a loop (lines 306-324), each request should contain only one file. The backend check should validate individual file size (e.g., 10 MB per file) rather than total size, or the frontend should enforce a stricter limit like 9 MB to account for request overhead.
| if (addFiles.some((file) => file.size > 9.9 * 1024 * 1024)) { | |
| if (addFiles.some((file) => file.size > 9 * 1024 * 1024)) { |
| multiple={type === "uploads"} | ||
| /> } | ||
| { isUpload && ( | ||
| type === "uploads" && maxAttempts !== 1 ? |
There was a problem hiding this comment.
Unclear condition: The condition uses 'maxAttempts !== 1' to decide between FileDockQuestion and FileQuestion. However, based on the PR description, the intention is to use FileDockQuestion for questions with "number of attempts different than 0". This condition would show FileDockQuestion for maxAttempts of 0, 2, 3, etc., but not for 1. Consider clarifying: should this be 'maxAttempts !== 1' (current), 'maxAttempts !== 0' (non-zero attempts), or 'maxAttempts === 0 || maxAttempts > 1' (unlimited or multiple attempts)? The current logic seems inconsistent with the PR description.
| type === "uploads" && maxAttempts !== 1 ? | |
| type === "uploads" && maxAttempts !== 0 ? |
Note: #199 is build on this PR and should probably be merged instead of this one.
Provide a simpler user interface for questions with multiple file uploads and with number of attempts different than 1. Users can drag files into the question (or select them with a select button) and remove them. No submit button.
Also upload each file individually to avoid https requests larger than 10 MB.