디렉터리 관련 TanStack Query 코드 리팩토링

This commit is contained in:
static
2025-07-17 11:43:22 +09:00
parent 164c5f660b
commit e10b600293
9 changed files with 139 additions and 208 deletions

View File

@@ -1,11 +1,6 @@
import { get, writable, type Writable } from "svelte/store"; import { get, writable, type Writable } from "svelte/store";
import { callGetApi } from "$lib/hooks"; import { callGetApi } from "$lib/hooks";
import { import {
getDirectoryInfos as getDirectoryInfosFromIndexedDB,
getDirectoryInfo as getDirectoryInfoFromIndexedDB,
storeDirectoryInfo,
deleteDirectoryInfo,
getFileInfos as getFileInfosFromIndexedDB,
getFileInfo as getFileInfoFromIndexedDB, getFileInfo as getFileInfoFromIndexedDB,
storeFileInfo, storeFileInfo,
deleteFileInfo, deleteFileInfo,
@@ -14,35 +9,15 @@ import {
storeCategoryInfo, storeCategoryInfo,
updateCategoryInfo as updateCategoryInfoInIndexedDB, updateCategoryInfo as updateCategoryInfoInIndexedDB,
deleteCategoryInfo, deleteCategoryInfo,
type DirectoryId,
type CategoryId, type CategoryId,
} from "$lib/indexedDB"; } from "$lib/indexedDB";
import { unwrapDataKey, decryptString } from "$lib/modules/crypto"; import { unwrapDataKey, decryptString } from "$lib/modules/crypto";
import type { import type {
CategoryInfoResponse, CategoryInfoResponse,
CategoryFileListResponse, CategoryFileListResponse,
DirectoryInfoResponse,
FileInfoResponse, FileInfoResponse,
} from "$lib/server/schemas"; } 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 { export interface FileInfo {
id: number; id: number;
dataKey?: CryptoKey; dataKey?: CryptoKey;
@@ -75,92 +50,9 @@ export type CategoryInfo =
isFileRecursive: boolean; isFileRecursive: boolean;
}; };
const directoryInfoStore = new Map<DirectoryId, Writable<DirectoryInfo | null>>();
const fileInfoStore = new Map<number, Writable<FileInfo | null>>(); const fileInfoStore = new Map<number, Writable<FileInfo | null>>();
const categoryInfoStore = new Map<CategoryId, Writable<CategoryInfo | null>>(); const categoryInfoStore = new Map<CategoryId, Writable<CategoryInfo | null>>();
const fetchDirectoryInfoFromIndexedDB = async (
id: DirectoryId,
info: Writable<DirectoryInfo | null>,
) => {
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<DirectoryInfo | null>,
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<DirectoryInfo | null>,
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<FileInfo | null>) => { const fetchFileInfoFromIndexedDB = async (id: number, info: Writable<FileInfo | null>) => {
if (get(info)) return; if (get(info)) return;

View File

@@ -26,15 +26,33 @@ import {
encryptString, encryptString,
decryptString, decryptString,
} from "$lib/modules/crypto"; } from "$lib/modules/crypto";
import type { DirectoryInfo } from "$lib/modules/filesystem";
import type { import type {
DirectoryInfoResponse,
DirectoryDeleteResponse,
DirectoryRenameRequest,
DirectoryCreateRequest, DirectoryCreateRequest,
DirectoryCreateResponse, DirectoryCreateResponse,
DirectoryInfoResponse,
DirectoryRenameRequest,
} from "$lib/server/schemas"; } from "$lib/server/schemas";
import type { MasterKey } from "$lib/stores"; 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<DirectoryId>(); const initializedDirectoryIds = new Set<DirectoryId>();
let temporaryIdCounter = -1; let temporaryIdCounter = -1;
@@ -99,15 +117,10 @@ export const getDirectoryInfo = (id: DirectoryId, masterKey: CryptoKey) => {
export type DirectoryInfoStore = ReturnType<typeof getDirectoryInfo>; export type DirectoryInfoStore = ReturnType<typeof getDirectoryInfo>;
export const useDirectoryCreate = (parentId: DirectoryId) => { export const useDirectoryCreation = (parentId: DirectoryId, masterKey: MasterKey) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return createMutation< return createMutation<void, Error, { name: string }, { tempId: number }>({
{ id: number; dataKey: CryptoKey; dataKeyVersion: Date }, mutationFn: async ({ name }) => {
Error,
{ name: string; masterKey: MasterKey },
{ prevParentInfo: DirectoryInfo | undefined; tempId: number }
>({
mutationFn: async ({ name, masterKey }) => {
const { dataKey, dataKeyVersion } = await generateDataKey(); const { dataKey, dataKeyVersion } = await generateDataKey();
const nameEncrypted = await encryptString(name, dataKey); const nameEncrypted = await encryptString(name, dataKey);
@@ -119,30 +132,9 @@ export const useDirectoryCreate = (parentId: DirectoryId) => {
name: nameEncrypted.ciphertext, name: nameEncrypted.ciphertext,
nameIv: nameEncrypted.iv, nameIv: nameEncrypted.iv,
}); });
if (!res.ok) throw new Error("Failed to create directory");
const { directory: id }: DirectoryCreateResponse = await res.json(); const { directory: id }: DirectoryCreateResponse = await res.json();
return { id, dataKey, dataKeyVersion };
},
onMutate: async ({ name }) => {
await queryClient.cancelQueries({ queryKey: ["directory", parentId] });
const prevParentInfo = queryClient.getQueryData<DirectoryInfo>(["directory", parentId]);
const tempId = temporaryIdCounter--;
if (prevParentInfo) {
queryClient.setQueryData<DirectoryInfo>(["directory", parentId], {
...prevParentInfo,
subDirectoryIds: [...prevParentInfo.subDirectoryIds, tempId],
});
queryClient.setQueryData<DirectoryInfo>(["directory", tempId], {
id: tempId,
name,
subDirectoryIds: [],
fileIds: [],
});
}
return { prevParentInfo, tempId };
},
onSuccess: async ({ id, dataKey, dataKeyVersion }, { name }) => {
queryClient.setQueryData<DirectoryInfo>(["directory", id], { queryClient.setQueryData<DirectoryInfo>(["directory", id], {
id, id,
name, name,
@@ -153,13 +145,38 @@ export const useDirectoryCreate = (parentId: DirectoryId) => {
}); });
await storeDirectoryInfo({ id, parentId, name }); await storeDirectoryInfo({ id, parentId, name });
}, },
onError: (error, { name }, context) => { onMutate: async ({ name }) => {
if (context?.prevParentInfo) { const tempId = temporaryIdCounter--;
queryClient.setQueryData<DirectoryInfo>(["directory", parentId], context.prevParentInfo); queryClient.setQueryData<DirectoryInfo>(["directory", tempId], {
} id: tempId,
console.error(`Failed to create directory "${name}" in parent ${parentId}:`, error); name,
subDirectoryIds: [],
fileIds: [],
});
await queryClient.cancelQueries({ queryKey: ["directory", parentId] });
queryClient.setQueryData<DirectoryInfo>(["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<DirectoryInfo>(["directory", parentId], (prevParentInfo) => {
if (!prevParentInfo) return undefined;
return {
...prevParentInfo,
subDirectoryIds: prevParentInfo.subDirectoryIds.filter((id) => id !== context.tempId),
};
});
}
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ["directory", parentId] }); queryClient.invalidateQueries({ queryKey: ["directory", parentId] });
}, },
}); });
@@ -176,15 +193,18 @@ export const useDirectoryRename = () => {
dataKeyVersion: Date; dataKeyVersion: Date;
newName: string; newName: string;
}, },
{ prevInfo: (DirectoryInfo & { id: number }) | undefined } { oldName: string | undefined }
>({ >({
mutationFn: async ({ id, dataKey, dataKeyVersion, newName }) => { mutationFn: async ({ id, dataKey, dataKeyVersion, newName }) => {
const newNameEncrypted = await encryptString(newName, dataKey); const newNameEncrypted = await encryptString(newName, dataKey);
await callPostApi<DirectoryRenameRequest>(`/api/directory/${id}/rename`, { const res = await callPostApi<DirectoryRenameRequest>(`/api/directory/${id}/rename`, {
dekVersion: dataKeyVersion.toISOString(), dekVersion: dataKeyVersion.toISOString(),
name: newNameEncrypted.ciphertext, name: newNameEncrypted.ciphertext,
nameIv: newNameEncrypted.iv, nameIv: newNameEncrypted.iv,
}); });
if (!res.ok) throw new Error("Failed to rename directory");
await updateDirectoryInfo(id, { name: newName });
}, },
onMutate: async ({ id, newName }) => { onMutate: async ({ id, newName }) => {
await queryClient.cancelQueries({ queryKey: ["directory", id] }); await queryClient.cancelQueries({ queryKey: ["directory", id] });
@@ -195,61 +215,62 @@ export const useDirectoryRename = () => {
...prevInfo, ...prevInfo,
name: newName, name: newName,
}); });
await updateDirectoryInfo(id, { name: newName });
} }
return { prevInfo }; return { oldName: prevInfo?.name };
}, },
onSuccess: async (data, { id, newName }) => { onError: (_error, { id }, context) => {
await updateDirectoryInfo(id, { name: newName }); if (context?.oldName) {
}, queryClient.setQueryData<DirectoryInfo & { id: number }>(["directory", id], (prevInfo) => {
onError: (error, { id }, context) => { if (!prevInfo) return undefined;
if (context?.prevInfo) { return { ...prevInfo, name: context.oldName! };
queryClient.setQueryData<DirectoryInfo>(["directory", id], context.prevInfo); });
} }
console.error("Failed to rename directory:", error);
}, },
onSettled: (data, error, { id }) => { onSettled: (_data, _error, { id }) => {
queryClient.invalidateQueries({ queryKey: ["directory", id] }); queryClient.invalidateQueries({ queryKey: ["directory", id] });
}, },
}); });
}; };
export const useDirectoryDelete = (parentId: DirectoryId) => { export const useDirectoryDeletion = (parentId: DirectoryId) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return createMutation< return createMutation<{ deletedFiles: number[] }, Error, { id: number }, {}>({
void,
Error,
{ id: number },
{ prevInfo: (DirectoryInfo & { id: number }) | undefined }
>({
mutationFn: async ({ id }) => { 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 }) => { onMutate: async ({ id }) => {
await queryClient.cancelQueries({ queryKey: ["directory", parentId] }); await queryClient.cancelQueries({ queryKey: ["directory", parentId] });
queryClient.setQueryData<DirectoryInfo>(["directory", parentId], (prevParentInfo) => {
const prevParentInfo = queryClient.getQueryData<DirectoryInfo>(["directory", parentId]); if (!prevParentInfo) return undefined;
if (prevParentInfo) { return {
queryClient.setQueryData<DirectoryInfo>(["directory", parentId], {
...prevParentInfo, ...prevParentInfo,
subDirectoryIds: prevParentInfo.subDirectoryIds.filter((subId) => subId !== id), subDirectoryIds: prevParentInfo.subDirectoryIds.filter((subId) => subId !== id),
};
});
return {};
},
onError: (_error, { id }, context) => {
if (context) {
queryClient.setQueryData<DirectoryInfo>(["directory", parentId], (prevParentInfo) => {
if (!prevParentInfo) return undefined;
return {
...prevParentInfo,
subDirectoryIds: [...prevParentInfo.subDirectoryIds, id],
};
}); });
} }
const prevInfo = queryClient.getQueryData<DirectoryInfo & { id: number }>(["directory", id]);
return { prevInfo };
}, },
onSuccess: async (data, { id }) => { onSettled: () => {
await deleteDirectoryInfo(id);
},
onError: (error, { id }, context) => {
if (context?.prevInfo) {
queryClient.setQueryData<DirectoryInfo>(["directory", parentId], context?.prevInfo);
}
console.error("Failed to delete directory:", error);
},
onSettled: (data, error, { id }) => {
queryClient.invalidateQueries({ queryKey: ["directory", parentId] }); queryClient.invalidateQueries({ queryKey: ["directory", parentId] });
}, },
}); });

