From e10b600293d511526f8861621ded76aada150b5a Mon Sep 17 00:00:00 2001 From: static Date: Thu, 17 Jul 2025 11:43:22 +0900 Subject: [PATCH] =?UTF-8?q?=EB=94=94=EB=A0=89=ED=84=B0=EB=A6=AC=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20TanStack=20Query=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/modules/filesystem.ts | 108 ----------- src/lib/modules/filesystem2.ts | 175 ++++++++++-------- src/lib/server/db/file.ts | 14 +- src/lib/server/schemas/directory.ts | 1 + src/lib/server/services/directory.ts | 3 +- .../(main)/directory/[[id]]/+page.svelte | 25 +-- .../DirectoryEntries/DirectoryEntries.svelte | 10 +- .../DirectoryEntries/SubDirectory.svelte | 4 +- .../api/directory/[id]/delete/+server.ts | 7 +- 9 files changed, 139 insertions(+), 208 deletions(-) diff --git a/src/lib/modules/filesystem.ts b/src/lib/modules/filesystem.ts index c160534..354f23e 100644 --- a/src/lib/modules/filesystem.ts +++ b/src/lib/modules/filesystem.ts @@ -1,11 +1,6 @@ import { get, writable, type Writable } from "svelte/store"; import { callGetApi } from "$lib/hooks"; import { - getDirectoryInfos as getDirectoryInfosFromIndexedDB, - getDirectoryInfo as getDirectoryInfoFromIndexedDB, - storeDirectoryInfo, - deleteDirectoryInfo, - getFileInfos as getFileInfosFromIndexedDB, getFileInfo as getFileInfoFromIndexedDB, storeFileInfo, deleteFileInfo, @@ -14,35 +9,15 @@ import { storeCategoryInfo, updateCategoryInfo as updateCategoryInfoInIndexedDB, deleteCategoryInfo, - type DirectoryId, type CategoryId, } from "$lib/indexedDB"; import { unwrapDataKey, decryptString } from "$lib/modules/crypto"; import type { CategoryInfoResponse, CategoryFileListResponse, - DirectoryInfoResponse, FileInfoResponse, } from "$lib/server/schemas"; -export type DirectoryInfo = - | { - id: "root"; - dataKey?: undefined; - dataKeyVersion?: undefined; - name?: undefined; - subDirectoryIds: number[]; - fileIds: number[]; - } - | { - id: number; - dataKey?: CryptoKey; - dataKeyVersion?: Date; - name: string; - subDirectoryIds: number[]; - fileIds: number[]; - }; - export interface FileInfo { id: number; dataKey?: CryptoKey; @@ -75,92 +50,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, name: directory.name, subDirectoryIds, fileIds }); - } -}; - -const fetchDirectoryInfoFromServer = async ( - id: DirectoryId, - info: Writable, - masterKey: CryptoKey, -) => { - const res = await callGetApi(`/api/directory/${id}`); - if (res.status === 404) { - info.set(null); - await deleteDirectoryInfo(id as number); - return; - } else if (!res.ok) { - throw new Error("Failed to fetch directory information"); - } - - const { - metadata, - subDirectories: subDirectoryIds, - files: fileIds, - }: DirectoryInfoResponse = await res.json(); - - if (id === "root") { - info.set({ id, subDirectoryIds, fileIds }); - } else { - const { dataKey } = await unwrapDataKey(metadata!.dek, masterKey); - const name = await decryptString(metadata!.name, metadata!.nameIv, dataKey); - - info.set({ - id, - dataKey, - dataKeyVersion: new Date(metadata!.dekVersion), - name, - subDirectoryIds, - fileIds, - }); - await storeDirectoryInfo({ id, parentId: metadata!.parent, name }); - } -}; - -const fetchDirectoryInfo = async ( - id: DirectoryId, - info: Writable, - masterKey: CryptoKey, -) => { - await fetchDirectoryInfoFromIndexedDB(id, info); - await fetchDirectoryInfoFromServer(id, info, masterKey); -}; - -export const getDirectoryInfo = (id: DirectoryId, masterKey: CryptoKey) => { - // TODO: MEK rotation - - let info = directoryInfoStore.get(id); - if (!info) { - info = writable(null); - directoryInfoStore.set(id, info); - } - - fetchDirectoryInfo(id, info, masterKey); // Intended - return info; -}; - const fetchFileInfoFromIndexedDB = async (id: number, info: Writable) => { if (get(info)) return; diff --git a/src/lib/modules/filesystem2.ts b/src/lib/modules/filesystem2.ts index 07ac748..9cfbeb5 100644 --- a/src/lib/modules/filesystem2.ts +++ b/src/lib/modules/filesystem2.ts @@ -26,15 +26,33 @@ import { encryptString, decryptString, } from "$lib/modules/crypto"; -import type { DirectoryInfo } from "$lib/modules/filesystem"; import type { + DirectoryInfoResponse, + DirectoryDeleteResponse, + DirectoryRenameRequest, DirectoryCreateRequest, DirectoryCreateResponse, - DirectoryInfoResponse, - DirectoryRenameRequest, } from "$lib/server/schemas"; import type { MasterKey } from "$lib/stores"; +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[]; + }; + const initializedDirectoryIds = new Set(); let temporaryIdCounter = -1; @@ -99,15 +117,10 @@ export const getDirectoryInfo = (id: DirectoryId, masterKey: CryptoKey) => { export type DirectoryInfoStore = ReturnType; -export const useDirectoryCreate = (parentId: DirectoryId) => { +export const useDirectoryCreation = (parentId: DirectoryId, masterKey: MasterKey) => { const queryClient = useQueryClient(); - return createMutation< - { id: number; dataKey: CryptoKey; dataKeyVersion: Date }, - Error, - { name: string; masterKey: MasterKey }, - { prevParentInfo: DirectoryInfo | undefined; tempId: number } - >({ - mutationFn: async ({ name, masterKey }) => { + return createMutation({ + mutationFn: async ({ name }) => { const { dataKey, dataKeyVersion } = await generateDataKey(); const nameEncrypted = await encryptString(name, dataKey); @@ -119,30 +132,9 @@ export const useDirectoryCreate = (parentId: DirectoryId) => { name: nameEncrypted.ciphertext, nameIv: nameEncrypted.iv, }); + if (!res.ok) throw new Error("Failed to create directory"); + const { directory: id }: DirectoryCreateResponse = await res.json(); - return { id, dataKey, dataKeyVersion }; - }, - onMutate: async ({ name }) => { - await queryClient.cancelQueries({ queryKey: ["directory", parentId] }); - - const prevParentInfo = queryClient.getQueryData(["directory", parentId]); - const tempId = temporaryIdCounter--; - if (prevParentInfo) { - queryClient.setQueryData(["directory", parentId], { - ...prevParentInfo, - subDirectoryIds: [...prevParentInfo.subDirectoryIds, tempId], - }); - queryClient.setQueryData(["directory", tempId], { - id: tempId, - name, - subDirectoryIds: [], - fileIds: [], - }); - } - - return { prevParentInfo, tempId }; - }, - onSuccess: async ({ id, dataKey, dataKeyVersion }, { name }) => { queryClient.setQueryData(["directory", id], { id, name, @@ -153,13 +145,38 @@ export const useDirectoryCreate = (parentId: DirectoryId) => { }); await storeDirectoryInfo({ id, parentId, name }); }, - onError: (error, { name }, context) => { - if (context?.prevParentInfo) { - queryClient.setQueryData(["directory", parentId], context.prevParentInfo); - } - console.error(`Failed to create directory "${name}" in parent ${parentId}:`, error); + onMutate: async ({ name }) => { + const tempId = temporaryIdCounter--; + queryClient.setQueryData(["directory", tempId], { + id: tempId, + name, + subDirectoryIds: [], + fileIds: [], + }); + + await queryClient.cancelQueries({ queryKey: ["directory", parentId] }); + queryClient.setQueryData(["directory", parentId], (prevParentInfo) => { + if (!prevParentInfo) return undefined; + return { + ...prevParentInfo, + subDirectoryIds: [...prevParentInfo.subDirectoryIds, tempId], + }; + }); + + return { tempId }; }, - onSettled: (id) => { + onError: (_error, _variables, context) => { + if (context) { + queryClient.setQueryData(["directory", parentId], (prevParentInfo) => { + if (!prevParentInfo) return undefined; + return { + ...prevParentInfo, + subDirectoryIds: prevParentInfo.subDirectoryIds.filter((id) => id !== context.tempId), + }; + }); + } + }, + onSettled: () => { queryClient.invalidateQueries({ queryKey: ["directory", parentId] }); }, }); @@ -176,15 +193,18 @@ export const useDirectoryRename = () => { dataKeyVersion: Date; newName: string; }, - { prevInfo: (DirectoryInfo & { id: number }) | undefined } + { oldName: string | undefined } >({ mutationFn: async ({ id, dataKey, dataKeyVersion, newName }) => { const newNameEncrypted = await encryptString(newName, dataKey); - await callPostApi(`/api/directory/${id}/rename`, { + const res = await callPostApi(`/api/directory/${id}/rename`, { dekVersion: dataKeyVersion.toISOString(), name: newNameEncrypted.ciphertext, nameIv: newNameEncrypted.iv, }); + if (!res.ok) throw new Error("Failed to rename directory"); + + await updateDirectoryInfo(id, { name: newName }); }, onMutate: async ({ id, newName }) => { await queryClient.cancelQueries({ queryKey: ["directory", id] }); @@ -195,61 +215,62 @@ export const useDirectoryRename = () => { ...prevInfo, name: newName, }); - await updateDirectoryInfo(id, { name: newName }); } - return { prevInfo }; + return { oldName: prevInfo?.name }; }, - onSuccess: async (data, { id, newName }) => { - await updateDirectoryInfo(id, { name: newName }); - }, - onError: (error, { id }, context) => { - if (context?.prevInfo) { - queryClient.setQueryData(["directory", id], context.prevInfo); + onError: (_error, { id }, context) => { + if (context?.oldName) { + queryClient.setQueryData(["directory", id], (prevInfo) => { + if (!prevInfo) return undefined; + return { ...prevInfo, name: context.oldName! }; + }); } - console.error("Failed to rename directory:", error); }, - onSettled: (data, error, { id }) => { + onSettled: (_data, _error, { id }) => { queryClient.invalidateQueries({ queryKey: ["directory", id] }); }, }); }; -export const useDirectoryDelete = (parentId: DirectoryId) => { +export const useDirectoryDeletion = (parentId: DirectoryId) => { const queryClient = useQueryClient(); - return createMutation< - void, - Error, - { id: number }, - { prevInfo: (DirectoryInfo & { id: number }) | undefined } - >({ + return createMutation<{ deletedFiles: number[] }, Error, { id: number }, {}>({ mutationFn: async ({ id }) => { - await callPostApi(`/api/directory/${id}/delete`); + const res = await callPostApi(`/api/directory/${id}/delete`); + if (!res.ok) throw new Error("Failed to delete directory"); + + const { deletedDirectories, deletedFiles }: DirectoryDeleteResponse = await res.json(); + await Promise.all([ + ...deletedDirectories.map(deleteDirectoryInfo), + ...deletedFiles.map(deleteFileInfo), + ]); + + return { deletedFiles }; }, onMutate: async ({ id }) => { await queryClient.cancelQueries({ queryKey: ["directory", parentId] }); - - const prevParentInfo = queryClient.getQueryData(["directory", parentId]); - if (prevParentInfo) { - queryClient.setQueryData(["directory", parentId], { + queryClient.setQueryData(["directory", parentId], (prevParentInfo) => { + if (!prevParentInfo) return undefined; + return { ...prevParentInfo, subDirectoryIds: prevParentInfo.subDirectoryIds.filter((subId) => subId !== id), + }; + }); + return {}; + }, + onError: (_error, { id }, context) => { + if (context) { + queryClient.setQueryData(["directory", parentId], (prevParentInfo) => { + if (!prevParentInfo) return undefined; + return { + ...prevParentInfo, + subDirectoryIds: [...prevParentInfo.subDirectoryIds, id], + }; }); } - - const prevInfo = queryClient.getQueryData(["directory", id]); - return { prevInfo }; }, - onSuccess: async (data, { id }) => { - await deleteDirectoryInfo(id); - }, - onError: (error, { id }, context) => { - if (context?.prevInfo) { - queryClient.setQueryData(["directory", parentId], context?.prevInfo); - } - console.error("Failed to delete directory:", error); - }, - onSettled: (data, error, { id }) => { + onSettled: () => { queryClient.invalidateQueries({ queryKey: ["directory", parentId] }); }, }); diff --git a/src/lib/server/db/file.ts b/src/lib/server/db/file.ts index e8ecaa5..f393ce5 100644 --- a/src/lib/server/db/file.ts +++ b/src/lib/server/db/file.ts @@ -181,7 +181,10 @@ export const unregisterDirectory = async (userId: number, directoryId: number) = }; const unregisterDirectoryRecursively = async ( directoryId: number, - ): Promise<{ id: number; path: string; thumbnailPath: string | null }[]> => { + ): Promise<{ + subDirectories: { id: number }[]; + files: { id: number; path: string; thumbnailPath: string | null }[]; + }> => { const files = await unregisterFiles(directoryId); const subDirectories = await trx .selectFrom("directory") @@ -189,7 +192,7 @@ export const unregisterDirectory = async (userId: number, directoryId: number) = .where("parent_id", "=", directoryId) .where("user_id", "=", userId) .execute(); - const subDirectoryFilePaths = await Promise.all( + const subDirectoryEntries = await Promise.all( subDirectories.map(async ({ id }) => await unregisterDirectoryRecursively(id)), ); @@ -201,7 +204,12 @@ export const unregisterDirectory = async (userId: number, directoryId: number) = if (deleteRes.numDeletedRows === 0n) { throw new IntegrityError("Directory not found"); } - return files.concat(...subDirectoryFilePaths); + return { + subDirectories: subDirectoryEntries + .flatMap(({ subDirectories }) => subDirectories) + .concat(subDirectories), + files: subDirectoryEntries.flatMap(({ files }) => files).concat(files), + }; }; return await unregisterDirectoryRecursively(directoryId); }); diff --git a/src/lib/server/schemas/directory.ts b/src/lib/server/schemas/directory.ts index cc58c12..a1fa381 100644 --- a/src/lib/server/schemas/directory.ts +++ b/src/lib/server/schemas/directory.ts @@ -19,6 +19,7 @@ export const directoryInfoResponse = z.object({ export type DirectoryInfoResponse = z.output; export const directoryDeleteResponse = z.object({ + deletedDirectories: z.number().int().positive().array(), deletedFiles: z.number().int().positive().array(), }); export type DirectoryDeleteResponse = z.output; diff --git a/src/lib/server/services/directory.ts b/src/lib/server/services/directory.ts index 9403f4d..c8b70d8 100644 --- a/src/lib/server/services/directory.ts +++ b/src/lib/server/services/directory.ts @@ -42,8 +42,9 @@ const safeUnlink = async (path: string | null) => { export const deleteDirectory = async (userId: number, directoryId: number) => { try { - const files = await unregisterDirectory(userId, directoryId); + const { subDirectories, files } = await unregisterDirectory(userId, directoryId); return { + directories: [...subDirectories.map(({ id }) => id), directoryId], files: files.map(({ id, path, thumbnailPath }) => { safeUnlink(path); // Intended safeUnlink(thumbnailPath); // Intended diff --git a/src/routes/(main)/directory/[[id]]/+page.svelte b/src/routes/(main)/directory/[[id]]/+page.svelte index 25679e8..198cfc5 100644 --- a/src/routes/(main)/directory/[[id]]/+page.svelte +++ b/src/routes/(main)/directory/[[id]]/+page.svelte @@ -1,15 +1,14 @@