mirror of
https://github.com/kmc7468/arkvault.git
synced 2025-12-12 21:08:46 +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";
|
||||
|
||||
@@ -1,32 +1,27 @@
|
||||
<script lang="ts">
|
||||
import FileSaver from "file-saver";
|
||||
import type { Writable } from "svelte/store";
|
||||
import { TopBar } from "$lib/components";
|
||||
import { masterKeyStore } from "$lib/stores";
|
||||
import { decryptFileMetadata, requestFileDownload } from "./service";
|
||||
import { getFileInfo } from "$lib/modules/file";
|
||||
import { masterKeyStore, type FileInfo } from "$lib/stores";
|
||||
import { requestFileDownload } from "./service";
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
let metadata = $state<Awaited<ReturnType<typeof decryptFileMetadata>> | undefined>();
|
||||
let info: Writable<FileInfo | null> | undefined = $state();
|
||||
let isDownloaded = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
if ($masterKeyStore) {
|
||||
decryptFileMetadata(data.metadata, $masterKeyStore.get(data.metadata.mekVersion)!.key).then(
|
||||
async (_metadata) => {
|
||||
metadata = _metadata;
|
||||
info = getFileInfo(data.id, $masterKeyStore?.get(1)?.key!);
|
||||
isDownloaded = false;
|
||||
});
|
||||
|
||||
const file = await requestFileDownload(
|
||||
data.id,
|
||||
data.metadata.contentIv,
|
||||
_metadata.dataKey,
|
||||
);
|
||||
|
||||
// TODO: Preview
|
||||
|
||||
const blob = new Blob([file]);
|
||||
|
||||
FileSaver.saveAs(blob, metadata.name);
|
||||
},
|
||||
);
|
||||
$effect(() => {
|
||||
if (info && $info && !isDownloaded) {
|
||||
isDownloaded = true;
|
||||
requestFileDownload(data.id, $info.contentIv, $info.dataKey).then((content) => {
|
||||
FileSaver.saveAs(new Blob([content], { type: "application/octet-stream" }), $info.name);
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -35,8 +30,4 @@
|
||||
<title>파일</title>
|
||||
</svelte:head>
|
||||
|
||||
{#if metadata}
|
||||
<TopBar title={metadata.name} />
|
||||
{:else}
|
||||
<TopBar />
|
||||
{/if}
|
||||
<TopBar title={$info?.name} />
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { error } from "@sveltejs/kit";
|
||||
import { z } from "zod";
|
||||
import { callGetApi } from "$lib/hooks";
|
||||
import type { FileInfoResponse } from "$lib/server/schemas";
|
||||
import type { PageLoad } from "./$types";
|
||||
|
||||
export const load: PageLoad = async ({ params, fetch }) => {
|
||||
export const load: PageLoad = async ({ params }) => {
|
||||
const zodRes = z
|
||||
.object({
|
||||
id: z.coerce.number().int().positive(),
|
||||
@@ -13,12 +11,5 @@ export const load: PageLoad = async ({ params, fetch }) => {
|
||||
if (!zodRes.success) error(404, "Not found");
|
||||
const { id } = zodRes.data;
|
||||
|
||||
const res = await callGetApi(`/api/file/${id}`, fetch);
|
||||
if (!res.ok) error(404, "Not found");
|
||||
|
||||
const fileInfo: FileInfoResponse = await res.json();
|
||||
return {
|
||||
id,
|
||||
metadata: fileInfo,
|
||||
};
|
||||
return { id };
|
||||
};
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { decryptData } from "$lib/modules/crypto";
|
||||
|
||||
export { decryptFileMetadata } from "$lib/services/file";
|
||||
|
||||
export const requestFileDownload = (
|
||||
fileId: number,
|
||||
fileEncryptedIv: string,
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
<script lang="ts">
|
||||
import type { Writable } from "svelte/store";
|
||||
import { goto } from "$app/navigation";
|
||||
import { TopBar } from "$lib/components";
|
||||
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 CreateDirectoryModal from "./CreateDirectoryModal.svelte";
|
||||
import DeleteDirectoryEntryModal from "./DeleteDirectoryEntryModal.svelte";
|
||||
import DirectoryEntry from "./DirectoryEntry.svelte";
|
||||
import DirectoryEntryMenuBottomSheet from "./DirectoryEntryMenuBottomSheet.svelte";
|
||||
import File from "./File.svelte";
|
||||
import RenameDirectoryEntryModal from "./RenameDirectoryEntryModal.svelte";
|
||||
import SubDirectory from "./SubDirectory.svelte";
|
||||
import {
|
||||
decryptDirectoryMetadata,
|
||||
decryptFileMetadata,
|
||||
requestDirectoryCreation,
|
||||
requestFileUpload,
|
||||
requestDirectoryEntryRename,
|
||||
@@ -23,6 +24,7 @@
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
let info: Writable<DirectoryInfo | null> | undefined = $state();
|
||||
let fileInput: HTMLInputElement | undefined = $state();
|
||||
let selectedEntry: SelectedDirectoryEntry | undefined = $state();
|
||||
|
||||
@@ -33,48 +35,6 @@
|
||||
let isRenameDirectoryEntryModalOpen = $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) => {
|
||||
await requestDirectoryCreation(name, data.id, $masterKeyStore?.get(1)!);
|
||||
isCreateDirectoryModalOpen = false;
|
||||
@@ -86,6 +46,10 @@
|
||||
|
||||
requestFileUpload(file, data.id, $masterKeyStore?.get(1)!);
|
||||
};
|
||||
|
||||
$effect(() => {
|
||||
info = getDirectoryInfo(data.id, $masterKeyStore?.get(1)?.key!);
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -94,50 +58,42 @@
|
||||
|
||||
<input bind:this={fileInput} onchange={uploadFile} type="file" class="hidden" />
|
||||
|
||||
<div class="px-4">
|
||||
{#if data.id !== "root"}
|
||||
{#if !metadata}
|
||||
<TopBar />
|
||||
{: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}
|
||||
<div class="flex min-h-full flex-col px-4">
|
||||
<div class="flex-shrink-0">
|
||||
{#if data.id !== "root"}
|
||||
<TopBar title={$info?.name} />
|
||||
{/if}
|
||||
</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>
|
||||
|
||||
<FloatingButton
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { error } from "@sveltejs/kit";
|
||||
import { z } from "zod";
|
||||
import { callGetApi } from "$lib/hooks";
|
||||
import type { DirectoryInfoResponse, FileInfoResponse } from "$lib/server/schemas";
|
||||
import type { PageLoad } from "./$types";
|
||||
|
||||
export const load: PageLoad = async ({ params, fetch }) => {
|
||||
export const load: PageLoad = async ({ params }) => {
|
||||
const zodRes = z
|
||||
.object({
|
||||
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");
|
||||
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 {
|
||||
id: directoryId,
|
||||
metadata: directoryInfo.metadata,
|
||||
subDirectories: subDirectoryInfos,
|
||||
files: fileInfos,
|
||||
id: id ? id : ("root" as const),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
generateDataKey,
|
||||
wrapDataKey,
|
||||
unwrapDataKey,
|
||||
encryptData,
|
||||
encryptString,
|
||||
decryptString,
|
||||
} from "$lib/modules/crypto";
|
||||
import type {
|
||||
DirectoryRenameRequest,
|
||||
DirectoryInfoResponse,
|
||||
DirectoryCreateRequest,
|
||||
FileRenameRequest,
|
||||
FileUploadRequest,
|
||||
} from "$lib/server/schemas";
|
||||
import type { MasterKey } from "$lib/stores";
|
||||
|
||||
export { decryptFileMetadata } from "$lib/services/file";
|
||||
|
||||
export interface SelectedDirectoryEntry {
|
||||
type: "directory" | "file";
|
||||
id: number;
|
||||
@@ -27,18 +22,6 @@ export interface SelectedDirectoryEntry {
|
||||
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 (
|
||||
name: string,
|
||||
parentId: "root" | number,
|
||||
|
||||
Reference in New Issue
Block a user