diff --git a/src/lib/components/atoms/RowVirtualizer.svelte b/src/lib/components/atoms/RowVirtualizer.svelte index cd7f35f..aa68bd2 100644 --- a/src/lib/components/atoms/RowVirtualizer.svelte +++ b/src/lib/components/atoms/RowVirtualizer.svelte @@ -16,7 +16,7 @@ let element: HTMLElement | undefined = $state(); let scrollMargin = $state(0); - const virtualizer = $derived( + let virtualizer = $derived( createWindowVirtualizer({ count, estimateSize: itemHeight, diff --git a/src/lib/components/molecules/Categories/Categories.svelte b/src/lib/components/molecules/Categories/Categories.svelte index 54368c6..a4a123b 100644 --- a/src/lib/components/molecules/Categories/Categories.svelte +++ b/src/lib/components/molecules/Categories/Categories.svelte @@ -1,59 +1,29 @@ {#if categoriesWithName.length > 0}
- {#each categoriesWithName as { info }} + {#each categoriesWithName as category} import type { Component } from "svelte"; import type { SvelteHTMLElements } from "svelte/elements"; - import type { Writable } from "svelte/store"; import { ActionEntryButton } from "$lib/components/atoms"; import { CategoryLabel } from "$lib/components/molecules"; - import type { CategoryInfo } from "$lib/modules/filesystem"; + import type { SubCategoryInfo } from "$lib/modules/filesystem2.svelte"; import type { SelectedCategory } from "./service"; interface Props { - info: Writable; + info: SubCategoryInfo; menuIcon?: Component; onclick: (category: SelectedCategory) => void; onMenuClick?: (category: SelectedCategory) => void; } let { info, menuIcon, onclick, onMenuClick }: Props = $props(); - - const openCategory = () => { - const { id, dataKey, dataKeyVersion, name } = $info as CategoryInfo; - if (!dataKey || !dataKeyVersion) return; // TODO: Error handling - - onclick({ id, dataKey, dataKeyVersion, name }); - }; - - const openMenu = () => { - const { id, dataKey, dataKeyVersion, name } = $info as CategoryInfo; - if (!dataKey || !dataKeyVersion) return; // TODO: Error handling - - onMenuClick!({ id, dataKey, dataKeyVersion, name }); - }; -{#if $info} - - - -{/if} + onclick(info)} + actionButtonIcon={menuIcon} + onActionButtonClick={() => onMenuClick?.(info)} +> + + diff --git a/src/lib/components/molecules/Categories/service.ts b/src/lib/components/molecules/Categories/service.ts index 08c41db..683d516 100644 --- a/src/lib/components/molecules/Categories/service.ts +++ b/src/lib/components/molecules/Categories/service.ts @@ -1,6 +1,5 @@ export interface SelectedCategory { id: number; - dataKey: CryptoKey; - dataKeyVersion: Date; + dataKey?: { key: CryptoKey; version: Date }; name: string; } diff --git a/src/lib/components/molecules/SubCategories.svelte b/src/lib/components/molecules/SubCategories.svelte index 9c84a89..a271309 100644 --- a/src/lib/components/molecules/SubCategories.svelte +++ b/src/lib/components/molecules/SubCategories.svelte @@ -1,10 +1,8 @@
@@ -53,14 +43,12 @@ {#if subCategoryCreatePosition === "top"} {@render subCategoryCreate()} {/if} - {#key info} - - {/key} + {#if subCategoryCreatePosition === "bottom"} {@render subCategoryCreate()} {/if} diff --git a/src/lib/components/organisms/Category/Category.svelte b/src/lib/components/organisms/Category/Category.svelte index b42aeef..4824f82 100644 --- a/src/lib/components/organisms/Category/Category.svelte +++ b/src/lib/components/organisms/Category/Category.svelte @@ -1,11 +1,8 @@
@@ -89,26 +58,24 @@

하위 카테고리의 파일

- {#key info} - 48 + (index + 1 < files.length ? 4 : 0)} - > - {#snippet item(index)} - {@const { info, isRecursive } = files[index]!} -
- -
- {/snippet} - {#snippet placeholder()} -

이 카테고리에 추가된 파일이 없어요.

- {/snippet} -
- {/key} + 48 + (index + 1 < files.length ? 4 : 0)} + > + {#snippet item(index)} + {@const { details } = files[index]!} +
+ +
+ {/snippet} + {#snippet placeholder()} +

이 카테고리에 추가된 파일이 없어요.

+ {/snippet} +
{/if}
diff --git a/src/lib/components/organisms/Category/File.svelte b/src/lib/components/organisms/Category/File.svelte index 8e3fc12..88e821a 100644 --- a/src/lib/components/organisms/Category/File.svelte +++ b/src/lib/components/organisms/Category/File.svelte @@ -1,59 +1,38 @@ -{#if $info} - - - -{/if} + onclick(info)} + actionButtonIcon={onRemoveClick && IconClose} + onActionButtonClick={() => onRemoveClick?.(info)} +> + {#await thumbnailPromise} + + {:then thumbnail} + + {/await} + diff --git a/src/lib/components/organisms/Category/service.ts b/src/lib/components/organisms/Category/service.ts index fb6e640..3c78d2f 100644 --- a/src/lib/components/organisms/Category/service.ts +++ b/src/lib/components/organisms/Category/service.ts @@ -1,8 +1,4 @@ -export { requestFileThumbnailDownload } from "$lib/services/file"; - export interface SelectedFile { id: number; - dataKey: CryptoKey; - dataKeyVersion: Date; name: string; } diff --git a/src/lib/modules/filesystem.ts b/src/lib/modules/filesystem.ts index e01145b..5020793 100644 --- a/src/lib/modules/filesystem.ts +++ b/src/lib/modules/filesystem.ts @@ -3,11 +3,6 @@ import { 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 { trpc, isTRPCClientError } from "$trpc/client"; @@ -25,28 +20,7 @@ export interface FileInfo { categoryIds: number[]; } -export type CategoryInfo = - | { - id: "root"; - dataKey?: undefined; - dataKeyVersion?: undefined; - name?: undefined; - subCategoryIds: number[]; - files?: undefined; - isFileRecursive?: undefined; - } - | { - id: number; - dataKey?: CryptoKey; - dataKeyVersion?: Date; - name: string; - subCategoryIds: number[]; - files: { id: number; isRecursive: boolean }[]; - isFileRecursive: boolean; - }; - const fileInfoStore = new Map>(); -const categoryInfoStore = new Map>(); const fetchFileInfoFromIndexedDB = async (id: number, info: Writable) => { if (get(info)) return; @@ -130,124 +104,3 @@ export const getFileInfo = (fileId: number, masterKey: CryptoKey) => { fetchFileInfo(fileId, info, masterKey); // Intended return info; }; - -const fetchCategoryInfoFromIndexedDB = async ( - id: CategoryId, - info: Writable, -) => { - if (get(info)) return; - - const [category, subCategories] = await Promise.all([ - id !== "root" ? getCategoryInfoFromIndexedDB(id) : undefined, - getCategoryInfosFromIndexedDB(id), - ]); - const subCategoryIds = subCategories.map(({ id }) => id); - - if (id === "root") { - info.set({ id, subCategoryIds }); - } else { - if (!category) return; - info.set({ - id, - name: category.name, - subCategoryIds, - files: category.files, - isFileRecursive: category.isFileRecursive, - }); - } -}; - -const fetchCategoryInfoFromServer = async ( - id: CategoryId, - info: Writable, - masterKey: CryptoKey, -) => { - let data; - try { - data = await trpc().category.get.query({ id }); - } catch (e) { - if (isTRPCClientError(e) && e.data?.code === "NOT_FOUND") { - info.set(null); - await deleteCategoryInfo(id as number); - return; - } - throw new Error("Failed to fetch category information"); - } - - const { metadata, subCategories } = data; - - if (id === "root") { - info.set({ id, subCategoryIds: subCategories }); - } else { - const { dataKey } = await unwrapDataKey(metadata!.dek, masterKey); - const name = await decryptString(metadata!.name, metadata!.nameIv, dataKey); - - let files; - try { - files = await trpc().category.files.query({ id, recurse: true }); - } catch { - throw new Error("Failed to fetch category files"); - } - - const filesMapped = files.map(({ file, isRecursive }) => ({ id: file, isRecursive })); - let isFileRecursive: boolean | undefined = undefined; - - info.update((value) => { - const newValue = { - isFileRecursive: false, - ...value, - id, - dataKey, - dataKeyVersion: new Date(metadata!.dekVersion), - name, - subCategoryIds: subCategories, - files: filesMapped, - }; - isFileRecursive = newValue.isFileRecursive; - return newValue; - }); - await storeCategoryInfo({ - id, - parentId: metadata!.parent, - name, - files: filesMapped, - isFileRecursive: isFileRecursive!, - }); - } -}; - -const fetchCategoryInfo = async ( - id: CategoryId, - info: Writable, - masterKey: CryptoKey, -) => { - await fetchCategoryInfoFromIndexedDB(id, info); - await fetchCategoryInfoFromServer(id, info, masterKey); -}; - -export const getCategoryInfo = (categoryId: CategoryId, masterKey: CryptoKey) => { - // TODO: MEK rotation - - let info = categoryInfoStore.get(categoryId); - if (!info) { - info = writable(null); - categoryInfoStore.set(categoryId, info); - } - - fetchCategoryInfo(categoryId, info, masterKey); // Intended - return info; -}; - -export const updateCategoryInfo = async ( - categoryId: number, - changes: { isFileRecursive?: boolean }, -) => { - await updateCategoryInfoInIndexedDB(categoryId, changes); - categoryInfoStore.get(categoryId)?.update((value) => { - if (!value) return value; - if (changes.isFileRecursive !== undefined) { - value.isFileRecursive = changes.isFileRecursive; - } - return value; - }); -}; diff --git a/src/lib/modules/filesystem2.svelte.ts b/src/lib/modules/filesystem2.svelte.ts index ade5342..01514cb 100644 --- a/src/lib/modules/filesystem2.svelte.ts +++ b/src/lib/modules/filesystem2.svelte.ts @@ -54,13 +54,14 @@ interface FileInfo { } export type SummarizedFileInfo = Omit; +export type CategoryFileInfo = SummarizedFileInfo & { isRecursive: boolean }; interface LocalCategoryInfo { id: number; - dataKey: DataKey | undefined; + dataKey?: DataKey | undefined; name: string; - subCategories: Omit[]; - files: { id: number; name: string; isRecursive: boolean }[]; + subCategories: SubCategoryInfo[]; + files: CategoryFileInfo[]; isFileRecursive: boolean; } @@ -68,13 +69,19 @@ interface RootCategoryInfo { id: "root"; dataKey?: undefined; name?: undefined; - subCategories: Omit[]; + subCategories: SubCategoryInfo[]; files?: undefined; + isFileRecursive?: undefined; } export type CategoryInfo = LocalCategoryInfo | RootCategoryInfo; +export type SubCategoryInfo = Omit< + LocalCategoryInfo, + "subCategories" | "files" | "isFileRecursive" +>; const directoryInfoCache = new Map>(); +const categoryInfoCache = new Map>(); export const getDirectoryInfo = async (id: DirectoryId, masterKey: CryptoKey) => { const info = directoryInfoCache.get(id); @@ -189,3 +196,146 @@ const fetchDirectoryInfoFromServer = async ( const decryptDate = async (ciphertext: string, iv: string, dataKey: CryptoKey) => { return new Date(parseInt(await decryptString(ciphertext, iv, dataKey), 10)); }; + +export const getCategoryInfo = async (id: CategoryId, masterKey: CryptoKey) => { + const info = categoryInfoCache.get(id); + if (info instanceof Promise) { + return info; + } + + const { promise, resolve } = Promise.withResolvers(); + if (!info) { + categoryInfoCache.set(id, promise); + const categoryInfo = await fetchCategoryInfoFromIndexedDB(id); + if (categoryInfo) { + const state = $state(categoryInfo); + categoryInfoCache.set(id, state); + resolve(state); + } + } + + fetchCategoryInfoFromServer(id, masterKey).then((categoryInfo) => { + if (!categoryInfo) return; + + let info = categoryInfoCache.get(id); + if (info instanceof Promise) { + const state = $state(categoryInfo); + categoryInfoCache.set(id, state); + resolve(state); + } else { + Object.assign(info!, categoryInfo); + resolve(info!); + } + }); + + return info ?? promise; +}; + +const fetchCategoryInfoFromIndexedDB = async ( + id: CategoryId, +): Promise => { + const [category, subCategories] = await Promise.all([ + id !== "root" ? getCategoryInfoFromIndexedDB(id) : undefined, + getCategoryInfosFromIndexedDB(id), + ]); + const files = category + ? await Promise.all( + category.files.map(async (file) => { + const fileInfo = await getFileInfoFromIndexedDB(file.id); + return fileInfo + ? { + id: file.id, + contentType: fileInfo.contentType, + name: fileInfo.name, + createdAt: fileInfo.createdAt, + lastModifiedAt: fileInfo.lastModifiedAt, + isRecursive: file.isRecursive, + } + : undefined; + }), + ) + : undefined; + + if (id === "root") { + return { id, subCategories }; + } else if (category) { + return { + id, + name: category.name, + subCategories, + files: files!.filter((file) => !!file), + isFileRecursive: category.isFileRecursive, + }; + } +}; + +const fetchCategoryInfoFromServer = async ( + id: CategoryId, + masterKey: CryptoKey, +): Promise => { + try { + const { + metadata, + subCategories: subCategoriesRaw, + files: filesRaw, + } = await trpc().category.get.query({ id, recurse: true }); + const [subCategories, files] = await Promise.all([ + Promise.all( + subCategoriesRaw.map(async (category) => { + const { dataKey } = await unwrapDataKey(category.dek, masterKey); + const name = await decryptString(category.name, category.nameIv, dataKey); + return { + id: category.id, + dataKey: { key: dataKey, version: category.dekVersion }, + name, + }; + }), + ), + id !== "root" + ? 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, + isRecursive: file.isRecursive, + }; + }), + ) + : undefined, + ]); + + if (id === "root") { + return { id, subCategories }; + } else { + const { dataKey } = await unwrapDataKey(metadata!.dek, masterKey); + const name = await decryptString(metadata!.name, metadata!.nameIv, dataKey); + return { + id, + dataKey: { key: dataKey, version: metadata!.dekVersion }, + name, + subCategories, + files: files!, + isFileRecursive: false, + }; + } + } catch (e) { + if (isTRPCClientError(e) && e.data?.code === "NOT_FOUND") { + categoryInfoCache.delete(id); + await deleteCategoryInfo(id as number); + return; + } + throw new Error("Failed to fetch category information"); + } +}; diff --git a/src/lib/server/db/file.ts b/src/lib/server/db/file.ts index a524ff4..7bea6db 100644 --- a/src/lib/server/db/file.ts +++ b/src/lib/server/db/file.ts @@ -304,39 +304,51 @@ export const getAllFilesByCategory = async ( recurse: boolean, ) => { const files = await db - .withRecursive("cte", (db) => + .withRecursive("category_tree", (db) => db .selectFrom("category") - .leftJoin("file_category", "category.id", "file_category.category_id") - .select(["id", "parent_id", "user_id", "file_category.file_id"]) - .select(sql`0`.as("depth")) + .select(["id", sql`0`.as("depth")]) .where("id", "=", categoryId) + .where("user_id", "=", userId) .$if(recurse, (qb) => qb.unionAll((db) => db .selectFrom("category") - .leftJoin("file_category", "category.id", "file_category.category_id") - .innerJoin("cte", "category.parent_id", "cte.id") - .select([ - "category.id", - "category.parent_id", - "category.user_id", - "file_category.file_id", - ]) - .select(sql`cte.depth + 1`.as("depth")), + .innerJoin("category_tree", "category.parent_id", "category_tree.id") + .select(["category.id", sql`depth + 1`.as("depth")]), ), ), ) - .selectFrom("cte") + .selectFrom("category_tree") + .innerJoin("file_category", "category_tree.id", "file_category.category_id") + .innerJoin("file", "file_category.file_id", "file.id") .select(["file_id", "depth"]) + .selectAll("file") .distinctOn("file_id") - .where("user_id", "=", userId) - .where("file_id", "is not", null) - .$narrowType<{ file_id: NotNull }>() .orderBy("file_id") .orderBy("depth") .execute(); - return files.map(({ file_id, depth }) => ({ id: file_id, isRecursive: depth > 0 })); + return files.map( + (file) => + ({ + id: file.file_id, + parentId: file.parent_id ?? "root", + userId: file.user_id, + path: file.path, + mekVersion: file.master_encryption_key_version, + encDek: file.encrypted_data_encryption_key, + dekVersion: file.data_encryption_key_version, + hskVersion: file.hmac_secret_key_version, + contentHmac: file.content_hmac, + contentType: file.content_type, + encContentIv: file.encrypted_content_iv, + encContentHash: file.encrypted_content_hash, + encName: file.encrypted_name, + encCreatedAt: file.encrypted_created_at, + encLastModifiedAt: file.encrypted_last_modified_at, + isRecursive: file.depth > 0, + }) satisfies File & { isRecursive: boolean }, + ); }; export const getAllFileIds = async (userId: number) => { diff --git a/src/routes/(fullscreen)/file/[id]/+page.svelte b/src/routes/(fullscreen)/file/[id]/+page.svelte index 9e0ddc0..ab85dc7 100644 --- a/src/routes/(fullscreen)/file/[id]/+page.svelte +++ b/src/routes/(fullscreen)/file/[id]/+page.svelte @@ -6,12 +6,7 @@ import { page } from "$app/state"; import { FullscreenDiv } from "$lib/components/atoms"; import { Categories, IconEntryButton, TopBar } from "$lib/components/molecules"; - import { - getFileInfo, - getCategoryInfo, - type FileInfo, - type CategoryInfo, - } from "$lib/modules/filesystem"; + import { getFileInfo, type FileInfo } from "$lib/modules/filesystem"; import { captureVideoThumbnail } from "$lib/modules/thumbnail"; import { fileDownloadStatusStore, isFileDownloading, masterKeyStore } from "$lib/stores"; import AddToCategoryBottomSheet from "./AddToCategoryBottomSheet.svelte"; @@ -32,7 +27,7 @@ let { data } = $props(); let info: Writable | undefined = $state(); - let categories: Writable[] = $state([]); + // let categories: Writable[] = $state([]); let isMenuOpen = $state(false); let isAddToCategoryBottomSheetOpen = $state(false); @@ -90,10 +85,10 @@ viewerType = undefined; }); - $effect(() => { - categories = - $info?.categoryIds.map((id) => getCategoryInfo(id, $masterKeyStore?.get(1)?.key!)) ?? []; - }); + // $effect(() => { + // categories = + // $info?.categoryIds.map((id) => getCategoryInfo(id, $masterKeyStore?.get(1)?.key!)) ?? []; + // }); $effect(() => { if ($info && $info.dataKey && $info.contentIv) { @@ -190,12 +185,12 @@

카테고리

- goto(`/category/${id}`)} onCategoryMenuClick={({ id }) => removeFromCategory(id)} - /> + /> --> (isAddToCategoryBottomSheetOpen = true)} diff --git a/src/routes/(fullscreen)/file/[id]/AddToCategoryBottomSheet.svelte b/src/routes/(fullscreen)/file/[id]/AddToCategoryBottomSheet.svelte index f1d0200..91dc7e5 100644 --- a/src/routes/(fullscreen)/file/[id]/AddToCategoryBottomSheet.svelte +++ b/src/routes/(fullscreen)/file/[id]/AddToCategoryBottomSheet.svelte @@ -1,9 +1,8 @@ -{#if $category} - - - - (category = getCategoryInfo(id, $masterKeyStore?.get(1)?.key!))} - onSubCategoryCreateClick={() => (isCategoryCreateModalOpen = true)} - subCategoryCreatePosition="top" - /> - {#if $category.id !== "root"} - - - - {/if} - - -{/if} +{#await categoryInfoPromise then categoryInfo} + {#if categoryInfo} + + + + (categoryInfoPromise = getCategoryInfo(id, $masterKeyStore?.get(1)?.key!))} + onSubCategoryCreateClick={() => (isCategoryCreateModalOpen = true)} + subCategoryCreatePosition="top" + /> + {#if categoryInfo.id !== "root"} + + + + {/if} + + - { - if (await requestCategoryCreation(name, $category!.id, $masterKeyStore?.get(1)!)) { - category = getCategoryInfo($category!.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME - return true; - } - return false; - }} -/> + { + if (await requestCategoryCreation(name, categoryInfo.id, $masterKeyStore?.get(1)!)) { + categoryInfoPromise = getCategoryInfo(categoryInfo.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME + return true; + } + return false; + }} + /> + {/if} +{/await} diff --git a/src/routes/(main)/category/[[id]]/+page.svelte b/src/routes/(main)/category/[[id]]/+page.svelte index 9b3e195..4a038d9 100644 --- a/src/routes/(main)/category/[[id]]/+page.svelte +++ b/src/routes/(main)/category/[[id]]/+page.svelte @@ -1,9 +1,8 @@ @@ -50,68 +34,70 @@ 카테고리 -{#if data.id !== "root"} - -{/if} -
- {#if $info && isFileRecursive !== undefined} - goto(`/file/${id}?from=category`)} - onFileRemoveClick={async ({ id }) => { - await requestFileRemovalFromCategory(id, data.id as number); - info = getCategoryInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME +{#await infoPromise then info} + {#if info} + {#if info.id !== "root"} + + {/if} +
+ goto(`/file/${id}?from=category`)} + onFileRemoveClick={async ({ id }) => { + await requestFileRemovalFromCategory(id, data.id as number); + infoPromise = getCategoryInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME + }} + onSubCategoryClick={({ id }) => goto(`/category/${id}`)} + onSubCategoryCreateClick={() => (isCategoryCreateModalOpen = true)} + onSubCategoryMenuClick={(subCategory) => { + context.selectedCategory = subCategory; + isCategoryMenuBottomSheetOpen = true; + }} + /> +
+ + { + if (await requestCategoryCreation(name, data.id, $masterKeyStore?.get(1)!)) { + infoPromise = getCategoryInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME + return true; + } + return false; }} - onSubCategoryClick={({ id }) => goto(`/category/${id}`)} - onSubCategoryCreateClick={() => (isCategoryCreateModalOpen = true)} - onSubCategoryMenuClick={(subCategory) => { - context.selectedCategory = subCategory; - isCategoryMenuBottomSheetOpen = true; + /> + + { + isCategoryMenuBottomSheetOpen = false; + isCategoryRenameModalOpen = true; + }} + onDeleteClick={() => { + isCategoryMenuBottomSheetOpen = false; + isCategoryDeleteModalOpen = true; + }} + /> + { + if (await requestCategoryRename(context.selectedCategory!, newName)) { + infoPromise = getCategoryInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME + return true; + } + return false; + }} + /> + { + if (await requestCategoryDeletion(context.selectedCategory!)) { + infoPromise = getCategoryInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME + return true; + } + return false; }} /> {/if} -
- - { - if (await requestCategoryCreation(name, data.id, $masterKeyStore?.get(1)!)) { - info = getCategoryInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME - return true; - } - return false; - }} -/> - - { - isCategoryMenuBottomSheetOpen = false; - isCategoryRenameModalOpen = true; - }} - onDeleteClick={() => { - isCategoryMenuBottomSheetOpen = false; - isCategoryDeleteModalOpen = true; - }} -/> - { - if (await requestCategoryRename(context.selectedCategory!, newName)) { - info = getCategoryInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME - return true; - } - return false; - }} -/> - { - if (await requestCategoryDeletion(context.selectedCategory!)) { - info = getCategoryInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME - return true; - } - return false; - }} -/> +{/await} diff --git a/src/routes/(main)/category/[[id]]/service.svelte.ts b/src/routes/(main)/category/[[id]]/service.svelte.ts index 18f68fd..c415cf5 100644 --- a/src/routes/(main)/category/[[id]]/service.svelte.ts +++ b/src/routes/(main)/category/[[id]]/service.svelte.ts @@ -17,12 +17,17 @@ export const useContext = () => { }; export const requestCategoryRename = async (category: SelectedCategory, newName: string) => { - const newNameEncrypted = await encryptString(newName, category.dataKey); + if (!category.dataKey) { + // TODO: Error Handling + return false; + } + + const newNameEncrypted = await encryptString(newName, category.dataKey.key); try { await trpc().category.rename.mutate({ id: category.id, - dekVersion: category.dataKeyVersion, + dekVersion: category.dataKey.version, name: newNameEncrypted.ciphertext, nameIv: newNameEncrypted.iv, }); diff --git a/src/routes/(main)/directory/[[id]]/DirectoryEntries/DirectoryEntries.svelte b/src/routes/(main)/directory/[[id]]/DirectoryEntries/DirectoryEntries.svelte index 530cb97..5375574 100644 --- a/src/routes/(main)/directory/[[id]]/DirectoryEntries/DirectoryEntries.svelte +++ b/src/routes/(main)/directory/[[id]]/DirectoryEntries/DirectoryEntries.svelte @@ -39,7 +39,7 @@ details, }); - const entries = $derived([ + let entries = $derived([ ...(showParentEntry ? ([{ type: "parent" }] as const) : []), ...sortEntries(info.subDirectories.map(toEntry("directory"))), ...sortEntries([ diff --git a/src/routes/(main)/directory/[[id]]/DirectoryEntries/File.svelte b/src/routes/(main)/directory/[[id]]/DirectoryEntries/File.svelte index fdc225c..9a972aa 100644 --- a/src/routes/(main)/directory/[[id]]/DirectoryEntries/File.svelte +++ b/src/routes/(main)/directory/[[id]]/DirectoryEntries/File.svelte @@ -35,7 +35,13 @@ actionButtonIcon={IconMoreVert} onActionButtonClick={() => action(onOpenMenuClick)} > - {#await thumbnailPromise then thumbnail} + {#await thumbnailPromise} + + {:then thumbnail} { if (!entry.dataKey) { // TODO: Error Handling - console.log("hi"); return false; } diff --git a/src/trpc/routers/category.ts b/src/trpc/routers/category.ts index 2be80c8..9b2567a 100644 --- a/src/trpc/routers/category.ts +++ b/src/trpc/routers/category.ts @@ -9,6 +9,7 @@ const categoryRouter = router({ .input( z.object({ id: categoryIdSchema, + recurse: z.boolean().default(false), }), ) .query(async ({ ctx, input }) => { @@ -20,7 +21,12 @@ const categoryRouter = router({ throw new TRPCError({ code: "NOT_FOUND", message: "Invalid category id" }); } - const categories = await CategoryRepo.getAllCategoriesByParent(ctx.session.userId, input.id); + const [categories, files] = await Promise.all([ + CategoryRepo.getAllCategoriesByParent(ctx.session.userId, input.id), + input.id !== "root" + ? FileRepo.getAllFilesByCategory(ctx.session.userId, input.id, input.recurse) + : undefined, + ]); return { metadata: category && { parent: category.parentId, @@ -30,7 +36,28 @@ const categoryRouter = router({ name: category.encName.ciphertext, nameIv: category.encName.iv, }, - subCategories: categories.map(({ id }) => id), + subCategories: categories.map((category) => ({ + id: category.id, + mekVersion: category.mekVersion, + dek: category.encDek, + dekVersion: category.dekVersion, + name: category.encName.ciphertext, + nameIv: category.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, + isRecursive: file.isRecursive, + })), }; }), @@ -113,27 +140,6 @@ const categoryRouter = router({ } }), - files: roleProcedure["activeClient"] - .input( - z.object({ - id: z.int().positive(), - recurse: z.boolean().default(false), - }), - ) - .query(async ({ ctx, input }) => { - const category = await CategoryRepo.getCategory(ctx.session.userId, input.id); - if (!category) { - throw new TRPCError({ code: "NOT_FOUND", message: "Invalid category id" }); - } - - const files = await FileRepo.getAllFilesByCategory( - ctx.session.userId, - input.id, - input.recurse, - ); - return files.map(({ id, isRecursive }) => ({ file: id, isRecursive })); - }), - addFile: roleProcedure["activeClient"] .input( z.object({