From 7aa6ba0eab9c267b2016c67792232192341adb94 Mon Sep 17 00:00:00 2001 From: static Date: Fri, 17 Jan 2025 12:22:51 +0900 Subject: [PATCH] =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EB=B0=8F=20=EB=94=94?= =?UTF-8?q?=EB=A0=89=ED=84=B0=EB=A6=AC=20=EB=AA=A9=EB=A1=9D=EC=9D=84=20Ind?= =?UTF-8?q?exedDB=EC=97=90=20=EC=BA=90=EC=8B=B1=ED=95=98=EB=8F=84=EB=A1=9D?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/indexedDB/filesystem.ts | 52 +++++ src/lib/indexedDB/index.ts | 1 + src/lib/modules/file/index.ts | 1 - src/lib/modules/file/info.ts | 104 ---------- src/lib/modules/file/upload.ts | 2 +- src/lib/modules/filesystem.ts | 192 ++++++++++++++++++ src/lib/server/schemas/directory.ts | 3 +- src/lib/server/schemas/file.ts | 3 +- src/lib/server/services/directory.ts | 2 +- src/lib/server/services/file.ts | 1 + src/lib/stores/file.ts | 34 ---- .../(fullscreen)/file/[id]/+page.svelte | 8 +- .../(fullscreen)/setting/cache/+page.svelte | 5 +- .../(fullscreen)/setting/cache/File.svelte | 2 +- .../(main)/directory/[[id]]/+page.svelte | 4 +- .../DirectoryEntries/DirectoryEntries.svelte | 11 +- .../[[id]]/DirectoryEntries/File.svelte | 6 +- .../DirectoryEntries/SubDirectory.svelte | 6 +- src/routes/(main)/directory/[[id]]/service.ts | 2 +- src/routes/api/directory/[id]/+server.ts | 1 + src/routes/api/directory/create/+server.ts | 4 +- src/routes/api/file/[id]/+server.ts | 2 + src/routes/api/file/upload/+server.ts | 4 +- 23 files changed, 285 insertions(+), 165 deletions(-) create mode 100644 src/lib/indexedDB/filesystem.ts delete mode 100644 src/lib/modules/file/info.ts create mode 100644 src/lib/modules/filesystem.ts diff --git a/src/lib/indexedDB/filesystem.ts b/src/lib/indexedDB/filesystem.ts new file mode 100644 index 0000000..a6567ff --- /dev/null +++ b/src/lib/indexedDB/filesystem.ts @@ -0,0 +1,52 @@ +import { Dexie, type EntityTable } from "dexie"; + +export type DirectoryId = "root" | number; + +interface DirectoryInfo { + id: number; + parentId: DirectoryId; + name: string; +} + +interface FileInfo { + id: number; + parentId: DirectoryId; + name: string; + contentType: string; + createdAt?: Date; + lastModifiedAt: Date; +} + +const filesystem = new Dexie("filesystem") as Dexie & { + directory: EntityTable; + file: EntityTable; +}; + +filesystem.version(1).stores({ + directory: "id, parentId", + file: "id, parentId", +}); + +export const getDirectoryInfos = async (parentId: DirectoryId) => { + return await filesystem.directory.where({ parentId }).toArray(); +}; + +export const getDirectoryInfo = async (id: number) => { + return await filesystem.directory.get(id); +}; + +export const storeDirectoryInfo = async (directoryInfo: DirectoryInfo) => { + await filesystem.directory.put(directoryInfo); +}; + +export const getFileInfos = async (parentId: DirectoryId) => { + return await filesystem.file.where({ parentId }).toArray(); +}; + +export const getFileInfo = async (id: number) => { + return await filesystem.file.get(id); +}; + +export const storeFileInfo = async (fileInfo: FileInfo) => { + await filesystem.file.put(fileInfo); +}; diff --git a/src/lib/indexedDB/index.ts b/src/lib/indexedDB/index.ts index c9bb3d0..4ca1202 100644 --- a/src/lib/indexedDB/index.ts +++ b/src/lib/indexedDB/index.ts @@ -1,2 +1,3 @@ export * from "./cacheIndex"; +export * from "./filesystem"; export * from "./keyStore"; diff --git a/src/lib/modules/file/index.ts b/src/lib/modules/file/index.ts index c4ea9aa..bb3d0e6 100644 --- a/src/lib/modules/file/index.ts +++ b/src/lib/modules/file/index.ts @@ -1,3 +1,2 @@ export * from "./cache"; -export * from "./info"; export * from "./upload"; diff --git a/src/lib/modules/file/info.ts b/src/lib/modules/file/info.ts deleted file mode 100644 index 342febc..0000000 --- a/src/lib/modules/file/info.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { writable, type Writable } from "svelte/store"; -import { callGetApi } from "$lib/hooks"; -import { unwrapDataKey, decryptString } from "$lib/modules/crypto"; -import type { DirectoryInfoResponse, FileInfoResponse } from "$lib/server/schemas"; -import { - directoryInfoStore, - fileInfoStore, - type DirectoryInfo, - type FileInfo, -} from "$lib/stores/file"; - -const fetchDirectoryInfo = async ( - directoryId: "root" | number, - masterKey: CryptoKey, - infoStore: Writable, -) => { - const res = await callGetApi(`/api/directory/${directoryId}`); - if (!res.ok) throw new Error("Failed to fetch directory information"); - const { metadata, subDirectories, files }: DirectoryInfoResponse = await res.json(); - - let newInfo: DirectoryInfo; - if (directoryId === "root") { - newInfo = { - id: "root", - subDirectoryIds: subDirectories, - fileIds: files, - }; - } else { - const { dataKey } = await unwrapDataKey(metadata!.dek, masterKey); - newInfo = { - id: directoryId, - dataKey, - dataKeyVersion: new Date(metadata!.dekVersion), - name: await decryptString(metadata!.name, metadata!.nameIv, dataKey), - subDirectoryIds: subDirectories, - fileIds: files, - }; - } - - infoStore.update(() => newInfo); -}; - -export const getDirectoryInfo = (directoryId: "root" | number, masterKey: CryptoKey) => { - // TODO: MEK rotation - - let info = directoryInfoStore.get(directoryId); - if (!info) { - info = writable(null); - directoryInfoStore.set(directoryId, info); - } - - fetchDirectoryInfo(directoryId, masterKey, info); - return info; -}; - -const decryptDate = async (ciphertext: string, iv: string, dataKey: CryptoKey) => { - return new Date(parseInt(await decryptString(ciphertext, iv, dataKey), 10)); -}; - -const fetchFileInfo = async ( - fileId: number, - masterKey: CryptoKey, - infoStore: Writable, -) => { - const res = await callGetApi(`/api/file/${fileId}`); - if (!res.ok) { - if (res.status === 404) { - infoStore.update(() => null); - return; - } - throw new Error("Failed to fetch file information"); - } - const metadata: FileInfoResponse = await res.json(); - - const { dataKey } = await unwrapDataKey(metadata.dek, masterKey); - const newInfo: FileInfo = { - id: fileId, - dataKey, - dataKeyVersion: new Date(metadata.dekVersion), - contentType: metadata.contentType, - contentIv: metadata.contentIv, - name: await decryptString(metadata.name, metadata.nameIv, dataKey), - createdAt: - metadata.createdAt && metadata.createdAtIv - ? await decryptDate(metadata.createdAt, metadata.createdAtIv, dataKey) - : undefined, - lastModifiedAt: await decryptDate(metadata.lastModifiedAt, metadata.lastModifiedAtIv, dataKey), - }; - - infoStore.update(() => newInfo); -}; - -export const getFileInfo = (fileId: number, masterKey: CryptoKey) => { - // TODO: MEK rotation - - let info = fileInfoStore.get(fileId); - if (!info) { - info = writable(null); - fileInfoStore.set(fileId, info); - } - - fetchFileInfo(fileId, masterKey, info); - return info; -}; diff --git a/src/lib/modules/file/upload.ts b/src/lib/modules/file/upload.ts index 09dffe1..b24f444 100644 --- a/src/lib/modules/file/upload.ts +++ b/src/lib/modules/file/upload.ts @@ -191,7 +191,7 @@ export const uploadFile = async ( form.set( "metadata", JSON.stringify({ - parentId, + parent: parentId, mekVersion: masterKey.version, dek: dataKeyWrapped, dekVersion: dataKeyVersion.toISOString(), diff --git a/src/lib/modules/filesystem.ts b/src/lib/modules/filesystem.ts new file mode 100644 index 0000000..e1c929e --- /dev/null +++ b/src/lib/modules/filesystem.ts @@ -0,0 +1,192 @@ +import { get, writable, type Writable } from "svelte/store"; +import { callGetApi } from "$lib/hooks"; +import { + getDirectoryInfos as getDirectoryInfosFromIndexedDB, + getDirectoryInfo as getDirectoryInfoFromIndexedDB, + storeDirectoryInfo, + getFileInfos as getFileInfosFromIndexedDB, + getFileInfo as getFileInfoFromIndexedDB, + storeFileInfo, + type DirectoryId, +} from "$lib/indexedDB"; +import { unwrapDataKey, decryptString } from "$lib/modules/crypto"; +import type { DirectoryInfoResponse, FileInfoResponse } from "$lib/server/schemas"; + +export type DirectoryInfo = + | { + id: "root"; + dataKey?: undefined; + dataKeyVersion?: undefined; + name?: undefined; + subDirectoryIds: number[]; + fileIds: number[]; + } + | { + id: number; + dataKey?: CryptoKey; + dataKeyVersion?: Date; + name: string; + subDirectoryIds: number[]; + fileIds: number[]; + }; + +export interface FileInfo { + id: number; + dataKey?: CryptoKey; + dataKeyVersion?: Date; + contentType: string; + contentIv?: string; + name: string; + createdAt?: Date; + lastModifiedAt: Date; +} + +const directoryInfoStore = new Map>(); +const fileInfoStore = new Map>(); + +const fetchDirectoryInfoFromIndexedDB = async ( + id: DirectoryId, + info: Writable, +) => { + if (get(info)) return; + + const [directory, subDirectories, files] = await Promise.all([ + id !== "root" ? getDirectoryInfoFromIndexedDB(id) : undefined, + getDirectoryInfosFromIndexedDB(id), + getFileInfosFromIndexedDB(id), + ]); + const subDirectoryIds = subDirectories.map(({ id }) => id); + const fileIds = files.map(({ id }) => id); + + if (id === "root") { + info.set({ id, subDirectoryIds, fileIds }); + } else { + if (!directory) return; + info.set({ id, name: directory.name, subDirectoryIds, fileIds }); + } +}; + +const fetchDirectoryInfoFromServer = async ( + id: DirectoryId, + info: Writable, + masterKey: CryptoKey, +) => { + const res = await callGetApi(`/api/directory/${id}`); + if (!res.ok) throw new Error("Failed to fetch directory information"); // TODO: Handle 404 + const { + metadata, + subDirectories: subDirectoryIds, + files: fileIds, + }: DirectoryInfoResponse = await res.json(); + + if (id === "root") { + info.set({ id, subDirectoryIds, fileIds }); + } else { + const { dataKey } = await unwrapDataKey(metadata!.dek, masterKey); + const name = await decryptString(metadata!.name, metadata!.nameIv, dataKey); + + info.set({ + id, + dataKey, + dataKeyVersion: new Date(metadata!.dekVersion), + name, + subDirectoryIds, + fileIds, + }); + await storeDirectoryInfo({ id, parentId: metadata!.parent, name }); + } +}; + +const fetchDirectoryInfo = async ( + id: DirectoryId, + info: Writable, + masterKey: CryptoKey, +) => { + await fetchDirectoryInfoFromIndexedDB(id, info); + await fetchDirectoryInfoFromServer(id, info, masterKey); +}; + +export const getDirectoryInfo = (id: DirectoryId, masterKey: CryptoKey) => { + // TODO: MEK rotation + + let info = directoryInfoStore.get(id); + if (!info) { + info = writable(null); + directoryInfoStore.set(id, info); + } + + fetchDirectoryInfo(id, info, masterKey); + return info; +}; + +const fetchFileInfoFromIndexedDB = async (id: number, info: Writable) => { + if (get(info)) return; + + const file = await getFileInfoFromIndexedDB(id); + if (!file) return; + + info.set(file); +}; + +const decryptDate = async (ciphertext: string, iv: string, dataKey: CryptoKey) => { + return new Date(parseInt(await decryptString(ciphertext, iv, dataKey), 10)); +}; + +const fetchFileInfoFromServer = async ( + id: number, + info: Writable, + masterKey: CryptoKey, +) => { + const res = await callGetApi(`/api/file/${id}`); + if (!res.ok) throw new Error("Failed to fetch file information"); // TODO: Handle 404 + const metadata: FileInfoResponse = await res.json(); + + const { dataKey } = await unwrapDataKey(metadata.dek, masterKey); + const name = await decryptString(metadata.name, metadata.nameIv, dataKey); + const createdAt = + metadata.createdAt && metadata.createdAtIv + ? await decryptDate(metadata.createdAt, metadata.createdAtIv, dataKey) + : undefined; + const lastModifiedAt = await decryptDate( + metadata.lastModifiedAt, + metadata.lastModifiedAtIv, + dataKey, + ); + + info.set({ + id, + dataKey, + dataKeyVersion: new Date(metadata.dekVersion), + contentType: metadata.contentType, + contentIv: metadata.contentIv, + name, + createdAt, + lastModifiedAt, + }); + await storeFileInfo({ + id, + parentId: metadata.parent, + name, + contentType: metadata.contentType, + createdAt, + lastModifiedAt, + }); +}; + +const fetchFileInfo = async (id: number, info: Writable, masterKey: CryptoKey) => { + await fetchFileInfoFromIndexedDB(id, info); + await fetchFileInfoFromServer(id, info, masterKey); +}; + +export const getFileInfo = (fileId: number, masterKey: CryptoKey) => { + // TODO: MEK rotation + + let info = fileInfoStore.get(fileId); + if (!info) { + info = writable(null); + fileInfoStore.set(fileId, info); + } + + fetchFileInfo(fileId, info, masterKey); + return info; +}; diff --git a/src/lib/server/schemas/directory.ts b/src/lib/server/schemas/directory.ts index 17d5720..15a5886 100644 --- a/src/lib/server/schemas/directory.ts +++ b/src/lib/server/schemas/directory.ts @@ -3,6 +3,7 @@ import { z } from "zod"; export const directoryInfoResponse = z.object({ metadata: z .object({ + parent: z.union([z.enum(["root"]), z.number().int().positive()]), mekVersion: z.number().int().positive(), dek: z.string().base64().nonempty(), dekVersion: z.string().datetime(), @@ -28,7 +29,7 @@ export const directoryRenameRequest = z.object({ export type DirectoryRenameRequest = z.infer; export const directoryCreateRequest = z.object({ - parentId: z.union([z.enum(["root"]), z.number().int().positive()]), + parent: z.union([z.enum(["root"]), z.number().int().positive()]), mekVersion: z.number().int().positive(), dek: z.string().base64().nonempty(), dekVersion: z.string().datetime(), diff --git a/src/lib/server/schemas/file.ts b/src/lib/server/schemas/file.ts index f6ac315..781baf2 100644 --- a/src/lib/server/schemas/file.ts +++ b/src/lib/server/schemas/file.ts @@ -2,6 +2,7 @@ import mime from "mime"; import { z } from "zod"; export const fileInfoResponse = z.object({ + parent: z.union([z.enum(["root"]), z.number().int().positive()]), mekVersion: z.number().int().positive(), dek: z.string().base64().nonempty(), dekVersion: z.string().datetime(), @@ -38,7 +39,7 @@ export const duplicateFileScanResponse = z.object({ export type DuplicateFileScanResponse = z.infer; export const fileUploadRequest = z.object({ - parentId: z.union([z.enum(["root"]), z.number().int().positive()]), + parent: z.union([z.enum(["root"]), z.number().int().positive()]), mekVersion: z.number().int().positive(), dek: z.string().base64().nonempty(), dekVersion: z.string().datetime(), diff --git a/src/lib/server/services/directory.ts b/src/lib/server/services/directory.ts index 3f6b55d..4dc14ce 100644 --- a/src/lib/server/services/directory.ts +++ b/src/lib/server/services/directory.ts @@ -19,9 +19,9 @@ export const getDirectoryInformation = async (userId: number, directoryId: "root const directories = await getAllDirectoriesByParent(userId, directoryId); const files = await getAllFilesByParent(userId, directoryId); - return { metadata: directory && { + parentId: directory.parentId ?? ("root" as const), mekVersion: directory.mekVersion, encDek: directory.encDek, dekVersion: directory.dekVersion, diff --git a/src/lib/server/services/file.ts b/src/lib/server/services/file.ts index f8153ca..3589bed 100644 --- a/src/lib/server/services/file.ts +++ b/src/lib/server/services/file.ts @@ -23,6 +23,7 @@ export const getFileInformation = async (userId: number, fileId: number) => { } return { + parentId: file.parentId ?? ("root" as const), mekVersion: file.mekVersion, encDek: file.encDek, dekVersion: file.dekVersion, diff --git a/src/lib/stores/file.ts b/src/lib/stores/file.ts index f7bf8b4..d3234d8 100644 --- a/src/lib/stores/file.ts +++ b/src/lib/stores/file.ts @@ -1,34 +1,4 @@ import { writable, type Writable } from "svelte/store"; - -export type DirectoryInfo = - | { - id: "root"; - dataKey?: undefined; - dataKeyVersion?: undefined; - name?: undefined; - subDirectoryIds: number[]; - fileIds: number[]; - } - | { - id: number; - dataKey: CryptoKey; - dataKeyVersion: Date; - name: string; - subDirectoryIds: number[]; - fileIds: number[]; - }; - -export interface FileInfo { - id: number; - dataKey: CryptoKey; - dataKeyVersion: Date; - contentType: string; - contentIv: string; - name: string; - createdAt?: Date; - lastModifiedAt: Date; -} - export interface FileUploadStatus { name: string; parentId: "root" | number; @@ -45,8 +15,4 @@ export interface FileUploadStatus { estimated?: number; } -export const directoryInfoStore = new Map<"root" | number, Writable>(); - -export const fileInfoStore = new Map>(); - export const fileUploadStatusStore = writable[]>([]); diff --git a/src/routes/(fullscreen)/file/[id]/+page.svelte b/src/routes/(fullscreen)/file/[id]/+page.svelte index b9fc0ff..4d735ba 100644 --- a/src/routes/(fullscreen)/file/[id]/+page.svelte +++ b/src/routes/(fullscreen)/file/[id]/+page.svelte @@ -3,8 +3,8 @@ import { untrack } from "svelte"; import type { Writable } from "svelte/store"; import { TopBar } from "$lib/components"; - import { getFileInfo } from "$lib/modules/file"; - import { masterKeyStore, type FileInfo } from "$lib/stores"; + import { getFileInfo, type FileInfo } from "$lib/modules/filesystem"; + import { masterKeyStore } from "$lib/stores"; import { requestFileDownload } from "./service"; type ContentType = "image" | "video"; @@ -27,7 +27,7 @@ }); $effect(() => { - if ($info && !isDownloaded) { + if ($info?.contentIv && $info?.dataKey && !isDownloaded) { untrack(() => { isDownloaded = true; @@ -37,7 +37,7 @@ contentType = "video"; } - requestFileDownload(data.id, $info.contentIv, $info.dataKey).then(async (res) => { + requestFileDownload(data.id, $info.contentIv!, $info.dataKey!).then(async (res) => { content = new Blob([res], { type: $info.contentType }); if (content.type === "image/heic" || content.type === "image/heif") { const { default: heic2any } = await import("heic2any"); diff --git a/src/routes/(fullscreen)/setting/cache/+page.svelte b/src/routes/(fullscreen)/setting/cache/+page.svelte index fac9f31..f7581ea 100644 --- a/src/routes/(fullscreen)/setting/cache/+page.svelte +++ b/src/routes/(fullscreen)/setting/cache/+page.svelte @@ -3,8 +3,9 @@ import type { Writable } from "svelte/store"; import { TopBar } from "$lib/components"; import type { FileCacheIndex } from "$lib/indexedDB"; - import { getFileCacheIndex, getFileInfo } from "$lib/modules/file"; - import { masterKeyStore, type FileInfo } from "$lib/stores"; + import { getFileCacheIndex } from "$lib/modules/file"; + import { getFileInfo, type FileInfo } from "$lib/modules/filesystem"; + import { masterKeyStore } from "$lib/stores"; import File from "./File.svelte"; import { formatFileSize, deleteFileCache as doDeleteFileCache } from "./service"; diff --git a/src/routes/(fullscreen)/setting/cache/File.svelte b/src/routes/(fullscreen)/setting/cache/File.svelte index e92cc5c..2ee02c7 100644 --- a/src/routes/(fullscreen)/setting/cache/File.svelte +++ b/src/routes/(fullscreen)/setting/cache/File.svelte @@ -1,7 +1,7 @@ {#if subDirectories.length + files.length > 0} -
+
{#each subDirectories as { info }} {/each} diff --git a/src/routes/(main)/directory/[[id]]/DirectoryEntries/File.svelte b/src/routes/(main)/directory/[[id]]/DirectoryEntries/File.svelte index 80479ea..9946d36 100644 --- a/src/routes/(main)/directory/[[id]]/DirectoryEntries/File.svelte +++ b/src/routes/(main)/directory/[[id]]/DirectoryEntries/File.svelte @@ -1,6 +1,6 @@