From e4413ddbf64a7f8d2f5bd574f2d4eb2d5411a03a Mon Sep 17 00:00:00 2001 From: static Date: Tue, 30 Dec 2025 23:30:50 +0900 Subject: [PATCH] =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=EC=97=90=EC=84=9C=EC=9D=98=20=EB=84=A4=ED=8A=B8?= =?UTF-8?q?=EC=9B=8C=ED=81=AC=20=ED=98=B8=EC=B6=9C=20=EC=B5=9C=EC=A0=81?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../atoms/buttons/FileThumbnailButton.svelte | 48 ++-- src/lib/components/organisms/Gallery.svelte | 124 +++------- src/lib/modules/file/download.svelte.ts | 97 ++++++++ src/lib/modules/file/download.ts | 84 ------- src/lib/modules/file/index.ts | 2 +- src/lib/modules/filesystem.ts | 106 --------- src/lib/modules/filesystem2.svelte.ts | 99 +++++++- src/lib/server/db/file.ts | 13 +- src/lib/stores/file.ts | 25 -- src/lib/stores/index.ts | 1 - .../(fullscreen)/file/[id]/+page.svelte | 215 +++++++++--------- .../file/[id]/DownloadStatus.svelte | 21 +- .../(fullscreen)/file/downloads/+page.svelte | 28 +-- .../(fullscreen)/file/downloads/File.svelte | 94 ++++---- src/routes/(fullscreen)/gallery/+page.svelte | 17 +- .../(fullscreen)/settings/cache/+page.svelte | 18 +- .../(fullscreen)/settings/cache/File.svelte | 11 +- .../settings/thumbnail/+page.svelte | 21 +- .../settings/thumbnail/File.svelte | 36 ++- .../settings/thumbnail/service.svelte.ts | 14 +- .../[[id]]/DownloadStatusCard.svelte | 22 +- src/routes/(main)/home/+page.svelte | 19 +- src/routes/+layout.svelte | 15 +- src/trpc/routers/file.ts | 9 +- 24 files changed, 521 insertions(+), 618 deletions(-) create mode 100644 src/lib/modules/file/download.svelte.ts delete mode 100644 src/lib/modules/file/download.ts delete mode 100644 src/lib/modules/filesystem.ts delete mode 100644 src/lib/stores/file.ts diff --git a/src/lib/components/atoms/buttons/FileThumbnailButton.svelte b/src/lib/components/atoms/buttons/FileThumbnailButton.svelte index c18101c..c71ff6b 100644 --- a/src/lib/components/atoms/buttons/FileThumbnailButton.svelte +++ b/src/lib/components/atoms/buttons/FileThumbnailButton.svelte @@ -1,42 +1,34 @@ -{#if $info} - -{/if} + {/await} + diff --git a/src/lib/components/organisms/Gallery.svelte b/src/lib/components/organisms/Gallery.svelte index 1fcb4ff..fb96775 100644 --- a/src/lib/components/organisms/Gallery.svelte +++ b/src/lib/components/organisms/Gallery.svelte @@ -1,98 +1,48 @@ @@ -101,8 +51,8 @@ itemHeight={(index) => rows[index]!.type === "header" ? 28 - : Math.ceil(rows[index]!.items.length / 4) * 181 + - (Math.ceil(rows[index]!.items.length / 4) - 1) * 4 + + : Math.ceil(rows[index]!.files.length / 4) * 181 + + (Math.ceil(rows[index]!.files.length / 4) - 1) * 4 + 16} class="flex flex-grow flex-col" > @@ -112,8 +62,8 @@

{row.label}

{:else}
- {#each row.items as { info }} - + {#each row.files as file} + {/each}
{/if} @@ -123,8 +73,6 @@

{#if files.length === 0} 업로드된 파일이 없어요. - {:else if filesWithDate.length === 0} - 파일 목록을 불러오고 있어요. {:else} 사진 또는 동영상이 없어요. {/if} diff --git a/src/lib/modules/file/download.svelte.ts b/src/lib/modules/file/download.svelte.ts new file mode 100644 index 0000000..4c53ed0 --- /dev/null +++ b/src/lib/modules/file/download.svelte.ts @@ -0,0 +1,97 @@ +import axios from "axios"; +import { limitFunction } from "p-limit"; +import { decryptData } from "$lib/modules/crypto"; + +export interface FileDownloadState { + id: number; + status: + | "download-pending" + | "downloading" + | "decryption-pending" + | "decrypting" + | "decrypted" + | "canceled" + | "error"; + progress?: number; + rate?: number; + estimated?: number; + result?: ArrayBuffer; +} + +export type LiveFileDownloadState = FileDownloadState & { + status: "download-pending" | "downloading" | "decryption-pending" | "decrypting"; +}; + +let downloadingFiles: FileDownloadState[] = $state([]); + +export const isFileDownloading = ( + status: FileDownloadState["status"], +): status is LiveFileDownloadState["status"] => + ["download-pending", "downloading", "decryption-pending", "decrypting"].includes(status); + +export const getFileDownloadState = (fileId: number) => { + return downloadingFiles.find((file) => file.id === fileId && isFileDownloading(file.status)); +}; + +export const getDownloadingFiles = () => { + return downloadingFiles.filter((file): file is LiveFileDownloadState => + isFileDownloading(file.status), + ); +}; + +export const clearDownloadedFiles = () => { + downloadingFiles = downloadingFiles.filter((file) => isFileDownloading(file.status)); +}; + +const requestFileDownload = limitFunction( + async (state: FileDownloadState, id: number) => { + state.status = "download-pending"; + + const res = await axios.get(`/api/file/${id}/download`, { + responseType: "arraybuffer", + onDownloadProgress: ({ progress, rate, estimated }) => { + state.progress = progress; + state.rate = rate; + state.estimated = estimated; + }, + }); + const fileEncrypted: ArrayBuffer = res.data; + + state.status = "decryption-pending"; + return fileEncrypted; + }, + { concurrency: 1 }, +); + +const decryptFile = limitFunction( + async ( + state: FileDownloadState, + fileEncrypted: ArrayBuffer, + fileEncryptedIv: string, + dataKey: CryptoKey, + ) => { + state.status = "decrypting"; + + const fileBuffer = await decryptData(fileEncrypted, fileEncryptedIv, dataKey); + + state.status = "decrypted"; + state.result = fileBuffer; + return fileBuffer; + }, + { concurrency: 4 }, +); + +export const downloadFile = async (id: number, fileEncryptedIv: string, dataKey: CryptoKey) => { + downloadingFiles.push({ + id, + status: "download-pending", + }); + const state = downloadingFiles.at(-1)!; + + try { + return await decryptFile(state, await requestFileDownload(state, id), fileEncryptedIv, dataKey); + } catch (e) { + state.status = "error"; + throw e; + } +}; diff --git a/src/lib/modules/file/download.ts b/src/lib/modules/file/download.ts deleted file mode 100644 index b0efb30..0000000 --- a/src/lib/modules/file/download.ts +++ /dev/null @@ -1,84 +0,0 @@ -import axios from "axios"; -import { limitFunction } from "p-limit"; -import { writable, type Writable } from "svelte/store"; -import { decryptData } from "$lib/modules/crypto"; -import { fileDownloadStatusStore, type FileDownloadStatus } from "$lib/stores"; - -const requestFileDownload = limitFunction( - async (status: Writable, id: number) => { - status.update((value) => { - value.status = "downloading"; - return value; - }); - - const res = await axios.get(`/api/file/${id}/download`, { - responseType: "arraybuffer", - onDownloadProgress: ({ progress, rate, estimated }) => { - status.update((value) => { - value.progress = progress; - value.rate = rate; - value.estimated = estimated; - return value; - }); - }, - }); - const fileEncrypted: ArrayBuffer = res.data; - - status.update((value) => { - value.status = "decryption-pending"; - return value; - }); - return fileEncrypted; - }, - { concurrency: 1 }, -); - -const decryptFile = limitFunction( - async ( - status: Writable, - fileEncrypted: ArrayBuffer, - fileEncryptedIv: string, - dataKey: CryptoKey, - ) => { - status.update((value) => { - value.status = "decrypting"; - return value; - }); - - const fileBuffer = await decryptData(fileEncrypted, fileEncryptedIv, dataKey); - - status.update((value) => { - value.status = "decrypted"; - value.result = fileBuffer; - return value; - }); - return fileBuffer; - }, - { concurrency: 4 }, -); - -export const downloadFile = async (id: number, fileEncryptedIv: string, dataKey: CryptoKey) => { - const status = writable({ - id, - status: "download-pending", - }); - fileDownloadStatusStore.update((value) => { - value.push(status); - return value; - }); - - try { - return await decryptFile( - status, - await requestFileDownload(status, id), - fileEncryptedIv, - dataKey, - ); - } catch (e) { - status.update((value) => { - value.status = "error"; - return value; - }); - throw e; - } -}; diff --git a/src/lib/modules/file/index.ts b/src/lib/modules/file/index.ts index 3b99989..871d299 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 "./download.svelte"; export * from "./upload.svelte"; diff --git a/src/lib/modules/filesystem.ts b/src/lib/modules/filesystem.ts deleted file mode 100644 index 5020793..0000000 --- a/src/lib/modules/filesystem.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { get, writable, type Writable } from "svelte/store"; -import { - getFileInfo as getFileInfoFromIndexedDB, - storeFileInfo, - deleteFileInfo, -} from "$lib/indexedDB"; -import { unwrapDataKey, decryptString } from "$lib/modules/crypto"; -import { trpc, isTRPCClientError } from "$trpc/client"; - -export interface FileInfo { - id: number; - parentId: DirectoryId; - dataKey?: CryptoKey; - dataKeyVersion?: Date; - contentType: string; - contentIv?: string; - name: string; - createdAt?: Date; - lastModifiedAt: Date; - categoryIds: number[]; -} - -const fileInfoStore = new Map>(); - -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, -) => { - let metadata; - try { - metadata = await trpc().file.get.query({ id }); - } catch (e) { - if (isTRPCClientError(e) && e.data?.code === "NOT_FOUND") { - info.set(null); - await deleteFileInfo(id); - return; - } - throw new Error("Failed to fetch file information"); - } - 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, - parentId: metadata.parent, - dataKey, - dataKeyVersion: new Date(metadata.dekVersion), - contentType: metadata.contentType, - contentIv: metadata.contentIv, - name, - createdAt, - lastModifiedAt, - categoryIds: metadata.categories, - }); - await storeFileInfo({ - id, - parentId: metadata.parent, - name, - contentType: metadata.contentType, - createdAt, - lastModifiedAt, - categoryIds: metadata.categories, - }); -}; - -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); // Intended - return info; -}; diff --git a/src/lib/modules/filesystem2.svelte.ts b/src/lib/modules/filesystem2.svelte.ts index 01514cb..eeb3766 100644 --- a/src/lib/modules/filesystem2.svelte.ts +++ b/src/lib/modules/filesystem2.svelte.ts @@ -41,12 +41,12 @@ interface RootDirectoryInfo { export type DirectoryInfo = LocalDirectoryInfo | RootDirectoryInfo; export type SubDirectoryInfo = Omit; -interface FileInfo { +export interface FileInfo { id: number; parentId: DirectoryId; dataKey?: DataKey; contentType: string; - contentIv: string | undefined; + contentIv?: string; name: string; createdAt?: Date; lastModifiedAt: Date; @@ -81,6 +81,7 @@ export type SubCategoryInfo = Omit< >; const directoryInfoCache = new Map>(); +const fileInfoCache = new Map>(); const categoryInfoCache = new Map>(); export const getDirectoryInfo = async (id: DirectoryId, masterKey: CryptoKey) => { @@ -197,6 +198,100 @@ const decryptDate = async (ciphertext: string, iv: string, dataKey: CryptoKey) = return new Date(parseInt(await decryptString(ciphertext, iv, dataKey), 10)); }; +export const getFileInfo = async (id: number, masterKey: CryptoKey) => { + const info = fileInfoCache.get(id); + if (info instanceof Promise) { + return info; + } + + const { promise, resolve } = Promise.withResolvers(); + if (!info) { + fileInfoCache.set(id, promise); + } + + monotonicResolve( + [!info && fetchFileInfoFromIndexedDB(id), fetchFileInfoFromServer(id, masterKey)], + (fileInfo) => { + let info = fileInfoCache.get(id); + if (info instanceof Promise) { + const state = $state(fileInfo); + fileInfoCache.set(id, state); + resolve(state); + } else { + Object.assign(info!, fileInfo); + resolve(info!); + } + }, + ); + return info ?? promise; +}; + +const fetchFileInfoFromIndexedDB = async (id: number): Promise => { + const file = await getFileInfoFromIndexedDB(id); + const categories = await Promise.all( + file?.categoryIds.map(async (categoryId) => { + const categoryInfo = await getCategoryInfoFromIndexedDB(categoryId); + return categoryInfo ? { id: categoryId, name: categoryInfo.name } : undefined; + }) ?? [], + ); + + if (file) { + return { + id, + parentId: file.parentId, + contentType: file.contentType, + name: file.name, + createdAt: file.createdAt, + lastModifiedAt: file.lastModifiedAt, + categories: categories.filter((category) => !!category), + }; + } +}; + +const fetchFileInfoFromServer = async ( + id: number, + masterKey: CryptoKey, +): Promise => { + try { + const { categories: categoriesRaw, ...metadata } = await trpc().file.get.query({ id }); + const categories = await Promise.all( + categoriesRaw.map(async (category) => { + const { dataKey } = await unwrapDataKey(category.dek, masterKey); + const name = await decryptString(category.name, category.nameIv, dataKey); + return { id: category.id, name }; + }), + ); + + const { dataKey } = await unwrapDataKey(metadata.dek, masterKey); + const [name, createdAt, lastModifiedAt] = await Promise.all([ + decryptString(metadata.name, metadata.nameIv, dataKey), + metadata.createdAt + ? decryptDate(metadata.createdAt, metadata.createdAtIv!, dataKey) + : undefined, + decryptDate(metadata.lastModifiedAt, metadata.lastModifiedAtIv, dataKey), + ]); + + return { + id, + parentId: metadata.parent, + dataKey: { key: dataKey, version: new Date(metadata.dekVersion) }, + contentType: metadata.contentType, + contentIv: metadata.contentIv, + name, + createdAt, + lastModifiedAt, + categories, + }; + } catch (e) { + if (isTRPCClientError(e) && e.data?.code === "NOT_FOUND") { + fileInfoCache.delete(id); + await deleteFileInfo(id); + return; + } + throw new Error("Failed to fetch file information"); + } +}; + export const getCategoryInfo = async (id: CategoryId, masterKey: CryptoKey) => { const info = categoryInfoCache.get(id); if (info instanceof Promise) { diff --git a/src/lib/server/db/file.ts b/src/lib/server/db/file.ts index 7bea6db..45ae0f4 100644 --- a/src/lib/server/db/file.ts +++ b/src/lib/server/db/file.ts @@ -1,4 +1,4 @@ -import { sql, type NotNull } from "kysely"; +import { sql } from "kysely"; import pg from "pg"; import { IntegrityError } from "./error"; import db from "./kysely"; @@ -486,10 +486,17 @@ export const addFileToCategory = async (fileId: number, categoryId: number) => { export const getAllFileCategories = async (fileId: number) => { const categories = await db .selectFrom("file_category") - .select("category_id") + .innerJoin("category", "file_category.category_id", "category.id") + .selectAll("category") .where("file_id", "=", fileId) .execute(); - return categories.map(({ category_id }) => ({ id: category_id })); + return categories.map((category) => ({ + id: category.id, + mekVersion: category.master_encryption_key_version, + encDek: category.encrypted_data_encryption_key, + dekVersion: category.data_encryption_key_version, + encName: category.encrypted_name, + })); }; export const removeFileFromCategory = async (fileId: number, categoryId: number) => { diff --git a/src/lib/stores/file.ts b/src/lib/stores/file.ts deleted file mode 100644 index 0aab6d1..0000000 --- a/src/lib/stores/file.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { writable, type Writable } from "svelte/store"; - -export interface FileDownloadStatus { - id: number; - status: - | "download-pending" - | "downloading" - | "decryption-pending" - | "decrypting" - | "decrypted" - | "canceled" - | "error"; - progress?: number; - rate?: number; - estimated?: number; - result?: ArrayBuffer; -} - -export const fileDownloadStatusStore = writable[]>([]); - -export const isFileDownloading = ( - status: FileDownloadStatus["status"], -): status is "download-pending" | "downloading" | "decryption-pending" | "decrypting" => { - return ["download-pending", "downloading", "decryption-pending", "decrypting"].includes(status); -}; diff --git a/src/lib/stores/index.ts b/src/lib/stores/index.ts index 537209a..668f46f 100644 --- a/src/lib/stores/index.ts +++ b/src/lib/stores/index.ts @@ -1,2 +1 @@ -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 ab85dc7..d5ba57c 100644 --- a/src/routes/(fullscreen)/file/[id]/+page.svelte +++ b/src/routes/(fullscreen)/file/[id]/+page.svelte @@ -1,14 +1,14 @@ -{#if $status && isFileDownloading($status.status)} +{#if isFileDownloading(state.status)}

- {#if $status.status === "download-pending"} + {#if state.status === "download-pending"} 다운로드를 기다리는 중 - {:else if $status.status === "downloading"} + {:else if state.status === "downloading"} 다운로드하는 중 - {:else if $status.status === "decryption-pending"} + {:else if state.status === "decryption-pending"} 복호화를 기다리는 중 - {:else if $status.status === "decrypting"} + {:else if state.status === "decrypting"} 복호화하는 중 {/if}

- {#if $status.status === "downloading"} + {#if state.status === "downloading"} 전송됨 - {Math.floor(($status.progress ?? 0) * 100)}% · {formatNetworkSpeed(($status.rate ?? 0) * 8)} + {Math.floor((state.progress ?? 0) * 100)}% · {formatNetworkSpeed((state.rate ?? 0) * 8)} {/if}

diff --git a/src/routes/(fullscreen)/file/downloads/+page.svelte b/src/routes/(fullscreen)/file/downloads/+page.svelte index e1bad0e..f9bfa84 100644 --- a/src/routes/(fullscreen)/file/downloads/+page.svelte +++ b/src/routes/(fullscreen)/file/downloads/+page.svelte @@ -1,19 +1,21 @@ @@ -22,9 +24,9 @@ -
- {#each downloadingFiles as status} - + {#await downloadingFilesPromise then downloadingFiles} + {#each downloadingFiles as { state, fileInfo }} + {/each} -
+ {/await}
diff --git a/src/routes/(fullscreen)/file/downloads/File.svelte b/src/routes/(fullscreen)/file/downloads/File.svelte index 3bfe292..6a2a0ca 100644 --- a/src/routes/(fullscreen)/file/downloads/File.svelte +++ b/src/routes/(fullscreen)/file/downloads/File.svelte @@ -1,7 +1,6 @@ -{#if $fileInfo} -
-
- {#if $status.status === "download-pending"} - - {:else if $status.status === "downloading"} - - {:else if $status.status === "decryption-pending"} - - {:else if $status.status === "decrypting"} - - {:else if $status.status === "decrypted"} - - {:else if $status.status === "error"} - - {/if} -
-
-

- {$fileInfo.name} -

-

- {#if $status.status === "download-pending"} - 다운로드를 기다리는 중 - {:else if $status.status === "downloading"} - 전송됨 - {Math.floor(($status.progress ?? 0) * 100)}% · - {formatNetworkSpeed(($status.rate ?? 0) * 8)} - {:else if $status.status === "decryption-pending"} - 복호화를 기다리는 중 - {:else if $status.status === "decrypting"} - 복호화하는 중 - {:else if $status.status === "decrypted"} - 다운로드 완료 - {:else if $status.status === "error"} - 다운로드 실패 - {/if} -

-
+
+
+ {#if state.status === "download-pending"} + + {:else if state.status === "downloading"} + + {:else if state.status === "decryption-pending"} + + {:else if state.status === "decrypting"} + + {:else if state.status === "decrypted"} + + {:else if state.status === "error"} + + {/if}
-{/if} +
+

+ {info.name} +

+

+ {#if state.status === "download-pending"} + 다운로드를 기다리는 중 + {:else if state.status === "downloading"} + 전송됨 + {Math.floor((state.progress ?? 0) * 100)}% · + {formatNetworkSpeed((state.rate ?? 0) * 8)} + {:else if state.status === "decryption-pending"} + 복호화를 기다리는 중 + {:else if state.status === "decrypting"} + 복호화하는 중 + {:else if state.status === "decrypted"} + 다운로드 완료 + {:else if state.status === "error"} + 다운로드 실패 + {/if} +

+
+
diff --git a/src/routes/(fullscreen)/gallery/+page.svelte b/src/routes/(fullscreen)/gallery/+page.svelte index b6f8239..ab73c70 100644 --- a/src/routes/(fullscreen)/gallery/+page.svelte +++ b/src/routes/(fullscreen)/gallery/+page.svelte @@ -1,18 +1,20 @@ @@ -22,5 +24,8 @@ - goto(`/file/${id}?from=gallery`)} /> + !!file)} + onFileClick={({ id }) => goto(`/file/${id}?from=gallery`)} + /> diff --git a/src/routes/(fullscreen)/settings/cache/+page.svelte b/src/routes/(fullscreen)/settings/cache/+page.svelte index cf8192d..271ae96 100644 --- a/src/routes/(fullscreen)/settings/cache/+page.svelte +++ b/src/routes/(fullscreen)/settings/cache/+page.svelte @@ -1,18 +1,17 @@
- {#if $info} + {#if info}
@@ -28,8 +27,8 @@
{/if}
- {#if $info} -

{$info.name}

+ {#if info} +

{info.name}

{:else}

삭제된 파일

{/if} diff --git a/src/routes/(fullscreen)/settings/thumbnail/+page.svelte b/src/routes/(fullscreen)/settings/thumbnail/+page.svelte index d9cd692..8830133 100644 --- a/src/routes/(fullscreen)/settings/thumbnail/+page.svelte +++ b/src/routes/(fullscreen)/settings/thumbnail/+page.svelte @@ -5,7 +5,7 @@ import { BottomDiv, Button, FullscreenDiv } from "$lib/components/atoms"; import { IconEntryButton, TopBar } from "$lib/components/molecules"; import { deleteAllFileThumbnailCaches } from "$lib/modules/file"; - import { getFileInfo } from "$lib/modules/filesystem"; + import { getFileInfo } from "$lib/modules/filesystem2.svelte"; import { masterKeyStore } from "$lib/stores"; import File from "./File.svelte"; import { @@ -20,19 +20,20 @@ const generateAllThumbnails = () => { persistentStates.files.forEach(({ info }) => { - const fileInfo = get(info); - if (fileInfo) { - requestThumbnailGeneration(fileInfo); + if (info) { + requestThumbnailGeneration(info); } }); }; - onMount(() => { - persistentStates.files = data.files.map((fileId) => ({ - id: fileId, - info: getFileInfo(fileId, $masterKeyStore?.get(1)?.key!), - status: getGenerationStatus(fileId), - })); + onMount(async () => { + persistentStates.files = await Promise.all( + data.files.map(async (fileId) => ({ + id: fileId, + info: await getFileInfo(fileId, $masterKeyStore?.get(1)?.key!), + status: getGenerationStatus(fileId), + })), + ); }); diff --git a/src/routes/(fullscreen)/settings/thumbnail/File.svelte b/src/routes/(fullscreen)/settings/thumbnail/File.svelte index 93c23ad..e5699e2 100644 --- a/src/routes/(fullscreen)/settings/thumbnail/File.svelte +++ b/src/routes/(fullscreen)/settings/thumbnail/File.svelte @@ -13,34 +13,32 @@ import type { Writable } from "svelte/store"; import { ActionEntryButton } from "$lib/components/atoms"; import { DirectoryEntryLabel } from "$lib/components/molecules"; - import type { FileInfo } from "$lib/modules/filesystem"; + import type { FileInfo } from "$lib/modules/filesystem2.svelte"; import { formatDateTime } from "$lib/utils"; import type { GenerationStatus } from "./service.svelte"; import IconCamera from "~icons/material-symbols/camera"; interface Props { - info: Writable; - onclick: (selectedFile: FileInfo) => void; - onGenerateThumbnailClick: (selectedFile: FileInfo) => void; + info: FileInfo; + onclick: (file: FileInfo) => void; + onGenerateThumbnailClick: (file: FileInfo) => void; generationStatus?: Writable; } let { info, onclick, onGenerateThumbnailClick, generationStatus }: Props = $props(); -{#if $info} - onclick($info)} - actionButtonIcon={!$generationStatus || $generationStatus === "error" ? IconCamera : undefined} - onActionButtonClick={() => onGenerateThumbnailClick($info)} - actionButtonClass="text-gray-800" - > - {@const subtext = - $generationStatus && $generationStatus !== "uploaded" - ? subtexts[$generationStatus] - : formatDateTime($info.createdAt ?? $info.lastModifiedAt)} - - -{/if} + onclick(info)} + actionButtonIcon={!$generationStatus || $generationStatus === "error" ? IconCamera : undefined} + onActionButtonClick={() => onGenerateThumbnailClick(info)} + actionButtonClass="text-gray-800" +> + {@const subtext = + $generationStatus && $generationStatus !== "uploaded" + ? subtexts[$generationStatus] + : formatDateTime(info.createdAt ?? info.lastModifiedAt)} + + diff --git a/src/routes/(fullscreen)/settings/thumbnail/service.svelte.ts b/src/routes/(fullscreen)/settings/thumbnail/service.svelte.ts index d8f288c..6b0acb2 100644 --- a/src/routes/(fullscreen)/settings/thumbnail/service.svelte.ts +++ b/src/routes/(fullscreen)/settings/thumbnail/service.svelte.ts @@ -2,7 +2,7 @@ import { limitFunction } from "p-limit"; import { get, writable, type Writable } from "svelte/store"; import { encryptData } from "$lib/modules/crypto"; import { storeFileThumbnailCache } from "$lib/modules/file"; -import type { FileInfo } from "$lib/modules/filesystem"; +import type { FileInfo } from "$lib/modules/filesystem2.svelte"; import { generateThumbnail as doGenerateThumbnail } from "$lib/modules/thumbnail"; import { requestFileDownload, requestFileThumbnailUpload } from "$lib/services/file"; @@ -17,7 +17,7 @@ export type GenerationStatus = interface File { id: number; - info: Writable; + info: FileInfo; status?: Writable; } @@ -129,7 +129,11 @@ export const requestThumbnailGeneration = async (fileInfo: FileInfo) => { let fileSize = 0; try { - const file = await requestFileDownload(fileInfo.id, fileInfo.contentIv!, fileInfo.dataKey!); + const file = await requestFileDownload( + fileInfo.id, + fileInfo.contentIv!, + fileInfo.dataKey?.key!, + ); fileSize = file.byteLength; memoryUsage += fileSize; @@ -141,11 +145,11 @@ export const requestThumbnailGeneration = async (fileInfo: FileInfo) => { status, file, fileInfo.contentType, - fileInfo.dataKey!, + fileInfo.dataKey?.key!, ); if ( !thumbnail || - !(await requestThumbnailUpload(status, fileInfo.id, fileInfo.dataKeyVersion!, thumbnail)) + !(await requestThumbnailUpload(status, fileInfo.id, fileInfo.dataKey?.version!, thumbnail)) ) { status.set("error"); } diff --git a/src/routes/(main)/directory/[[id]]/DownloadStatusCard.svelte b/src/routes/(main)/directory/[[id]]/DownloadStatusCard.svelte index 18bb159..590cb8f 100644 --- a/src/routes/(main)/directory/[[id]]/DownloadStatusCard.svelte +++ b/src/routes/(main)/directory/[[id]]/DownloadStatusCard.svelte @@ -1,7 +1,5 @@ {#if downloadingFiles.length > 0} diff --git a/src/routes/(main)/home/+page.svelte b/src/routes/(main)/home/+page.svelte index 0ace1ab..21c3695 100644 --- a/src/routes/(main)/home/+page.svelte +++ b/src/routes/(main)/home/+page.svelte @@ -1,17 +1,18 @@ @@ -28,7 +29,9 @@ {#if mediaFiles.length > 0}
{#each mediaFiles as file} - goto(`/file/${id}`)} /> + {#if file} + goto(`/file/${id}`)} /> + {/if} {/each}
{/if} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 612cfe4..9aadffd 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,23 +1,14 @@