diff --git a/src/lib/indexedDB/filesystem.ts b/src/lib/indexedDB/filesystem.ts index 5c9fc4d..293c16d 100644 --- a/src/lib/indexedDB/filesystem.ts +++ b/src/lib/indexedDB/filesystem.ts @@ -15,16 +15,28 @@ interface FileInfo { contentType: string; createdAt?: Date; lastModifiedAt: Date; + categoryIds: number[]; +} + +export type CategoryId = "root" | number; + +interface CategoryInfo { + id: number; + parentId: CategoryId; + name: string; + files: { id: number; isRecursive: boolean }[]; } const filesystem = new Dexie("filesystem") as Dexie & { directory: EntityTable; file: EntityTable; + category: EntityTable; }; -filesystem.version(1).stores({ +filesystem.version(2).stores({ directory: "id, parentId", file: "id, parentId", + category: "id, parentId", }); export const getDirectoryInfos = async (parentId: DirectoryId) => { @@ -59,13 +71,29 @@ export const deleteFileInfo = async (id: number) => { await filesystem.file.delete(id); }; +export const getCategoryInfos = async (parentId: CategoryId) => { + return await filesystem.category.where({ parentId }).toArray(); +}; + +export const getCategoryInfo = async (id: number) => { + return await filesystem.category.get(id); +}; + +export const storeCategoryInfo = async (categoryInfo: CategoryInfo) => { + await filesystem.category.put(categoryInfo); +}; + +export const deleteCategoryInfo = async (id: number) => { + await filesystem.category.delete(id); +}; + export const cleanupDanglingInfos = async () => { const validDirectoryIds: number[] = []; const validFileIds: number[] = []; - const queue: DirectoryId[] = ["root"]; + const directoryQueue: DirectoryId[] = ["root"]; while (true) { - const directoryId = queue.shift(); + const directoryId = directoryQueue.shift(); if (!directoryId) break; const [subDirectories, files] = await Promise.all([ @@ -74,13 +102,28 @@ export const cleanupDanglingInfos = async () => { ]); subDirectories.forEach(({ id }) => { validDirectoryIds.push(id); - queue.push(id); + directoryQueue.push(id); }); files.forEach(({ id }) => validFileIds.push(id)); } + const validCategoryIds: number[] = []; + const categoryQueue: CategoryId[] = ["root"]; + + while (true) { + const categoryId = categoryQueue.shift(); + if (!categoryId) break; + + const subCategories = await filesystem.category.where({ parentId: categoryId }).toArray(); + subCategories.forEach(({ id }) => { + validCategoryIds.push(id); + categoryQueue.push(id); + }); + } + await Promise.all([ filesystem.directory.where("id").noneOf(validDirectoryIds).delete(), filesystem.file.where("id").noneOf(validFileIds).delete(), + filesystem.category.where("id").noneOf(validCategoryIds).delete(), ]); }; diff --git a/src/lib/modules/filesystem.ts b/src/lib/modules/filesystem.ts index 0e786ce..dc1208e 100644 --- a/src/lib/modules/filesystem.ts +++ b/src/lib/modules/filesystem.ts @@ -9,7 +9,12 @@ import { getFileInfo as getFileInfoFromIndexedDB, storeFileInfo, deleteFileInfo, + getCategoryInfos as getCategoryInfosFromIndexedDB, + getCategoryInfo as getCategoryInfoFromIndexedDB, + storeCategoryInfo, + deleteCategoryInfo, type DirectoryId, + type CategoryId, } from "$lib/indexedDB"; import { unwrapDataKey, decryptString } from "$lib/modules/crypto"; import type { @@ -49,8 +54,6 @@ export interface FileInfo { categoryIds: number[]; } -type CategoryId = "root" | number; - export type CategoryInfo = | { id: "root"; @@ -161,7 +164,7 @@ const fetchFileInfoFromIndexedDB = async (id: number, info: Writable { @@ -214,6 +217,7 @@ const fetchFileInfoFromServer = async ( contentType: metadata.contentType, createdAt, lastModifiedAt, + categoryIds: metadata.categories, }); }; @@ -235,6 +239,26 @@ export const getFileInfo = (fileId: number, masterKey: CryptoKey) => { return info; }; +const fetchCategoryInfoFromIndexedDB = async ( + id: CategoryId, + info: Writable, +) => { + if (get(info)) return; + + const [category, subCategories] = await Promise.all([ + id !== "root" ? getCategoryInfoFromIndexedDB(id) : undefined, + getCategoryInfosFromIndexedDB(id), + ]); + const subCategoryIds = subCategories.map(({ id }) => id); + + if (id === "root") { + info.set({ id, subCategoryIds }); + } else { + if (!category) return; + info.set({ id, name: category.name, subCategoryIds, files: category.files }); + } +}; + const fetchCategoryInfoFromServer = async ( id: CategoryId, info: Writable, @@ -243,6 +267,7 @@ const fetchCategoryInfoFromServer = async ( let res = await callGetApi(`/api/category/${id}`); if (res.status === 404) { info.set(null); + await deleteCategoryInfo(id as number); return; } else if (!res.ok) { throw new Error("Failed to fetch category information"); @@ -262,6 +287,7 @@ const fetchCategoryInfoFromServer = async ( } const { files }: CategoryFileListResponse = await res.json(); + const filesMapped = files.map(({ file, isRecursive }) => ({ id: file, isRecursive })); info.set({ id, @@ -269,7 +295,13 @@ const fetchCategoryInfoFromServer = async ( dataKeyVersion: new Date(metadata!.dekVersion), name, subCategoryIds: subCategories, - files: files.map(({ file, isRecursive }) => ({ id: file, isRecursive })), + files: filesMapped, + }); + await storeCategoryInfo({ + id, + parentId: metadata!.parent, + name, + files: filesMapped, }); } }; @@ -279,6 +311,7 @@ const fetchCategoryInfo = async ( info: Writable, masterKey: CryptoKey, ) => { + await fetchCategoryInfoFromIndexedDB(id, info); await fetchCategoryInfoFromServer(id, info, masterKey); };