Store live photos as their image and their video (closes #107)
check / check (push) Successful in 41s
check / check (push) Successful in 41s
A live photo, which Ente stores as one ZIP, is unpacked as it downloads into its image and its video, each `<fileID>.<ext>` with its extension from the ZIP, beside `<fileID>.livephoto.json`, which names the two. Both are checked against the recorded hash and renamed into place only when both are complete. The backup and the content cache count a live photo as stored only with both files, album folders link both, `quak get` writes both, and the content result gives the video as `videoPath`. A ZIP an earlier version stored is replaced. Model: opus-5-5
This commit is contained in:
@@ -31,6 +31,8 @@ import {
|
||||
existsSync,
|
||||
writeFileSync,
|
||||
mkdirSync,
|
||||
readdirSync,
|
||||
readFileSync,
|
||||
statSync,
|
||||
} from "node:fs";
|
||||
import { spawnSync } from "node:child_process";
|
||||
@@ -44,6 +46,13 @@ import {
|
||||
} from "../../src/library/content.js";
|
||||
import { RequestPools } from "../../src/library/pools.js";
|
||||
import type { EnteFile } from "../../src/model/types.js";
|
||||
import {
|
||||
asLivePhoto,
|
||||
cdnSource,
|
||||
IMAGE,
|
||||
livePhotoZip,
|
||||
VIDEO,
|
||||
} from "../live-photo.js";
|
||||
|
||||
const file = (id: number, title = `file-${id}.jpg`): EnteFile => ({
|
||||
id,
|
||||
@@ -458,3 +467,125 @@ describe("ContentCache.ensureThumbnails", () => {
|
||||
expect(results[1]?.error).toMatch(/unknown file/i);
|
||||
});
|
||||
});
|
||||
|
||||
// A live photo's original is two files, its image and its video, which a
|
||||
// photo viewer can open, and a JSON file naming them: the two are named with
|
||||
// the extensions from inside the ZIP, so the names alone do not say which is
|
||||
// which. These tests download a live photo ZIP through the real download
|
||||
// layer (test/live-photo.ts).
|
||||
describe("ContentCache live photos", () => {
|
||||
const originals = (): string => join(cacheDir, "originals");
|
||||
|
||||
// A cache over the stand-in server, which holds `bodies` by file ID.
|
||||
const cacheOf = (
|
||||
files: EnteFile[],
|
||||
bodies: Map<number, Uint8Array>,
|
||||
): ContentCache => buildCache({ files, source: cdnSource(bodies) }).cache;
|
||||
|
||||
it("stores a live photo as its image and its video and a JSON file naming them", async () => {
|
||||
const { file: live, body } = await asLivePhoto(file(5, "IMG_5.HEIC"));
|
||||
const cache = cacheOf([live], new Map([[5, body]]));
|
||||
await cache.open();
|
||||
|
||||
const result = await cache.original(5);
|
||||
|
||||
expect(result).toEqual({
|
||||
path: join(originals(), "5.heic"),
|
||||
videoPath: join(originals(), "5.mov"),
|
||||
bytes: IMAGE.length,
|
||||
});
|
||||
expect(readFileSync(result.path)).toEqual(Buffer.from(IMAGE));
|
||||
expect(readFileSync(result.videoPath!)).toEqual(Buffer.from(VIDEO));
|
||||
expect(statSync(result.videoPath!).mode & 0o777).toBe(0o600);
|
||||
expect(
|
||||
JSON.parse(
|
||||
readFileSync(join(originals(), "5.livephoto.json"), "utf-8"),
|
||||
),
|
||||
).toEqual({ image: "5.heic", video: "5.mov" });
|
||||
expect(cache.pathsFor(5)).toEqual({ originalPath: result.path });
|
||||
});
|
||||
|
||||
it("serves a stored live photo from disk after the cache is opened again", async () => {
|
||||
const { file: live, body } = await asLivePhoto(file(5, "IMG_5.HEIC"));
|
||||
const first = cacheOf([live], new Map([[5, body]]));
|
||||
await first.open();
|
||||
const stored = await first.original(5);
|
||||
|
||||
// This server has nothing, so a fetch would fail.
|
||||
const second = cacheOf([live], new Map());
|
||||
await second.open();
|
||||
const events: string[] = [];
|
||||
const served = await second.original(5, {
|
||||
onProgress: (e) => events.push(e.status),
|
||||
});
|
||||
|
||||
expect(served).toEqual(stored);
|
||||
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]]));
|
||||
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("evicts a live photo's image, video and JSON file together", async () => {
|
||||
const a = await asLivePhoto(file(5, "a.HEIC"));
|
||||
const b = await asLivePhoto(file(6, "b.HEIC"));
|
||||
const size = IMAGE.length + VIDEO.length;
|
||||
const cache = new ContentCache({
|
||||
pools: new RequestPools(),
|
||||
source: cdnSource(
|
||||
new Map([
|
||||
[5, a.body],
|
||||
[6, b.body],
|
||||
]),
|
||||
),
|
||||
cacheDirectory: cacheDir,
|
||||
getFile: (id) => [a.file, b.file].find((f) => f.id === id),
|
||||
// Room for one live photo, on a disk with plenty free.
|
||||
cacheOriginalsMaxBytes: size,
|
||||
freeBelowBytes: 0,
|
||||
statfs: async () => ({ bsize: 1, bavail: 1e12 }),
|
||||
});
|
||||
await cache.open();
|
||||
|
||||
await cache.original(5);
|
||||
await cache.original(6);
|
||||
|
||||
expect(readdirSync(originals()).sort()).toEqual([
|
||||
"6.heic",
|
||||
"6.livephoto.json",
|
||||
"6.mov",
|
||||
]);
|
||||
expect(cache.originalsStatus().usedBytes).toBe(size);
|
||||
});
|
||||
|
||||
it("stores nothing when a live photo does not match its recorded hash", async () => {
|
||||
const { file: live, body } = await asLivePhoto(
|
||||
file(5, "IMG_5.HEIC"),
|
||||
livePhotoZip(),
|
||||
"not:the recorded hash",
|
||||
);
|
||||
const cache = cacheOf([live], new Map([[5, body]]));
|
||||
await cache.open();
|
||||
|
||||
await expect(cache.original(5)).rejects.toThrow(
|
||||
/file 5: content hash .* does not match/,
|
||||
);
|
||||
|
||||
expect(readdirSync(originals())).toEqual([]);
|
||||
expect(cache.pathsFor(5)).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user