View File

@@ -181,7 +181,10 @@ export const unregisterDirectory = async (userId: number, directoryId: number) =
}; };
const unregisterDirectoryRecursively = async ( const unregisterDirectoryRecursively = async (
directoryId: number, 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 files = await unregisterFiles(directoryId);
const subDirectories = await trx const subDirectories = await trx
.selectFrom("directory") .selectFrom("directory")
@@ -189,7 +192,7 @@ export const unregisterDirectory = async (userId: number, directoryId: number) =
.where("parent_id", "=", directoryId) .where("parent_id", "=", directoryId)
.where("user_id", "=", userId) .where("user_id", "=", userId)
.execute(); .execute();
const subDirectoryFilePaths = await Promise.all( const subDirectoryEntries = await Promise.all(
subDirectories.map(async ({ id }) => await unregisterDirectoryRecursively(id)), subDirectories.map(async ({ id }) => await unregisterDirectoryRecursively(id)),
); );
@@ -201,7 +204,12 @@ export const unregisterDirectory = async (userId: number, directoryId: number) =
if (deleteRes.numDeletedRows === 0n) { if (deleteRes.numDeletedRows === 0n) {
throw new IntegrityError("Directory not found"); 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); return await unregisterDirectoryRecursively(directoryId);
}); });

View File

