A library row with archive.filename "Cube (1).gcode.3mf.gcode.3mf" uploaded
to /Cube_(1).gcode.3mf.3mf (single-iteration strip + append). Post-print
cleanup looked at /Cube_(1).3mf and /Cube_(1).gcode (subtask_name + ext),
missed the on-card file, and A1 firmware re-ran it on next power-on.
- New derive_remote_filename() helper in backend/app/utils/filename.py:
iterative strip of .gcode.3mf/.3mf suffixes, append single .3mf,
space->underscore. isinstance() guard raises TypeError on non-str
input rather than entering the strip loop with a duck-typed
object that returns truthy sentinels from endswith.
- Three previously-duplicated upload sites (background_dispatch reprint +
library, print_scheduler queue) now share the helper.
- SD cleanup fetches archive.filename and tries the derived path first,
with the legacy subtask_name + ext paths kept as fallbacks.
- 10 new unit tests pin the reproducer + edge cases (doubled .gcode.3mf,
doubled .3mf, raw .gcode preserved, idempotence, Unicode, plus the
type guard against MagicMock / None / int inputs).
Bambu printer SD cards are FAT32/exFAT, which forbids < > : " / \ | ? *
plus control chars and trailing dots/spaces. Library rename only blocked
path separators, so a name like L|R.3mf was accepted and only failed
later at FTP upload with 553 Could not create file - far from the rename
action that caused it. Bambu Studio refuses these names in its save
dialog; Bambuddy now does the same.
New backend/app/utils/filename.py centralises validation. Wired into
update_file, upload_file, print_library_file, and queue add. Existing
rows with bad names are left alone (no silent rewrite of user data);
users get an actionable 400 pointing at rename.
Frontend rename modal mirrors the same set client-side with inline error.
New fileManager.invalidFilenameChar i18n key translated across all 9
locales. 26 new tests in test_filename_validation.py.