fix(library): add one-time DB cursor migration to clean legacy Blobs and implement hierarchical Add Folder tree matching
This commit is contained in:
@@ -162,6 +162,45 @@ export function getCachedThumb(id: string): Blob | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
async function migrateLegacyDatabase(db: IDBDatabase): Promise<void> {
|
||||
if (typeof window === 'undefined') return;
|
||||
const MIGRATED_KEY = 'recipescam-migrated-v2';
|
||||
if (localStorage.getItem(MIGRATED_KEY) === 'true') return;
|
||||
if (!db.objectStoreNames.contains(THUMBS) || !db.objectStoreNames.contains(PHOTOS)) return;
|
||||
|
||||
try {
|
||||
await new Promise<void>((resolve) => {
|
||||
const tx = db.transaction([PHOTOS, THUMBS], 'readwrite');
|
||||
const photoStore = tx.objectStore(PHOTOS);
|
||||
const thumbStore = tx.objectStore(THUMBS);
|
||||
const req = photoStore.openCursor();
|
||||
|
||||
req.onsuccess = () => {
|
||||
const cursor = req.result;
|
||||
if (cursor) {
|
||||
const val = cursor.value as any;
|
||||
if (val && val.thumb) {
|
||||
const thumbBlob = val.thumb as Blob;
|
||||
const id = val.id as string;
|
||||
cacheThumb(id, thumbBlob);
|
||||
thumbStore.put({ id, thumb: thumbBlob });
|
||||
delete val.thumb;
|
||||
cursor.update(val);
|
||||
}
|
||||
cursor.continue();
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
req.onerror = () => resolve();
|
||||
tx.oncomplete = () => resolve();
|
||||
});
|
||||
localStorage.setItem(MIGRATED_KEY, 'true');
|
||||
} catch {
|
||||
// Non-fatal if migration is interrupted
|
||||
}
|
||||
}
|
||||
|
||||
let dbPromise: Promise<IDBDatabase> | null = null;
|
||||
|
||||
function openDb(): Promise<IDBDatabase> {
|
||||
@@ -180,12 +219,44 @@ function openDb(): Promise<IDBDatabase> {
|
||||
}
|
||||
if (!db.objectStoreNames.contains(EDITS)) db.createObjectStore(EDITS, { keyPath: 'photoId' });
|
||||
};
|
||||
req.onsuccess = () => resolve(req.result);
|
||||
req.onsuccess = () => {
|
||||
const db = req.result;
|
||||
void migrateLegacyDatabase(db).then(() => resolve(db));
|
||||
};
|
||||
req.onerror = () => reject(req.error);
|
||||
});
|
||||
return dbPromise;
|
||||
}
|
||||
|
||||
export async function findFolderRelationship(
|
||||
newHandle: FileSystemDirectoryHandle,
|
||||
existingFolders: LibraryFolder[]
|
||||
): Promise<
|
||||
| { type: 'subfolder'; parent: LibraryFolder; relPath: string }
|
||||
| { type: 'parent'; child: LibraryFolder }
|
||||
| { type: 'root' }
|
||||
> {
|
||||
for (const existing of existingFolders) {
|
||||
try {
|
||||
const relParts = await existing.handle.resolve(newHandle);
|
||||
if (relParts && relParts.length > 0) {
|
||||
return { type: 'subfolder', parent: existing, relPath: relParts.join('/') };
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
for (const existing of existingFolders) {
|
||||
try {
|
||||
const relParts = await newHandle.resolve(existing.handle);
|
||||
if (relParts && relParts.length > 0) {
|
||||
return { type: 'parent', child: existing };
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return { type: 'root' };
|
||||
}
|
||||
|
||||
// One request per call: the transaction is the store's own, which is all a
|
||||
// keyed get/put/delete and a getAll ever need. A cursor-based scan (the folder
|
||||
// walk) goes through `walk` below instead, because it holds one transaction
|
||||
@@ -1311,30 +1382,6 @@ export async function readPhotos(folder?: string): Promise<LibraryPhoto[] | null
|
||||
const rows = await ask<LibraryPhoto[]>(PHOTOS, 'readonly', (s) =>
|
||||
folder ? s.index('folder').getAll(folder) : s.getAll()
|
||||
);
|
||||
if (db.objectStoreNames.contains(THUMBS)) {
|
||||
const legacyPhotosToUpdate: LibraryPhoto[] = [];
|
||||
const legacyThumbsToPut: { id: string; thumb: Blob }[] = [];
|
||||
for (const row of rows) {
|
||||
if (row.thumb) {
|
||||
const t = row.thumb;
|
||||
cacheThumb(row.id, t);
|
||||
legacyThumbsToPut.push({ id: row.id, thumb: t });
|
||||
const { thumb, ...meta } = row;
|
||||
legacyPhotosToUpdate.push(meta as LibraryPhoto);
|
||||
}
|
||||
}
|
||||
if (legacyThumbsToPut.length > 0) {
|
||||
void (async () => {
|
||||
try {
|
||||
const tx = db.transaction([PHOTOS, THUMBS], 'readwrite');
|
||||
const pStore = tx.objectStore(PHOTOS);
|
||||
const tStore = tx.objectStore(THUMBS);
|
||||
for (const item of legacyThumbsToPut) tStore.put(item);
|
||||
for (const item of legacyPhotosToUpdate) pStore.put(item);
|
||||
} catch {}
|
||||
})();
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
if (rows[i].thumb) delete (rows[i] as { thumb?: Blob | null }).thumb;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user