썸네일 업로드도 새로운 업로드 방식으로 변경

This commit is contained in:
static
2026-01-11 14:07:32 +09:00
parent 3628e6d21a
commit 57c27b76be
17 changed files with 527 additions and 430 deletions

View File

@@ -6,7 +6,7 @@ interface Thumbnail {
id: number;
path: string;
updatedAt: Date;
encContentIv: string;
encContentIv: string | null;
}
interface FileThumbnail extends Thumbnail {
@@ -18,7 +18,7 @@ export const updateFileThumbnail = async (
fileId: number,
dekVersion: Date,
path: string,
encContentIv: string,
encContentIv: string | null,
) => {
return await db.transaction().execute(async (trx) => {
const file = await trx

View File

@@ -8,23 +8,31 @@ export const up = async (db: Kysely<any>) => {
.alterColumn("encrypted_content_iv", (col) => col.dropNotNull())
.execute();
// media.ts
await db.schema
.alterTable("thumbnail")
.alterColumn("encrypted_content_iv", (col) => col.dropNotNull())
.execute();
// upload.ts
await db.schema
.createTable("upload_session")
.addColumn("id", "uuid", (col) => col.primaryKey().defaultTo(sql`gen_random_uuid()`))
.addColumn("type", "text", (col) => col.notNull())
.addColumn("user_id", "integer", (col) => col.references("user.id").notNull())
.addColumn("total_chunks", "integer", (col) => col.notNull())
.addColumn("uploaded_chunks", sql`integer[]`, (col) => col.notNull().defaultTo(sql`'{}'`))
.addColumn("expires_at", "timestamp(3)", (col) => col.notNull())
.addColumn("parent_id", "integer", (col) => col.references("directory.id"))
.addColumn("master_encryption_key_version", "integer", (col) => col.notNull())
.addColumn("encrypted_data_encryption_key", "text", (col) => col.notNull())
.addColumn("data_encryption_key_version", "timestamp(3)", (col) => col.notNull())
.addColumn("master_encryption_key_version", "integer")
.addColumn("encrypted_data_encryption_key", "text")
.addColumn("data_encryption_key_version", "timestamp(3)")
.addColumn("hmac_secret_key_version", "integer")
.addColumn("content_type", "text", (col) => col.notNull())
.addColumn("encrypted_name", "json", (col) => col.notNull())
.addColumn("content_type", "text")
.addColumn("encrypted_name", "json")
.addColumn("encrypted_created_at", "json")
.addColumn("encrypted_last_modified_at", "json", (col) => col.notNull())
.addColumn("encrypted_last_modified_at", "json")
.addColumn("file_id", "integer", (col) => col.references("file.id"))
.addForeignKeyConstraint(
"upload_session_fk01",
["user_id", "master_encryption_key_version"],
@@ -43,6 +51,10 @@ export const up = async (db: Kysely<any>) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const down = async (db: Kysely<any>) => {
await db.schema.dropTable("upload_session").execute();
await db.schema
.alterTable("thumbnail")
.alterColumn("encrypted_content_iv", (col) => col.setNotNull())
.execute();
await db.schema
.alterTable("file")
.alterColumn("encrypted_content_iv", (col) => col.setNotNull())

View File

@@ -7,7 +7,7 @@ interface ThumbnailTable {
category_id: number | null;
path: string;
updated_at: Date;
encrypted_content_iv: string; // Base64
encrypted_content_iv: string | null; // Base64
}
declare module "./index" {

View File

@@ -3,20 +3,25 @@ import type { Ciphertext } from "./util";
interface UploadSessionTable {
id: Generated<string>;
type: "file" | "thumbnail";
user_id: number;
total_chunks: number;
uploaded_chunks: Generated<number[]>;
expires_at: Date;
// For file uploads
parent_id: number | null;
master_encryption_key_version: number;
encrypted_data_encryption_key: string; // Base64
data_encryption_key_version: Date;
master_encryption_key_version: number | null;
encrypted_data_encryption_key: string | null; // Base64
data_encryption_key_version: Date | null;
hmac_secret_key_version: number | null;
content_type: string;
encrypted_name: Ciphertext;
content_type: string | null;
encrypted_name: Ciphertext | null;
encrypted_created_at: Ciphertext | null;
encrypted_last_modified_at: Ciphertext;
encrypted_last_modified_at: Ciphertext | null;
// For thumbnail uploads
file_id: number | null;
}
declare module "./index" {

View File

@@ -3,13 +3,16 @@ import { IntegrityError } from "./error";
import db from "./kysely";
import type { Ciphertext } from "./schema";
interface UploadSession {
interface BaseUploadSession {
id: string;
userId: number;
totalChunks: number;
uploadedChunks: number[];
expiresAt: Date;
}
interface FileUploadSession extends BaseUploadSession {
type: "file";
parentId: DirectoryId;
mekVersion: number;
encDek: string;
@@ -21,7 +24,15 @@ interface UploadSession {
encLastModifiedAt: Ciphertext;
}
export const createUploadSession = async (params: Omit<UploadSession, "id" | "uploadedChunks">) => {
interface ThumbnailUploadSession extends BaseUploadSession {
type: "thumbnail";
fileId: number;
dekVersion: Date;
}
export const createFileUploadSession = async (
params: Omit<FileUploadSession, "id" | "type" | "uploadedChunks">,
) => {
return await db.transaction().execute(async (trx) => {
const mek = await trx
.selectFrom("master_encryption_key")
@@ -52,6 +63,7 @@ export const createUploadSession = async (params: Omit<UploadSession, "id" | "up
const { sessionId } = await trx
.insertInto("upload_session")
.values({
type: "file",
user_id: params.userId,
total_chunks: params.totalChunks,
expires_at: params.expiresAt,
@@ -71,6 +83,40 @@ export const createUploadSession = async (params: Omit<UploadSession, "id" | "up
});
};
export const createThumbnailUploadSession = async (
params: Omit<ThumbnailUploadSession, "id" | "type" | "uploadedChunks" | "totalChunks">,
) => {
return await db.transaction().execute(async (trx) => {
const file = await trx
.selectFrom("file")
.select("data_encryption_key_version")
.where("id", "=", params.fileId)
.where("user_id", "=", params.userId)
.limit(1)
.forUpdate()
.executeTakeFirst();
if (!file) {
throw new IntegrityError("File not found");
} else if (file.data_encryption_key_version.getTime() !== params.dekVersion.getTime()) {
throw new IntegrityError("Invalid DEK version");
}
const { sessionId } = await trx
.insertInto("upload_session")
.values({
type: "thumbnail",
user_id: params.userId,
total_chunks: 1,
expires_at: params.expiresAt,
file_id: params.fileId,
data_encryption_key_version: params.dekVersion,
})
.returning("id as sessionId")
.executeTakeFirstOrThrow();
return { id: sessionId };
});
};
export const getUploadSession = async (sessionId: string, userId: number) => {
const session = await db
.selectFrom("upload_session")
@@ -80,24 +126,39 @@ export const getUploadSession = async (sessionId: string, userId: number) => {
.where("expires_at", ">", new Date())
.limit(1)
.executeTakeFirst();
return session
? ({
id: session.id,
userId: session.user_id,
totalChunks: session.total_chunks,
uploadedChunks: session.uploaded_chunks,
expiresAt: session.expires_at,
parentId: session.parent_id ?? "root",
mekVersion: session.master_encryption_key_version,
encDek: session.encrypted_data_encryption_key,
dekVersion: session.data_encryption_key_version,
hskVersion: session.hmac_secret_key_version,
contentType: session.content_type,
encName: session.encrypted_name,
encCreatedAt: session.encrypted_created_at,
encLastModifiedAt: session.encrypted_last_modified_at,
} satisfies UploadSession)
: null;
if (!session) return null;
if (session.type === "file") {
return {
type: "file",
id: session.id,
userId: session.user_id,
totalChunks: session.total_chunks,
uploadedChunks: session.uploaded_chunks,
expiresAt: session.expires_at,
parentId: session.parent_id ?? "root",
mekVersion: session.master_encryption_key_version!,
encDek: session.encrypted_data_encryption_key!,
dekVersion: session.data_encryption_key_version!,
hskVersion: session.hmac_secret_key_version,
contentType: session.content_type!,
encName: session.encrypted_name!,
encCreatedAt: session.encrypted_created_at,
encLastModifiedAt: session.encrypted_last_modified_at!,
} satisfies FileUploadSession;
} else {
return {
type: "thumbnail",
id: session.id,
userId: session.user_id,
totalChunks: session.total_chunks,
uploadedChunks: session.uploaded_chunks,
expiresAt: session.expires_at,
fileId: session.file_id!,
dekVersion: session.data_encryption_key_version!,
} satisfies ThumbnailUploadSession;
}
};
export const markChunkAsUploaded = async (sessionId: string, chunkIndex: number) => {