@@ -19,6 +19,7 @@ export const directoryInfoResponse = z.object({
export type DirectoryInfoResponse = z.output<typeof directoryInfoResponse>; export type DirectoryInfoResponse = z.output<typeof directoryInfoResponse>;
export const directoryDeleteResponse = z.object({ export const directoryDeleteResponse = z.object({
deletedDirectories: z.number().int().positive().array(),
deletedFiles: z.number().int().positive().array(), deletedFiles: z.number().int().positive().array(),
}); });
export type DirectoryDeleteResponse = z.output<typeof directoryDeleteResponse>; export type DirectoryDeleteResponse = z.output<typeof directoryDeleteResponse>;

View File

@@ -42,8 +42,9 @@ const safeUnlink = async (path: string | null) => {
export const deleteDirectory = async (userId: number, directoryId: number) => { export const deleteDirectory = async (userId: number, directoryId: number) => {
try { try {
const files = await unregisterDirectory(userId, directoryId); const { subDirectories, files } = await unregisterDirectory(userId, directoryId);
return { return {
directories: [...subDirectories.map(({ id }) => id), directoryId],
files: files.map(({ id, path, thumbnailPath }) => { files: files.map(({ id, path, thumbnailPath }) => {
safeUnlink(path); // Intended safeUnlink(path); // Intended
safeUnlink(thumbnailPath); // Intended safeUnlink(thumbnailPath); // Intended

View File

@@ -1,15 +1,14 @@
<script lang="ts"> <script lang="ts">
import { onMount } from "svelte"; import { onMount } from "svelte";
import type { Writable } from "svelte/store";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { FloatingButton } from "$lib/components/atoms"; import { FloatingButton } from "$lib/components/atoms";
import { TopBar } from "$lib/components/molecules"; import { TopBar } from "$lib/components/molecules";
import { type DirectoryInfo } from "$lib/modules/filesystem"; import { deleteFileCache, deleteFileThumbnailCache } from "$lib/modules/file";
import { import {
getDirectoryInfo, getDirectoryInfo,
useDirectoryCreate, useDirectoryCreation,
useDirectoryRename, useDirectoryRename,
useDirectoryDelete, useDirectoryDeletion,
} from "$lib/modules/filesystem2"; } from "$lib/modules/filesystem2";
import { masterKeyStore, hmacSecretStore } from "$lib/stores"; import { masterKeyStore, hmacSecretStore } from "$lib/stores";
import DirectoryCreateModal from "./DirectoryCreateModal.svelte"; import DirectoryCreateModal from "./DirectoryCreateModal.svelte";
@@ -35,9 +34,9 @@
let context = createContext(); let context = createContext();
let info = $derived(getDirectoryInfo(data.id, $masterKeyStore?.get(1)?.key!)); let info = $derived(getDirectoryInfo(data.id, $masterKeyStore?.get(1)?.key!));
let requestDirectoryCreation = $derived(useDirectoryCreate(data.id)); let requestDirectoryCreation = $derived(useDirectoryCreation(data.id, $masterKeyStore?.get(1)!));
let requestDirectoryRename = useDirectoryRename(); let requestDirectoryRename = useDirectoryRename();
let requestDirectoryDeletion = $derived(useDirectoryDelete(data.id)); let requestDirectoryDeletion = $derived(useDirectoryDeletion(data.id));
let fileInput: HTMLInputElement | undefined = $state(); let fileInput: HTMLInputElement | undefined = $state();
let duplicatedFile: File | undefined = $state(); let duplicatedFile: File | undefined = $state();
@@ -135,10 +134,7 @@
<DirectoryCreateModal <DirectoryCreateModal
bind:isOpen={isDirectoryCreateModalOpen} bind:isOpen={isDirectoryCreateModalOpen}
onCreateClick={async (name) => { onCreateClick={async (name) => {
$requestDirectoryCreation.mutate({ $requestDirectoryCreation.mutate({ name });
name,
masterKey: $masterKeyStore?.get(1)!,
});
return true; // TODO return true; // TODO
}} }}
/> />
@@ -190,9 +186,16 @@
bind:isOpen={isEntryDeleteModalOpen} bind:isOpen={isEntryDeleteModalOpen}
onDeleteClick={async () => { onDeleteClick={async () => {
if (context.selectedEntry!.type === "directory") { if (context.selectedEntry!.type === "directory") {
$requestDirectoryDeletion.mutate({ const res = await $requestDirectoryDeletion.mutateAsync({
id: context.selectedEntry!.id, id: context.selectedEntry!.id,
}); });
if (!res) return false;
await Promise.all(
res.deletedFiles.flatMap((fileId) => [
deleteFileCache(fileId),
deleteFileThumbnailCache(fileId),
]),
);
return true; // TODO return true; // TODO
} else { } else {
if (await requestEntryDeletion(context.selectedEntry!)) { if (await requestEntryDeletion(context.selectedEntry!)) {

View File

@@ -1,8 +1,12 @@
<script lang="ts"> <script lang="ts">
import { untrack } from "svelte"; import { untrack } from "svelte";
import { get, type Readable, type Writable } from "svelte/store"; import { get, type Writable } from "svelte/store";
import { getFileInfo, type DirectoryInfo, type FileInfo } from "$lib/modules/filesystem"; import { getFileInfo, type FileInfo } from "$lib/modules/filesystem";
import { getDirectoryInfo, type DirectoryInfoStore } from "$lib/modules/filesystem2"; import {
getDirectoryInfo,
type DirectoryInfo,
type DirectoryInfoStore,
} from "$lib/modules/filesystem2";
import { SortBy, sortEntries } from "$lib/modules/util"; import { SortBy, sortEntries } from "$lib/modules/util";
import { import {
fileUploadStatusStore, fileUploadStatusStore,

View File

@@ -1,9 +1,7 @@
<script lang="ts"> <script lang="ts">
import type { Writable } from "svelte/store";
import { ActionEntryButton } from "$lib/components/atoms"; import { ActionEntryButton } from "$lib/components/atoms";
import { DirectoryEntryLabel } from "$lib/components/molecules"; import { DirectoryEntryLabel } from "$lib/components/molecules";
import type { DirectoryInfo } from "$lib/modules/filesystem"; import type { DirectoryInfo, DirectoryInfoStore } from "$lib/modules/filesystem2";
import type { DirectoryInfoStore } from "$lib/modules/filesystem2";
import type { SelectedEntry } from "../service.svelte"; import type { SelectedEntry } from "../service.svelte";
import IconMoreVert from "~icons/material-symbols/more-vert"; import IconMoreVert from "~icons/material-symbols/more-vert";

View File

@@ -16,8 +16,11 @@ export const POST: RequestHandler = async ({ locals, params }) => {
if (!zodRes.success) error(400, "Invalid path parameters"); if (!zodRes.success) error(400, "Invalid path parameters");
const { id } = zodRes.data; const { id } = zodRes.data;
const { files } = await deleteDirectory(userId, id); const { directories, files } = await deleteDirectory(userId, id);
return json( return json(
directoryDeleteResponse.parse({ deletedFiles: files } satisfies DirectoryDeleteResponse), directoryDeleteResponse.parse({
deletedDirectories: directories,
deletedFiles: files,
} satisfies DirectoryDeleteResponse),
); );
}; };