|
|
|
@@ -436,22 +436,67 @@ export async function listFolders(): Promise<LibraryFolder[]> {
|
|
|
|
|
// the screen down, and it is the folder's own handle, one row, that is needed to
|
|
|
|
|
// ask for it again. The folder part of a path is opened once and kept: a wall's
|
|
|
|
|
// worth of frames is nearly always a handful of folders.
|
|
|
|
|
const dirHandles = new Map<string, FileSystemDirectoryHandle | null>();
|
|
|
|
|
const dirHandles = new Map<string, FileSystemDirectoryHandle>();
|
|
|
|
|
|
|
|
|
|
export async function fileHandleFor(photo: LibraryPhoto): Promise<FileSystemFileHandle | null> {
|
|
|
|
|
if (photo.handle) return photo.handle;
|
|
|
|
|
if (photo.remoteId) return null;
|
|
|
|
|
// The folder a frame's file is asked of: the one record the catalogue kept of it,
|
|
|
|
|
// and the permission its handle comes back without — the browser drops that when
|
|
|
|
|
// the tab closes, and the reader's click on the frame is the one gesture that can
|
|
|
|
|
// ask for it again. It is asked for here, at the click, and not only at start-up,
|
|
|
|
|
// where nobody is there to answer: without it every frame of a folder read
|
|
|
|
|
// yesterday answers "moved or deleted" while the file sits untouched on the disk.
|
|
|
|
|
async function folderFor(photo: LibraryPhoto): Promise<FileSystemDirectoryHandle | null> {
|
|
|
|
|
const folder = (await listFolders()).find((f) => f.handle && normPath(f.name) === normPath(photo.folder));
|
|
|
|
|
if (!folder?.handle) return null;
|
|
|
|
|
const key = `${folder.name}\u0000${photo.dir}`;
|
|
|
|
|
if (!dirHandles.has(key)) dirHandles.set(key, photo.dir ? await openAt(folder.handle, photo.dir) : folder.handle);
|
|
|
|
|
const dir = dirHandles.get(key);
|
|
|
|
|
if (!(await ensurePermission(folder.handle))) return null;
|
|
|
|
|
// The row's own `dir` is normalised here rather than trusted: a catalogue read
|
|
|
|
|
// by an older build can carry a backslash or an empty string where a path
|
|
|
|
|
// belongs, and a wrong directory answers `getFileHandle` with a `NotFoundError`
|
|
|
|
|
// that no permission ask can undo.
|
|
|
|
|
const rel = normPath(photo.dir ?? '');
|
|
|
|
|
const key = `${folder.name}\u0000${rel}`;
|
|
|
|
|
const held = dirHandles.get(key);
|
|
|
|
|
if (held) return held;
|
|
|
|
|
const dir = rel ? await openAt(folder.handle, rel) : folder.handle;
|
|
|
|
|
// A folder that will not open is not written down: a refusal is the reader's to
|
|
|
|
|
// take back, and a null kept here would answer for every frame of that folder
|
|
|
|
|
// for the rest of the visit instead of asking again on the next click.
|
|
|
|
|
if (!dir) return null;
|
|
|
|
|
dirHandles.set(key, dir);
|
|
|
|
|
return dir;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The file asked of the directory it was catalogued in, by name and then — only
|
|
|
|
|
// when the name answers nothing — by a pass over the directory. `getFileHandle`
|
|
|
|
|
// matches the name byte for byte, while the disk underneath Windows does not: a
|
|
|
|
|
// row that carries a case the file no longer has asks for a name the directory
|
|
|
|
|
// will not answer with, and Explorer shows the frame sitting there all the same.
|
|
|
|
|
// The listing costs one pass over one directory and runs on the failing ask
|
|
|
|
|
// alone, so a frame that opens pays nothing for it.
|
|
|
|
|
async function fileIn(dir: FileSystemDirectoryHandle, name: string): Promise<FileSystemFileHandle | null> {
|
|
|
|
|
try {
|
|
|
|
|
return await dir.getFileHandle(photo.name);
|
|
|
|
|
return await dir.getFileHandle(name);
|
|
|
|
|
} catch {
|
|
|
|
|
// The ask is answered below, and where it is not, by the caller.
|
|
|
|
|
}
|
|
|
|
|
const want = name.toLowerCase();
|
|
|
|
|
try {
|
|
|
|
|
for await (const entry of dir.values()) {
|
|
|
|
|
if (entry.kind === 'file' && entry.name.toLowerCase() === want) return entry as FileSystemFileHandle;
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function fileHandleFor(photo: LibraryPhoto): Promise<FileSystemFileHandle | null> {
|
|
|
|
|
if (photo.remoteId) return null;
|
|
|
|
|
const dir = await folderFor(photo);
|
|
|
|
|
// A folder that cannot be had is not a file that is not there, and the caller
|
|
|
|
|
// tells them apart: `no-folder` is a drive to plug back in, `null` a file the
|
|
|
|
|
// directory does not answer for. `readPhotoFile` is the only caller.
|
|
|
|
|
if (!dir) throw new Error('no-folder');
|
|
|
|
|
return fileIn(dir, photo.name);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// A folder the reader has just handed back, and what the catalogue knows of the
|
|
|
|
@@ -2440,6 +2485,21 @@ export async function readPhotoFile(photo: LibraryPhoto): Promise<File> {
|
|
|
|
|
const blob = await res.blob();
|
|
|
|
|
return new File([blob], photo.name, { type: blob.type || 'application/octet-stream' });
|
|
|
|
|
}
|
|
|
|
|
// A row an older build wrote carries its own handle, and the browser hands it
|
|
|
|
|
// back with no permission on it — the same ask the folder gets, for the same
|
|
|
|
|
// reason. Without it the shortcut answers `NotAllowedError` and a frame that is
|
|
|
|
|
// where the reader left it reads as moved or deleted. A handle that will not
|
|
|
|
|
// answer is no shortcut at all: the folder below is the way in, and the one
|
|
|
|
|
// that says why the frame is not there.
|
|
|
|
|
if (photo.handle) {
|
|
|
|
|
try {
|
|
|
|
|
if (await ensurePermission(photo.handle)) return await photo.handle.getFile();
|
|
|
|
|
} catch {
|
|
|
|
|
// A shortcut that will not answer is a stale one: the drive went away or
|
|
|
|
|
// the file did. Falling through costs the folder walk below and buys the
|
|
|
|
|
// reader the real reason instead of "moved or deleted".
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const handle = await fileHandleFor(photo);
|
|
|
|
|
if (!handle) throw new Error('no-file');
|
|
|
|
|
return handle.getFile();
|
|
|
|
|