library: the reading is copied into a folder of the reader's own, so a cleared profile gets it back without a frame being read twice
This commit is contained in:
@@ -572,7 +572,17 @@ export async function scanFolder(
|
||||
// exactly as the catalogue already holds it. This is what makes a second scan
|
||||
// of the same folder cheap, and what finishes a roll whose first scan was cut
|
||||
// short — the frames it got through are skipped, the rest are read.
|
||||
if (seen && seen.size === file.size && seen.mtime === file.lastModified && seen.thumb) return;
|
||||
if (seen && seen.size === file.size && seen.mtime === file.lastModified && seen.thumb) {
|
||||
// A row restored from a backup has no handle: a handle is not a thing that
|
||||
// can be written to a file, so the row comes back with everything but the
|
||||
// one thing that opens the frame, and the walk is where it gets it back.
|
||||
// The row is put again — the bytes of the frame are still never read.
|
||||
if (!seen.handle) {
|
||||
batch.push({ ...seen, handle });
|
||||
if (batch.length >= BATCH) await flush();
|
||||
}
|
||||
return;
|
||||
}
|
||||
// A RAW is read whole because that is the only way LibRaw can seek to the
|
||||
// preview the camera wrote inside it; a JPEG is handed to the decoder as it
|
||||
// is, and only its header is read for the date. Both tiles come off what the
|
||||
@@ -966,3 +976,83 @@ export async function listEditedIds(): Promise<Set<string>> {
|
||||
return new Set();
|
||||
}
|
||||
}
|
||||
|
||||
export async function listEdits(): Promise<LibraryEdit[]> {
|
||||
try {
|
||||
return await ask<LibraryEdit[]>(EDITS, 'readonly', (s) => s.getAll());
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// --- restore ---------------------------------------------------------------
|
||||
|
||||
// The catalogue as it travels: the rows without the two things that cannot be
|
||||
// written to a file — a handle, which only the visitor's picker can hand out, and
|
||||
// the tile itself, which is a blob and would be a second copy of the backup
|
||||
// folder. Everything else is here, `mtime` and `size` and `star` included, which
|
||||
// is what lets the walk that follows match every frame and read none of them.
|
||||
export type CatalogueRow = Omit<LibraryPhoto, 'handle' | 'thumb'>;
|
||||
|
||||
// The rows a set of ids stands for, gaps left as gaps — one transaction for the
|
||||
// lot, where `getPhoto` would be one per frame.
|
||||
async function rowsAt(ids: string[]): Promise<(LibraryPhoto | undefined)[]> {
|
||||
const db = await openDb();
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(PHOTOS, 'readonly');
|
||||
const store = tx.objectStore(PHOTOS);
|
||||
const out: (LibraryPhoto | undefined)[] = [];
|
||||
ids.forEach((id, i) => {
|
||||
const req = store.get(id);
|
||||
req.onsuccess = () => {
|
||||
out[i] = req.result as LibraryPhoto | undefined;
|
||||
};
|
||||
});
|
||||
tx.oncomplete = () => resolve(out);
|
||||
tx.onerror = () => reject(tx.error);
|
||||
});
|
||||
}
|
||||
|
||||
// Put a catalogue back. A frame the catalogue already holds and can open — one
|
||||
// with a handle — is left exactly as it is: the backup's copy of it would come
|
||||
// back without one, which is a working catalogue traded for a worse one. Every
|
||||
// other row goes in as it came out. `thumb` fetches the tile that was backed up
|
||||
// beside the row, and a tile that will not read leaves the row without one: the
|
||||
// grid paints a blank cell until the next scan reads the frame.
|
||||
export async function restoreCatalogue(
|
||||
photos: CatalogueRow[],
|
||||
edits: LibraryEdit[],
|
||||
thumb?: (id: string) => Promise<Blob | null>
|
||||
): Promise<number> {
|
||||
const db = await openDb();
|
||||
const size = 200;
|
||||
let put = 0;
|
||||
for (let at = 0; at < photos.length; at += size) {
|
||||
const slice = photos.slice(at, at + size);
|
||||
const had = await rowsAt(slice.map((r) => r.id));
|
||||
const want = slice.filter((_, i) => !had[i]?.handle);
|
||||
if (!want.length) continue;
|
||||
const rows = await Promise.all(
|
||||
want.map(async (row) => ({ ...row, thumb: thumb ? await thumb(row.id) : null }) as unknown as LibraryPhoto)
|
||||
);
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const tx = db.transaction(PHOTOS, 'readwrite');
|
||||
const store = tx.objectStore(PHOTOS);
|
||||
for (const row of rows) store.put(row);
|
||||
tx.oncomplete = () => resolve();
|
||||
tx.onerror = () => reject(tx.error);
|
||||
});
|
||||
put += rows.length;
|
||||
}
|
||||
for (let at = 0; at < edits.length; at += size) {
|
||||
const slice = edits.slice(at, at + size);
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const tx = db.transaction(EDITS, 'readwrite');
|
||||
const store = tx.objectStore(EDITS);
|
||||
for (const row of slice) store.put(row);
|
||||
tx.oncomplete = () => resolve();
|
||||
tx.onerror = () => reject(tx.error);
|
||||
});
|
||||
}
|
||||
return put;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user