diff --git a/src/lib/components/atoms/buttons/FileThumbnailButton.svelte b/src/lib/components/atoms/buttons/FileThumbnailButton.svelte index bb7761e..6c5632c 100644 --- a/src/lib/components/atoms/buttons/FileThumbnailButton.svelte +++ b/src/lib/components/atoms/buttons/FileThumbnailButton.svelte @@ -1,7 +1,6 @@ diff --git a/src/lib/components/organisms/Category/File.svelte b/src/lib/components/organisms/Category/File.svelte index d4f5d4d..166de06 100644 --- a/src/lib/components/organisms/Category/File.svelte +++ b/src/lib/components/organisms/Category/File.svelte @@ -1,9 +1,8 @@ onRemoveClick?.(info)} > - {#await thumbnailPromise} - - {:then thumbnail} - - {/await} + diff --git a/src/lib/modules/file/cache.ts b/src/lib/modules/file/cache.ts index ccb187e..fe3c66c 100644 --- a/src/lib/modules/file/cache.ts +++ b/src/lib/modules/file/cache.ts @@ -1,15 +1,12 @@ -import { LRUCache } from "lru-cache"; import { getFileCacheIndex as getFileCacheIndexFromIndexedDB, storeFileCacheIndex, deleteFileCacheIndex, type FileCacheIndex, } from "$lib/indexedDB"; -import { readFile, writeFile, deleteFile, deleteDirectory } from "$lib/modules/opfs"; -import { getThumbnailUrl } from "$lib/modules/thumbnail"; +import { readFile, writeFile, deleteFile } from "$lib/modules/opfs"; const fileCacheIndex = new Map(); -const loadedThumbnails = new LRUCache({ max: 100 }); export const prepareFileCache = async () => { for (const cache of await getFileCacheIndexFromIndexedDB()) { @@ -51,30 +48,3 @@ export const deleteFileCache = async (fileId: number) => { await deleteFile(`/cache/${fileId}`); await deleteFileCacheIndex(fileId); }; - -export const getFileThumbnailCache = async (fileId: number) => { - const thumbnail = loadedThumbnails.get(fileId); - if (thumbnail) return thumbnail; - - const thumbnailBuffer = await readFile(`/thumbnail/file/${fileId}`); - if (!thumbnailBuffer) return null; - - const thumbnailUrl = getThumbnailUrl(thumbnailBuffer); - loadedThumbnails.set(fileId, thumbnailUrl); - return thumbnailUrl; -}; - -export const storeFileThumbnailCache = async (fileId: number, thumbnailBuffer: ArrayBuffer) => { - await writeFile(`/thumbnail/file/${fileId}`, thumbnailBuffer); - loadedThumbnails.set(fileId, getThumbnailUrl(thumbnailBuffer)); -}; - -export const deleteFileThumbnailCache = async (fileId: number) => { - loadedThumbnails.delete(fileId); - await deleteFile(`/thumbnail/file/${fileId}`); -}; - -export const deleteAllFileThumbnailCaches = async () => { - loadedThumbnails.clear(); - await deleteDirectory("/thumbnail/file"); -}; diff --git a/src/lib/modules/file/index.ts b/src/lib/modules/file/index.ts index 871d299..9e9ce0c 100644 --- a/src/lib/modules/file/index.ts +++ b/src/lib/modules/file/index.ts @@ -1,3 +1,4 @@ export * from "./cache"; export * from "./download.svelte"; +export * from "./thumbnail"; export * from "./upload.svelte"; diff --git a/src/lib/modules/file/thumbnail.ts b/src/lib/modules/file/thumbnail.ts new file mode 100644 index 0000000..f923153 --- /dev/null +++ b/src/lib/modules/file/thumbnail.ts @@ -0,0 +1,90 @@ +import { LRUCache } from "lru-cache"; +import { writable, type Writable } from "svelte/store"; +import { browser } from "$app/environment"; +import { decryptData } from "$lib/modules/crypto"; +import type { SummarizedFileInfo } from "$lib/modules/filesystem"; +import { readFile, writeFile, deleteFile, deleteDirectory } from "$lib/modules/opfs"; +import { getThumbnailUrl } from "$lib/modules/thumbnail"; +import { isTRPCClientError, trpc } from "$trpc/client"; + +const loadedThumbnails = new LRUCache>({ max: 100 }); +const loadingThumbnails = new Map>(); + +const fetchFromOpfs = async (fileId: number) => { + const thumbnailBuffer = await readFile(`/thumbnail/file/${fileId}`); + if (thumbnailBuffer) { + return getThumbnailUrl(thumbnailBuffer); + } +}; + +const fetchFromServer = async (fileId: number, dataKey: CryptoKey) => { + try { + const [thumbnailEncrypted, { contentIv: thumbnailEncryptedIv }] = await Promise.all([ + fetch(`/api/file/${fileId}/thumbnail/download`), + trpc().file.thumbnail.query({ id: fileId }), + ]); + const thumbnailBuffer = await decryptData( + await thumbnailEncrypted.arrayBuffer(), + thumbnailEncryptedIv, + dataKey, + ); + + void writeFile(`/thumbnail/file/${fileId}`, thumbnailBuffer); + return getThumbnailUrl(thumbnailBuffer); + } catch (e) { + if (isTRPCClientError(e) && e.data?.code === "NOT_FOUND") { + return null; + } + throw e; + } +}; + +export const getFileThumbnail = (file: SummarizedFileInfo) => { + if ( + !browser || + !(file.contentType.startsWith("image/") || file.contentType.startsWith("video/")) + ) { + return undefined; + } + + const thumbnail = loadedThumbnails.get(file.id); + if (thumbnail) return thumbnail; + + let loadingThumbnail = loadingThumbnails.get(file.id); + if (loadingThumbnail) return loadingThumbnail; + + loadingThumbnail = writable(undefined); + loadingThumbnails.set(file.id, loadingThumbnail); + + fetchFromOpfs(file.id) + .then((thumbnail) => thumbnail ?? (file.dataKey && fetchFromServer(file.id, file.dataKey.key))) + .then((thumbnail) => { + if (thumbnail) { + loadingThumbnail.set(thumbnail); + loadedThumbnails.set(file.id, loadingThumbnail as Writable); + } + loadingThumbnails.delete(file.id); + }); + return loadingThumbnail; +}; + +export const storeFileThumbnailCache = async (fileId: number, thumbnailBuffer: ArrayBuffer) => { + await writeFile(`/thumbnail/file/${fileId}`, thumbnailBuffer); + + const oldThumbnail = loadedThumbnails.get(fileId); + if (oldThumbnail) { + oldThumbnail.set(getThumbnailUrl(thumbnailBuffer)); + } else { + loadedThumbnails.set(fileId, writable(getThumbnailUrl(thumbnailBuffer))); + } +}; + +export const deleteFileThumbnailCache = async (fileId: number) => { + loadedThumbnails.delete(fileId); + await deleteFile(`/thumbnail/file/${fileId}`); +}; + +export const deleteAllFileThumbnailCaches = async () => { + loadedThumbnails.clear(); + await deleteDirectory(`/thumbnail/file`); +}; diff --git a/src/lib/services/file.ts b/src/lib/services/file.ts index da05824..05a92e1 100644 --- a/src/lib/services/file.ts +++ b/src/lib/services/file.ts @@ -1,15 +1,11 @@ import { getAllFileInfos } from "$lib/indexedDB/filesystem"; -import { decryptData } from "$lib/modules/crypto"; import { getFileCache, storeFileCache, deleteFileCache, - getFileThumbnailCache, - storeFileThumbnailCache, - deleteFileThumbnailCache, downloadFile, + deleteFileThumbnailCache, } from "$lib/modules/file"; -import { getThumbnailUrl } from "$lib/modules/thumbnail"; import type { FileThumbnailUploadRequest } from "$lib/server/schemas"; import { trpc } from "$trpc/client"; @@ -44,29 +40,6 @@ export const requestFileThumbnailUpload = async ( return await fetch(`/api/file/${fileId}/thumbnail/upload`, { method: "POST", body: form }); }; -export const requestFileThumbnailDownload = async (fileId: number, dataKey?: CryptoKey) => { - const cache = await getFileThumbnailCache(fileId); - if (cache || !dataKey) return cache; - - let thumbnailInfo; - try { - thumbnailInfo = await trpc().file.thumbnail.query({ id: fileId }); - } catch { - // TODO: Error Handling - return null; - } - const { contentIv: thumbnailEncryptedIv } = thumbnailInfo; - - const res = await fetch(`/api/file/${fileId}/thumbnail/download`); - if (!res.ok) return null; - - const thumbnailEncrypted = await res.arrayBuffer(); - const thumbnailBuffer = await decryptData(thumbnailEncrypted, thumbnailEncryptedIv, dataKey); - - storeFileThumbnailCache(fileId, thumbnailBuffer); // Intended - return getThumbnailUrl(thumbnailBuffer); -}; - export const requestDeletedFilesCleanup = async () => { let liveFiles; try { diff --git a/src/routes/(main)/directory/[[id]]/DirectoryEntries/File.svelte b/src/routes/(main)/directory/[[id]]/DirectoryEntries/File.svelte index 41f1a84..741bac3 100644 --- a/src/routes/(main)/directory/[[id]]/DirectoryEntries/File.svelte +++ b/src/routes/(main)/directory/[[id]]/DirectoryEntries/File.svelte @@ -1,9 +1,8 @@