DB 마이그레이션 스크립트 재생성 및 간단한 이미지/비디오 뷰어 구현

This commit is contained in:
static
2025-01-06 22:55:11 +09:00
parent 3168c441b9
commit 1c06a604c5
15 changed files with 111 additions and 16 deletions

View File

@@ -64,6 +64,7 @@ const fetchFileInfo = async (fileId: number, masterKey: CryptoKey) => {
id: fileId,
dataKey,
dataKeyVersion: metadata.dekVersion,
contentType: metadata.contentType,
contentIv: metadata.contentIv,
name: await decryptString(metadata.name, metadata.nameIv, dataKey),
};

View File

@@ -21,6 +21,7 @@ export interface NewFileParams {
mekVersion: number;
encDek: string;
dekVersion: Date;
contentType: string;
encContentIv: string;
encName: string;
encNameIv: string;
@@ -137,6 +138,7 @@ export const registerNewFile = async (params: NewFileParams) => {
createdAt: now,
userId: params.userId,
mekVersion: params.mekVersion,
contentType: params.contentType,
encDek: params.encDek,
dekVersion: params.dekVersion,
encContentIv: params.encContentIv,

View File

@@ -47,6 +47,7 @@ export const file = sqliteTable(
mekVersion: integer("master_encryption_key_version").notNull(),
encDek: text("encrypted_data_encryption_key").notNull().unique(), // Base64
dekVersion: integer("data_encryption_key_version", { mode: "timestamp_ms" }).notNull(),
contentType: text("content_type").notNull(),
encContentIv: text("encrypted_content_iv").notNull(), // Base64
encName: ciphertext("encrypted_name").notNull(),
},

View File

@@ -1,3 +1,4 @@
import mime from "mime";
import { z } from "zod";
export const fileRenameRequest = z.object({
@@ -12,6 +13,10 @@ export const fileInfoResponse = z.object({
mekVersion: z.number().int().positive(),
dek: z.string().base64().nonempty(),
dekVersion: z.date(),
contentType: z
.string()
.nonempty()
.refine((value) => mime.getExtension(value) !== null), // MIME type
contentIv: z.string().base64().nonempty(),
name: z.string().base64().nonempty(),
nameIv: z.string().base64().nonempty(),
@@ -23,6 +28,10 @@ export const fileUploadRequest = z.object({
mekVersion: z.number().int().positive(),
dek: z.string().base64().nonempty(),
dekVersion: z.coerce.date(),
contentType: z
.string()
.nonempty()
.refine((value) => mime.getExtension(value) !== null), // MIME type
contentIv: z.string().base64().nonempty(),
name: z.string().base64().nonempty(),
nameIv: z.string().base64().nonempty(),

View File

@@ -83,6 +83,7 @@ export const getFileInformation = async (userId: number, fileId: number) => {
mekVersion: file.mekVersion,
encDek: file.encDek,
dekVersion: file.dekVersion,
contentType: file.contentType,
encContentIv: file.encContentIv,
encName: file.encName,
};

View File

@@ -22,6 +22,7 @@ export interface FileInfo {
id: number;
dataKey: CryptoKey;
dataKeyVersion: Date;
contentType: string;
contentIv: string;
name: string;
}

View File

@@ -1,26 +1,48 @@
<script lang="ts">
import FileSaver from "file-saver";
import { untrack } from "svelte";
import type { Writable } from "svelte/store";
import { TopBar } from "$lib/components";
import { getFileInfo } from "$lib/modules/file";
import { masterKeyStore, type FileInfo } from "$lib/stores";
import { requestFileDownload } from "./service";
type ContentType = "image" | "video";
let { data } = $props();
let info: Writable<FileInfo | null> | undefined = $state();
let isDownloaded = $state(false);
let content: ArrayBuffer | undefined = $state();
let contentType: ContentType | undefined = $state();
$effect(() => {
info = getFileInfo(data.id, $masterKeyStore?.get(1)?.key!);
isDownloaded = false;
content = undefined;
contentType = undefined;
});
$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);
if ($info && !isDownloaded) {
untrack(() => {
isDownloaded = true;
if ($info.contentType.startsWith("image/")) {
contentType = "image";
} else if ($info.contentType.startsWith("video/")) {
contentType = "video";
}
requestFileDownload(data.id, $info.contentIv, $info.dataKey).then((res) => {
content = res;
if (!contentType) {
FileSaver.saveAs(new Blob([res], { type: $info.contentType }), $info.name);
}
});
});
}
});
@@ -30,4 +52,32 @@
<title>파일</title>
</svelte:head>
<TopBar title={$info?.name} />
<div class="flex h-full flex-col">
<div class="flex-shrink-0">
<TopBar title={$info?.name} />
</div>
<div class="flex w-full flex-grow flex-col items-center py-4">
{#snippet viewerLoading(message: string)}
<div class="flex flex-grow items-center justify-center">
<p class="text-gray-500">{message}</p>
</div>
{/snippet}
{#if contentType === "image"}
{#if $info && content}
{@const src = URL.createObjectURL(new Blob([content], { type: $info.contentType }))}
<img {src} alt={$info.name} />
{:else}
{@render viewerLoading("이미지를 불러오고 있어요.")}
{/if}
{:else if contentType === "video"}
{#if $info && content}
{@const src = URL.createObjectURL(new Blob([content], { type: $info.contentType }))}
<!-- svelte-ignore a11y_media_has_caption -->
<video {src} controls></video>
{:else}
{@render viewerLoading("비디오를 불러오고 있어요.")}
{/if}
{/if}
</div>
</div>

View File

@@ -56,6 +56,7 @@ export const requestFileUpload = async (
mekVersion: masterKey.version,
dek: await wrapDataKey(dataKey, masterKey.key),
dekVersion: dataKeyVersion,
contentType: file.type,
contentIv: fileEncrypted.iv,
name: nameEncrypted.ciphertext,
nameIv: nameEncrypted.iv,

View File

@@ -16,7 +16,7 @@ export const GET: RequestHandler = async ({ cookies, params }) => {
if (!zodRes.success) error(400, "Invalid path parameters");
const { id } = zodRes.data;
const { createdAt, mekVersion, encDek, dekVersion, encContentIv, encName } =
const { createdAt, mekVersion, encDek, dekVersion, contentType, encContentIv, encName } =
await getFileInformation(userId, id);
return json(
fileInfoResponse.parse({
@@ -24,6 +24,7 @@ export const GET: RequestHandler = async ({ cookies, params }) => {
mekVersion,
dek: encDek,
dekVersion,
contentType: contentType,
contentIv: encContentIv,
name: encName.ciphertext,
nameIv: encName.iv,

View File

@@ -16,7 +16,8 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
const zodRes = fileUploadRequest.safeParse(JSON.parse(metadata));
if (!zodRes.success) error(400, "Invalid request body");
const { parentId, mekVersion, dek, dekVersion, contentIv, name, nameIv } = zodRes.data;
const { parentId, mekVersion, dek, dekVersion, contentType, contentIv, name, nameIv } =
zodRes.data;
await uploadFile(
{
@@ -25,6 +26,7 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
mekVersion,
encDek: dek,
dekVersion,
contentType,
encContentIv: contentIv,
encName: name,
encNameIv: nameIv,