Compare commits

..
1 Commits
Author SHA1 Message Date
sneak 3cabeba249 An album whose folder cannot be made no longer stops the backup (closes #197)
check / check (push) Waiting to run
Each album's folder, links and JSON are written inside one try. A failure
prints `FAILED album <name>: <reason>`, lists the album's ID in the new
`failedAlbums` of the result and of `--json`, and makes `quak backup` exit 1;
the other albums and `failures.json` are still written.

An album whose folder would have another album's JSON name, ignoring case,
gets its ID. A name over 255 bytes (250 for a folder, which leaves room for
`.json`) gets the ID and is cut short between two characters by
`shortenToBytes` in `src/filename.ts`. A link's ID goes right before its
extension.

Apart from its call to `uniqueNames`, the album loop is only indented into
the try.

Model: opus-5-5
2026-10-16 02:47:00 +00:00
3 changed files with 25 additions and 14 deletions
+6 -5
View File
@@ -815,11 +815,12 @@ directory would have the name of another collection's JSON, again ignoring case,
gets its ID added as well: beside an album `x`, whose JSON is `x.json`, an album
named `x.json` gets the directory `x.json (12)/`. A name too long for a file
name, which most file systems limit to 255 bytes, gets its ID added too. A name
with its ID added is cut short between two characters, just far enough that it
fits: first the part before the ID, and once nothing is left of that, a
symlink's extension. A collection's directory name also leaves room for the
`.json` its JSON adds. IDs never change, so a name stays the same from run to
run until such a clash appears or goes away.
with its ID added is cut short between two characters in the part before the ID,
just far enough that it fits. When a symlink's extension alone leaves no room
for the ID, the extension is cut short and the part before the ID keeps only the
bytes that cut leaves over. A collection's directory name also leaves room for
the `.json` its JSON adds. IDs never change, so a name stays the same from run
to run until such a clash appears or goes away.
A collection whose directory or JSON cannot be written, such as one whose
directory name is taken by a file you put in `collections/`, does not stop the
+6 -4
View File
@@ -276,10 +276,12 @@ const rebuildSymlink = (linkPath: string, target: string): void => {
// case (two names that differ only in case are one entry on a case-insensitive
// file system), or it is too long for a file name; then it gets ` (<id>)`,
// right before the extension when `beforeExtension` is set. To fit, the part
// before the ID is cut short as far as it must be, and once nothing is left of
// it, so is the extension. With `withJSON`, each entry also has the JSON file
// `<name>.json` beside it, as an album does: a name then leaves room for
// `.json`, and one that is another entry's JSON name gets its ID added too.
// before the ID is cut short as far as it must be. When the extension alone
// leaves no room for the ID, the extension is cut short and the part before
// the ID keeps only the bytes that cut leaves over. With `withJSON`, each
// entry also has the JSON file `<name>.json` beside it, as an album does: a
// name then leaves room for `.json`, and one that is another entry's JSON name
// gets its ID added too.
// Two names with different IDs added never match, but one can match another
// entry's own name (`IMG (6).JPG`), so this repeats until no name is shared.
// IDs are stable, so the names are too.
+13 -5
View File
@@ -1737,27 +1737,35 @@ describe("backup album folders", () => {
expect(readLedger(outDir).files["5"]).toMatchObject({ attempts: 1 });
});
// The extension of this title is 303 bytes, so it leaves no room for the
// ID: nothing is left of the name before the ID, and the extension is cut
// short after it. The extension holds a "-", so the original is saved
// with `.bin`, a name that fits.
// The extensions of these titles leave no room for the ID, so each is cut
// short and the name before the ID keeps only the bytes that cut leaves
// over. File 1's extension is 303 bytes and is cut to 251, which leaves
// none. File 2's is 271 bytes and is cut to 250, since "€" takes three
// bytes, which leaves one, for the "a". Neither extension is only letters
// and digits, so both originals are saved with `.bin`, a name that fits.
it("cuts the extension of a title short when it leaves no room for the ID", async () => {
const outDir = join(root, "backup");
const lib = libraryOf([
{
collection: collection(10, "Trip"),
files: [file(1, 10, `a.${"L".repeat(300)}-x`)],
files: [
file(1, 10, `a.${"L".repeat(300)}-x`),
file(2, 10, `a.${"€".repeat(90)}`),
],
},
]);
const result = await runBackup(lib, { downloadDirectory: outDir });
const link = ` (1).${"L".repeat(250)}`;
const link2 = `a (2).${"€".repeat(83)}`;
expect(Buffer.byteLength(link)).toBe(255);
expect(Buffer.byteLength(link2)).toBe(255);
expect(result.failed).toBe(0);
expect(tree(outDir)).toEqual([
"Trip/",
`Trip/${link} -> ${linkTo("1.bin")}`,
`Trip/${link2} -> ${linkTo("2.bin")}`,
"Trip.json",
]);
});