Remove the content cache's handling of an earlier version's live-photo ZIP (closes #151)
check / check (push) Failing after 55s
check / check (push) Failing after 55s
quak keeps no handling of old data. The content cache no longer checks whether a live photo's file with no JSON file naming it is a ZIP an earlier version cached, and no longer removes it; such a file is left alone, as any other one already was. The README sentences and the tests about that ZIP are gone, and one download test no longer names it in its title. How a live photo is stored and served today is unchanged. Model: opus-5-5
This commit is contained in:
@@ -879,10 +879,7 @@ current account's records name.
|
|||||||
|
|
||||||
A live photo's original is cached as at its save path: its image and its video,
|
A live photo's original is cached as at its save path: its image and its video,
|
||||||
each `originals/<fileID>.<ext>` with its own extension, and
|
each `originals/<fileID>.<ext>` with its own extension, and
|
||||||
`originals/<fileID>.livephoto.json` naming them; the two are evicted together. A
|
`originals/<fileID>.livephoto.json` naming them; the two are evicted together.
|
||||||
live photo that an earlier version cached as its ZIP is not served: the library
|
|
||||||
removes the ZIP when it opens the cache, and fetches the two files when the
|
|
||||||
photo is next read or precached.
|
|
||||||
|
|
||||||
A stored file appears only via an atomic temp-then-rename, so its presence means
|
A stored file appears only via an atomic temp-then-rename, so its presence means
|
||||||
it is complete. Every downloaded original (by `quak get`, the cache, or
|
it is complete. Every downloaded original (by `quak get`, the cache, or
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ declares one.
|
|||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
|
- 2026-10-01: The content cache no longer looks for a live photo that an earlier
|
||||||
|
version cached as one ZIP (issue 151). When the cache opens, a live photo's
|
||||||
|
file that no JSON file names is now always left alone.
|
||||||
|
|
||||||
- 2026-10-01: `examples/download-albums.ts` logs in, opens the library, and for
|
- 2026-10-01: `examples/download-albums.ts` logs in, opens the library, and for
|
||||||
every album downloads each photo to its save path, writes the photo's record
|
every album downloads each photo to its save path, writes the photo's record
|
||||||
and EXIF fields to a JSON file beside it, and writes the album's photos to
|
and EXIF fields to a JSON file beside it, and writes the album's photos to
|
||||||
|
|||||||
+5
-38
@@ -29,14 +29,7 @@
|
|||||||
// the cache does not count as saved there, but is copied there rather than
|
// the cache does not count as saved there, but is copied there rather than
|
||||||
// fetched again.
|
// fetched again.
|
||||||
|
|
||||||
import {
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
||||||
closeSync,
|
|
||||||
existsSync,
|
|
||||||
openSync,
|
|
||||||
readFileSync,
|
|
||||||
readSync,
|
|
||||||
statSync,
|
|
||||||
} from "node:fs";
|
|
||||||
import {
|
import {
|
||||||
chmod,
|
chmod,
|
||||||
copyFile,
|
copyFile,
|
||||||
@@ -281,25 +274,6 @@ const fileSize = (path: string): number | undefined => {
|
|||||||
const hasContent = (path: string | undefined): boolean =>
|
const hasContent = (path: string | undefined): boolean =>
|
||||||
path !== undefined && (fileSize(path) ?? 0) > 0;
|
path !== undefined && (fileSize(path) ?? 0) > 0;
|
||||||
|
|
||||||
// Whether the file at `path` begins as a ZIP does, with `PK\x03\x04`. False
|
|
||||||
// when it cannot be read.
|
|
||||||
const isZip = (path: string): boolean => {
|
|
||||||
try {
|
|
||||||
const fd = openSync(path, "r");
|
|
||||||
try {
|
|
||||||
const head = Buffer.alloc(4);
|
|
||||||
return (
|
|
||||||
readSync(fd, head, 0, 4, 0) === 4 &&
|
|
||||||
head.toString("latin1") === "PK\x03\x04"
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
closeSync(fd);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// A live photo's image and video are named with the extensions from inside its
|
// A live photo's image and video are named with the extensions from inside its
|
||||||
// ZIP, so their names alone do not say which is which. Wherever the cache or a
|
// ZIP, so their names alone do not say which is which. Wherever the cache or a
|
||||||
// save path stores one, a JSON file of this name beside them names both.
|
// save path stores one, a JSON file of this name beside them names both.
|
||||||
@@ -713,8 +687,7 @@ export class ContentCache implements PhotoContent, ThumbnailsAPI {
|
|||||||
await this.touch(cached.path);
|
await this.touch(cached.path);
|
||||||
return { ...cached, bytes: size, cached: true };
|
return { ...cached, bytes: size, cached: true };
|
||||||
}
|
}
|
||||||
// A recorded file that has since gone, or a live photo an earlier
|
// A recorded file that has since gone re-fetches below.
|
||||||
// version stored as one ZIP, re-fetches below.
|
|
||||||
known.delete(fileID);
|
known.delete(fileID);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -983,20 +956,14 @@ export class ContentCache implements PhotoContent, ThumbnailsAPI {
|
|||||||
if (id === undefined || !existsSync(path)) continue;
|
if (id === undefined || !existsSync(path)) continue;
|
||||||
// A live photo's image and video are one entry, as the JSON file
|
// A live photo's image and video are one entry, as the JSON file
|
||||||
// beside them names them. A live photo's file with no such JSON
|
// beside them names them. A live photo's file with no such JSON
|
||||||
// file is not its original. If it is a ZIP, it is the one an
|
// file is not its original and is left alone: another process may
|
||||||
// earlier version stored under the image's name, and is removed.
|
// have just stored it and not yet written the JSON file.
|
||||||
// Any other is left alone: another process may have just stored
|
|
||||||
// it and not yet written the JSON file.
|
|
||||||
const livePhoto = names.has(livePhotoJSONName(String(id)))
|
const livePhoto = names.has(livePhotoJSONName(String(id)))
|
||||||
? readLivePhotoJSON(dir, String(id))
|
? readLivePhotoJSON(dir, String(id))
|
||||||
: undefined;
|
: undefined;
|
||||||
if (livePhoto !== undefined) {
|
if (livePhoto !== undefined) {
|
||||||
into.set(id, livePhoto);
|
into.set(id, livePhoto);
|
||||||
} else if (isLivePhoto(id)) {
|
} else if (!isLivePhoto(id)) {
|
||||||
if (isZip(path)) {
|
|
||||||
await rm(path, { force: true }).catch(() => undefined);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
into.set(id, { path });
|
into.set(id, { path });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1883,7 +1883,7 @@ describe("downloadFile live photos", () => {
|
|||||||
expect(readdirSync(t.dir).sort()).toEqual(["f.JPG", "f.bin"]);
|
expect(readdirSync(t.dir).sort()).toEqual(["f.JPG", "f.bin"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("replaces what was at the destination, such as an earlier ZIP of the two", async () => {
|
it("replaces what was at the destination", async () => {
|
||||||
const t = setup(livePhotoZip(), livePhoto);
|
const t = setup(livePhotoZip(), livePhoto);
|
||||||
writeFileSync(t.outPath, livePhotoZip());
|
writeFileSync(t.outPath, livePhotoZip());
|
||||||
|
|
||||||
|
|||||||
@@ -176,52 +176,6 @@ describe("Library content wiring", () => {
|
|||||||
await lib.close();
|
await lib.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("removes a live photo's ZIP an earlier version cached when it opens, and precaches its image and video", async () => {
|
|
||||||
const { file: live, body } = await asLivePhoto(file(1, 1));
|
|
||||||
class LiveClient extends MockClient {
|
|
||||||
override async filesSince(): Promise<FilesPage> {
|
|
||||||
return { files: [live], deleted: [], cursor: 1 };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const originals = join(root, "cache", "originals");
|
|
||||||
const open = (opts: Partial<LibraryOptions>): Promise<Library> =>
|
|
||||||
Library.open({
|
|
||||||
client: new LiveClient(),
|
|
||||||
cacheDirectory: join(root, "cache"),
|
|
||||||
contentSource: cdnSource(new Map([[1, body]])),
|
|
||||||
refreshIntervalSeconds: 3600,
|
|
||||||
precacheThumbnails: false,
|
|
||||||
precacheOriginals: false,
|
|
||||||
...opts,
|
|
||||||
});
|
|
||||||
// A first run records the library, so the next one knows that file 1
|
|
||||||
// is a live photo when it opens the cache.
|
|
||||||
await (await open({})).close();
|
|
||||||
writeFileSync(join(originals, "1.jpg"), livePhotoZip());
|
|
||||||
|
|
||||||
let precached!: () => void;
|
|
||||||
const done = new Promise<void>((r) => (precached = r));
|
|
||||||
const lib = await open({
|
|
||||||
precacheOriginals: true,
|
|
||||||
onProgress: (e) => {
|
|
||||||
if (e.operation === "precacheOriginals" && e.status === "done")
|
|
||||||
precached();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
expect(existsSync(join(originals, "1.jpg"))).toBe(false);
|
|
||||||
await done;
|
|
||||||
|
|
||||||
expect(readdirSync(originals).sort()).toEqual([
|
|
||||||
"1.heic",
|
|
||||||
"1.livephoto.json",
|
|
||||||
"1.mov",
|
|
||||||
]);
|
|
||||||
expect(lib.photos.byID({ fileID: 1 })!.record().originalPath).toBe(
|
|
||||||
join(originals, "1.heic"),
|
|
||||||
);
|
|
||||||
await lib.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("throws from content methods when opened without a content source", async () => {
|
it("throws from content methods when opened without a content source", async () => {
|
||||||
const lib = await Library.open({
|
const lib = await Library.open({
|
||||||
client: new MockClient(),
|
client: new MockClient(),
|
||||||
|
|||||||
@@ -566,43 +566,6 @@ describe("ContentCache live photos", () => {
|
|||||||
expect(events).toEqual(["skipped"]);
|
expect(events).toEqual(["skipped"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("replaces a live photo an earlier version stored as a ZIP under the image's name", async () => {
|
|
||||||
const { file: live, body } = await asLivePhoto(file(5, "IMG_5.HEIC"));
|
|
||||||
mkdirSync(originals(), { recursive: true });
|
|
||||||
writeFileSync(join(originals(), "5.HEIC"), livePhotoZip());
|
|
||||||
const cache = cacheOf([live], new Map([[5, body]]));
|
|
||||||
// Opened without being told that file 5 is a live photo, the cache
|
|
||||||
// records the ZIP, and does not serve it.
|
|
||||||
await cache.open();
|
|
||||||
|
|
||||||
const result = await cache.original(5);
|
|
||||||
|
|
||||||
expect(result.videoPath).toBe(join(originals(), "5.mov"));
|
|
||||||
expect(readdirSync(originals()).sort()).toEqual([
|
|
||||||
"5.heic",
|
|
||||||
"5.livephoto.json",
|
|
||||||
"5.mov",
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("removes a live photo's ZIP an earlier version stored when it opens, so the precache fetches the image and video", async () => {
|
|
||||||
const { file: live, body } = await asLivePhoto(file(5, "IMG_5.HEIC"));
|
|
||||||
mkdirSync(originals(), { recursive: true });
|
|
||||||
writeFileSync(join(originals(), "5.HEIC"), livePhotoZip());
|
|
||||||
const cache = cacheOf([live], new Map([[5, body]]));
|
|
||||||
|
|
||||||
await cache.open((fileID) => fileID === 5);
|
|
||||||
|
|
||||||
expect(readdirSync(originals())).toEqual([]);
|
|
||||||
expect(cache.pathsFor(5)).toEqual({});
|
|
||||||
const [fetched] = await cache.ensureOriginals({ fileIDs: [5] });
|
|
||||||
expect(fetched).toEqual({
|
|
||||||
fileID: 5,
|
|
||||||
path: join(originals(), "5.heic"),
|
|
||||||
});
|
|
||||||
expect(cache.pathsFor(5)).toEqual({ originalPath: fetched!.path });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("leaves the image and video another process has just stored when it opens before their JSON file is written", async () => {
|
it("leaves the image and video another process has just stored when it opens before their JSON file is written", async () => {
|
||||||
const { file: live, body } = await asLivePhoto(file(5, "IMG_5.HEIC"));
|
const { file: live, body } = await asLivePhoto(file(5, "IMG_5.HEIC"));
|
||||||
const server = cdnSource(new Map([[5, body]]));
|
const server = cdnSource(new Map([[5, body]]));
|
||||||
|
|||||||
Reference in New Issue
Block a user