mirror of
https://github.com/kmc7468/arkvault.git
synced 2025-12-12 21:08:46 +00:00
DB 마이그레이션 스크립트 재생성 및 간단한 이미지/비디오 뷰어 구현
This commit is contained in:
@@ -32,7 +32,7 @@ CREATE TABLE `directory` (
|
||||
`user_id` integer NOT NULL,
|
||||
`master_encryption_key_version` integer NOT NULL,
|
||||
`encrypted_data_encryption_key` text NOT NULL,
|
||||
`encrypted_at` integer NOT NULL,
|
||||
`data_encryption_key_version` integer NOT NULL,
|
||||
`encrypted_name` text NOT NULL,
|
||||
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action,
|
||||
FOREIGN KEY (`parent_id`) REFERENCES `directory`(`id`) ON UPDATE no action ON DELETE no action,
|
||||
@@ -47,7 +47,9 @@ CREATE TABLE `file` (
|
||||
`user_id` integer NOT NULL,
|
||||
`master_encryption_key_version` integer NOT NULL,
|
||||
`encrypted_data_encryption_key` text NOT NULL,
|
||||
`encrypted_at` integer NOT NULL,
|
||||
`data_encryption_key_version` integer NOT NULL,
|
||||
`content_type` text NOT NULL,
|
||||
`encrypted_content_iv` text NOT NULL,
|
||||
`encrypted_name` text NOT NULL,
|
||||
FOREIGN KEY (`parent_id`) REFERENCES `directory`(`id`) ON UPDATE no action ON DELETE no action,
|
||||
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action,
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "901e84cd-f9eb-4329-a374-f71264675515",
|
||||
"id": "929c6bca-d0c0-4899-afc6-a0a498226f28",
|
||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||
"tables": {
|
||||
"client": {
|
||||
@@ -262,8 +262,8 @@
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"encrypted_at": {
|
||||
"name": "encrypted_at",
|
||||
"data_encryption_key_version": {
|
||||
"name": "data_encryption_key_version",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
@@ -384,13 +384,27 @@
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"encrypted_at": {
|
||||
"name": "encrypted_at",
|
||||
"data_encryption_key_version": {
|
||||
"name": "data_encryption_key_version",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"content_type": {
|
||||
"name": "content_type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"encrypted_content_iv": {
|
||||
"name": "encrypted_content_iv",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"encrypted_name": {
|
||||
"name": "encrypted_name",
|
||||
"type": "text",
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "6",
|
||||
"when": 1735748192401,
|
||||
"tag": "0000_lazy_scarecrow",
|
||||
"when": 1736170919561,
|
||||
"tag": "0000_handy_captain_marvel",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"eslint-plugin-tailwindcss": "^3.17.5",
|
||||
"file-saver": "^2.0.5",
|
||||
"globals": "^15.0.0",
|
||||
"mime": "^4.0.6",
|
||||
"prettier": "^3.3.2",
|
||||
"prettier-plugin-svelte": "^3.2.6",
|
||||
"prettier-plugin-tailwindcss": "^0.6.5",
|
||||
|
||||
9
pnpm-lock.yaml
generated
9
pnpm-lock.yaml
generated
@@ -88,6 +88,9 @@ devDependencies:
|
||||
globals:
|
||||
specifier: ^15.0.0
|
||||
version: 15.14.0
|
||||
mime:
|
||||
specifier: ^4.0.6
|
||||
version: 4.0.6
|
||||
prettier:
|
||||
specifier: ^3.3.2
|
||||
version: 3.4.2
|
||||
@@ -2689,6 +2692,12 @@ packages:
|
||||
picomatch: 2.3.1
|
||||
dev: true
|
||||
|
||||
/mime@4.0.6:
|
||||
resolution: {integrity: sha512-4rGt7rvQHBbaSOF9POGkk1ocRP16Md1x36Xma8sz8h8/vfCUI2OtEIeCqe4Ofes853x4xDoPiFLIT47J5fI/7A==}
|
||||
engines: {node: '>=16'}
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/mimic-response@3.1.0:
|
||||
resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
},
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -22,6 +22,7 @@ export interface FileInfo {
|
||||
id: number;
|
||||
dataKey: CryptoKey;
|
||||
dataKeyVersion: Date;
|
||||
contentType: string;
|
||||
contentIv: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user