A live photo's default names fit, and no cut name starts with a dot
check / check (push) Canceled after 0s

`defaultFileName` in `src/filename.ts` now gives the name a file is saved
under when the caller gives no path, and `originalName`, `thumbnailName`,
`downloadFile` and `downloadThumbnail` all call it. For a live photo's image
and video it puts the part's own extension in before cutting the name short,
so those names fit too; a path from `--out` or `outPath` is kept as given.
`shortenKeepingExtension` keeps the first character before the extension,
cutting a long extension short to leave room for it, so a cut name no longer
starts with a dot. The backup's links for such a title change to match.
TODO.md names the open bug and describes what the code does.

Model: opus-5-5
This commit is contained in:
2026-10-18 17:05:49 +00:00
parent fd40714a6d
commit e03e99b486
11 changed files with 284 additions and 107 deletions
+47
View File
@@ -1923,6 +1923,53 @@ describe("downloadFile live photos", () => {
]);
});
// A file system refuses a file name over 255 bytes. Each name is cut
// short once its own extension is in it.
const imageName = `${"t".repeat(250)}.heic`;
const videoName = `${"t".repeat(251)}.mov`;
// The title fits, but ".heic" is one byte longer than ".jpg".
it("cuts the image's name short for a 255-byte title ending in .jpg when no outPath is given", async () => {
const title = `${"t".repeat(251)}.jpg`;
const t = setup(livePhotoZip(), { ...livePhoto, title });
const result = await inDirectory(t.dir, () =>
downloadFile(t.api, t.file),
);
expect(Buffer.byteLength(title)).toBe(255);
expect(Buffer.byteLength(imageName)).toBe(255);
expect(Buffer.byteLength(videoName)).toBe(255);
expect(result.path).toBe(imageName);
expect(result.videoPath).toBe(videoName);
expect(readdirSync(t.dir).sort()).toEqual([imageName, videoName]);
});
it("cuts the names of the image and the video short for a 300-byte title with no extension when no outPath is given", async () => {
const t = setup(livePhotoZip(), {
...livePhoto,
title: "t".repeat(300),
});
const result = await inDirectory(t.dir, () =>
downloadFile(t.api, t.file),
);
expect(result.path).toBe(imageName);
expect(result.videoPath).toBe(videoName);
expect(readdirSync(t.dir).sort()).toEqual([imageName, videoName]);
});
// The caller's path is used as is; only the extension is each part's own.
it("keeps an outPath too long for a file name as given", async () => {
const t = setup(livePhotoZip(), livePhoto);
const outPath = join(t.dir, `${"t".repeat(251)}.jpg`);
await expect(downloadFile(t.api, t.file, outPath)).rejects.toThrow(
/ENAMETOOLONG/,
);
});
it("gives each part its own extension from the ZIP, letters and digits only", async () => {
const zip = livePhotoZip({ "image.JPG": IMAGE, "video.m-4v": VIDEO });
const t = setup(zip, livePhoto);