mirror of
https://github.com/kmc7468/arkvault.git
synced 2025-12-15 06:18:48 +00:00
DB 마이그레이션 스크립트 재생성 및 간단한 이미지/비디오 뷰어 구현
This commit is contained in:
@@ -32,7 +32,7 @@ CREATE TABLE `directory` (
|
|||||||
`user_id` integer NOT NULL,
|
`user_id` integer NOT NULL,
|
||||||
`master_encryption_key_version` integer NOT NULL,
|
`master_encryption_key_version` integer NOT NULL,
|
||||||
`encrypted_data_encryption_key` text 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,
|
`encrypted_name` text NOT NULL,
|
||||||
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action,
|
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,
|
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,
|
`user_id` integer NOT NULL,
|
||||||
`master_encryption_key_version` integer NOT NULL,
|
`master_encryption_key_version` integer NOT NULL,
|
||||||
`encrypted_data_encryption_key` text 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,
|
`encrypted_name` text NOT NULL,
|
||||||
FOREIGN KEY (`parent_id`) REFERENCES `directory`(`id`) ON UPDATE no action ON DELETE no action,
|
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,
|
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action,
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": "6",
|
"version": "6",
|
||||||
"dialect": "sqlite",
|
"dialect": "sqlite",
|
||||||
"id": "901e84cd-f9eb-4329-a374-f71264675515",
|
"id": "929c6bca-d0c0-4899-afc6-a0a498226f28",
|
||||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||||
"tables": {
|
"tables": {
|
||||||
"client": {
|
"client": {
|
||||||
@@ -262,8 +262,8 @@
|
|||||||
"notNull": true,
|
"notNull": true,
|
||||||
"autoincrement": false
|
"autoincrement": false
|
||||||
},
|
},
|
||||||
"encrypted_at": {
|
"data_encryption_key_version": {
|
||||||
"name": "encrypted_at",
|
"name": "data_encryption_key_version",
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true,
|
"notNull": true,
|
||||||
@@ -384,13 +384,27 @@
|
|||||||
"notNull": true,
|
"notNull": true,
|
||||||
"autoincrement": false
|
"autoincrement": false
|
||||||
},
|
},
|
||||||
"encrypted_at": {
|
"data_encryption_key_version": {
|
||||||
"name": "encrypted_at",
|
"name": "data_encryption_key_version",
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true,
|
"notNull": true,
|
||||||
"autoincrement": false
|
"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": {
|
"encrypted_name": {
|
||||||
"name": "encrypted_name",
|
"name": "encrypted_name",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
{
|
{
|
||||||
"idx": 0,
|
"idx": 0,
|
||||||
"version": "6",
|
"version": "6",
|
||||||
"when": 1735748192401,
|
"when": 1736170919561,
|
||||||
"tag": "0000_lazy_scarecrow",
|
"tag": "0000_handy_captain_marvel",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
"eslint-plugin-tailwindcss": "^3.17.5",
|
"eslint-plugin-tailwindcss": "^3.17.5",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"globals": "^15.0.0",
|
"globals": "^15.0.0",
|
||||||
|
"mime": "^4.0.6",
|
||||||
"prettier": "^3.3.2",
|
"prettier": "^3.3.2",
|
||||||
"prettier-plugin-svelte": "^3.2.6",
|
"prettier-plugin-svelte": "^3.2.6",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.5",
|
"prettier-plugin-tailwindcss": "^0.6.5",
|
||||||
|
|||||||
9
pnpm-lock.yaml
generated
9
pnpm-lock.yaml
generated
@@ -88,6 +88,9 @@ devDependencies:
|
|||||||
globals:
|
globals:
|
||||||
specifier: ^15.0.0
|
specifier: ^15.0.0
|
||||||
version: 15.14.0
|
version: 15.14.0
|
||||||
|
mime:
|
||||||
|
specifier: ^4.0.6
|
||||||
|
version: 4.0.6
|
||||||
prettier:
|
prettier:
|
||||||
specifier: ^3.3.2
|
specifier: ^3.3.2
|
||||||
version: 3.4.2
|
version: 3.4.2
|
||||||
@@ -2689,6 +2692,12 @@ packages:
|
|||||||
picomatch: 2.3.1
|
picomatch: 2.3.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/mime@4.0.6:
|
||||||
|
resolution: {integrity: sha512-4rGt7rvQHBbaSOF9POGkk1ocRP16Md1x36Xma8sz8h8/vfCUI2OtEIeCqe4Ofes853x4xDoPiFLIT47J5fI/7A==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
hasBin: true
|
||||||
|
dev: true
|
||||||
|
|
||||||
/mimic-response@3.1.0:
|
/mimic-response@3.1.0:
|
||||||
resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==}
|
resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ const fetchFileInfo = async (fileId: number, masterKey: CryptoKey) => {
|
|||||||
id: fileId,
|
id: fileId,
|
||||||
dataKey,
|
dataKey,
|
||||||
dataKeyVersion: metadata.dekVersion,
|
dataKeyVersion: metadata.dekVersion,
|
||||||
|
contentType: metadata.contentType,
|
||||||
contentIv: metadata.contentIv,
|
contentIv: metadata.contentIv,
|
||||||
name: await decryptString(metadata.name, metadata.nameIv, dataKey),
|
name: await decryptString(metadata.name, metadata.nameIv, dataKey),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export interface NewFileParams {
|
|||||||
mekVersion: number;
|
mekVersion: number;
|
||||||
encDek: string;
|
encDek: string;
|
||||||
dekVersion: Date;
|
dekVersion: Date;
|
||||||
|
contentType: string;
|
||||||
encContentIv: string;
|
encContentIv: string;
|
||||||
encName: string;
|
encName: string;
|
||||||
encNameIv: string;
|
encNameIv: string;
|
||||||
@@ -137,6 +138,7 @@ export const registerNewFile = async (params: NewFileParams) => {
|
|||||||
createdAt: now,
|
createdAt: now,
|
||||||
userId: params.userId,
|
userId: params.userId,
|
||||||
mekVersion: params.mekVersion,
|
mekVersion: params.mekVersion,
|
||||||
|
contentType: params.contentType,
|
||||||
encDek: params.encDek,
|
encDek: params.encDek,
|
||||||
dekVersion: params.dekVersion,
|
dekVersion: params.dekVersion,
|
||||||
encContentIv: params.encContentIv,
|
encContentIv: params.encContentIv,
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export const file = sqliteTable(
|
|||||||
mekVersion: integer("master_encryption_key_version").notNull(),
|
mekVersion: integer("master_encryption_key_version").notNull(),
|
||||||
encDek: text("encrypted_data_encryption_key").notNull().unique(), // Base64
|
encDek: text("encrypted_data_encryption_key").notNull().unique(), // Base64
|
||||||
dekVersion: integer("data_encryption_key_version", { mode: "timestamp_ms" }).notNull(),
|
dekVersion: integer("data_encryption_key_version", { mode: "timestamp_ms" }).notNull(),
|
||||||
|
contentType: text("content_type").notNull(),
|
||||||
encContentIv: text("encrypted_content_iv").notNull(), // Base64
|
encContentIv: text("encrypted_content_iv").notNull(), // Base64
|
||||||
encName: ciphertext("encrypted_name").notNull(),
|
encName: ciphertext("encrypted_name").notNull(),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import mime from "mime";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
export const fileRenameRequest = z.object({
|
export const fileRenameRequest = z.object({
|
||||||
@@ -12,6 +13,10 @@ export const fileInfoResponse = z.object({
|
|||||||
mekVersion: z.number().int().positive(),
|
mekVersion: z.number().int().positive(),
|
||||||
dek: z.string().base64().nonempty(),
|
dek: z.string().base64().nonempty(),
|
||||||
dekVersion: z.date(),
|
dekVersion: z.date(),
|
||||||
|
contentType: z
|
||||||
|
.string()
|
||||||
|
.nonempty()
|
||||||
|
.refine((value) => mime.getExtension(value) !== null), // MIME type
|
||||||
contentIv: z.string().base64().nonempty(),
|
contentIv: z.string().base64().nonempty(),
|
||||||
name: z.string().base64().nonempty(),
|
name: z.string().base64().nonempty(),
|
||||||
nameIv: z.string().base64().nonempty(),
|
nameIv: z.string().base64().nonempty(),
|
||||||
@@ -23,6 +28,10 @@ export const fileUploadRequest = z.object({
|
|||||||
mekVersion: z.number().int().positive(),
|
mekVersion: z.number().int().positive(),
|
||||||
dek: z.string().base64().nonempty(),
|
dek: z.string().base64().nonempty(),
|
||||||
dekVersion: z.coerce.date(),
|
dekVersion: z.coerce.date(),
|
||||||
|
contentType: z
|
||||||
|
.string()
|
||||||
|
.nonempty()
|
||||||
|
.refine((value) => mime.getExtension(value) !== null), // MIME type
|
||||||
contentIv: z.string().base64().nonempty(),
|
contentIv: z.string().base64().nonempty(),
|
||||||
name: z.string().base64().nonempty(),
|
name: z.string().base64().nonempty(),
|
||||||
nameIv: z.string().base64().nonempty(),
|
nameIv: z.string().base64().nonempty(),
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ export const getFileInformation = async (userId: number, fileId: number) => {
|
|||||||
mekVersion: file.mekVersion,
|
mekVersion: file.mekVersion,
|
||||||
encDek: file.encDek,
|
encDek: file.encDek,
|
||||||
dekVersion: file.dekVersion,
|
dekVersion: file.dekVersion,
|
||||||
|
contentType: file.contentType,
|
||||||
encContentIv: file.encContentIv,
|
encContentIv: file.encContentIv,
|
||||||
encName: file.encName,
|
encName: file.encName,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export interface FileInfo {
|
|||||||
id: number;
|
id: number;
|
||||||
dataKey: CryptoKey;
|
dataKey: CryptoKey;
|
||||||
dataKeyVersion: Date;
|
dataKeyVersion: Date;
|
||||||
|
contentType: string;
|
||||||
contentIv: string;
|
contentIv: string;
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,48 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import FileSaver from "file-saver";
|
import FileSaver from "file-saver";
|
||||||
|
import { untrack } from "svelte";
|
||||||
import type { Writable } from "svelte/store";
|
import type { Writable } from "svelte/store";
|
||||||
import { TopBar } from "$lib/components";
|
import { TopBar } from "$lib/components";
|
||||||
import { getFileInfo } from "$lib/modules/file";
|
import { getFileInfo } from "$lib/modules/file";
|
||||||
import { masterKeyStore, type FileInfo } from "$lib/stores";
|
import { masterKeyStore, type FileInfo } from "$lib/stores";
|
||||||
import { requestFileDownload } from "./service";
|
import { requestFileDownload } from "./service";
|
||||||
|
|
||||||
|
type ContentType = "image" | "video";
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
|
|
||||||
let info: Writable<FileInfo | null> | undefined = $state();
|
let info: Writable<FileInfo | null> | undefined = $state();
|
||||||
let isDownloaded = $state(false);
|
let isDownloaded = $state(false);
|
||||||
|
|
||||||
|
let content: ArrayBuffer | undefined = $state();
|
||||||
|
let contentType: ContentType | undefined = $state();
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
info = getFileInfo(data.id, $masterKeyStore?.get(1)?.key!);
|
info = getFileInfo(data.id, $masterKeyStore?.get(1)?.key!);
|
||||||
isDownloaded = false;
|
isDownloaded = false;
|
||||||
|
|
||||||
|
content = undefined;
|
||||||
|
contentType = undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (info && $info && !isDownloaded) {
|
if ($info && !isDownloaded) {
|
||||||
|
untrack(() => {
|
||||||
isDownloaded = true;
|
isDownloaded = true;
|
||||||
requestFileDownload(data.id, $info.contentIv, $info.dataKey).then((content) => {
|
|
||||||
FileSaver.saveAs(new Blob([content], { type: "application/octet-stream" }), $info.name);
|
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>
|
<title>파일</title>
|
||||||
</svelte:head>
|
</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,
|
mekVersion: masterKey.version,
|
||||||
dek: await wrapDataKey(dataKey, masterKey.key),
|
dek: await wrapDataKey(dataKey, masterKey.key),
|
||||||
dekVersion: dataKeyVersion,
|
dekVersion: dataKeyVersion,
|
||||||
|
contentType: file.type,
|
||||||
contentIv: fileEncrypted.iv,
|
contentIv: fileEncrypted.iv,
|
||||||
name: nameEncrypted.ciphertext,
|
name: nameEncrypted.ciphertext,
|
||||||
nameIv: nameEncrypted.iv,
|
nameIv: nameEncrypted.iv,
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export const GET: RequestHandler = async ({ cookies, params }) => {
|
|||||||
if (!zodRes.success) error(400, "Invalid path parameters");
|
if (!zodRes.success) error(400, "Invalid path parameters");
|
||||||
const { id } = zodRes.data;
|
const { id } = zodRes.data;
|
||||||
|
|
||||||
const { createdAt, mekVersion, encDek, dekVersion, encContentIv, encName } =
|
const { createdAt, mekVersion, encDek, dekVersion, contentType, encContentIv, encName } =
|
||||||
await getFileInformation(userId, id);
|
await getFileInformation(userId, id);
|
||||||
return json(
|
return json(
|
||||||
fileInfoResponse.parse({
|
fileInfoResponse.parse({
|
||||||
@@ -24,6 +24,7 @@ export const GET: RequestHandler = async ({ cookies, params }) => {
|
|||||||
mekVersion,
|
mekVersion,
|
||||||
dek: encDek,
|
dek: encDek,
|
||||||
dekVersion,
|
dekVersion,
|
||||||
|
contentType: contentType,
|
||||||
contentIv: encContentIv,
|
contentIv: encContentIv,
|
||||||
name: encName.ciphertext,
|
name: encName.ciphertext,
|
||||||
nameIv: encName.iv,
|
nameIv: encName.iv,
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
|
|||||||
|
|
||||||
const zodRes = fileUploadRequest.safeParse(JSON.parse(metadata));
|
const zodRes = fileUploadRequest.safeParse(JSON.parse(metadata));
|
||||||
if (!zodRes.success) error(400, "Invalid request body");
|
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(
|
await uploadFile(
|
||||||
{
|
{
|
||||||
@@ -25,6 +26,7 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
|
|||||||
mekVersion,
|
mekVersion,
|
||||||
encDek: dek,
|
encDek: dek,
|
||||||
dekVersion,
|
dekVersion,
|
||||||
|
contentType,
|
||||||
encContentIv: contentIv,
|
encContentIv: contentIv,
|
||||||
encName: name,
|
encName: name,
|
||||||
encNameIv: nameIv,
|
encNameIv: nameIv,
|
||||||
|
|||||||
Reference in New Issue
Block a user