From 409ae09f4fb290bcf4049aebc4d0bc15d39f53f5 Mon Sep 17 00:00:00 2001 From: static Date: Tue, 30 Dec 2025 17:21:54 +0900 Subject: [PATCH] =?UTF-8?q?=EB=94=94=EB=A0=89=ED=84=B0=EB=A6=AC=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=EC=97=90=EC=84=9C=EC=9D=98=20?= =?UTF-8?q?=EB=84=A4=ED=8A=B8=EC=9B=8C=ED=81=AC=20=ED=98=B8=EC=B6=9C=20?= =?UTF-8?q?=EC=B5=9C=EC=A0=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/atoms/RowVirtualizer.svelte | 6 +- src/lib/indexedDB/filesystem.ts | 4 - src/lib/modules/file/index.ts | 2 +- .../file/{upload.ts => upload.svelte.ts} | 108 +++++----- src/lib/modules/filesystem.ts | 116 ----------- src/lib/modules/filesystem2.svelte.ts | 191 ++++++++++++++++++ src/lib/server/db/category.ts | 2 - src/lib/server/db/file.ts | 2 - src/lib/stores/file.ts | 24 --- src/lib/types/filesystem.d.ts | 2 + src/lib/utils/index.ts | 1 + src/lib/utils/promise.ts | 16 ++ src/lib/utils/sort.ts | 3 +- .../(fullscreen)/file/uploads/+page.svelte | 17 +- .../(fullscreen)/file/uploads/File.svelte | 37 ++-- .../(main)/directory/[[id]]/+page.svelte | 189 +++++++++-------- .../DirectoryEntries/DirectoryEntries.svelte | 117 +++-------- .../[[id]]/DirectoryEntries/File.svelte | 72 +++---- .../DirectoryEntries/SubDirectory.svelte | 43 ++-- .../DirectoryEntries/UploadingFile.svelte | 51 +++-- .../[[id]]/DirectoryEntries/service.ts | 1 - .../directory/[[id]]/UploadStatusCard.svelte | 20 +- .../(main)/directory/[[id]]/service.svelte.ts | 15 +- src/routes/+layout.svelte | 5 +- src/trpc/routers/directory.ts | 23 ++- 25 files changed, 507 insertions(+), 560 deletions(-) rename src/lib/modules/file/{upload.ts => upload.svelte.ts} (77%) create mode 100644 src/lib/modules/filesystem2.svelte.ts create mode 100644 src/lib/types/filesystem.d.ts create mode 100644 src/lib/utils/promise.ts delete mode 100644 src/routes/(main)/directory/[[id]]/DirectoryEntries/service.ts diff --git a/src/lib/components/atoms/RowVirtualizer.svelte b/src/lib/components/atoms/RowVirtualizer.svelte index e821c5f..55dc7a1 100644 --- a/src/lib/components/atoms/RowVirtualizer.svelte +++ b/src/lib/components/atoms/RowVirtualizer.svelte @@ -1,6 +1,6 @@
diff --git a/src/lib/indexedDB/filesystem.ts b/src/lib/indexedDB/filesystem.ts index cf60b93..107009d 100644 --- a/src/lib/indexedDB/filesystem.ts +++ b/src/lib/indexedDB/filesystem.ts @@ -1,7 +1,5 @@ import { Dexie, type EntityTable } from "dexie"; -export type DirectoryId = "root" | number; - interface DirectoryInfo { id: number; parentId: DirectoryId; @@ -18,8 +16,6 @@ interface FileInfo { categoryIds: number[]; } -export type CategoryId = "root" | number; - interface CategoryInfo { id: number; parentId: CategoryId; diff --git a/src/lib/modules/file/index.ts b/src/lib/modules/file/index.ts index 42a5613..3b99989 100644 --- a/src/lib/modules/file/index.ts +++ b/src/lib/modules/file/index.ts @@ -1,3 +1,3 @@ export * from "./cache"; export * from "./download"; -export * from "./upload"; +export * from "./upload.svelte"; diff --git a/src/lib/modules/file/upload.ts b/src/lib/modules/file/upload.svelte.ts similarity index 77% rename from src/lib/modules/file/upload.ts rename to src/lib/modules/file/upload.svelte.ts index 31aabd8..da205f3 100644 --- a/src/lib/modules/file/upload.ts +++ b/src/lib/modules/file/upload.svelte.ts @@ -1,7 +1,6 @@ import axios from "axios"; import ExifReader from "exifreader"; import { limitFunction } from "p-limit"; -import { writable, type Writable } from "svelte/store"; import { encodeToBase64, generateDataKey, @@ -17,14 +16,45 @@ import type { FileUploadRequest, FileUploadResponse, } from "$lib/server/schemas"; -import { - fileUploadStatusStore, - type MasterKey, - type HmacSecret, - type FileUploadStatus, -} from "$lib/stores"; +import type { MasterKey, HmacSecret } from "$lib/stores"; import { trpc } from "$trpc/client"; +export interface FileUploadState { + name: string; + parentId: DirectoryId; + status: + | "encryption-pending" + | "encrypting" + | "upload-pending" + | "uploading" + | "uploaded" + | "canceled" + | "error"; + progress?: number; + rate?: number; + estimated?: number; +} + +export type LiveFileUploadState = FileUploadState & { + status: "encryption-pending" | "encrypting" | "upload-pending" | "uploading"; +}; + +let uploadingFiles: FileUploadState[] = $state([]); + +const isFileUploading = (status: FileUploadState["status"]) => + ["encryption-pending", "encrypting", "upload-pending", "uploading"].includes(status); + +export const getUploadingFiles = (parentId?: DirectoryId) => { + return uploadingFiles.filter( + (file): file is LiveFileUploadState => + (parentId === undefined || file.parentId === parentId) && isFileUploading(file.status), + ); +}; + +export const clearUploadedFiles = () => { + uploadingFiles = uploadingFiles.filter((file) => isFileUploading(file.status)); +}; + const requestDuplicateFileScan = limitFunction( async (file: File, hmacSecret: HmacSecret, onDuplicate: () => Promise) => { const fileBuffer = await file.arrayBuffer(); @@ -76,16 +106,8 @@ const extractExifDateTime = (fileBuffer: ArrayBuffer) => { }; const encryptFile = limitFunction( - async ( - status: Writable, - file: File, - fileBuffer: ArrayBuffer, - masterKey: MasterKey, - ) => { - status.update((value) => { - value.status = "encrypting"; - return value; - }); + async (state: FileUploadState, file: File, fileBuffer: ArrayBuffer, masterKey: MasterKey) => { + state.status = "encrypting"; const fileType = getFileType(file); @@ -109,10 +131,7 @@ const encryptFile = limitFunction( const thumbnailBuffer = await thumbnail?.arrayBuffer(); const thumbnailEncrypted = thumbnailBuffer && (await encryptData(thumbnailBuffer, dataKey)); - status.update((value) => { - value.status = "upload-pending"; - return value; - }); + state.status = "upload-pending"; return { dataKeyWrapped, @@ -130,20 +149,14 @@ const encryptFile = limitFunction( ); const requestFileUpload = limitFunction( - async (status: Writable, form: FormData, thumbnailForm: FormData | null) => { - status.update((value) => { - value.status = "uploading"; - return value; - }); + async (state: FileUploadState, form: FormData, thumbnailForm: FormData | null) => { + state.status = "uploading"; const res = await axios.post("/api/file/upload", form, { onUploadProgress: ({ progress, rate, estimated }) => { - status.update((value) => { - value.progress = progress; - value.rate = rate; - value.estimated = estimated; - return value; - }); + state.progress = progress; + state.rate = rate; + state.estimated = estimated; }, }); const { file }: FileUploadResponse = res.data; @@ -157,10 +170,7 @@ const requestFileUpload = limitFunction( } } - status.update((value) => { - value.status = "uploaded"; - return value; - }); + state.status = "uploaded"; return { fileId: file }; }, @@ -176,15 +186,12 @@ export const uploadFile = async ( ): Promise< { fileId: number; fileBuffer: ArrayBuffer; thumbnailBuffer?: ArrayBuffer } | undefined > => { - const status = writable({ + uploadingFiles.push({ name: file.name, parentId, status: "encryption-pending", }); - fileUploadStatusStore.update((value) => { - value.push(status); - return value; - }); + const state = uploadingFiles.at(-1)!; try { const { fileBuffer, fileSigned } = await requestDuplicateFileScan( @@ -193,14 +200,8 @@ export const uploadFile = async ( onDuplicate, ); if (!fileBuffer || !fileSigned) { - status.update((value) => { - value.status = "canceled"; - return value; - }); - fileUploadStatusStore.update((value) => { - value = value.filter((v) => v !== status); - return value; - }); + state.status = "canceled"; + uploadingFiles = uploadingFiles.filter((file) => file !== state); return undefined; } @@ -214,7 +215,7 @@ export const uploadFile = async ( createdAtEncrypted, lastModifiedAtEncrypted, thumbnail, - } = await encryptFile(status, file, fileBuffer, masterKey); + } = await encryptFile(state, file, fileBuffer, masterKey); const form = new FormData(); form.set( @@ -252,13 +253,10 @@ export const uploadFile = async ( thumbnailForm.set("content", new Blob([thumbnail.ciphertext])); } - const { fileId } = await requestFileUpload(status, form, thumbnailForm); + const { fileId } = await requestFileUpload(state, form, thumbnailForm); return { fileId, fileBuffer, thumbnailBuffer: thumbnail?.plaintext }; } catch (e) { - status.update((value) => { - value.status = "error"; - return value; - }); + state.status = "error"; throw e; } }; diff --git a/src/lib/modules/filesystem.ts b/src/lib/modules/filesystem.ts index f2995ef..e01145b 100644 --- a/src/lib/modules/filesystem.ts +++ b/src/lib/modules/filesystem.ts @@ -1,10 +1,5 @@ import { get, writable, type Writable } from "svelte/store"; import { - getDirectoryInfos as getDirectoryInfosFromIndexedDB, - getDirectoryInfo as getDirectoryInfoFromIndexedDB, - storeDirectoryInfo, - deleteDirectoryInfo, - getFileInfos as getFileInfosFromIndexedDB, getFileInfo as getFileInfoFromIndexedDB, storeFileInfo, deleteFileInfo, @@ -13,32 +8,10 @@ import { storeCategoryInfo, updateCategoryInfo as updateCategoryInfoInIndexedDB, deleteCategoryInfo, - type DirectoryId, - type CategoryId, } from "$lib/indexedDB"; import { unwrapDataKey, decryptString } from "$lib/modules/crypto"; import { trpc, isTRPCClientError } from "$trpc/client"; -export type DirectoryInfo = - | { - id: "root"; - parentId?: undefined; - dataKey?: undefined; - dataKeyVersion?: undefined; - name?: undefined; - subDirectoryIds: number[]; - fileIds: number[]; - } - | { - id: number; - parentId: DirectoryId; - dataKey?: CryptoKey; - dataKeyVersion?: Date; - name: string; - subDirectoryIds: number[]; - fileIds: number[]; - }; - export interface FileInfo { id: number; parentId: DirectoryId; @@ -72,98 +45,9 @@ export type CategoryInfo = isFileRecursive: boolean; }; -const directoryInfoStore = new Map>(); const fileInfoStore = new Map>(); const categoryInfoStore = 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, - parentId: directory.parentId, - name: directory.name, - subDirectoryIds, - fileIds, - }); - } -}; - -const fetchDirectoryInfoFromServer = async ( - id: DirectoryId, - info: Writable, - masterKey: CryptoKey, -) => { - let data; - try { - data = await trpc().directory.get.query({ id }); - } catch (e) { - if (isTRPCClientError(e) && e.data?.code === "NOT_FOUND") { - info.set(null); - await deleteDirectoryInfo(id as number); - return; - } - throw new Error("Failed to fetch directory information"); - } - - const { metadata, subDirectories: subDirectoryIds, files: fileIds } = data; - - 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, - parentId: metadata!.parent, - 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); // Intended - return info; -}; - const fetchFileInfoFromIndexedDB = async (id: number, info: Writable) => { if (get(info)) return; diff --git a/src/lib/modules/filesystem2.svelte.ts b/src/lib/modules/filesystem2.svelte.ts new file mode 100644 index 0000000..ade5342 --- /dev/null +++ b/src/lib/modules/filesystem2.svelte.ts @@ -0,0 +1,191 @@ +import { + getDirectoryInfos as getDirectoryInfosFromIndexedDB, + getDirectoryInfo as getDirectoryInfoFromIndexedDB, + storeDirectoryInfo, + deleteDirectoryInfo, + getFileInfos as getFileInfosFromIndexedDB, + getFileInfo as getFileInfoFromIndexedDB, + storeFileInfo, + deleteFileInfo, + getCategoryInfos as getCategoryInfosFromIndexedDB, + getCategoryInfo as getCategoryInfoFromIndexedDB, + storeCategoryInfo, + updateCategoryInfo as updateCategoryInfoInIndexedDB, + deleteCategoryInfo, +} from "$lib/indexedDB"; +import { unwrapDataKey, decryptString } from "$lib/modules/crypto"; +import { monotonicResolve } from "$lib/utils"; +import { trpc, isTRPCClientError } from "$trpc/client"; + +type DataKey = { key: CryptoKey; version: Date }; + +interface LocalDirectoryInfo { + id: number; + parentId: DirectoryId; + dataKey?: DataKey; + name: string; + subDirectories: SubDirectoryInfo[]; + files: SummarizedFileInfo[]; +} + +interface RootDirectoryInfo { + id: "root"; + parentId?: undefined; + dataKey?: undefined; + dataKeyVersion?: undefined; + name?: undefined; + subDirectories: SubDirectoryInfo[]; + files: SummarizedFileInfo[]; +} + +export type DirectoryInfo = LocalDirectoryInfo | RootDirectoryInfo; +export type SubDirectoryInfo = Omit; + +interface FileInfo { + id: number; + parentId: DirectoryId; + dataKey?: DataKey; + contentType: string; + contentIv: string | undefined; + name: string; + createdAt?: Date; + lastModifiedAt: Date; + categories: { id: number; name: string }[]; +} + +export type SummarizedFileInfo = Omit; + +interface LocalCategoryInfo { + id: number; + dataKey: DataKey | undefined; + name: string; + subCategories: Omit[]; + files: { id: number; name: string; isRecursive: boolean }[]; + isFileRecursive: boolean; +} + +interface RootCategoryInfo { + id: "root"; + dataKey?: undefined; + name?: undefined; + subCategories: Omit[]; + files?: undefined; +} + +export type CategoryInfo = LocalCategoryInfo | RootCategoryInfo; + +const directoryInfoCache = new Map>(); + +export const getDirectoryInfo = async (id: DirectoryId, masterKey: CryptoKey) => { + const info = directoryInfoCache.get(id); + if (info instanceof Promise) { + return info; + } + + const { promise, resolve } = Promise.withResolvers(); + if (!info) { + directoryInfoCache.set(id, promise); + } + + monotonicResolve( + [!info && fetchDirectoryInfoFromIndexedDB(id), fetchDirectoryInfoFromServer(id, masterKey)], + (directoryInfo) => { + let info = directoryInfoCache.get(id); + if (info instanceof Promise) { + const state = $state(directoryInfo); + directoryInfoCache.set(id, state); + resolve(state); + } else { + Object.assign(info!, directoryInfo); + resolve(info!); + } + }, + ); + return info ?? promise; +}; + +const fetchDirectoryInfoFromIndexedDB = async ( + id: DirectoryId, +): Promise => { + const [directory, subDirectories, files] = await Promise.all([ + id !== "root" ? getDirectoryInfoFromIndexedDB(id) : undefined, + getDirectoryInfosFromIndexedDB(id), + getFileInfosFromIndexedDB(id), + ]); + + if (id === "root") { + return { id, subDirectories, files }; + } else if (directory) { + return { id, parentId: directory.parentId, name: directory.name, subDirectories, files }; + } +}; + +const fetchDirectoryInfoFromServer = async ( + id: DirectoryId, + masterKey: CryptoKey, +): Promise => { + try { + const { + metadata, + subDirectories: subDirectoriesRaw, + files: filesRaw, + } = await trpc().directory.get.query({ id }); + const [subDirectories, files] = await Promise.all([ + Promise.all( + subDirectoriesRaw.map(async (directory) => { + const { dataKey } = await unwrapDataKey(directory.dek, masterKey); + const name = await decryptString(directory.name, directory.nameIv, dataKey); + return { + id: directory.id, + dataKey: { key: dataKey, version: directory.dekVersion }, + name, + }; + }), + ), + Promise.all( + filesRaw.map(async (file) => { + const { dataKey } = await unwrapDataKey(file.dek, masterKey); + const [name, createdAt, lastModifiedAt] = await Promise.all([ + decryptString(file.name, file.nameIv, dataKey), + file.createdAt ? decryptDate(file.createdAt, file.createdAtIv!, dataKey) : undefined, + decryptDate(file.lastModifiedAt, file.lastModifiedAtIv, dataKey), + ]); + return { + id: file.id, + dataKey: { key: dataKey, version: file.dekVersion }, + contentType: file.contentType, + name, + createdAt, + lastModifiedAt, + }; + }), + ), + ]); + + if (id === "root") { + return { id, subDirectories, files }; + } else { + const { dataKey } = await unwrapDataKey(metadata!.dek, masterKey); + const name = await decryptString(metadata!.name, metadata!.nameIv, dataKey); + return { + id, + parentId: metadata!.parent, + dataKey: { key: dataKey, version: metadata!.dekVersion }, + name, + subDirectories, + files, + }; + } + } catch (e) { + if (isTRPCClientError(e) && e.data?.code === "NOT_FOUND") { + directoryInfoCache.delete(id); + await deleteDirectoryInfo(id as number); + return; + } + throw new Error("Failed to fetch directory information"); + } +}; + +const decryptDate = async (ciphertext: string, iv: string, dataKey: CryptoKey) => { + return new Date(parseInt(await decryptString(ciphertext, iv, dataKey), 10)); +}; diff --git a/src/lib/server/db/category.ts b/src/lib/server/db/category.ts index f5c22ff..e20138c 100644 --- a/src/lib/server/db/category.ts +++ b/src/lib/server/db/category.ts @@ -2,8 +2,6 @@ import { IntegrityError } from "./error"; import db from "./kysely"; import type { Ciphertext } from "./schema"; -export type CategoryId = "root" | number; - interface Category { id: number; parentId: CategoryId; diff --git a/src/lib/server/db/file.ts b/src/lib/server/db/file.ts index c3169fc..a524ff4 100644 --- a/src/lib/server/db/file.ts +++ b/src/lib/server/db/file.ts @@ -4,8 +4,6 @@ import { IntegrityError } from "./error"; import db from "./kysely"; import type { Ciphertext } from "./schema"; -export type DirectoryId = "root" | number; - interface Directory { id: number; parentId: DirectoryId; diff --git a/src/lib/stores/file.ts b/src/lib/stores/file.ts index 61db95d..0aab6d1 100644 --- a/src/lib/stores/file.ts +++ b/src/lib/stores/file.ts @@ -1,21 +1,5 @@ import { writable, type Writable } from "svelte/store"; -export interface FileUploadStatus { - name: string; - parentId: "root" | number; - status: - | "encryption-pending" - | "encrypting" - | "upload-pending" - | "uploading" - | "uploaded" - | "canceled" - | "error"; - progress?: number; - rate?: number; - estimated?: number; -} - export interface FileDownloadStatus { id: number; status: @@ -32,16 +16,8 @@ export interface FileDownloadStatus { result?: ArrayBuffer; } -export const fileUploadStatusStore = writable[]>([]); - export const fileDownloadStatusStore = writable[]>([]); -export const isFileUploading = ( - status: FileUploadStatus["status"], -): status is "encryption-pending" | "encrypting" | "upload-pending" | "uploading" => { - return ["encryption-pending", "encrypting", "upload-pending", "uploading"].includes(status); -}; - export const isFileDownloading = ( status: FileDownloadStatus["status"], ): status is "download-pending" | "downloading" | "decryption-pending" | "decrypting" => { diff --git a/src/lib/types/filesystem.d.ts b/src/lib/types/filesystem.d.ts new file mode 100644 index 0000000..2cb91a7 --- /dev/null +++ b/src/lib/types/filesystem.d.ts @@ -0,0 +1,2 @@ +type DirectoryId = "root" | number; +type CategoryId = "root" | number; diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts index 1db9577..9dc3631 100644 --- a/src/lib/utils/index.ts +++ b/src/lib/utils/index.ts @@ -1,3 +1,4 @@ export * from "./format"; export * from "./gotoStateful"; +export * from "./promise"; export * from "./sort"; diff --git a/src/lib/utils/promise.ts b/src/lib/utils/promise.ts new file mode 100644 index 0000000..a4b0fb2 --- /dev/null +++ b/src/lib/utils/promise.ts @@ -0,0 +1,16 @@ +export const monotonicResolve = ( + promises: (Promise | false)[], + callback: (value: T) => void, +) => { + let latestResolvedIndex = -1; + + promises.forEach((promise, index) => { + if (!promise) return; + promise.then((value) => { + if (value !== undefined && index > latestResolvedIndex) { + latestResolvedIndex = index; + callback(value); + } + }); + }); +}; diff --git a/src/lib/utils/sort.ts b/src/lib/utils/sort.ts index 2385e55..a92c444 100644 --- a/src/lib/utils/sort.ts +++ b/src/lib/utils/sort.ts @@ -32,7 +32,7 @@ const sortByDateAsc: SortFunc = ({ date: a }, { date: b }) => { const sortByDateDesc: SortFunc = (a, b) => -sortByDateAsc(a, b); -export const sortEntries = (entries: T[], sortBy: SortBy) => { +export const sortEntries = (entries: T[], sortBy = SortBy.NAME_ASC) => { let sortFunc: SortFunc; switch (sortBy) { @@ -54,4 +54,5 @@ export const sortEntries = (entries: T[], sortBy: SortBy) = } entries.sort(sortFunc); + return entries; }; diff --git a/src/routes/(fullscreen)/file/uploads/+page.svelte b/src/routes/(fullscreen)/file/uploads/+page.svelte index d456322..687b72b 100644 --- a/src/routes/(fullscreen)/file/uploads/+page.svelte +++ b/src/routes/(fullscreen)/file/uploads/+page.svelte @@ -1,19 +1,10 @@ @@ -23,8 +14,8 @@
- {#each uploadingFiles as status} - + {#each getUploadingFiles() as file} + {/each}
diff --git a/src/routes/(fullscreen)/file/uploads/File.svelte b/src/routes/(fullscreen)/file/uploads/File.svelte index 2435240..7b40ac5 100644 --- a/src/routes/(fullscreen)/file/uploads/File.svelte +++ b/src/routes/(fullscreen)/file/uploads/File.svelte @@ -1,6 +1,5 @@
- {#if $status.status === "encryption-pending"} + {#if state.status === "encryption-pending"} - {:else if $status.status === "encrypting"} + {:else if state.status === "encrypting"} - {:else if $status.status === "upload-pending"} + {:else if state.status === "upload-pending"} - {:else if $status.status === "uploading"} + {:else if state.status === "uploading"} - {:else if $status.status === "uploaded"} + {:else if state.status === "uploaded"} - {:else if $status.status === "error"} + {:else if state.status === "error"} {/if}
-

- {$status.name} +

+ {state.name}

- {#if $status.status === "encryption-pending"} + {#if state.status === "encryption-pending"} 준비 중 - {:else if $status.status === "encrypting"} + {:else if state.status === "encrypting"} 암호화하는 중 - {:else if $status.status === "upload-pending"} + {:else if state.status === "upload-pending"} 업로드를 기다리는 중 - {:else if $status.status === "uploading"} + {:else if state.status === "uploading"} 전송됨 - {Math.floor(($status.progress ?? 0) * 100)}% · {formatNetworkSpeed(($status.rate ?? 0) * 8)} - {:else if $status.status === "uploaded"} + {Math.floor((state.progress ?? 0) * 100)}% · {formatNetworkSpeed((state.rate ?? 0) * 8)} + {:else if state.status === "uploaded"} 업로드 완료 - {:else if $status.status === "error"} + {:else if state.status === "error"} 업로드 실패 {/if}

diff --git a/src/routes/(main)/directory/[[id]]/+page.svelte b/src/routes/(main)/directory/[[id]]/+page.svelte index a4edf30..1bd2c5c 100644 --- a/src/routes/(main)/directory/[[id]]/+page.svelte +++ b/src/routes/(main)/directory/[[id]]/+page.svelte @@ -1,11 +1,10 @@ @@ -89,106 +88,106 @@ -
- {#if showTopBar} - - {/if} - {#if $info} -
-
- goto("/file/uploads")} /> - goto("/file/downloads")} /> -
- {#key $info} +{#await infoPromise then info} + {#if info} +
+ {#if showTopBar} + + {/if} +
+
+ goto("/file/uploads")} /> + goto("/file/downloads")} /> +
goto(`/${type}/${id}`)} onEntryMenuClick={(entry) => { context.selectedEntry = entry; isEntryMenuBottomSheetOpen = true; }} - showParentEntry={isFromFilePage && $info.parentId !== undefined} + showParentEntry={isFromFilePage && info.parentId !== undefined} onParentClick={() => goto( - $info.parentId === "root" + info.parentId === "root" ? "/directory?from=file" - : `/directory/${$info.parentId}?from=file`, + : `/directory/${info.parentId}?from=file`, )} /> - {/key} +
+ + { + isEntryCreateBottomSheetOpen = true; + }} + class="bottom-24 right-4" + /> + { + isEntryCreateBottomSheetOpen = false; + isDirectoryCreateModalOpen = true; + }} + onFileUploadClick={() => { + isEntryCreateBottomSheetOpen = false; + fileInput?.click(); + }} + /> + { + if (await requestDirectoryCreation(name, data.id, $masterKeyStore?.get(1)!)) { + infoPromise = getDirectoryInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME + return true; + } + return false; + }} + /> + { + resolveForDuplicateFileModal?.(false); + isDuplicateFileModalOpen = false; + }} + onUploadClick={() => { + resolveForDuplicateFileModal?.(true); + isDuplicateFileModalOpen = false; + }} + /> + + { + isEntryMenuBottomSheetOpen = false; + isEntryRenameModalOpen = true; + }} + onDeleteClick={() => { + isEntryMenuBottomSheetOpen = false; + isEntryDeleteModalOpen = true; + }} + /> + { + if (await requestEntryRename(context.selectedEntry!, newName)) { + infoPromise = getDirectoryInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME + return true; + } + return false; + }} + /> + { + if (await requestEntryDeletion(context.selectedEntry!)) { + infoPromise = getDirectoryInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME + return true; + } + return false; + }} + /> {/if} -
- - { - isEntryCreateBottomSheetOpen = true; - }} - class="bottom-24 right-4" -/> - { - isEntryCreateBottomSheetOpen = false; - isDirectoryCreateModalOpen = true; - }} - onFileUploadClick={() => { - isEntryCreateBottomSheetOpen = false; - fileInput?.click(); - }} -/> - { - if (await requestDirectoryCreation(name, data.id, $masterKeyStore?.get(1)!)) { - info = getDirectoryInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME - return true; - } - return false; - }} -/> - { - resolveForDuplicateFileModal?.(false); - isDuplicateFileModalOpen = false; - }} - onUploadClick={() => { - resolveForDuplicateFileModal?.(true); - isDuplicateFileModalOpen = false; - }} -/> - - { - isEntryMenuBottomSheetOpen = false; - isEntryRenameModalOpen = true; - }} - onDeleteClick={() => { - isEntryMenuBottomSheetOpen = false; - isEntryDeleteModalOpen = true; - }} -/> - { - if (await requestEntryRename(context.selectedEntry!, newName)) { - info = getDirectoryInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME - return true; - } - return false; - }} -/> - { - if (await requestEntryDeletion(context.selectedEntry!)) { - info = getDirectoryInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME - return true; - } - return false; - }} -/> +{/await} diff --git a/src/routes/(main)/directory/[[id]]/DirectoryEntries/DirectoryEntries.svelte b/src/routes/(main)/directory/[[id]]/DirectoryEntries/DirectoryEntries.svelte index 527bd1b..b761176 100644 --- a/src/routes/(main)/directory/[[id]]/DirectoryEntries/DirectoryEntries.svelte +++ b/src/routes/(main)/directory/[[id]]/DirectoryEntries/DirectoryEntries.svelte @@ -1,21 +1,9 @@ {#if subDirectories.length + files.length > 0 || showParentEntry} @@ -124,8 +55,8 @@ {/if} - {#each subDirectories as { info }} - + {#each subDirectories as subDirectory} + {/each} {#if files.length > 0} {#if file.type === "file"} - + {:else} - + {/if}
{/snippet} diff --git a/src/routes/(main)/directory/[[id]]/DirectoryEntries/File.svelte b/src/routes/(main)/directory/[[id]]/DirectoryEntries/File.svelte index 67d7e36..fdc225c 100644 --- a/src/routes/(main)/directory/[[id]]/DirectoryEntries/File.svelte +++ b/src/routes/(main)/directory/[[id]]/DirectoryEntries/File.svelte @@ -1,66 +1,46 @@ -{#if $info} - + action(onclick)} + actionButtonIcon={IconMoreVert} + onActionButtonClick={() => action(onOpenMenuClick)} +> + {#await thumbnailPromise then thumbnail} - -{/if} + {/await} + diff --git a/src/routes/(main)/directory/[[id]]/DirectoryEntries/SubDirectory.svelte b/src/routes/(main)/directory/[[id]]/DirectoryEntries/SubDirectory.svelte index 5454695..0d65cc2 100644 --- a/src/routes/(main)/directory/[[id]]/DirectoryEntries/SubDirectory.svelte +++ b/src/routes/(main)/directory/[[id]]/DirectoryEntries/SubDirectory.svelte @@ -1,44 +1,29 @@ -{#if $info} - - - -{/if} + action(onclick)} + actionButtonIcon={IconMoreVert} + onActionButtonClick={() => action(onOpenMenuClick)} +> + + diff --git a/src/routes/(main)/directory/[[id]]/DirectoryEntries/UploadingFile.svelte b/src/routes/(main)/directory/[[id]]/DirectoryEntries/UploadingFile.svelte index bf5e85a..0ec7263 100644 --- a/src/routes/(main)/directory/[[id]]/DirectoryEntries/UploadingFile.svelte +++ b/src/routes/(main)/directory/[[id]]/DirectoryEntries/UploadingFile.svelte @@ -1,38 +1,35 @@ -{#if isFileUploading($status.status)} -
-
- -
-
-

- {$status.name} -

-

- {#if $status.status === "encryption-pending"} - 준비 중 - {:else if $status.status === "encrypting"} - 암호화하는 중 - {:else if $status.status === "upload-pending"} - 업로드를 기다리는 중 - {:else if $status.status === "uploading"} - 전송됨 {Math.floor(($status.progress ?? 0) * 100)}% · - {formatNetworkSpeed(($status.rate ?? 0) * 8)} - {/if} -

-
+
+
+
-{/if} +
+

+ {state.name} +

+

+ {#if state.status === "encryption-pending"} + 준비 중 + {:else if state.status === "encrypting"} + 암호화하는 중 + {:else if state.status === "upload-pending"} + 업로드를 기다리는 중 + {:else if state.status === "uploading"} + 전송됨 {Math.floor((state.progress ?? 0) * 100)}% · + {formatNetworkSpeed((state.rate ?? 0) * 8)} + {/if} +

+
+
diff --git a/src/routes/(main)/directory/[[id]]/DirectoryEntries/service.ts b/src/routes/(main)/directory/[[id]]/DirectoryEntries/service.ts deleted file mode 100644 index d4b47f8..0000000 --- a/src/routes/(main)/directory/[[id]]/DirectoryEntries/service.ts +++ /dev/null @@ -1 +0,0 @@ -export { requestFileThumbnailDownload } from "$lib/services/file"; diff --git a/src/routes/(main)/directory/[[id]]/UploadStatusCard.svelte b/src/routes/(main)/directory/[[id]]/UploadStatusCard.svelte index 1ac40b3..578c368 100644 --- a/src/routes/(main)/directory/[[id]]/UploadStatusCard.svelte +++ b/src/routes/(main)/directory/[[id]]/UploadStatusCard.svelte @@ -1,7 +1,5 @@ {#if uploadingFiles.length > 0} diff --git a/src/routes/(main)/directory/[[id]]/service.svelte.ts b/src/routes/(main)/directory/[[id]]/service.svelte.ts index c94cc1e..db1f114 100644 --- a/src/routes/(main)/directory/[[id]]/service.svelte.ts +++ b/src/routes/(main)/directory/[[id]]/service.svelte.ts @@ -14,8 +14,7 @@ import { trpc } from "$trpc/client"; export interface SelectedEntry { type: "directory" | "file"; id: number; - dataKey: CryptoKey; - dataKeyVersion: Date; + dataKey: { key: CryptoKey; version: Date } | undefined; name: string; } @@ -97,20 +96,26 @@ export const requestFileUpload = async ( }; export const requestEntryRename = async (entry: SelectedEntry, newName: string) => { - const newNameEncrypted = await encryptString(newName, entry.dataKey); + if (!entry.dataKey) { + // TODO: Error Handling + console.log("hi"); + return false; + } + + const newNameEncrypted = await encryptString(newName, entry.dataKey.key); try { if (entry.type === "directory") { await trpc().directory.rename.mutate({ id: entry.id, - dekVersion: entry.dataKeyVersion, + dekVersion: entry.dataKey.version, name: newNameEncrypted.ciphertext, nameIv: newNameEncrypted.iv, }); } else { await trpc().file.rename.mutate({ id: entry.id, - dekVersion: entry.dataKeyVersion, + dekVersion: entry.dataKey.version, name: newNameEncrypted.ciphertext, nameIv: newNameEncrypted.iv, }); diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index e4bca97..612cfe4 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -2,10 +2,9 @@ import { onMount } from "svelte"; import { get } from "svelte/store"; import { goto as svelteGoto } from "$app/navigation"; + import { getUploadingFiles } from "$lib/modules/file"; import { - fileUploadStatusStore, fileDownloadStatusStore, - isFileUploading, isFileDownloading, clientKeyStore, masterKeyStore, @@ -16,7 +15,7 @@ const protectFileUploadAndDownload = (e: BeforeUnloadEvent) => { if ( - $fileUploadStatusStore.some((status) => isFileUploading(get(status).status)) || + getUploadingFiles().length > 0 || $fileDownloadStatusStore.some((status) => isFileDownloading(get(status).status)) ) { e.preventDefault(); diff --git a/src/trpc/routers/directory.ts b/src/trpc/routers/directory.ts index e060c23..6e1e358 100644 --- a/src/trpc/routers/directory.ts +++ b/src/trpc/routers/directory.ts @@ -32,8 +32,27 @@ const directoryRouter = router({ name: directory.encName.ciphertext, nameIv: directory.encName.iv, }, - subDirectories: directories.map(({ id }) => id), - files: files.map(({ id }) => id), + subDirectories: directories.map((directory) => ({ + id: directory.id, + mekVersion: directory.mekVersion, + dek: directory.encDek, + dekVersion: directory.dekVersion, + name: directory.encName.ciphertext, + nameIv: directory.encName.iv, + })), + files: files.map((file) => ({ + id: file.id, + mekVersion: file.mekVersion, + dek: file.encDek, + dekVersion: file.dekVersion, + contentType: file.contentType, + name: file.encName.ciphertext, + nameIv: file.encName.iv, + createdAt: file.encCreatedAt?.ciphertext, + createdAtIv: file.encCreatedAt?.iv, + lastModifiedAt: file.encLastModifiedAt.ciphertext, + lastModifiedAtIv: file.encLastModifiedAt.iv, + })), }; }),