이전 버전에서 업로드된 파일을 청크 업로드 방식으로 마이그레이션할 수 있는 기능 추가

This commit is contained in:
static
2026-01-12 08:40:07 +09:00
parent 594c3654c9
commit 27e90ef4d7
12 changed files with 531 additions and 3 deletions

View File

@@ -9,6 +9,7 @@ type IntegrityErrorMessages =
// File
| "Directory not found"
| "File not found"
| "File is not legacy"
| "File not found in category"
| "File already added to category"
| "Invalid DEK version"

View File

@@ -334,6 +334,16 @@ export const getAllFileIds = async (userId: number) => {
return files.map(({ id }) => id);
};
export const getLegacyFileIds = async (userId: number) => {
const files = await db
.selectFrom("file")
.select("id")
.where("user_id", "=", userId)
.where("encrypted_content_iv", "is not", null)
.execute();
return files.map(({ id }) => id);
};
export const getAllFileIdsByContentHmac = async (
userId: number,
hskVersion: number,
@@ -482,6 +492,52 @@ export const unregisterFile = async (userId: number, fileId: number) => {
});
};
export const migrateFileContent = async (
trx: typeof db,
userId: number,
fileId: number,
newPath: string,
encContentHash: string,
) => {
const file = await trx
.selectFrom("file")
.select(["path", "encrypted_content_iv"])
.where("id", "=", fileId)
.where("user_id", "=", userId)
.limit(1)
.forUpdate()
.executeTakeFirst();
if (!file) {
throw new IntegrityError("File not found");
}
if (!file.encrypted_content_iv) {
throw new IntegrityError("File is not legacy");
}
await trx
.updateTable("file")
.set({
path: newPath,
encrypted_content_iv: null,
encrypted_content_hash: encContentHash,
})
.where("id", "=", fileId)
.where("user_id", "=", userId)
.execute();
await trx
.insertInto("file_log")
.values({
file_id: fileId,
timestamp: new Date(),
action: "migrate",
})
.execute();
return file.path;
};
export const addFileToCategory = async (fileId: number, categoryId: number) => {
await db.transaction().execute(async (trx) => {
try {

View File

@@ -41,7 +41,7 @@ interface FileLogTable {
id: Generated<number>;
file_id: number;
timestamp: ColumnType<Date, Date, never>;
action: "create" | "rename" | "add-to-category" | "remove-from-category";
action: "create" | "rename" | "migrate" | "add-to-category" | "remove-from-category";
new_name: Ciphertext | null;
category_id: number | null;
}

View File

@@ -3,7 +3,7 @@ import type { Ciphertext } from "./utils";
interface UploadSessionTable {
id: string;
type: "file" | "thumbnail";
type: "file" | "thumbnail" | "migration";
user_id: number;
path: string;
total_chunks: number;

View File

@@ -31,6 +31,11 @@ interface ThumbnailUploadSession extends BaseUploadSession {
dekVersion: Date;
}
interface MigrationUploadSession extends BaseUploadSession {
type: "migration";
fileId: number;
}
export const createFileUploadSession = async (
params: Omit<FileUploadSession, "type" | "uploadedChunks">,
) => {
@@ -118,6 +123,39 @@ export const createThumbnailUploadSession = async (
});
};
export const createMigrationUploadSession = async (
params: Omit<MigrationUploadSession, "type" | "uploadedChunks">,
) => {
await db.transaction().execute(async (trx) => {
const file = await trx
.selectFrom("file")
.select("encrypted_content_iv")
.where("id", "=", params.fileId)
.where("user_id", "=", params.userId)
.limit(1)
.forUpdate()
.executeTakeFirst();
if (!file) {
throw new IntegrityError("File not found");
} else if (!file.encrypted_content_iv) {
throw new IntegrityError("File is not legacy");
}
await trx
.insertInto("upload_session")
.values({
id: params.id,
type: "migration",
user_id: params.userId,
path: params.path,
total_chunks: params.totalChunks,
expires_at: params.expiresAt,
file_id: params.fileId,
})
.execute();
});
};
export const getUploadSession = async (sessionId: string, userId: number) => {
const session = await db
.selectFrom("upload_session")
@@ -148,7 +186,7 @@ export const getUploadSession = async (sessionId: string, userId: number) => {
encCreatedAt: session.encrypted_created_at,
encLastModifiedAt: session.encrypted_last_modified_at!,
} satisfies FileUploadSession;
} else {
} else if (session.type === "thumbnail") {
return {
type: "thumbnail",
id: session.id,
@@ -160,6 +198,17 @@ export const getUploadSession = async (sessionId: string, userId: number) => {
fileId: session.file_id!,
dekVersion: session.data_encryption_key_version!,
} satisfies ThumbnailUploadSession;
} else {
return {
type: "migration",
id: session.id,
userId: session.user_id,
path: session.path,
totalChunks: session.total_chunks,
uploadedChunks: session.uploaded_chunks,
expiresAt: session.expires_at,
fileId: session.file_id!,
} satisfies MigrationUploadSession;
}
};