Skip to content

Comments

Add 'FileDockQuestion'#198

Open
janezd wants to merge 1 commit intobiolab:masterfrom
janezd:upload-dock
Open

Add 'FileDockQuestion'#198
janezd wants to merge 1 commit intobiolab:masterfrom
janezd:upload-dock

Conversation

@janezd
Copy link
Contributor

@janezd janezd commented Feb 19, 2026

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.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 FileDockQuestion component 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 removeFiles function
  • 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.

Comment on lines +306 to +324
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;
}
}
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +130 to +136
((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
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ?? ""'.

Suggested change
((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 ?? "")

Copilot uses AI. Check for mistakes.
utils/zip.ts Outdated
Comment on lines 86 to 87
try {
await fs.promises.rm(path.join(dir!, file), {force: true});
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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});

Copilot uses AI. Check for mistakes.
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)) {
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
if (addFiles.some((file) => file.size > 9.9 * 1024 * 1024)) {
if (addFiles.some((file) => file.size > 9 * 1024 * 1024)) {

Copilot uses AI. Check for mistakes.
multiple={type === "uploads"}
/> }
{ isUpload && (
type === "uploads" && maxAttempts !== 1 ?
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
type === "uploads" && maxAttempts !== 1 ?
type === "uploads" && maxAttempts !== 0 ?

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant