Skip to content

Fix duplicate file naming for "Download All" submissions#2312

Open
jhs-panda wants to merge 2 commits intomasterfrom
download-add-fix
Open

Fix duplicate file naming for "Download All" submissions#2312
jhs-panda wants to merge 2 commits intomasterfrom
download-add-fix

Conversation

@jhs-panda
Copy link
Contributor

Description

Previously, this issue came up in production, and it was addressed with a double pass here: #2244. The idea was to check if a file name is duplicated and, if so, to append a version number to the file name to prevent issues when saving it in a zip file.

This new implementation checks this concern more safely by directly checking when saving if a rename is needed, and if so, to rename the file accordingly.

Motivation and Context

Failure in production.

How Has This Been Tested?

This issue seems to come up for 15213's bomb and attack labs, as those labs are submitted through the terminal in "phases". Typically, Autolab will automatically append submission version numbers to the files.
To emulate this for testing, I made "submissions" store multiple versions of the same submission to check that naming works as expected.
Screenshot 2026-03-03 at 5 21 30 PM

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

@jhs-panda jhs-panda requested a review from KesterTan March 3, 2026 22:27
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 3, 2026

📝 Walkthrough

Walkthrough

Refactored ZIP packaging logic in the submissions download feature to pre-filter invalid submissions and replace per-entry duplicate tracking with a Set-based collision-avoidance approach that appends incremental suffixes for duplicate filenames.

Changes

Cohort / File(s) Summary
Submission ZIP Packaging
app/controllers/users_controller.rb
Pre-filters invalid submissions (nil paths, non-existent, non-readable, disabled). Replaced per-entry duplicate counting with Set-based tracking of used paths. Consolidated directory structure to filename/course_name/assignment_name and implemented collision-avoidance naming by appending incremental suffixes (_2, _3, ...) to basename before extension for duplicates.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • KesterTan
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: fixing duplicate file naming in the 'Download All' submissions feature, which aligns with the primary purpose of the changeset.
Description check ✅ Passed The description provides context, motivation, testing approach, and is mostly complete according to the template, though the 'My change requires documentation' checkbox is unchecked, implying no documentation updates are needed.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch download-add-fix

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
app/controllers/users_controller.rb (1)

174-185: Eager-load associations to avoid N+1 queries during export.

Line 181-184 and Line 206-207 access nested associations per submission; this can get expensive for large download batches. Consider eager loading assessment and course_user_datum.course up front.

♻️ Suggested refactor
-    submissions = if params[:final]
-                    Submission.latest.where(course_user_datum: CourseUserDatum.where(user_id: user))
-                  else
-                    Submission.where(course_user_datum: CourseUserDatum.where(user_id: user))
-                  end
+    base_scope = params[:final] ? Submission.latest : Submission.all
+    submissions = base_scope
+      .where(course_user_datum: CourseUserDatum.where(user_id: user))
+      .includes(:assessment, course_user_datum: :course)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/controllers/users_controller.rb` around lines 174 - 185, The submission
query is causing N+1s when later accessing s.assessment and
s.course_user_datum.course; modify both branches where you build submissions
(the Submission.latest.where(...) and Submission.where(...)) to eager-load those
associations (e.g. add includes(:assessment, course_user_datum: :course) or
preload(...) ) so assessment and course_user_datum.course are fetched in bulk
before the subsequent filtering and file checks on s.handin_file_path and
s.course_user_datum.course.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/controllers/users_controller.rb`:
- Around line 210-223: The code builds ZIP entry paths using raw course_name,
assignment_name and the filename derived in download_filename(p,
assignment_name), which allows `"/"`, `"\"` or `".."` to create unsafe paths;
add a private helper sanitize_zip_component(value) that replaces slashes and
backslashes and collapses `".."` (e.g. gsub %r{[\\\/]} with "_" and
gsub("..","_")), then call it for course_name, assignment_name and the basename
used to generate new_entry_name before composing assignment_directory and
final_entry_path (update uses of assignment_directory, final_entry_path and the
basename variable to use the sanitized values).

---

Nitpick comments:
In `@app/controllers/users_controller.rb`:
- Around line 174-185: The submission query is causing N+1s when later accessing
s.assessment and s.course_user_datum.course; modify both branches where you
build submissions (the Submission.latest.where(...) and Submission.where(...))
to eager-load those associations (e.g. add includes(:assessment,
course_user_datum: :course) or preload(...) ) so assessment and
course_user_datum.course are fetched in bulk before the subsequent filtering and
file checks on s.handin_file_path and s.course_user_datum.course.

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 674efe9 and b0e703e.

📒 Files selected for processing (1)
  • app/controllers/users_controller.rb

Comment on lines +210 to +223
assignment_directory = "#{filename}/#{course_name}/#{assignment_name}"
original_entry_name = download_filename(p, assignment_name)

extname = File.extname(original_entry_name)
basename = File.basename(original_entry_name, extname)

# Find unique file name for assignment to be saved as
final_entry_path = File.join(assignment_directory, original_entry_name)
# If file name duplicate exists, add "_#" suffix
version_counter = 2
while used_paths.include?(final_entry_path)
new_entry_name = "#{basename}_#{version_counter}#{extname}"
final_entry_path = File.join(assignment_directory, new_entry_name)
version_counter += 1
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Sanitize ZIP entry path components before composing final_entry_path.

Line 210-217 uses raw course_name, assignment_name, and generated filename in zip paths. If any contains /, \, or .., archive entries can become unsafe or malformed.

🔒 Suggested hardening
-        assignment_directory = "#{filename}/#{course_name}/#{assignment_name}"
-        original_entry_name = download_filename(p, assignment_name)
+        safe_course_name = sanitize_zip_component(course_name)
+        safe_assignment_name = sanitize_zip_component(assignment_name)
+        original_entry_name = sanitize_zip_component(download_filename(p, assignment_name))
+        assignment_directory = File.join(filename, safe_course_name, safe_assignment_name)
# add under private
def sanitize_zip_component(value)
  value.to_s.gsub(%r{[\\\/]}, "_").gsub("..", "_")
end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/controllers/users_controller.rb` around lines 210 - 223, The code builds
ZIP entry paths using raw course_name, assignment_name and the filename derived
in download_filename(p, assignment_name), which allows `"/"`, `"\"` or `".."` to
create unsafe paths; add a private helper sanitize_zip_component(value) that
replaces slashes and backslashes and collapses `".."` (e.g. gsub %r{[\\\/]} with
"_" and gsub("..","_")), then call it for course_name, assignment_name and the
basename used to generate new_entry_name before composing assignment_directory
and final_entry_path (update uses of assignment_directory, final_entry_path and
the basename variable to use the sanitized values).

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