diff --git a/src/lib/modules/file.ts b/src/lib/modules/file.ts new file mode 100644 index 0000000..25e301e --- /dev/null +++ b/src/lib/modules/file.ts @@ -0,0 +1,90 @@ +import { 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) => { + 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: metadata!.dekVersion, + name: await decryptString(metadata!.name, metadata!.nameIv, dataKey), + subDirectoryIds: subDirectories, + fileIds: files, + }; + } + + const info = directoryInfoStore.get(directoryId); + if (info) { + info.update(() => newInfo); + } else { + directoryInfoStore.set(directoryId, writable(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); + return info; +}; + +const fetchFileInfo = async (fileId: number, masterKey: CryptoKey) => { + const res = await callGetApi(`/api/file/${fileId}`); + if (!res.ok) 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: metadata.dekVersion, + contentIv: metadata.contentIv, + name: await decryptString(metadata.name, metadata.nameIv, dataKey), + }; + + const info = fileInfoStore.get(fileId); + if (info) { + info.update(() => newInfo); + } else { + fileInfoStore.set(fileId, writable(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); + return info; +}; diff --git a/src/lib/services/file.ts b/src/lib/services/file.ts deleted file mode 100644 index d97767e..0000000 --- a/src/lib/services/file.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { unwrapDataKey, decryptString } from "$lib/modules/crypto"; -import type { FileInfoResponse } from "$lib/server/schemas"; - -export const decryptFileMetadata = async (metadata: FileInfoResponse, masterKey: CryptoKey) => { - const { dataKey } = await unwrapDataKey(metadata.dek, masterKey); - return { - dataKey, - dataKeyVersion: metadata.dekVersion, - name: await decryptString(metadata.name, metadata.nameIv, dataKey), - }; -}; diff --git a/src/lib/stores/file.ts b/src/lib/stores/file.ts new file mode 100644 index 0000000..78e7691 --- /dev/null +++ b/src/lib/stores/file.ts @@ -0,0 +1,30 @@ +import 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; + contentIv: string; + name: string; +} + +export const directoryInfoStore = new Map<"root" | number, Writable>(); +export const fileInfoStore = new Map>(); diff --git a/src/lib/stores/index.ts b/src/lib/stores/index.ts index 668f46f..537209a 100644 --- a/src/lib/stores/index.ts +++ b/src/lib/stores/index.ts @@ -1 +1,2 @@ +export * from "./file"; export * from "./key"; diff --git a/src/routes/(fullscreen)/file/[id]/+page.svelte b/src/routes/(fullscreen)/file/[id]/+page.svelte index 380dfb2..ea9a886 100644 --- a/src/routes/(fullscreen)/file/[id]/+page.svelte +++ b/src/routes/(fullscreen)/file/[id]/+page.svelte @@ -1,32 +1,27 @@ @@ -35,8 +30,4 @@ 파일 -{#if metadata} - -{:else} - -{/if} + diff --git a/src/routes/(fullscreen)/file/[id]/+page.ts b/src/routes/(fullscreen)/file/[id]/+page.ts index 6ecfe77..0521107 100644 --- a/src/routes/(fullscreen)/file/[id]/+page.ts +++ b/src/routes/(fullscreen)/file/[id]/+page.ts @@ -1,10 +1,8 @@ import { error } from "@sveltejs/kit"; import { z } from "zod"; -import { callGetApi } from "$lib/hooks"; -import type { FileInfoResponse } from "$lib/server/schemas"; import type { PageLoad } from "./$types"; -export const load: PageLoad = async ({ params, fetch }) => { +export const load: PageLoad = async ({ params }) => { const zodRes = z .object({ id: z.coerce.number().int().positive(), @@ -13,12 +11,5 @@ export const load: PageLoad = async ({ params, fetch }) => { if (!zodRes.success) error(404, "Not found"); const { id } = zodRes.data; - const res = await callGetApi(`/api/file/${id}`, fetch); - if (!res.ok) error(404, "Not found"); - - const fileInfo: FileInfoResponse = await res.json(); - return { - id, - metadata: fileInfo, - }; + return { id }; }; diff --git a/src/routes/(fullscreen)/file/[id]/service.ts b/src/routes/(fullscreen)/file/[id]/service.ts index 16aba8b..fc97c3e 100644 --- a/src/routes/(fullscreen)/file/[id]/service.ts +++ b/src/routes/(fullscreen)/file/[id]/service.ts @@ -1,7 +1,5 @@ import { decryptData } from "$lib/modules/crypto"; -export { decryptFileMetadata } from "$lib/services/file"; - export const requestFileDownload = ( fileId: number, fileEncryptedIv: string, diff --git a/src/routes/(main)/directory/[[id]]/+page.svelte b/src/routes/(main)/directory/[[id]]/+page.svelte index 89c6c64..38cd8e0 100644 --- a/src/routes/(main)/directory/[[id]]/+page.svelte +++ b/src/routes/(main)/directory/[[id]]/+page.svelte @@ -1,17 +1,18 @@ @@ -94,50 +58,42 @@ -
- {#if data.id !== "root"} - {#if !metadata} - - {:else} - {#await metadata} - - {:then metadata} - - {/await} - {/if} - {/if} -
- {#if subDirectories} - {#await subDirectories then subDirectories} - {#each subDirectories as { id, dataKey, dataKeyVersion, name }} - goto(`/directory/${id}`)} - onOpenMenuClick={() => { - selectedEntry = { type: "directory", id, dataKey, dataKeyVersion, name }; - isDirectoryEntryMenuBottomSheetOpen = true; - }} - type="directory" - /> - {/each} - {/await} - {/if} - {#if files} - {#await files then files} - {#each files as { id, dataKey, dataKeyVersion, name }} - goto(`/file/${id}`)} - onOpenMenuClick={() => { - selectedEntry = { type: "file", id, dataKey, dataKeyVersion, name }; - isDirectoryEntryMenuBottomSheetOpen = true; - }} - type="file" - /> - {/each} - {/await} +
+
+ {#if data.id !== "root"} + {/if}
+ {#if $info && $info.subDirectoryIds.length + $info.fileIds.length > 0} +
+ {#each $info.subDirectoryIds as subDirectoryId} + {@const subDirectoryInfo = getDirectoryInfo(subDirectoryId, $masterKeyStore?.get(1)?.key!)} + goto(`/directory/${subDirectoryId}`)} + onOpenMenuClick={({ id, dataKey, dataKeyVersion, name }) => { + selectedEntry = { type: "directory", id, dataKey, dataKeyVersion, name }; + isDirectoryEntryMenuBottomSheetOpen = true; + }} + /> + {/each} + {#each $info.fileIds as fileId} + {@const fileInfo = getFileInfo(fileId, $masterKeyStore?.get(1)?.key!)} + goto(`/file/${fileId}`)} + onOpenMenuClick={({ dataKey, id, dataKeyVersion, name }) => { + selectedEntry = { type: "file", id, dataKey, dataKeyVersion, name }; + isDirectoryEntryMenuBottomSheetOpen = true; + }} + /> + {/each} +
+ {:else} +
+

폴더가 비어있어요.

+
+ {/if}
{ +export const load: PageLoad = async ({ params }) => { const zodRes = z .object({ id: z.coerce.number().int().positive().optional(), @@ -13,36 +11,7 @@ export const load: PageLoad = async ({ params, fetch }) => { if (!zodRes.success) error(404, "Not found"); const { id } = zodRes.data; - const directoryId = id ? id : ("root" as const); - const res = await callGetApi(`/api/directory/${directoryId}`, fetch); - if (!res.ok) error(404, "Not found"); - - const directoryInfo: DirectoryInfoResponse = await res.json(); - const subDirectoryInfos = await Promise.all( - directoryInfo.subDirectories.map(async (subDirectoryId) => { - const res = await callGetApi(`/api/directory/${subDirectoryId}`, fetch); - if (!res.ok) error(500, "Internal server error"); - return { - ...((await res.json()) as DirectoryInfoResponse), - id: subDirectoryId, - }; - }), - ); - const fileInfos = await Promise.all( - directoryInfo.files.map(async (fileId) => { - const res = await callGetApi(`/api/file/${fileId}`, fetch); - if (!res.ok) error(500, "Internal server error"); - return { - ...((await res.json()) as FileInfoResponse), - id: fileId, - }; - }), - ); - return { - id: directoryId, - metadata: directoryInfo.metadata, - subDirectories: subDirectoryInfos, - files: fileInfos, + id: id ? id : ("root" as const), }; }; diff --git a/src/routes/(main)/directory/[[id]]/DirectoryEntry.svelte b/src/routes/(main)/directory/[[id]]/DirectoryEntry.svelte deleted file mode 100644 index 29f11d0..0000000 --- a/src/routes/(main)/directory/[[id]]/DirectoryEntry.svelte +++ /dev/null @@ -1,54 +0,0 @@ - - - - -
setTimeout(onclick, 100)} class="h-12 w-full rounded-xl"> -
-
- {#if type === "directory"} - - {:else if type === "file"} - - {/if} -
-

- {name} -

- -
-
- - diff --git a/src/routes/(main)/directory/[[id]]/File.svelte b/src/routes/(main)/directory/[[id]]/File.svelte new file mode 100644 index 0000000..da9b843 --- /dev/null +++ b/src/routes/(main)/directory/[[id]]/File.svelte @@ -0,0 +1,53 @@ + + +{#if $info} + + +
setTimeout(onclick, 100)} class="h-12 w-full rounded-xl"> +
+
+ +
+

+ {$info.name} +

+ +
+
+{/if} + + diff --git a/src/routes/(main)/directory/[[id]]/SubDirectory.svelte b/src/routes/(main)/directory/[[id]]/SubDirectory.svelte new file mode 100644 index 0000000..6bf29b0 --- /dev/null +++ b/src/routes/(main)/directory/[[id]]/SubDirectory.svelte @@ -0,0 +1,55 @@ + + +{#if $info} + + +
setTimeout(onclick, 100)} class="h-12 w-full rounded-xl"> +
+
+ +
+

+ {$info.name} +

+ +
+
+{/if} + + diff --git a/src/routes/(main)/directory/[[id]]/service.ts b/src/routes/(main)/directory/[[id]]/service.ts index ce17472..79fe1b9 100644 --- a/src/routes/(main)/directory/[[id]]/service.ts +++ b/src/routes/(main)/directory/[[id]]/service.ts @@ -3,22 +3,17 @@ import { encodeToBase64, generateDataKey, wrapDataKey, - unwrapDataKey, encryptData, encryptString, - decryptString, } from "$lib/modules/crypto"; import type { DirectoryRenameRequest, - DirectoryInfoResponse, DirectoryCreateRequest, FileRenameRequest, FileUploadRequest, } from "$lib/server/schemas"; import type { MasterKey } from "$lib/stores"; -export { decryptFileMetadata } from "$lib/services/file"; - export interface SelectedDirectoryEntry { type: "directory" | "file"; id: number; @@ -27,18 +22,6 @@ export interface SelectedDirectoryEntry { name: string; } -export const decryptDirectoryMetadata = async ( - metadata: NonNullable, - masterKey: CryptoKey, -) => { - const { dataKey } = await unwrapDataKey(metadata.dek, masterKey); - return { - dataKey, - dataKeyVersion: metadata.dekVersion, - name: await decryptString(metadata.name, metadata.nameIv, dataKey), - }; -}; - export const requestDirectoryCreation = async ( name: string, parentId: "root" | number,