Save originals at photos/YYYY/YYYY-MM/YYYY-MM-DD/YYYY-MM-DD.fileID.ext; add photo.download() (closes #143)
Each original's save path is now YYYY/YYYY-MM/YYYY-MM-DD/YYYY-MM-DD.<fileID><ext> under the library's download directory, dated by takenAt in the machine's time zone. The directory defaults to photos/ in the working directory, resolved once at open. savePath is always a string, with or without a content cache, and isLocal is true only when the original is there. photo.download() puts the original at its save path: copied from the cache when the cache holds it, fetched straight there otherwise. lib.backup() uses the same code for each file, writes each file's JSON beside it, and links collections/ to the save paths. The backup has no originals/ folder. Model: opus-5-5
This commit is contained in:
@@ -7,8 +7,8 @@
|
||||
* 1. **Fetch once, then serve from disk.** The first `original`/`thumbnail`
|
||||
* fetches through the request pool and stores the bytes; the next finds the
|
||||
* file present and returns its path with a single `skipped` event and no
|
||||
* network. A file already sitting in the backup `downloadDirectory` counts
|
||||
* as present too.
|
||||
* network. A file already stored at its save path under the
|
||||
* `downloadDirectory` counts as present too.
|
||||
* 2. **Present-means-complete.** Content appears only by the streaming atomic
|
||||
* writer's rename, so a file that exists is whole. The directory listing
|
||||
* taken at `open()` is the record of what is cached, and the orphan temp
|
||||
@@ -41,6 +41,7 @@ import { join } from "node:path";
|
||||
|
||||
import {
|
||||
ContentCache,
|
||||
savePath,
|
||||
type ContentSource,
|
||||
type EnsureEvent,
|
||||
} from "../../src/library/content.js";
|
||||
@@ -152,12 +153,48 @@ const buildCache = (
|
||||
pools: args.pools ?? new RequestPools(),
|
||||
source,
|
||||
cacheDirectory: cacheDir,
|
||||
downloadDirectory: args.downloadDirectory,
|
||||
downloadDirectory: args.downloadDirectory ?? join(root, "photos"),
|
||||
getFile: (id) => byID.get(id),
|
||||
});
|
||||
return { cache, source };
|
||||
};
|
||||
|
||||
// Microseconds, as Ente stores times, for noon local time on a day, so the
|
||||
// machine's time zone cannot move the photo to another day.
|
||||
const noon = (year: number, month: number, day: number): number =>
|
||||
new Date(year, month - 1, day, 12).getTime() * 1000;
|
||||
|
||||
describe("savePath", () => {
|
||||
it("files an original by year, month and day under the root", () => {
|
||||
const f = file(12345, "IMG_0001.HEIC");
|
||||
f.metadata.creationTime = noon(2026, 3, 1);
|
||||
|
||||
expect(savePath("/photos", f)).toBe(
|
||||
"/photos/2026/2026-03/2026-03-01/2026-03-01.12345.HEIC",
|
||||
);
|
||||
});
|
||||
|
||||
it("dates an original by the date the user set, when there is one", () => {
|
||||
const f = file(7, "a.jpg");
|
||||
f.metadata.creationTime = noon(2026, 3, 1);
|
||||
f.pubMagicMetadata = { editedTime: noon(1999, 12, 31) };
|
||||
|
||||
expect(savePath("/photos", f)).toBe(
|
||||
"/photos/1999/1999-12/1999-12-31/1999-12-31.7.jpg",
|
||||
);
|
||||
});
|
||||
|
||||
it("takes the extension from the title it was uploaded with, not a new name", () => {
|
||||
const f = file(8, "upload");
|
||||
f.metadata.creationTime = noon(2026, 3, 1);
|
||||
f.pubMagicMetadata = { editedName: "renamed.png" };
|
||||
|
||||
expect(savePath("/photos", f)).toBe(
|
||||
"/photos/2026/2026-03/2026-03-01/2026-03-01.8.bin",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ContentCache.open", () => {
|
||||
it("creates the cache directories with 0700 permissions", async () => {
|
||||
const { cache } = buildCache();
|
||||
@@ -274,11 +311,17 @@ describe("ContentCache.original / thumbnail", () => {
|
||||
|
||||
it("serves a file already present in the download directory without fetching", async () => {
|
||||
const downloadDirectory = join(root, "backup");
|
||||
mkdirSync(join(downloadDirectory, "originals"), { recursive: true });
|
||||
const backupPath = join(downloadDirectory, "originals", "1.jpg");
|
||||
const day = join(downloadDirectory, "2026", "2026-03", "2026-03-01");
|
||||
mkdirSync(day, { recursive: true });
|
||||
const backupPath = join(day, "2026-03-01.1.jpg");
|
||||
writeFileSync(backupPath, "from-backup");
|
||||
const f = file(1);
|
||||
f.metadata.creationTime = noon(2026, 3, 1);
|
||||
|
||||
const { cache, source } = buildCache({ downloadDirectory });
|
||||
const { cache, source } = buildCache({
|
||||
downloadDirectory,
|
||||
files: [f],
|
||||
});
|
||||
await cache.open();
|
||||
|
||||
const events: EnsureEvent["status"][] = [];
|
||||
@@ -649,6 +692,7 @@ describe("ContentCache live photos", () => {
|
||||
]),
|
||||
),
|
||||
cacheDirectory: cacheDir,
|
||||
downloadDirectory: join(root, "photos"),
|
||||
getFile: (id) => [a.file, b.file].find((f) => f.id === id),
|
||||
// Room for one live photo, on a disk with plenty free.
|
||||
cacheOriginalsMaxBytes: size,
|
||||
|
||||
Reference in New Issue
Block a user