mirror of
https://github.com/kmc7468/arkvault.git
synced 2025-12-14 22:08:45 +00:00
파일/디렉터리 목록 캐싱 추가
This commit is contained in:
90
src/lib/modules/file.ts
Normal file
90
src/lib/modules/file.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import { writable } from "svelte/store";
|
||||||
|
import { callGetApi } from "$lib/hooks";
|
||||||
|
import { unwrapDataKey, decryptString } from "$lib/modules/crypto";
|
||||||
|
import type { DirectoryInfoResponse, FileInfoResponse } from "$lib/server/schemas";
|
||||||
|
import {
|
||||||
|
directoryInfoStore,
|
||||||
|
fileInfoStore,
|
||||||
|
type DirectoryInfo,
|
||||||
|
type FileInfo,
|
||||||
|
} from "$lib/stores/file";
|
||||||
|
|
||||||
|
const fetchDirectoryInfo = async (directoryId: "root" | number, masterKey: CryptoKey) => {
|
||||||
|
const res = await callGetApi(`/api/directory/${directoryId}`);
|
||||||
|
if (!res.ok) throw new Error("Failed to fetch directory information");
|
||||||
|
const { metadata, subDirectories, files }: DirectoryInfoResponse = await res.json();
|
||||||
|
|
||||||
|
let newInfo: DirectoryInfo;
|
||||||
|
if (directoryId === "root") {
|
||||||
|
newInfo = {
|
||||||
|
id: "root",
|
||||||
|
subDirectoryIds: subDirectories,
|
||||||
|
fileIds: files,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const { dataKey } = await unwrapDataKey(metadata!.dek, masterKey);
|
||||||
|
newInfo = {
|
||||||
|
id: directoryId,
|
||||||
|
dataKey,
|
||||||
|
dataKeyVersion: metadata!.dekVersion,
|
||||||
|
name: await decryptString(metadata!.name, metadata!.nameIv, dataKey),
|
||||||
|
subDirectoryIds: subDirectories,
|
||||||
|
fileIds: files,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const info = directoryInfoStore.get(directoryId);
|
||||||
|
if (info) {
|
||||||
|
info.update(() => newInfo);
|
||||||
|
} else {
|
||||||
|
directoryInfoStore.set(directoryId, writable(newInfo));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDirectoryInfo = (directoryId: "root" | number, masterKey: CryptoKey) => {
|
||||||
|
// TODO: MEK rotation
|
||||||
|
|
||||||
|
let info = directoryInfoStore.get(directoryId);
|
||||||
|
if (!info) {
|
||||||
|
info = writable(null);
|
||||||
|
directoryInfoStore.set(directoryId, info);
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchDirectoryInfo(directoryId, masterKey);
|
||||||
|
return info;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchFileInfo = async (fileId: number, masterKey: CryptoKey) => {
|
||||||
|
const res = await callGetApi(`/api/file/${fileId}`);
|
||||||
|
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 newInfo: FileInfo = {
|
||||||
|
id: fileId,
|
||||||
|
dataKey,
|
||||||
|
dataKeyVersion: metadata.dekVersion,
|
||||||
|
contentIv: metadata.contentIv,
|
||||||
|
name: await decryptString(metadata.name, metadata.nameIv, dataKey),
|
||||||
|
};
|
||||||
|
|
||||||
|
const info = fileInfoStore.get(fileId);
|
||||||
|
if (info) {
|
||||||
|
info.update(() => newInfo);
|
||||||
|
} else {
|
||||||
|
fileInfoStore.set(fileId, writable(newInfo));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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, masterKey);
|
||||||
|
return info;
|
||||||
|
};
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { unwrapDataKey, decryptString } from "$lib/modules/crypto";
|
|
||||||
import type { FileInfoResponse } from "$lib/server/schemas";
|
|
||||||
|
|
||||||
export const decryptFileMetadata = async (metadata: FileInfoResponse, masterKey: CryptoKey) => {
|
|
||||||
const { dataKey } = await unwrapDataKey(metadata.dek, masterKey);
|
|
||||||
return {
|
|
||||||
dataKey,
|
|
||||||
dataKeyVersion: metadata.dekVersion,
|
|
||||||
name: await decryptString(metadata.name, metadata.nameIv, dataKey),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
30
src/lib/stores/file.ts
Normal file
30
src/lib/stores/file.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import type { Writable } from "svelte/store";
|
||||||
|
|
||||||
|
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;
|
||||||
|
dataKeyVersion: Date;
|
||||||
|
contentIv: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const directoryInfoStore = new Map<"root" | number, Writable<DirectoryInfo | null>>();
|
||||||
|
export const fileInfoStore = new Map<number, Writable<FileInfo | null>>();
|
||||||
@@ -1 +1,2 @@
|
|||||||
|
export * from "./file";
|
||||||
export * from "./key";
|
export * from "./key";
|
||||||
|
|||||||
@@ -1,32 +1,27 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import FileSaver from "file-saver";
|
import FileSaver from "file-saver";
|
||||||
|
import type { Writable } from "svelte/store";
|
||||||
import { TopBar } from "$lib/components";
|
import { TopBar } from "$lib/components";
|
||||||
import { masterKeyStore } from "$lib/stores";
|
import { getFileInfo } from "$lib/modules/file";
|
||||||
import { decryptFileMetadata, requestFileDownload } from "./service";
|
import { masterKeyStore, type FileInfo } from "$lib/stores";
|
||||||
|
import { requestFileDownload } from "./service";
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
|
|
||||||
let metadata = $state<Awaited<ReturnType<typeof decryptFileMetadata>> | undefined>();
|
let info: Writable<FileInfo | null> | undefined = $state();
|
||||||
|
let isDownloaded = $state(false);
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if ($masterKeyStore) {
|
info = getFileInfo(data.id, $masterKeyStore?.get(1)?.key!);
|
||||||
decryptFileMetadata(data.metadata, $masterKeyStore.get(data.metadata.mekVersion)!.key).then(
|
isDownloaded = false;
|
||||||
async (_metadata) => {
|
});
|
||||||
metadata = _metadata;
|
|
||||||
|
|
||||||
const file = await requestFileDownload(
|
$effect(() => {
|
||||||
data.id,
|
if (info && $info && !isDownloaded) {
|
||||||
data.metadata.contentIv,
|
isDownloaded = true;
|
||||||
_metadata.dataKey,
|
requestFileDownload(data.id, $info.contentIv, $info.dataKey).then((content) => {
|
||||||
);
|
FileSaver.saveAs(new Blob([content], { type: "application/octet-stream" }), $info.name);
|
||||||
|
});
|
||||||
// TODO: Preview
|
|
||||||
|
|
||||||
const blob = new Blob([file]);
|
|
||||||
|
|
||||||
FileSaver.saveAs(blob, metadata.name);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -35,8 +30,4 @@
|
|||||||
<title>파일</title>
|
<title>파일</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
{#if metadata}
|
<TopBar title={$info?.name} />
|
||||||
<TopBar title={metadata.name} />
|
|
||||||
{:else}
|
|
||||||
<TopBar />
|
|
||||||
{/if}
|
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import { error } from "@sveltejs/kit";
|
import { error } from "@sveltejs/kit";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { callGetApi } from "$lib/hooks";
|
|
||||||
import type { FileInfoResponse } from "$lib/server/schemas";
|
|
||||||
import type { PageLoad } from "./$types";
|
import type { PageLoad } from "./$types";
|
||||||
|
|
||||||
export const load: PageLoad = async ({ params, fetch }) => {
|
export const load: PageLoad = async ({ params }) => {
|
||||||
const zodRes = z
|
const zodRes = z
|
||||||
.object({
|
.object({
|
||||||
id: z.coerce.number().int().positive(),
|
id: z.coerce.number().int().positive(),
|
||||||
@@ -13,12 +11,5 @@ export const load: PageLoad = async ({ params, fetch }) => {
|
|||||||
if (!zodRes.success) error(404, "Not found");
|
if (!zodRes.success) error(404, "Not found");
|
||||||
const { id } = zodRes.data;
|
const { id } = zodRes.data;
|
||||||
|
|
||||||
const res = await callGetApi(`/api/file/${id}`, fetch);
|
return { id };
|
||||||
if (!res.ok) error(404, "Not found");
|
|
||||||
|
|
||||||
const fileInfo: FileInfoResponse = await res.json();
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
metadata: fileInfo,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { decryptData } from "$lib/modules/crypto";
|
import { decryptData } from "$lib/modules/crypto";
|
||||||
|
|
||||||
export { decryptFileMetadata } from "$lib/services/file";
|
|
||||||
|
|
||||||
export const requestFileDownload = (
|
export const requestFileDownload = (
|
||||||
fileId: number,
|
fileId: number,
|
||||||
fileEncryptedIv: string,
|
fileEncryptedIv: string,
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import type { Writable } from "svelte/store";
|
||||||
import { goto } from "$app/navigation";
|
import { goto } from "$app/navigation";
|
||||||
import { TopBar } from "$lib/components";
|
import { TopBar } from "$lib/components";
|
||||||
import { FloatingButton } from "$lib/components/buttons";
|
import { FloatingButton } from "$lib/components/buttons";
|
||||||
import { masterKeyStore } from "$lib/stores";
|
import { getDirectoryInfo, getFileInfo } from "$lib/modules/file";
|
||||||
|
import { masterKeyStore, type DirectoryInfo } from "$lib/stores";
|
||||||
import CreateBottomSheet from "./CreateBottomSheet.svelte";
|
import CreateBottomSheet from "./CreateBottomSheet.svelte";
|
||||||
import CreateDirectoryModal from "./CreateDirectoryModal.svelte";
|
import CreateDirectoryModal from "./CreateDirectoryModal.svelte";
|
||||||
import DeleteDirectoryEntryModal from "./DeleteDirectoryEntryModal.svelte";
|
import DeleteDirectoryEntryModal from "./DeleteDirectoryEntryModal.svelte";
|
||||||
import DirectoryEntry from "./DirectoryEntry.svelte";
|
|
||||||
import DirectoryEntryMenuBottomSheet from "./DirectoryEntryMenuBottomSheet.svelte";
|
import DirectoryEntryMenuBottomSheet from "./DirectoryEntryMenuBottomSheet.svelte";
|
||||||
|
import File from "./File.svelte";
|
||||||
import RenameDirectoryEntryModal from "./RenameDirectoryEntryModal.svelte";
|
import RenameDirectoryEntryModal from "./RenameDirectoryEntryModal.svelte";
|
||||||
|
import SubDirectory from "./SubDirectory.svelte";
|
||||||
import {
|
import {
|
||||||
decryptDirectoryMetadata,
|
|
||||||
decryptFileMetadata,
|
|
||||||
requestDirectoryCreation,
|
requestDirectoryCreation,
|
||||||
requestFileUpload,
|
requestFileUpload,
|
||||||
requestDirectoryEntryRename,
|
requestDirectoryEntryRename,
|
||||||
@@ -23,6 +24,7 @@
|
|||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
|
|
||||||
|
let info: Writable<DirectoryInfo | null> | undefined = $state();
|
||||||
let fileInput: HTMLInputElement | undefined = $state();
|
let fileInput: HTMLInputElement | undefined = $state();
|
||||||
let selectedEntry: SelectedDirectoryEntry | undefined = $state();
|
let selectedEntry: SelectedDirectoryEntry | undefined = $state();
|
||||||
|
|
||||||
@@ -33,48 +35,6 @@
|
|||||||
let isRenameDirectoryEntryModalOpen = $state(false);
|
let isRenameDirectoryEntryModalOpen = $state(false);
|
||||||
let isDeleteDirectoryEntryModalOpen = $state(false);
|
let isDeleteDirectoryEntryModalOpen = $state(false);
|
||||||
|
|
||||||
// TODO: FIX ME
|
|
||||||
const metadata = $derived.by(() => {
|
|
||||||
const { metadata } = data;
|
|
||||||
if (metadata && $masterKeyStore) {
|
|
||||||
return decryptDirectoryMetadata(metadata, $masterKeyStore.get(metadata.mekVersion)!.key);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const subDirectories = $derived.by(() => {
|
|
||||||
const { subDirectories } = data;
|
|
||||||
if ($masterKeyStore) {
|
|
||||||
return Promise.all(
|
|
||||||
subDirectories.map(async (subDirectory) => {
|
|
||||||
const metadata = subDirectory.metadata!;
|
|
||||||
return {
|
|
||||||
...(await decryptDirectoryMetadata(
|
|
||||||
metadata,
|
|
||||||
$masterKeyStore.get(metadata.mekVersion)!.key,
|
|
||||||
)),
|
|
||||||
id: subDirectory.id,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
).then((subDirectories) => {
|
|
||||||
subDirectories.sort((a, b) => a.name.localeCompare(b.name));
|
|
||||||
return subDirectories;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const files = $derived.by(() => {
|
|
||||||
const { files } = data;
|
|
||||||
if ($masterKeyStore) {
|
|
||||||
return Promise.all(
|
|
||||||
files.map(async (file) => ({
|
|
||||||
...(await decryptFileMetadata(file!, $masterKeyStore.get(file.mekVersion)!.key)),
|
|
||||||
id: file.id,
|
|
||||||
})),
|
|
||||||
).then((files) => {
|
|
||||||
files.sort((a, b) => a.name.localeCompare(b.name));
|
|
||||||
return files;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const createDirectory = async (name: string) => {
|
const createDirectory = async (name: string) => {
|
||||||
await requestDirectoryCreation(name, data.id, $masterKeyStore?.get(1)!);
|
await requestDirectoryCreation(name, data.id, $masterKeyStore?.get(1)!);
|
||||||
isCreateDirectoryModalOpen = false;
|
isCreateDirectoryModalOpen = false;
|
||||||
@@ -86,6 +46,10 @@
|
|||||||
|
|
||||||
requestFileUpload(file, data.id, $masterKeyStore?.get(1)!);
|
requestFileUpload(file, data.id, $masterKeyStore?.get(1)!);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
info = getDirectoryInfo(data.id, $masterKeyStore?.get(1)?.key!);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@@ -94,50 +58,42 @@
|
|||||||
|
|
||||||
<input bind:this={fileInput} onchange={uploadFile} type="file" class="hidden" />
|
<input bind:this={fileInput} onchange={uploadFile} type="file" class="hidden" />
|
||||||
|
|
||||||
<div class="px-4">
|
<div class="flex min-h-full flex-col px-4">
|
||||||
{#if data.id !== "root"}
|
<div class="flex-shrink-0">
|
||||||
{#if !metadata}
|
{#if data.id !== "root"}
|
||||||
<TopBar />
|
<TopBar title={$info?.name} />
|
||||||
{:else}
|
|
||||||
{#await metadata}
|
|
||||||
<TopBar />
|
|
||||||
{:then metadata}
|
|
||||||
<TopBar title={metadata.name} />
|
|
||||||
{/await}
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
<div class="my-4 pb-[4.5rem]">
|
|
||||||
{#if subDirectories}
|
|
||||||
{#await subDirectories then subDirectories}
|
|
||||||
{#each subDirectories as { id, dataKey, dataKeyVersion, name }}
|
|
||||||
<DirectoryEntry
|
|
||||||
{name}
|
|
||||||
onclick={() => goto(`/directory/${id}`)}
|
|
||||||
onOpenMenuClick={() => {
|
|
||||||
selectedEntry = { type: "directory", id, dataKey, dataKeyVersion, name };
|
|
||||||
isDirectoryEntryMenuBottomSheetOpen = true;
|
|
||||||
}}
|
|
||||||
type="directory"
|
|
||||||
/>
|
|
||||||
{/each}
|
|
||||||
{/await}
|
|
||||||
{/if}
|
|
||||||
{#if files}
|
|
||||||
{#await files then files}
|
|
||||||
{#each files as { id, dataKey, dataKeyVersion, name }}
|
|
||||||
<DirectoryEntry
|
|
||||||
{name}
|
|
||||||
onclick={() => goto(`/file/${id}`)}
|
|
||||||
onOpenMenuClick={() => {
|
|
||||||
selectedEntry = { type: "file", id, dataKey, dataKeyVersion, name };
|
|
||||||
isDirectoryEntryMenuBottomSheetOpen = true;
|
|
||||||
}}
|
|
||||||
type="file"
|
|
||||||
/>
|
|
||||||
{/each}
|
|
||||||
{/await}
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
{#if $info && $info.subDirectoryIds.length + $info.fileIds.length > 0}
|
||||||
|
<div class="my-4 pb-[4.5rem]">
|
||||||
|
{#each $info.subDirectoryIds as subDirectoryId}
|
||||||
|
{@const subDirectoryInfo = getDirectoryInfo(subDirectoryId, $masterKeyStore?.get(1)?.key!)}
|
||||||
|
<SubDirectory
|
||||||
|
info={subDirectoryInfo}
|
||||||
|
onclick={() => goto(`/directory/${subDirectoryId}`)}
|
||||||
|
onOpenMenuClick={({ id, dataKey, dataKeyVersion, name }) => {
|
||||||
|
selectedEntry = { type: "directory", id, dataKey, dataKeyVersion, name };
|
||||||
|
isDirectoryEntryMenuBottomSheetOpen = true;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
{#each $info.fileIds as fileId}
|
||||||
|
{@const fileInfo = getFileInfo(fileId, $masterKeyStore?.get(1)?.key!)}
|
||||||
|
<File
|
||||||
|
info={fileInfo}
|
||||||
|
onclick={() => goto(`/file/${fileId}`)}
|
||||||
|
onOpenMenuClick={({ dataKey, id, dataKeyVersion, name }) => {
|
||||||
|
selectedEntry = { type: "file", id, dataKey, dataKeyVersion, name };
|
||||||
|
isDirectoryEntryMenuBottomSheetOpen = true;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="my-4 flex flex-grow items-center justify-center">
|
||||||
|
<p class="text-gray-500">폴더가 비어있어요.</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FloatingButton
|
<FloatingButton
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import { error } from "@sveltejs/kit";
|
import { error } from "@sveltejs/kit";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { callGetApi } from "$lib/hooks";
|
|
||||||
import type { DirectoryInfoResponse, FileInfoResponse } from "$lib/server/schemas";
|
|
||||||
import type { PageLoad } from "./$types";
|
import type { PageLoad } from "./$types";
|
||||||
|
|
||||||
export const load: PageLoad = async ({ params, fetch }) => {
|
export const load: PageLoad = async ({ params }) => {
|
||||||
const zodRes = z
|
const zodRes = z
|
||||||
.object({
|
.object({
|
||||||
id: z.coerce.number().int().positive().optional(),
|
id: z.coerce.number().int().positive().optional(),
|
||||||
@@ -13,36 +11,7 @@ export const load: PageLoad = async ({ params, fetch }) => {
|
|||||||
if (!zodRes.success) error(404, "Not found");
|
if (!zodRes.success) error(404, "Not found");
|
||||||
const { id } = zodRes.data;
|
const { id } = zodRes.data;
|
||||||
|
|
||||||
const directoryId = id ? id : ("root" as const);
|
|
||||||
const res = await callGetApi(`/api/directory/${directoryId}`, fetch);
|
|
||||||
if (!res.ok) error(404, "Not found");
|
|
||||||
|
|
||||||
const directoryInfo: DirectoryInfoResponse = await res.json();
|
|
||||||
const subDirectoryInfos = await Promise.all(
|
|
||||||
directoryInfo.subDirectories.map(async (subDirectoryId) => {
|
|
||||||
const res = await callGetApi(`/api/directory/${subDirectoryId}`, fetch);
|
|
||||||
if (!res.ok) error(500, "Internal server error");
|
|
||||||
return {
|
|
||||||
...((await res.json()) as DirectoryInfoResponse),
|
|
||||||
id: subDirectoryId,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const fileInfos = await Promise.all(
|
|
||||||
directoryInfo.files.map(async (fileId) => {
|
|
||||||
const res = await callGetApi(`/api/file/${fileId}`, fetch);
|
|
||||||
if (!res.ok) error(500, "Internal server error");
|
|
||||||
return {
|
|
||||||
...((await res.json()) as FileInfoResponse),
|
|
||||||
id: fileId,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: directoryId,
|
id: id ? id : ("root" as const),
|
||||||
metadata: directoryInfo.metadata,
|
|
||||||
subDirectories: subDirectoryInfos,
|
|
||||||
files: fileInfos,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import IconFolder from "~icons/material-symbols/folder";
|
|
||||||
import IconDraft from "~icons/material-symbols/draft";
|
|
||||||
import IconMoreVert from "~icons/material-symbols/more-vert";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
name: string;
|
|
||||||
onclick: () => void;
|
|
||||||
onOpenMenuClick: () => void;
|
|
||||||
type: "directory" | "file";
|
|
||||||
}
|
|
||||||
|
|
||||||
let { name, onclick, onOpenMenuClick, type }: Props = $props();
|
|
||||||
|
|
||||||
const openMenu = (e: Event) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setTimeout(() => {
|
|
||||||
onOpenMenuClick();
|
|
||||||
}, 100);
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
||||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
||||||
<div id="button" onclick={() => setTimeout(onclick, 100)} class="h-12 w-full rounded-xl">
|
|
||||||
<div id="button-content" class="flex h-full items-center gap-x-4 p-2 transition">
|
|
||||||
<div class="flex-shrink-0 text-lg">
|
|
||||||
{#if type === "directory"}
|
|
||||||
<IconFolder />
|
|
||||||
{:else if type === "file"}
|
|
||||||
<IconDraft class="text-blue-400" />
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
<p title={name} class="flex-grow truncate font-medium">
|
|
||||||
{name}
|
|
||||||
</p>
|
|
||||||
<button
|
|
||||||
id="open-menu"
|
|
||||||
onclick={openMenu}
|
|
||||||
class="flex-shrink-0 rounded-full p-1 active:bg-gray-100"
|
|
||||||
>
|
|
||||||
<IconMoreVert class="text-lg transition active:scale-95" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
#button:active:not(:has(#open-menu:active)) {
|
|
||||||
@apply bg-gray-100;
|
|
||||||
}
|
|
||||||
#button-content:active:not(:has(#open-menu:active)) {
|
|
||||||
@apply scale-95;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
53
src/routes/(main)/directory/[[id]]/File.svelte
Normal file
53
src/routes/(main)/directory/[[id]]/File.svelte
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Writable } from "svelte/store";
|
||||||
|
import type { FileInfo } from "$lib/stores";
|
||||||
|
|
||||||
|
import IconDraft from "~icons/material-symbols/draft";
|
||||||
|
import IconMoreVert from "~icons/material-symbols/more-vert";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
info: Writable<FileInfo | null>;
|
||||||
|
onclick: () => void;
|
||||||
|
onOpenMenuClick: (metadata: FileInfo) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { info, onclick, onOpenMenuClick }: Props = $props();
|
||||||
|
|
||||||
|
const openMenu = (e: Event) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setTimeout(() => {
|
||||||
|
onOpenMenuClick($info!);
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if $info}
|
||||||
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||||
|
<div id="button" onclick={() => setTimeout(onclick, 100)} class="h-12 w-full rounded-xl">
|
||||||
|
<div id="button-content" class="flex h-full items-center gap-x-4 p-2 transition">
|
||||||
|
<div class="flex-shrink-0 text-lg">
|
||||||
|
<IconDraft class="text-blue-400" />
|
||||||
|
</div>
|
||||||
|
<p title={$info.name} class="flex-grow truncate font-medium">
|
||||||
|
{$info.name}
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
id="open-menu"
|
||||||
|
onclick={openMenu}
|
||||||
|
class="flex-shrink-0 rounded-full p-1 active:bg-gray-100"
|
||||||
|
>
|
||||||
|
<IconMoreVert class="text-lg transition active:scale-95" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#button:active:not(:has(#open-menu:active)) {
|
||||||
|
@apply bg-gray-100;
|
||||||
|
}
|
||||||
|
#button-content:active:not(:has(#open-menu:active)) {
|
||||||
|
@apply scale-95;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
55
src/routes/(main)/directory/[[id]]/SubDirectory.svelte
Normal file
55
src/routes/(main)/directory/[[id]]/SubDirectory.svelte
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Writable } from "svelte/store";
|
||||||
|
import type { DirectoryInfo } from "$lib/stores";
|
||||||
|
|
||||||
|
import IconFolder from "~icons/material-symbols/folder";
|
||||||
|
import IconMoreVert from "~icons/material-symbols/more-vert";
|
||||||
|
|
||||||
|
type SubDirectoryInfo = DirectoryInfo & { id: number };
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
info: Writable<DirectoryInfo | null>;
|
||||||
|
onclick: () => void;
|
||||||
|
onOpenMenuClick: (metadata: SubDirectoryInfo) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { info, onclick, onOpenMenuClick }: Props = $props();
|
||||||
|
|
||||||
|
const openMenu = (e: Event) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setTimeout(() => {
|
||||||
|
onOpenMenuClick($info as SubDirectoryInfo);
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if $info}
|
||||||
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||||
|
<div id="button" onclick={() => setTimeout(onclick, 100)} class="h-12 w-full rounded-xl">
|
||||||
|
<div id="button-content" class="flex h-full items-center gap-x-4 p-2 transition">
|
||||||
|
<div class="flex-shrink-0 text-lg">
|
||||||
|
<IconFolder />
|
||||||
|
</div>
|
||||||
|
<p title={$info.name} class="flex-grow truncate font-medium">
|
||||||
|
{$info.name}
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
id="open-menu"
|
||||||
|
onclick={openMenu}
|
||||||
|
class="flex-shrink-0 rounded-full p-1 active:bg-gray-100"
|
||||||
|
>
|
||||||
|
<IconMoreVert class="text-lg transition active:scale-95" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#button:active:not(:has(#open-menu:active)) {
|
||||||
|
@apply bg-gray-100;
|
||||||
|
}
|
||||||
|
#button-content:active:not(:has(#open-menu:active)) {
|
||||||
|
@apply scale-95;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -3,22 +3,17 @@ import {
|
|||||||
encodeToBase64,
|
encodeToBase64,
|
||||||
generateDataKey,
|
generateDataKey,
|
||||||
wrapDataKey,
|
wrapDataKey,
|
||||||
unwrapDataKey,
|
|
||||||
encryptData,
|
encryptData,
|
||||||
encryptString,
|
encryptString,
|
||||||
decryptString,
|
|
||||||
} from "$lib/modules/crypto";
|
} from "$lib/modules/crypto";
|
||||||
import type {
|
import type {
|
||||||
DirectoryRenameRequest,
|
DirectoryRenameRequest,
|
||||||
DirectoryInfoResponse,
|
|
||||||
DirectoryCreateRequest,
|
DirectoryCreateRequest,
|
||||||
FileRenameRequest,
|
FileRenameRequest,
|
||||||
FileUploadRequest,
|
FileUploadRequest,
|
||||||
} from "$lib/server/schemas";
|
} from "$lib/server/schemas";
|
||||||
import type { MasterKey } from "$lib/stores";
|
import type { MasterKey } from "$lib/stores";
|
||||||
|
|
||||||
export { decryptFileMetadata } from "$lib/services/file";
|
|
||||||
|
|
||||||
export interface SelectedDirectoryEntry {
|
export interface SelectedDirectoryEntry {
|
||||||
type: "directory" | "file";
|
type: "directory" | "file";
|
||||||
id: number;
|
id: number;
|
||||||
@@ -27,18 +22,6 @@ export interface SelectedDirectoryEntry {
|
|||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const decryptDirectoryMetadata = async (
|
|
||||||
metadata: NonNullable<DirectoryInfoResponse["metadata"]>,
|
|
||||||
masterKey: CryptoKey,
|
|
||||||
) => {
|
|
||||||
const { dataKey } = await unwrapDataKey(metadata.dek, masterKey);
|
|
||||||
return {
|
|
||||||
dataKey,
|
|
||||||
dataKeyVersion: metadata.dekVersion,
|
|
||||||
name: await decryptString(metadata.name, metadata.nameIv, dataKey),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const requestDirectoryCreation = async (
|
export const requestDirectoryCreation = async (
|
||||||
name: string,
|
name: string,
|
||||||
parentId: "root" | number,
|
parentId: "root" | number,
|
||||||
|
|||||||
Reference in New Issue
Block a user