파일 관련 API 요청을 TanStack Query로 마이그레이션

This commit is contained in:
static
2025-07-17 13:09:02 +09:00
parent e10b600293
commit 27fcb7472e
18 changed files with 399 additions and 240 deletions

View File

@@ -3,7 +3,8 @@
import { get, type Writable } from "svelte/store";
import { CheckBox } from "$lib/components/atoms";
import { SubCategories, type SelectedCategory } from "$lib/components/molecules";
import { getFileInfo, type FileInfo, type CategoryInfo } from "$lib/modules/filesystem";
import type { CategoryInfo } from "$lib/modules/filesystem";
import { getFileInfo, type FileInfoStore } from "$lib/modules/filesystem2";
import { SortBy, sortEntries } from "$lib/modules/util";
import { masterKeyStore } from "$lib/stores";
import File from "./File.svelte";
@@ -33,9 +34,7 @@
isFileRecursive = $bindable(),
}: Props = $props();
let files: { name?: string; info: Writable<FileInfo | null>; isRecursive: boolean }[] = $state(
[],
);
let files: { name?: string; info: FileInfoStore; isRecursive: boolean }[] = $state([]);
$effect(() => {
files =
@@ -44,7 +43,7 @@
.map(({ id, isRecursive }) => {
const info = getFileInfo(id, $masterKeyStore?.get(1)?.key!);
return {
name: get(info)?.name,
name: get(info).data?.name,
info,
isRecursive,
};
@@ -58,8 +57,8 @@
const unsubscribes = files.map((file) =>
file.info.subscribe((value) => {
if (file.name === value?.name) return;
file.name = value?.name;
if (file.name === value.data?.name) return;
file.name = value.data?.name;
sort();
}),
);

View File

@@ -2,13 +2,13 @@
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, FileInfoStore } from "$lib/modules/filesystem2";
import { requestFileThumbnailDownload, type SelectedFile } from "./service";
import IconClose from "~icons/material-symbols/close";
interface Props {
info: Writable<FileInfo | null>;
info: FileInfoStore;
onclick: (selectedFile: SelectedFile) => void;
onRemoveClick?: (selectedFile: SelectedFile) => void;
}
@@ -18,22 +18,22 @@
let thumbnail: string | undefined = $state();
const openFile = () => {
const { id, dataKey, dataKeyVersion, name } = $info as FileInfo;
const { id, dataKey, dataKeyVersion, name } = $info.data as FileInfo;
if (!dataKey || !dataKeyVersion) return; // TODO: Error handling
onclick({ id, dataKey, dataKeyVersion, name });
};
const removeFile = () => {
const { id, dataKey, dataKeyVersion, name } = $info as FileInfo;
const { id, dataKey, dataKeyVersion, name } = $info.data as FileInfo;
if (!dataKey || !dataKeyVersion) return; // TODO: Error handling
onRemoveClick!({ id, dataKey, dataKeyVersion, name });
};
$effect(() => {
if ($info?.dataKey) {
requestFileThumbnailDownload($info.id, $info.dataKey)
if ($info.data?.dataKey) {
requestFileThumbnailDownload($info.data.id, $info.data.dataKey)
.then((thumbnailUrl) => {
thumbnail = thumbnailUrl ?? undefined;
})
@@ -47,13 +47,13 @@
});
</script>
{#if $info}
{#if $info.status === "success"}
<ActionEntryButton
class="h-12"
onclick={openFile}
actionButtonIcon={onRemoveClick && IconClose}
onActionButtonClick={removeFile}
>
<DirectoryEntryLabel type="file" {thumbnail} name={$info.name} />
<DirectoryEntryLabel type="file" {thumbnail} name={$info.data.name} />
</ActionEntryButton>
{/if}

View File

@@ -86,6 +86,10 @@ export const storeFileInfo = async (fileInfo: FileInfo) => {
await filesystem.file.put(fileInfo);
};
export const updateFileInfo = async (id: number, changes: { name?: string }) => {
await filesystem.file.update(id, changes);
};
export const deleteFileInfo = async (id: number) => {
await filesystem.file.delete(id);
};

View File

@@ -5,6 +5,7 @@ import { writable, type Writable } from "svelte/store";
import {
encodeToBase64,
generateDataKey,
makeAESKeyNonextractable,
wrapDataKey,
encryptData,
encryptString,
@@ -118,12 +119,14 @@ const encryptFile = limitFunction(
});
return {
dataKey: await makeAESKeyNonextractable(dataKey),
dataKeyWrapped,
dataKeyVersion,
fileType,
fileEncrypted,
fileEncryptedHash,
nameEncrypted,
createdAt,
createdAtEncrypted,
lastModifiedAtEncrypted,
thumbnail: thumbnailEncrypted && { plaintext: thumbnailBuffer, ...thumbnailEncrypted },
@@ -176,9 +179,7 @@ export const uploadFile = async (
hmacSecret: HmacSecret,
masterKey: MasterKey,
onDuplicate: () => Promise<boolean>,
): Promise<
{ fileId: number; fileBuffer: ArrayBuffer; thumbnailBuffer?: ArrayBuffer } | undefined
> => {
) => {
const status = writable<FileUploadStatus>({
name: file.name,
parentId,
@@ -208,12 +209,14 @@ export const uploadFile = async (
}
const {
dataKey,
dataKeyWrapped,
dataKeyVersion,
fileType,
fileEncrypted,
fileEncryptedHash,
nameEncrypted,
createdAt,
createdAtEncrypted,
lastModifiedAtEncrypted,
thumbnail,
@@ -256,7 +259,16 @@ export const uploadFile = async (
}
const { fileId } = await requestFileUpload(status, form, thumbnailForm);
return { fileId, fileBuffer, thumbnailBuffer: thumbnail?.plaintext };
return {
fileId,
fileDataKey: dataKey,
fileDataKeyVersion: dataKeyVersion,
fileType,
fileEncryptedIv: fileEncrypted.iv,
fileCreatedAt: createdAt,
fileBuffer,
thumbnailBuffer: thumbnail?.plaintext,
};
} catch (e) {
status.update((value) => {
value.status = "error";

View File

@@ -1,9 +1,6 @@
import { get, writable, type Writable } from "svelte/store";
import { callGetApi } from "$lib/hooks";
import {
getFileInfo as getFileInfoFromIndexedDB,
storeFileInfo,
deleteFileInfo,
getCategoryInfos as getCategoryInfosFromIndexedDB,
getCategoryInfo as getCategoryInfoFromIndexedDB,
storeCategoryInfo,
@@ -12,23 +9,7 @@ import {
type CategoryId,
} from "$lib/indexedDB";
import { unwrapDataKey, decryptString } from "$lib/modules/crypto";
import type {
CategoryInfoResponse,
CategoryFileListResponse,
FileInfoResponse,
} from "$lib/server/schemas";
export interface FileInfo {
id: number;
dataKey?: CryptoKey;
dataKeyVersion?: Date;
contentType: string;
contentIv?: string;
name: string;
createdAt?: Date;
lastModifiedAt: Date;
categoryIds: number[];
}
import type { CategoryInfoResponse, CategoryFileListResponse } from "$lib/server/schemas";
export type CategoryInfo =
| {
@@ -50,90 +31,8 @@ export type CategoryInfo =
isFileRecursive: boolean;
};
const fileInfoStore = new Map<number, Writable<FileInfo | null>>();
const categoryInfoStore = new Map<CategoryId, Writable<CategoryInfo | null>>();
const fetchFileInfoFromIndexedDB = async (id: number, info: Writable<FileInfo | null>) => {
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<FileInfo | null>,
masterKey: CryptoKey,
) => {
const res = await callGetApi(`/api/file/${id}`);
if (res.status === 404) {
info.set(null);
await deleteFileInfo(id);
return;
} else if (!res.ok) {
throw new Error("Failed to fetch file information");
}
const metadata: FileInfoResponse = await res.json();
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,
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<FileInfo | null>, 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;
};
const fetchCategoryInfoFromIndexedDB = async (
id: CategoryId,
info: Writable<CategoryInfo | null>,

View File

@@ -8,16 +8,8 @@ import {
updateDirectoryInfo,
deleteDirectoryInfo,
getFileInfos as getFileInfosFromIndexedDB,
getFileInfo as getFileInfoFromIndexedDB,
storeFileInfo,
deleteFileInfo,
getCategoryInfos as getCategoryInfosFromIndexedDB,
getCategoryInfo as getCategoryInfoFromIndexedDB,
storeCategoryInfo,
updateCategoryInfo as updateCategoryInfoInIndexedDB,
deleteCategoryInfo,
type DirectoryId,
type CategoryId,
} from "$lib/indexedDB";
import {
generateDataKey,
@@ -53,14 +45,14 @@ export type DirectoryInfo =
fileIds: number[];
};
const initializedDirectoryIds = new Set<DirectoryId>();
const initializedIds = new Set<DirectoryId>();
let temporaryIdCounter = -1;
const getInitialDirectoryInfo = async (id: DirectoryId) => {
if (!browser || initializedDirectoryIds.has(id)) {
if (!browser || initializedIds.has(id)) {
return undefined;
} else {
initializedDirectoryIds.add(id);
initializedIds.add(id);
}
const [directory, subDirectories, files] = await Promise.all([
@@ -254,7 +246,9 @@ export const useDirectoryDeletion = (parentId: DirectoryId) => {
if (!prevParentInfo) return undefined;
return {
...prevParentInfo,
subDirectoryIds: prevParentInfo.subDirectoryIds.filter((subId) => subId !== id),
subDirectoryIds: prevParentInfo.subDirectoryIds.filter(
(subDirectoryId) => subDirectoryId !== id,
),
};
});
return {};

View File

@@ -0,0 +1,238 @@
import { useQueryClient, createQuery, createMutation } from "@tanstack/svelte-query";
import { browser } from "$app/environment";
import { callGetApi, callPostApi } from "$lib/hooks";
import {
getFileInfo as getFileInfoFromIndexedDB,
storeFileInfo,
updateFileInfo,
deleteFileInfo,
type DirectoryId,
} from "$lib/indexedDB";
import { unwrapDataKey, encryptString, decryptString } from "$lib/modules/crypto";
import { uploadFile } from "$lib/modules/file";
import type { FileInfoResponse, FileRenameRequest } from "$lib/server/schemas";
import type { MasterKey, HmacSecret } from "$lib/stores";
import type { DirectoryInfo } from "./directory";
export interface FileInfo {
id: number;
dataKey?: CryptoKey;
dataKeyVersion?: Date;
contentType: string;
contentIv?: string;
name: string;
createdAt?: Date;
lastModifiedAt: Date;
categoryIds: number[];
}
const initializedFileIds = new Set<number>();
const getInitialFileInfo = async (id: number) => {
if (!browser || initializedFileIds.has(id)) {
return undefined;
}
initializedFileIds.add(id);
return await getFileInfoFromIndexedDB(id);
};
const decryptDate = async (ciphertext: string, iv: string, dataKey: CryptoKey) => {
return new Date(parseInt(await decryptString(ciphertext, iv, dataKey), 10));
};
export const getFileInfo = (id: number, masterKey: CryptoKey) => {
const queryClient = useQueryClient();
getInitialFileInfo(id).then((info) => {
if (info && !queryClient.getQueryData(["file", id])) {
queryClient.setQueryData<FileInfo>(["file", id], info);
}
}); // Intended
return createQuery<FileInfo>({
queryKey: ["file", id],
queryFn: async () => {
const res = await callGetApi(`/api/file/${id}`); // TODO: 404
const metadata: FileInfoResponse = await res.json();
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,
);
await storeFileInfo({
id,
parentId: metadata.parent,
name,
contentType: metadata.contentType,
createdAt,
lastModifiedAt,
categoryIds: metadata.categories,
});
return {
id,
dataKey,
dataKeyVersion: new Date(metadata.dekVersion),
contentType: metadata.contentType,
contentIv: metadata.contentIv,
name,
createdAt,
lastModifiedAt,
categoryIds: metadata.categories,
};
},
});
};
export type FileInfoStore = ReturnType<typeof getFileInfo>;
export const useFileUpload = (
parentId: DirectoryId,
masterKey: MasterKey,
hmacSecret: HmacSecret,
) => {
const queryClient = useQueryClient();
return createMutation<
{ fileId: number; fileBuffer: ArrayBuffer; thumbnailBuffer?: ArrayBuffer },
Error,
{ file: File; onDuplicate: () => Promise<boolean> },
{ tempId: number }
>({
mutationFn: async ({ file, onDuplicate }) => {
const res = await uploadFile(file, parentId, hmacSecret, masterKey, onDuplicate);
if (!res) throw new Error("Failed to upload file");
queryClient.setQueryData<FileInfo>(["file", res.fileId], {
id: res.fileId,
dataKey: res.fileDataKey,
dataKeyVersion: res.fileDataKeyVersion,
contentType: res.fileType,
contentIv: res.fileEncryptedIv,
name: file.name,
createdAt: res.fileCreatedAt,
lastModifiedAt: new Date(file.lastModified),
categoryIds: [],
});
await storeFileInfo({
id: res.fileId,
parentId,
name: file.name,
contentType: res.fileType,
createdAt: res.fileCreatedAt,
lastModifiedAt: new Date(file.lastModified),
categoryIds: [],
});
return {
fileId: res.fileId,
fileBuffer: res.fileBuffer,
thumbnailBuffer: res.thumbnailBuffer,
};
},
onSuccess: async ({ fileId }) => {
await queryClient.cancelQueries({ queryKey: ["directory", parentId] });
queryClient.setQueryData<DirectoryInfo>(["directory", parentId], (prevParentInfo) => {
if (!prevParentInfo) return undefined;
return {
...prevParentInfo,
fileIds: [...prevParentInfo.fileIds, fileId],
};
});
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ["directory", parentId] });
},
});
};
export const useFileRename = () => {
const queryClient = useQueryClient();
return createMutation<
void,
Error,
{
id: number;
dataKey: CryptoKey;
dataKeyVersion: Date;
newName: string;
},
{ oldName: string | undefined }
>({
mutationFn: async ({ id, dataKey, dataKeyVersion, newName }) => {
const newNameEncrypted = await encryptString(newName, dataKey);
const res = await callPostApi<FileRenameRequest>(`/api/file/${id}/rename`, {
dekVersion: dataKeyVersion.toISOString(),
name: newNameEncrypted.ciphertext,
nameIv: newNameEncrypted.iv,
});
if (!res.ok) throw new Error("Failed to rename file");
await updateFileInfo(id, { name: newName });
},
onMutate: async ({ id, newName }) => {
await queryClient.cancelQueries({ queryKey: ["file", id] });
const prevInfo = queryClient.getQueryData<FileInfo>(["file", id]);
if (prevInfo) {
queryClient.setQueryData<FileInfo>(["file", id], {
...prevInfo,
name: newName,
});
}
return { oldName: prevInfo?.name };
},
onError: (_error, { id }, context) => {
if (context?.oldName) {
queryClient.setQueryData<FileInfo>(["file", id], (prevInfo) => {
if (!prevInfo) return undefined;
return { ...prevInfo, name: context.oldName! };
});
}
},
onSettled: (_data, _error, { id }) => {
queryClient.invalidateQueries({ queryKey: ["file", id] });
},
});
};
export const useFileDeletion = (parentId: DirectoryId) => {
const queryClient = useQueryClient();
return createMutation<void, Error, { id: number }, {}>({
mutationFn: async ({ id }) => {
const res = await callPostApi(`/api/file/${id}/delete`);
if (!res.ok) throw new Error("Failed to delete file");
await deleteFileInfo(id);
},
onMutate: async ({ id }) => {
await queryClient.cancelQueries({ queryKey: ["directory", parentId] });
queryClient.setQueryData<DirectoryInfo>(["directory", parentId], (prevParentInfo) => {
if (!prevParentInfo) return undefined;
return {
...prevParentInfo,
fileIds: prevParentInfo.fileIds.filter((fileId) => fileId !== id),
};
});
return {};
},
onError: (_error, { id }, context) => {
if (context) {
queryClient.setQueryData<DirectoryInfo>(["directory", parentId], (prevParentInfo) => {
if (!prevParentInfo) return undefined;
return {
...prevParentInfo,
fileIds: [...prevParentInfo.fileIds, id],
};
});
}
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ["directory", parentId] });
},
});
};

View File

@@ -0,0 +1,2 @@
export * from "./directory";
export * from "./file";

View File

@@ -5,12 +5,8 @@
import { goto } from "$app/navigation";
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 { getCategoryInfo, type CategoryInfo } from "$lib/modules/filesystem";
import { getFileInfo } from "$lib/modules/filesystem2";
import { captureVideoThumbnail } from "$lib/modules/thumbnail";
import { fileDownloadStatusStore, isFileDownloading, masterKeyStore } from "$lib/stores";
import AddToCategoryBottomSheet from "./AddToCategoryBottomSheet.svelte";
@@ -28,7 +24,7 @@
let { data } = $props();
let info: Writable<FileInfo | null> | undefined = $state();
let info = $derived(getFileInfo(data.id, $masterKeyStore?.get(1)?.key!));
let categories: Writable<CategoryInfo | null>[] = $state([]);
let isAddToCategoryBottomSheetOpen = $state(false);
@@ -85,19 +81,19 @@
};
$effect(() => {
info = getFileInfo(data.id, $masterKeyStore?.get(1)?.key!);
data.id;
isDownloadRequested = false;
viewerType = undefined;
});
$effect(() => {
categories =
$info?.categoryIds.map((id) => getCategoryInfo(id, $masterKeyStore?.get(1)?.key!)) ?? [];
$info.data?.categoryIds.map((id) => getCategoryInfo(id, $masterKeyStore?.get(1)?.key!)) ?? [];
});
$effect(() => {
if ($info && $info.dataKey && $info.contentIv) {
const contentType = $info.contentType;
if ($info.data?.dataKey && $info.data?.contentIv) {
const contentType = $info.data.contentType;
if (contentType.startsWith("image")) {
viewerType = "image";
} else if (contentType.startsWith("video")) {
@@ -107,21 +103,23 @@
untrack(() => {
if (!downloadStatus && !isDownloadRequested) {
isDownloadRequested = true;
requestFileDownload(data.id, $info.contentIv!, $info.dataKey!).then(async (buffer) => {
const blob = await updateViewer(buffer, contentType);
if (!viewerType) {
FileSaver.saveAs(blob, $info.name);
}
});
requestFileDownload(data.id, $info.data.contentIv!, $info.data.dataKey!).then(
async (buffer) => {
const blob = await updateViewer(buffer, contentType);
if (!viewerType) {
FileSaver.saveAs(blob, $info.data.name);
}
},
);
}
});
}
});
$effect(() => {
if ($info && $downloadStatus?.status === "decrypted") {
if ($info.status === "success" && $downloadStatus?.status === "decrypted") {
untrack(
() => !isDownloadRequested && updateViewer($downloadStatus.result!, $info.contentType),
() => !isDownloadRequested && updateViewer($downloadStatus.result!, $info.data.contentType),
);
}
});
@@ -133,11 +131,11 @@
<title>파일</title>
</svelte:head>
<TopBar title={$info?.name} />
<TopBar title={$info.data?.name} />
<FullscreenDiv>
<div class="space-y-4 pb-4">
<DownloadStatus status={downloadStatus} />
{#if $info && viewerType}
{#if $info.status === "success" && viewerType}
<div class="flex w-full justify-center">
{#snippet viewerLoading(message: string)}
<p class="text-gray-500">{message}</p>
@@ -145,7 +143,7 @@
{#if viewerType === "image"}
{#if fileBlobUrl}
<img src={fileBlobUrl} alt={$info.name} onerror={convertHeicToJpeg} />
<img src={fileBlobUrl} alt={$info.data.name} onerror={convertHeicToJpeg} />
{:else}
{@render viewerLoading("이미지를 불러오고 있어요.")}
{/if}
@@ -156,7 +154,7 @@
<video bind:this={videoElement} src={fileBlobUrl} controls muted></video>
<IconEntryButton
icon={IconCamera}
onclick={() => updateThumbnail($info.dataKey!, $info.dataKeyVersion!)}
onclick={() => updateThumbnail($info.data.dataKey!, $info.data.dataKeyVersion!)}
class="w-full"
>
이 장면을 썸네일로 설정하기

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { get, type Writable } from "svelte/store";
import { getFileInfo, type FileInfo } from "$lib/modules/filesystem";
import { getFileInfo } from "$lib/modules/filesystem2";
import { formatNetworkSpeed } from "$lib/modules/util";
import { masterKeyStore, type FileDownloadStatus } from "$lib/stores";
@@ -17,14 +17,10 @@
let { status }: Props = $props();
let fileInfo: Writable<FileInfo | null> | undefined = $state();
$effect(() => {
fileInfo = getFileInfo(get(status).id, $masterKeyStore?.get(1)?.key!);
});
let fileInfo = $derived(getFileInfo(get(status).id, $masterKeyStore?.get(1)?.key!));
</script>
{#if $fileInfo}
{#if $fileInfo.status === "success"}
<div class="flex h-14 items-center gap-x-4 p-2">
<div class="flex-shrink-0 text-lg text-gray-600">
{#if $status.status === "download-pending"}
@@ -42,8 +38,8 @@
{/if}
</div>
<div class="flex-grow overflow-hidden">
<p title={$fileInfo.name} class="truncate font-medium">
{$fileInfo.name}
<p title={$fileInfo.data.name} class="truncate font-medium">
{$fileInfo.data.name}
</p>
<p class="text-xs text-gray-800">
{#if $status.status === "download-pending"}

View File

@@ -1,18 +1,17 @@
<script lang="ts">
import { onMount } from "svelte";
import type { Writable } from "svelte/store";
import { FullscreenDiv } from "$lib/components/atoms";
import { TopBar } from "$lib/components/molecules";
import type { FileCacheIndex } from "$lib/indexedDB";
import { getFileCacheIndex, deleteFileCache as doDeleteFileCache } from "$lib/modules/file";
import { getFileInfo, type FileInfo } from "$lib/modules/filesystem";
import { getFileInfo, type FileInfoStore } from "$lib/modules/filesystem2";
import { formatFileSize } from "$lib/modules/util";
import { masterKeyStore } from "$lib/stores";
import File from "./File.svelte";
interface FileCache {
index: FileCacheIndex;
fileInfo: Writable<FileInfo | null>;
fileInfo: FileInfoStore;
}
let fileCache: FileCache[] | undefined = $state();

View File

@@ -1,7 +1,6 @@
<script lang="ts">
import type { Writable } from "svelte/store";
import type { FileCacheIndex } from "$lib/indexedDB";
import type { FileInfo } from "$lib/modules/filesystem";
import type { FileInfoStore } from "$lib/modules/filesystem2";
import { formatDate, formatFileSize } from "$lib/modules/util";
import IconDraft from "~icons/material-symbols/draft";
@@ -10,7 +9,7 @@
interface Props {
index: FileCacheIndex;
info: Writable<FileInfo | null>;
info: FileInfoStore;
onDeleteClick: (fileId: number) => void;
}
@@ -28,8 +27,8 @@
</div>
{/if}
<div class="flex-grow overflow-hidden">
{#if $info}
<p title={$info.name} class="truncate font-medium">{$info.name}</p>
{#if $info.status === "success"}
<p title={$info.data.name} class="truncate font-medium">{$info.data.name}</p>
{:else}
<p class="font-medium">삭제된 파일</p>
{/if}

View File

@@ -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";
import { masterKeyStore } from "$lib/stores";
import File from "./File.svelte";
import {
@@ -21,8 +21,8 @@
const generateAllThumbnails = () => {
persistentStates.files.forEach(({ info }) => {
const fileInfo = get(info);
if (fileInfo) {
requestThumbnailGeneration(fileInfo);
if (fileInfo.data) {
requestThumbnailGeneration(fileInfo.data);
}
});
};

View File

@@ -13,14 +13,14 @@
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, FileInfoStore } from "$lib/modules/filesystem2";
import { formatDateTime } from "$lib/modules/util";
import type { GenerationStatus } from "./service.svelte";
import IconCamera from "~icons/material-symbols/camera";
interface Props {
info: Writable<FileInfo | null>;
info: FileInfoStore;
onclick: (selectedFile: FileInfo) => void;
onGenerateThumbnailClick: (selectedFile: FileInfo) => void;
generationStatus?: Writable<GenerationStatus>;
@@ -29,18 +29,18 @@
let { info, onclick, onGenerateThumbnailClick, generationStatus }: Props = $props();
</script>
{#if $info}
{#if $info.status === "success"}
<ActionEntryButton
class="h-14"
onclick={() => onclick($info)}
onclick={() => onclick($info.data)}
actionButtonIcon={!$generationStatus || $generationStatus === "error" ? IconCamera : undefined}
onActionButtonClick={() => onGenerateThumbnailClick($info)}
onActionButtonClick={() => onGenerateThumbnailClick($info.data)}
actionButtonClass="text-gray-800"
>
{@const subtext =
$generationStatus && $generationStatus !== "uploaded"
? subtexts[$generationStatus]
: formatDateTime($info.createdAt ?? $info.lastModifiedAt)}
<DirectoryEntryLabel type="file" name={$info.name} {subtext} />
: formatDateTime($info.data.createdAt ?? $info.data.lastModifiedAt)}
<DirectoryEntryLabel type="file" name={$info.data.name} {subtext} />
</ActionEntryButton>
{/if}

View File

@@ -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, FileInfoStore } from "$lib/modules/filesystem2";
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<FileInfo | null>;
info: FileInfoStore;
status?: Writable<GenerationStatus>;
}

View File

@@ -3,12 +3,20 @@
import { goto } from "$app/navigation";
import { FloatingButton } from "$lib/components/atoms";
import { TopBar } from "$lib/components/molecules";
import { deleteFileCache, deleteFileThumbnailCache } from "$lib/modules/file";
import {
storeFileCache,
deleteFileCache,
storeFileThumbnailCache,
deleteFileThumbnailCache,
} from "$lib/modules/file";
import {
getDirectoryInfo,
useDirectoryCreation,
useDirectoryRename,
useDirectoryDeletion,
useFileUpload,
useFileRename,
useFileDeletion,
} from "$lib/modules/filesystem2";
import { masterKeyStore, hmacSecretStore } from "$lib/stores";
import DirectoryCreateModal from "./DirectoryCreateModal.svelte";
@@ -20,13 +28,7 @@
import EntryMenuBottomSheet from "./EntryMenuBottomSheet.svelte";
import EntryRenameModal from "./EntryRenameModal.svelte";
import UploadStatusCard from "./UploadStatusCard.svelte";
import {
createContext,
requestHmacSecretDownload,
requestFileUpload,
requestEntryRename,
requestEntryDeletion,
} from "./service.svelte";
import { createContext, requestHmacSecretDownload } from "./service.svelte";
import IconAdd from "~icons/material-symbols/add";
@@ -37,6 +39,11 @@
let requestDirectoryCreation = $derived(useDirectoryCreation(data.id, $masterKeyStore?.get(1)!));
let requestDirectoryRename = useDirectoryRename();
let requestDirectoryDeletion = $derived(useDirectoryDeletion(data.id));
let requestFileUpload = $derived(
useFileUpload(data.id, $masterKeyStore?.get(1)!, $hmacSecretStore?.get(1)!),
);
let requestFileRename = $derived(useFileRename());
let requestFileDeletion = $derived(useFileDeletion(data.id));
let fileInput: HTMLInputElement | undefined = $state();
let duplicatedFile: File | undefined = $state();
@@ -55,21 +62,24 @@
if (!files || files.length === 0) return;
for (const file of files) {
requestFileUpload(file, data.id, $hmacSecretStore?.get(1)!, $masterKeyStore?.get(1)!, () => {
return new Promise((resolve) => {
duplicatedFile = file;
resolveForDuplicateFileModal = resolve;
isDuplicateFileModalOpen = true;
});
})
.then((res) => {
if (!res) return;
// TODO: FIXME
// info = getDirectoryInfo(data.id, $masterKeyStore?.get(1)?.key!);
$requestFileUpload
.mutateAsync({
file,
onDuplicate: () => {
return new Promise((resolve) => {
duplicatedFile = file;
resolveForDuplicateFileModal = resolve;
isDuplicateFileModalOpen = true;
});
},
})
.catch((e: Error) => {
// TODO: FIXME
console.error(e);
.then((res) => {
if (res) {
storeFileCache(res.fileId, res.fileBuffer); // Intended
if (res.thumbnailBuffer) {
storeFileThumbnailCache(res.fileId, res.thumbnailBuffer); // Intended
}
}
});
}
@@ -174,11 +184,13 @@
});
return true; // TODO
} else {
if (await requestEntryRename(context.selectedEntry!, newName)) {
// info = getDirectoryInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME
return true;
}
return false;
$requestFileRename.mutate({
id: context.selectedEntry!.id,
dataKey: context.selectedEntry!.dataKey,
dataKeyVersion: context.selectedEntry!.dataKeyVersion,
newName,
});
return true; // TODO
}
}}
/>
@@ -186,9 +198,7 @@
bind:isOpen={isEntryDeleteModalOpen}
onDeleteClick={async () => {
if (context.selectedEntry!.type === "directory") {
const res = await $requestDirectoryDeletion.mutateAsync({
id: context.selectedEntry!.id,
});
const res = await $requestDirectoryDeletion.mutateAsync({ id: context.selectedEntry!.id });
if (!res) return false;
await Promise.all(
res.deletedFiles.flatMap((fileId) => [
@@ -198,11 +208,12 @@
);
return true; // TODO
} else {
if (await requestEntryDeletion(context.selectedEntry!)) {
// info = getDirectoryInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME
return true;
}
return false;
await $requestFileDeletion.mutateAsync({ id: context.selectedEntry!.id });
await Promise.all([
deleteFileCache(context.selectedEntry!.id),
deleteFileThumbnailCache(context.selectedEntry!.id),
]);
return true; // TODO
}
}}
/>

View File

@@ -1,11 +1,12 @@
<script lang="ts">
import { untrack } from "svelte";
import { get, type Writable } from "svelte/store";
import { getFileInfo, type FileInfo } from "$lib/modules/filesystem";
import {
getDirectoryInfo,
getFileInfo,
type DirectoryInfo,
type DirectoryInfoStore,
type FileInfoStore,
} from "$lib/modules/filesystem2";
import { SortBy, sortEntries } from "$lib/modules/util";
import {
@@ -37,7 +38,7 @@
| {
type: "file";
name?: string;
info: Writable<FileInfo | null>;
info: FileInfoStore;
}
| {
type: "uploading-file";
@@ -60,7 +61,7 @@
const info = getFileInfo(id, $masterKeyStore?.get(1)?.key!);
return {
type: "file",
name: get(info)?.name,
name: get(info).data?.name,
info,
};
})
@@ -93,13 +94,21 @@
}),
)
.concat(
files.map((file) =>
file.info.subscribe((value) => {
if (file.name === value?.name) return;
file.name = value?.name;
sort();
}),
),
files.map((file) => {
if (file.type === "file") {
return file.info.subscribe((value) => {
if (file.name === value.data?.name) return;
file.name = value.data?.name;
sort();
});
} else {
return file.info.subscribe((value) => {
if (file.name === value.name) return;
file.name = value.name;
sort();
});
}
}),
);
return () => unsubscribes.forEach((unsubscribe) => unsubscribe());
});

View File

@@ -1,8 +1,7 @@
<script lang="ts">
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, FileInfoStore } from "$lib/modules/filesystem2";
import { formatDateTime } from "$lib/modules/util";
import { requestFileThumbnailDownload } from "./service";
import type { SelectedEntry } from "../service.svelte";
@@ -10,7 +9,7 @@
import IconMoreVert from "~icons/material-symbols/more-vert";
interface Props {
info: Writable<FileInfo | null>;
info: FileInfoStore;
onclick: (selectedEntry: SelectedEntry) => void;
onOpenMenuClick: (selectedEntry: SelectedEntry) => void;
}
@@ -20,22 +19,22 @@
let thumbnail: string | undefined = $state();
const openFile = () => {
const { id, dataKey, dataKeyVersion, name } = $info!;
const { id, dataKey, dataKeyVersion, name } = $info.data as FileInfo;
if (!dataKey || !dataKeyVersion) return; // TODO: Error handling
onclick({ type: "file", id, dataKey, dataKeyVersion, name });
};
const openMenu = () => {
const { id, dataKey, dataKeyVersion, name } = $info!;
const { id, dataKey, dataKeyVersion, name } = $info.data as FileInfo;
if (!dataKey || !dataKeyVersion) return; // TODO: Error handling
onOpenMenuClick({ type: "file", id, dataKey, dataKeyVersion, name });
};
$effect(() => {
if ($info?.dataKey) {
requestFileThumbnailDownload($info.id, $info.dataKey)
if ($info.data?.dataKey) {
requestFileThumbnailDownload($info.data.id, $info.data.dataKey)
.then((thumbnailUrl) => {
thumbnail = thumbnailUrl ?? undefined;
})
@@ -49,7 +48,7 @@
});
</script>
{#if $info}
{#if $info.status === "success"}
<ActionEntryButton
class="h-14"
onclick={openFile}
@@ -59,8 +58,8 @@
<DirectoryEntryLabel
type="file"
{thumbnail}
name={$info.name}
subtext={formatDateTime($info.createdAt ?? $info.lastModifiedAt)}
name={$info.data.name}
subtext={formatDateTime($info.data.createdAt ?? $info.data.lastModifiedAt)}
/>
</ActionEntryButton>
{/if}