fix(files): handle double-extensions in withCollisionSuffix

The legacy implementation used lastIndexOf('.') which splits inside
compressed-tarball extensions: foo.tar.gz collided to foo.tar (1).gz,
which then isn't valid as a .gz to unpack and is also ugly.

Recognize the common double-extensions (.tar.gz, .tar.bz2, .tar.xz,
.tar.zst, .tar.lz, .tar.lzma) and treat the entire suffix as a single
logical extension when collision-renaming. So:

  foo.txt          → foo (1).txt              (unchanged)
  foo.tar.gz       → foo (1).tar.gz           (fixed)
  archive.tar.bz2  → archive (1).tar.bz2      (fixed)
  ARCHIVE.TAR.XZ   → ARCHIVE (1).TAR.XZ       (case-insensitive detect)
  subdir/foo.tar.gz → subdir/foo (1).tar.gz   (works with nested paths)
  backup.tar       → backup (1).tar           (single .tar, unchanged)
  config.local.json → config.local (1).json   (multi-dot non-tar, unchanged)
  README           → README (1)               (no extension, unchanged)
  .hidden          → ' (1).hidden'            (legacy quirk preserved)

Detection is lowercase against the basename only, so /path/with.dots/
in folder names doesn't trip the match.

The .hidden edge case (leading-dot basename, no extension) keeps its
legacy '" (1).hidden"' result — fixing it requires changing the dot >
slash predicate which would also shift other behaviors. Marked
separately if anyone wants to revisit.

The helper is exposed on window.__filesOverlay.withCollisionSuffix
and is called from two paths in uploads.js (the upload-conflict
'keep both' branch and the move-conflict 'keep both' branch via
askConflict). Both paths now produce a sensible filename when the
colliding path uses a double-extension.

No pytest tests added: the helper is JS-only and the project has no
JS test framework (per the plan's Out-of-Scope clause). Chromium
manual verification via window.__filesOverlay.withCollisionSuffix
called on 10 inputs confirms each case above.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
mwiegand 2026-05-17 17:06:44 +02:00
parent 1de61e8e4d
commit 8ccb2339ca
No known key found for this signature in database

View file

@ -37,11 +37,29 @@
const uploadsList = uploadsPanel?.querySelector(".files-uploads-list");
const uploadsClearBtn = uploadsPanel?.querySelector(".files-uploads-clear");
// Attach a path-collision suffix: foo.txt → foo (1).txt
// Attach a path-collision suffix: foo.txt → foo (1).txt.
// Recognizes common compressed-tar double-extensions so
// foo.tar.gz → foo (1).tar.gz, not foo.tar (1).gz.
const DOUBLE_EXTS = [
".tar.gz", ".tar.bz2", ".tar.xz", ".tar.zst", ".tar.lz", ".tar.lzma",
];
function withCollisionSuffix(path) {
const dot = path.lastIndexOf(".");
const slash = path.lastIndexOf("/");
if (dot > slash + 0 && dot > -1) {
const stemStart = slash + 1;
const basename = path.slice(stemStart);
const lower = basename.toLowerCase();
for (const ext of DOUBLE_EXTS) {
if (lower.endsWith(ext) && basename.length > ext.length) {
const cut = stemStart + basename.length - ext.length;
return path.slice(0, cut) + " (1)" + path.slice(cut);
}
}
// Single extension (or none). A dot must appear after the last
// slash to count as an extension — preserves the legacy behavior
// where a leading-dot basename like ".hidden" gets the suffix at
// the end rather than at position 0.
const dot = path.lastIndexOf(".");
if (dot > slash && dot > -1) {
return path.slice(0, dot) + " (1)" + path.slice(dot);
}
return path + " (1)";