파일/폴더 삭제 및 이름 변경 구현 완료

This commit is contained in:
static
2025-01-06 14:30:00 +09:00
parent bd0dd3343a
commit 71f12c942b
6 changed files with 75 additions and 27 deletions

View File

@@ -20,7 +20,7 @@ export const callGetApi = async (input: RequestInfo, fetchInternal?: typeof fetc
export const callPostApi = async <T>( export const callPostApi = async <T>(
input: RequestInfo, input: RequestInfo,
payload: T, payload?: T,
fetchInternal?: typeof fetch, fetchInternal?: typeof fetch,
) => { ) => {
return await callApi( return await callApi(
@@ -28,7 +28,7 @@ export const callPostApi = async <T>(
{ {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload), body: payload ? JSON.stringify(payload) : undefined,
}, },
fetchInternal, fetchInternal,
); );

View File

@@ -1,11 +1,3 @@
<script module lang="ts">
export interface SelectedDirectoryEntry {
type: "directory" | "file";
id: number;
name: string;
}
</script>
<script lang="ts"> <script lang="ts">
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { TopBar } from "$lib/components"; import { TopBar } from "$lib/components";
@@ -22,6 +14,9 @@
decryptFileMetadata, decryptFileMetadata,
requestDirectoryCreation, requestDirectoryCreation,
requestFileUpload, requestFileUpload,
requestDirectoryEntryRename,
requestDirectoryEntryDeletion,
type SelectedDirectoryEntry,
} from "./service"; } from "./service";
import IconAdd from "~icons/material-symbols/add"; import IconAdd from "~icons/material-symbols/add";
@@ -114,12 +109,12 @@
<div class="my-4 pb-[4.5rem]"> <div class="my-4 pb-[4.5rem]">
{#if subDirectories} {#if subDirectories}
{#await subDirectories then subDirectories} {#await subDirectories then subDirectories}
{#each subDirectories as { id, name }} {#each subDirectories as { id, dataKey, name }}
<DirectoryEntry <DirectoryEntry
{name} {name}
onclick={() => goto(`/directory/${id}`)} onclick={() => goto(`/directory/${id}`)}
onOpenMenuClick={() => { onOpenMenuClick={() => {
selectedEntry = { type: "directory", id, name }; selectedEntry = { type: "directory", id, dataKey, name };
isDirectoryEntryMenuBottomSheetOpen = true; isDirectoryEntryMenuBottomSheetOpen = true;
}} }}
type="directory" type="directory"
@@ -129,12 +124,12 @@
{/if} {/if}
{#if files} {#if files}
{#await files then files} {#await files then files}
{#each files as { id, name }} {#each files as { id, dataKey, name }}
<DirectoryEntry <DirectoryEntry
{name} {name}
onclick={() => goto(`/file/${id}`)} onclick={() => goto(`/file/${id}`)}
onOpenMenuClick={() => { onOpenMenuClick={() => {
selectedEntry = { type: "file", id, name }; selectedEntry = { type: "file", id, dataKey, name };
isDirectoryEntryMenuBottomSheetOpen = true; isDirectoryEntryMenuBottomSheetOpen = true;
}} }}
type="file" type="file"
@@ -177,5 +172,19 @@
isDeleteDirectoryEntryModalOpen = true; isDeleteDirectoryEntryModalOpen = true;
}} }}
/> />
<RenameDirectoryEntryModal bind:isOpen={isRenameDirectoryEntryModalOpen} bind:selectedEntry /> <RenameDirectoryEntryModal
<DeleteDirectoryEntryModal bind:isOpen={isDeleteDirectoryEntryModalOpen} bind:selectedEntry /> bind:isOpen={isRenameDirectoryEntryModalOpen}
bind:selectedEntry
onRenameClick={async (newName) => {
await requestDirectoryEntryRename(selectedEntry!, newName);
return true;
}}
/>
<DeleteDirectoryEntryModal
bind:isOpen={isDeleteDirectoryEntryModalOpen}
bind:selectedEntry
onDeleteClick={async () => {
await requestDirectoryEntryDeletion(selectedEntry!);
return true;
}}
/>

View File

@@ -1,24 +1,27 @@
<script lang="ts"> <script lang="ts">
import { Modal } from "$lib/components"; import { Modal } from "$lib/components";
import { Button } from "$lib/components/buttons"; import { Button } from "$lib/components/buttons";
import type { SelectedDirectoryEntry } from "./+page.svelte"; import type { SelectedDirectoryEntry } from "./service";
interface Props { interface Props {
onDeleteClick: () => Promise<boolean>;
isOpen: boolean; isOpen: boolean;
selectedEntry: SelectedDirectoryEntry | undefined; selectedEntry: SelectedDirectoryEntry | undefined;
} }
let { isOpen = $bindable(), selectedEntry = $bindable() }: Props = $props(); let { onDeleteClick, isOpen = $bindable(), selectedEntry = $bindable() }: Props = $props();
const closeModal = () => { const closeModal = () => {
isOpen = false; isOpen = false;
selectedEntry = undefined; selectedEntry = undefined;
}; };
const deleteEntry = () => { const deleteEntry = async () => {
// TODO // TODO: Validation
closeModal(); if (await onDeleteClick()) {
closeModal();
}
}; };
</script> </script>

View File

@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { BottomSheet } from "$lib/components"; import { BottomSheet } from "$lib/components";
import { EntryButton } from "$lib/components/buttons"; import { EntryButton } from "$lib/components/buttons";
import type { SelectedDirectoryEntry } from "./+page.svelte"; import type { SelectedDirectoryEntry } from "./service";
import IconFolder from "~icons/material-symbols/folder"; import IconFolder from "~icons/material-symbols/folder";
import IconDraft from "~icons/material-symbols/draft"; import IconDraft from "~icons/material-symbols/draft";

View File

@@ -2,14 +2,15 @@
import { Modal } from "$lib/components"; import { Modal } from "$lib/components";
import { Button } from "$lib/components/buttons"; import { Button } from "$lib/components/buttons";
import { TextInput } from "$lib/components/inputs"; import { TextInput } from "$lib/components/inputs";
import type { SelectedDirectoryEntry } from "./+page.svelte"; import type { SelectedDirectoryEntry } from "./service";
interface Props { interface Props {
onRenameClick: (newName: string) => Promise<boolean>;
isOpen: boolean; isOpen: boolean;
selectedEntry: SelectedDirectoryEntry | undefined; selectedEntry: SelectedDirectoryEntry | undefined;
} }
let { isOpen = $bindable(), selectedEntry = $bindable() }: Props = $props(); let { onRenameClick, isOpen = $bindable(), selectedEntry = $bindable() }: Props = $props();
let name = $state(""); let name = $state("");
@@ -19,10 +20,12 @@
selectedEntry = undefined; selectedEntry = undefined;
}; };
const renameEntry = () => { const renameEntry = async () => {
// TODO // TODO: Validation
closeModal(); if (await onRenameClick(name)) {
closeModal();
}
}; };
$effect(() => { $effect(() => {

View File

@@ -9,20 +9,30 @@ import {
decryptString, decryptString,
} from "$lib/modules/crypto"; } from "$lib/modules/crypto";
import type { import type {
DirectoryRenameRequest,
DirectoryInfoResponse, DirectoryInfoResponse,
DirectoryCreateRequest, DirectoryCreateRequest,
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 { decryptFileMetadata } from "$lib/services/file";
export interface SelectedDirectoryEntry {
type: "directory" | "file";
id: number;
dataKey: CryptoKey;
name: string;
}
export const decryptDirectoryMetadata = async ( export const decryptDirectoryMetadata = async (
metadata: NonNullable<DirectoryInfoResponse["metadata"]>, metadata: NonNullable<DirectoryInfoResponse["metadata"]>,
masterKey: CryptoKey, masterKey: CryptoKey,
) => { ) => {
const { dataKey } = await unwrapDataKey(metadata.dek, masterKey); const { dataKey } = await unwrapDataKey(metadata.dek, masterKey);
return { return {
dataKey,
name: await decryptString(metadata.name, metadata.nameIv, dataKey), name: await decryptString(metadata.name, metadata.nameIv, dataKey),
}; };
}; };
@@ -71,3 +81,26 @@ export const requestFileUpload = async (
xhr.open("POST", "/api/file/upload"); xhr.open("POST", "/api/file/upload");
xhr.send(form); xhr.send(form);
}; };
export const requestDirectoryEntryRename = async (
entry: SelectedDirectoryEntry,
newName: string,
) => {
const newNameEncrypted = await encryptString(newName, entry.dataKey);
if (entry.type === "directory") {
await callPostApi<DirectoryRenameRequest>(`/api/directory/${entry.id}/rename`, {
name: newNameEncrypted.ciphertext,
nameIv: newNameEncrypted.iv,
});
} else {
await callPostApi<FileRenameRequest>(`/api/file/${entry.id}/rename`, {
name: newNameEncrypted.ciphertext,
nameIv: newNameEncrypted.iv,
});
}
};
export const requestDirectoryEntryDeletion = async (entry: SelectedDirectoryEntry) => {
await callPostApi(`/api/${entry.type}/${entry.id}/delete`);
};