From ea0f0e4a71e7d6bc569043dbed73b4217b124cd8 Mon Sep 17 00:00:00 2001 From: static Date: Tue, 14 Jan 2025 01:03:26 +0900 Subject: [PATCH] =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=BA=90=EC=8B=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks.client.ts | 10 +++- src/lib/indexedDB/cacheIndex.ts | 24 +++++++++ src/lib/indexedDB/index.ts | 2 + .../{indexedDB.ts => indexedDB/keyStore.ts} | 0 src/lib/modules/cache.ts | 33 ++++++++++++ src/lib/modules/opfs.ts | 53 +++++++++++++++++++ src/routes/(fullscreen)/file/[id]/service.ts | 7 ++- 7 files changed, 127 insertions(+), 2 deletions(-) create mode 100644 src/lib/indexedDB/cacheIndex.ts create mode 100644 src/lib/indexedDB/index.ts rename src/lib/{indexedDB.ts => indexedDB/keyStore.ts} (100%) create mode 100644 src/lib/modules/cache.ts create mode 100644 src/lib/modules/opfs.ts diff --git a/src/hooks.client.ts b/src/hooks.client.ts index 3f0ccfb..10d9e4e 100644 --- a/src/hooks.client.ts +++ b/src/hooks.client.ts @@ -1,5 +1,7 @@ import type { ClientInit } from "@sveltejs/kit"; import { getClientKey, getMasterKeys, getHmacSecrets } from "$lib/indexedDB"; +import { prepareFileCache } from "$lib/modules/cache"; +import { prepareOpfs } from "$lib/modules/opfs"; import { clientKeyStore, masterKeyStore, hmacSecretStore } from "$lib/stores"; const prepareClientKeyStore = async () => { @@ -29,5 +31,11 @@ const prepareHmacSecretStore = async () => { }; export const init: ClientInit = async () => { - await Promise.all([prepareClientKeyStore(), prepareMasterKeyStore(), prepareHmacSecretStore()]); + await Promise.all([ + prepareFileCache(), + prepareClientKeyStore(), + prepareMasterKeyStore(), + prepareHmacSecretStore(), + prepareOpfs(), + ]); }; diff --git a/src/lib/indexedDB/cacheIndex.ts b/src/lib/indexedDB/cacheIndex.ts new file mode 100644 index 0000000..fb03377 --- /dev/null +++ b/src/lib/indexedDB/cacheIndex.ts @@ -0,0 +1,24 @@ +import { Dexie, type EntityTable } from "dexie"; + +export interface FileCacheIndex { + fileId: number; + cachedAt: Date; + lastRetrievedAt: Date; + size: number; +} + +const cacheIndex = new Dexie("cacheIndex") as Dexie & { + fileCache: EntityTable; +}; + +cacheIndex.version(1).stores({ + fileCache: "fileId", +}); + +export const getFileCacheIndex = async () => { + return await cacheIndex.fileCache.toArray(); +}; + +export const storeFileCacheIndex = async (fileCacheIndex: FileCacheIndex) => { + await cacheIndex.fileCache.put(fileCacheIndex); +}; diff --git a/src/lib/indexedDB/index.ts b/src/lib/indexedDB/index.ts new file mode 100644 index 0000000..c9bb3d0 --- /dev/null +++ b/src/lib/indexedDB/index.ts @@ -0,0 +1,2 @@ +export * from "./cacheIndex"; +export * from "./keyStore"; diff --git a/src/lib/indexedDB.ts b/src/lib/indexedDB/keyStore.ts similarity index 100% rename from src/lib/indexedDB.ts rename to src/lib/indexedDB/keyStore.ts diff --git a/src/lib/modules/cache.ts b/src/lib/modules/cache.ts new file mode 100644 index 0000000..0bd4342 --- /dev/null +++ b/src/lib/modules/cache.ts @@ -0,0 +1,33 @@ +import { getFileCacheIndex, storeFileCacheIndex, type FileCacheIndex } from "$lib/indexedDB"; +import { readFileFromOpfs, writeFileToOpfs } from "$lib/modules/opfs"; + +const fileCacheIndex = new Map(); + +export const prepareFileCache = async () => { + for (const cache of await getFileCacheIndex()) { + fileCacheIndex.set(cache.fileId, cache); + } +}; + +export const getFileCache = async (fileId: number) => { + const cacheIndex = fileCacheIndex.get(fileId); + if (!cacheIndex) return null; + + cacheIndex.lastRetrievedAt = new Date(); + storeFileCacheIndex(cacheIndex); // Intended + return await readFileFromOpfs(`/cache/${fileId}`); +}; + +export const storeFileCache = async (fileId: number, fileBuffer: ArrayBuffer) => { + const now = new Date(); + await writeFileToOpfs(`/cache/${fileId}`, fileBuffer); + + const cacheIndex: FileCacheIndex = { + fileId, + cachedAt: now, + lastRetrievedAt: now, + size: fileBuffer.byteLength, + }; + fileCacheIndex.set(fileId, cacheIndex); + await storeFileCacheIndex(cacheIndex); +}; diff --git a/src/lib/modules/opfs.ts b/src/lib/modules/opfs.ts new file mode 100644 index 0000000..f96e6ba --- /dev/null +++ b/src/lib/modules/opfs.ts @@ -0,0 +1,53 @@ +let rootHandle: FileSystemDirectoryHandle | null = null; + +export const prepareOpfs = async () => { + rootHandle = await navigator.storage.getDirectory(); +}; + +const getFileHandle = async (path: string, create = true) => { + if (!rootHandle) { + throw new Error("OPFS not prepared"); + } else if (path[0] !== "/") { + throw new Error("Path must be absolute"); + } + + const parts = path.split("/"); + if (parts.length <= 1) { + throw new Error("Invalid path"); + } + + try { + let directoryHandle: FileSystemDirectoryHandle = rootHandle; + + for (const part of parts.slice(0, -1)) { + if (!part) continue; + directoryHandle = await directoryHandle.getDirectoryHandle(part, { create }); + } + + return directoryHandle.getFileHandle(parts[parts.length - 1]!, { create }); + } catch (e) { + if (e instanceof DOMException && e.name === "NotFoundError") { + return null; + } + throw e; + } +}; + +export const readFileFromOpfs = async (path: string) => { + const fileHandle = await getFileHandle(path, false); + if (!fileHandle) return null; + + const file = await fileHandle.getFile(); + return await file.arrayBuffer(); +}; + +export const writeFileToOpfs = async (path: string, data: ArrayBuffer) => { + const fileHandle = await getFileHandle(path); + const writable = await fileHandle!.createWritable(); + + try { + await writable.write(data); + } finally { + await writable.close(); + } +}; diff --git a/src/routes/(fullscreen)/file/[id]/service.ts b/src/routes/(fullscreen)/file/[id]/service.ts index fc97c3e..dfdb92b 100644 --- a/src/routes/(fullscreen)/file/[id]/service.ts +++ b/src/routes/(fullscreen)/file/[id]/service.ts @@ -1,10 +1,14 @@ +import { getFileCache, storeFileCache } from "$lib/modules/cache"; import { decryptData } from "$lib/modules/crypto"; -export const requestFileDownload = ( +export const requestFileDownload = async ( fileId: number, fileEncryptedIv: string, dataKey: CryptoKey, ) => { + const cache = await getFileCache(fileId); + if (cache) return cache; + return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.responseType = "arraybuffer"; @@ -21,6 +25,7 @@ export const requestFileDownload = ( dataKey, ); resolve(fileDecrypted); + await storeFileCache(fileId, fileDecrypted); }); // TODO: Progress, ...