파일 업로드 로직 리팩토링 2

This commit is contained in:
static
2026-01-12 16:58:28 +09:00
parent e7dc96bb47
commit c778a4fb9e
5 changed files with 129 additions and 117 deletions

View File

@@ -50,7 +50,14 @@ export const clearUploadedFiles = () => {
}; };
const requestDuplicateFileScan = limitFunction( const requestDuplicateFileScan = limitFunction(
async (file: File, hmacSecret: HmacSecret, onDuplicate: () => Promise<boolean>) => { async (
state: FileUploadState,
file: File,
hmacSecret: HmacSecret,
onDuplicate: () => Promise<boolean>,
) => {
state.status = "encryption-pending";
const hmacResult = await signMessageHmac(file, hmacSecret.secret); const hmacResult = await signMessageHmac(file, hmacSecret.secret);
const fileSigned = encodeToBase64(hmacResult); const fileSigned = encodeToBase64(hmacResult);
const files = await trpc().file.listByHash.query({ const files = await trpc().file.listByHash.query({
@@ -98,77 +105,101 @@ const extractExifDateTime = (fileBuffer: ArrayBuffer) => {
return new Date(utcDate - offsetMs); return new Date(utcDate - offsetMs);
}; };
const requestFileUpload2 = async ( interface FileMetadata {
state: FileUploadState, parentId: "root" | number;
file: Blob, name: string;
fileSigned: string, createdAt?: Date;
fileMetadata: { lastModifiedAt: Date;
parentId: "root" | number; }
name: string;
createdAt?: Date;
lastModifiedAt: Date;
},
masterKey: MasterKey,
hmacSecret: HmacSecret,
) => {
state.status = "encrypting";
const { dataKey, dataKeyVersion } = await generateDataKey(); const requestFileMetadataEncryption = limitFunction(
const dataKeyWrapped = await wrapDataKey(dataKey, masterKey.key); async (
state: FileUploadState,
file: Blob,
fileMetadata: FileMetadata,
masterKey: MasterKey,
hmacSecret: HmacSecret,
) => {
state.status = "encrypting";
const [nameEncrypted, createdAtEncrypted, lastModifiedAtEncrypted, thumbnailBuffer] = const { dataKey, dataKeyVersion } = await generateDataKey();
await Promise.all([ const dataKeyWrapped = await wrapDataKey(dataKey, masterKey.key);
encryptString(fileMetadata.name, dataKey),
fileMetadata.createdAt && encryptString(fileMetadata.createdAt.getTime().toString(), dataKey),
encryptString(fileMetadata.lastModifiedAt.getTime().toString(), dataKey),
generateThumbnail(file).then((blob) => blob?.arrayBuffer()),
]);
const { uploadId } = await trpc().upload.startFileUpload.mutate({ const [nameEncrypted, createdAtEncrypted, lastModifiedAtEncrypted, thumbnailBuffer] =
chunks: Math.ceil(file.size / CHUNK_SIZE), await Promise.all([
parent: fileMetadata.parentId, encryptString(fileMetadata.name, dataKey),
mekVersion: masterKey.version, fileMetadata.createdAt &&
dek: dataKeyWrapped, encryptString(fileMetadata.createdAt.getTime().toString(), dataKey),
dekVersion: dataKeyVersion, encryptString(fileMetadata.lastModifiedAt.getTime().toString(), dataKey),
hskVersion: hmacSecret.version, generateThumbnail(file).then((blob) => blob?.arrayBuffer()),
contentType: file.type, ]);
name: nameEncrypted.ciphertext,
nameIv: nameEncrypted.iv,
createdAt: createdAtEncrypted?.ciphertext,
createdAtIv: createdAtEncrypted?.iv,
lastModifiedAt: lastModifiedAtEncrypted.ciphertext,
lastModifiedAtIv: lastModifiedAtEncrypted.iv,
});
state.status = "uploading"; const { uploadId } = await trpc().upload.startFileUpload.mutate({
chunks: Math.ceil(file.size / CHUNK_SIZE),
await uploadBlob(uploadId, file, dataKey, { parent: fileMetadata.parentId,
onProgress(s) { mekVersion: masterKey.version,
state.progress = s.progress; dek: dataKeyWrapped,
state.rate = s.rateBps;
},
});
const { file: fileId } = await trpc().upload.completeFileUpload.mutate({
uploadId,
contentHmac: fileSigned,
});
if (thumbnailBuffer) {
const { uploadId } = await trpc().upload.startFileThumbnailUpload.mutate({
file: fileId,
dekVersion: dataKeyVersion, dekVersion: dataKeyVersion,
hskVersion: hmacSecret.version,
contentType: file.type,
name: nameEncrypted.ciphertext,
nameIv: nameEncrypted.iv,
createdAt: createdAtEncrypted?.ciphertext,
createdAtIv: createdAtEncrypted?.iv,
lastModifiedAt: lastModifiedAtEncrypted.ciphertext,
lastModifiedAtIv: lastModifiedAtEncrypted.iv,
}); });
await uploadBlob(uploadId, new Blob([thumbnailBuffer]), dataKey); state.status = "upload-pending";
return { uploadId, thumbnailBuffer, dataKey, dataKeyVersion };
},
{ concurrency: 4 },
);
await trpc().upload.completeFileThumbnailUpload.mutate({ uploadId }); const requestFileUpload = limitFunction(
} async (
state: FileUploadState,
uploadId: string,
file: Blob,
fileSigned: string,
thumbnailBuffer: ArrayBuffer | undefined,
dataKey: CryptoKey,
dataKeyVersion: Date,
) => {
state.status = "uploading";
state.status = "uploaded"; await uploadBlob(uploadId, file, dataKey, {
onProgress(s) {
state.progress = s.progress;
state.rate = s.rateBps;
},
});
return { fileId, thumbnailBuffer }; const { file: fileId } = await trpc().upload.completeFileUpload.mutate({
}; uploadId,
contentHmac: fileSigned,
});
if (thumbnailBuffer) {
try {
const { uploadId } = await trpc().upload.startFileThumbnailUpload.mutate({
file: fileId,
dekVersion: dataKeyVersion,
});
await uploadBlob(uploadId, new Blob([thumbnailBuffer]), dataKey);
await trpc().upload.completeFileThumbnailUpload.mutate({ uploadId });
} catch (e) {
console.error(e);
}
}
state.status = "uploaded";
return { fileId };
},
{ concurrency: 1 },
);
export const uploadFile = async ( export const uploadFile = async (
file: File, file: File,
@@ -185,51 +216,44 @@ export const uploadFile = async (
const state = uploadingFiles.at(-1)!; const state = uploadingFiles.at(-1)!;
return await scheduler.schedule(file.size, async () => { return await scheduler.schedule(file.size, async () => {
state.status = "encryption-pending";
try { try {
const { fileSigned } = await requestDuplicateFileScan(file, hmacSecret, onDuplicate); const { fileSigned } = await requestDuplicateFileScan(state, file, hmacSecret, onDuplicate);
if (!fileSigned) { if (!fileSigned) {
state.status = "canceled"; state.status = "canceled";
uploadingFiles = uploadingFiles.filter((file) => file !== state); uploadingFiles = uploadingFiles.filter((file) => file !== state);
return; return;
} }
let fileBuffer;
const fileType = getFileType(file); const fileType = getFileType(file);
const fileMetadata: FileMetadata = {
parentId,
name: file.name,
lastModifiedAt: new Date(file.lastModified),
};
if (fileType.startsWith("image/")) { if (fileType.startsWith("image/")) {
const fileBuffer = await file.arrayBuffer(); fileBuffer = await file.arrayBuffer();
const fileCreatedAt = extractExifDateTime(fileBuffer); fileMetadata.createdAt = extractExifDateTime(fileBuffer);
const { fileId, thumbnailBuffer } = await requestFileUpload2(
state,
new Blob([fileBuffer], { type: fileType }),
fileSigned,
{
parentId,
name: file.name,
createdAt: fileCreatedAt,
lastModifiedAt: new Date(file.lastModified),
},
masterKey,
hmacSecret,
);
return { fileId, fileBuffer, thumbnailBuffer };
} else {
const { fileId, thumbnailBuffer } = await requestFileUpload2(
state,
file,
fileSigned,
{
parentId,
name: file.name,
lastModifiedAt: new Date(file.lastModified),
},
masterKey,
hmacSecret,
);
return { fileId, thumbnailBuffer };
} }
const blob = new Blob([file], { type: fileType });
const { uploadId, thumbnailBuffer, dataKey, dataKeyVersion } =
await requestFileMetadataEncryption(state, blob, fileMetadata, masterKey, hmacSecret);
const { fileId } = await requestFileUpload(
state,
uploadId,
blob,
fileSigned,
thumbnailBuffer,
dataKey,
dataKeyVersion,
);
return { fileId, fileBuffer, thumbnailBuffer };
} catch (e) { } catch (e) {
state.status = "error"; state.status = "error";
throw e; throw e;

View File

@@ -18,7 +18,7 @@
info, info,
state: getMigrationState(info.id), state: getMigrationState(info.id),
})) }))
.filter((file) => file.state?.status !== "completed"), .filter((file) => file.state?.status !== "uploaded"),
); );
const migrateAllFiles = () => { const migrateAllFiles = () => {

View File

@@ -1,12 +1,9 @@
<script module lang="ts"> <script module lang="ts">
const subtexts = { const subtexts = {
queued: "대기 중", queued: "대기 중",
"download-pending": "다운로드를 기다리는 중",
downloading: "다운로드하는 중", downloading: "다운로드하는 중",
"encryption-pending": "암호화를 기다리는 중",
encrypting: "암호화하는 중",
"upload-pending": "업로드를 기다리는 중", "upload-pending": "업로드를 기다리는 중",
completed: "완료", uploaded: "",
error: "실패", error: "실패",
} as const; } as const;
</script> </script>

View File

@@ -9,13 +9,10 @@ import { trpc } from "$trpc/client";
export type MigrationStatus = export type MigrationStatus =
| "queued" | "queued"
| "download-pending"
| "downloading" | "downloading"
| "encryption-pending"
| "encrypting"
| "upload-pending" | "upload-pending"
| "uploading" | "uploading"
| "completed" | "uploaded"
| "error"; | "error";
export interface MigrationState { export interface MigrationState {
@@ -38,13 +35,13 @@ export const getMigrationState = (fileId: number) => {
export const clearMigrationStates = () => { export const clearMigrationStates = () => {
for (const [id, state] of states) { for (const [id, state] of states) {
if (state.status === "completed" || state.status === "error") { if (state.status === "uploaded" || state.status === "error") {
states.delete(id); states.delete(id);
} }
} }
}; };
const uploadMigrationChunks = limitFunction( const requestFileUpload = limitFunction(
async (state: MigrationState, fileId: number, fileBuffer: ArrayBuffer, dataKey: CryptoKey) => { async (state: MigrationState, fileId: number, fileBuffer: ArrayBuffer, dataKey: CryptoKey) => {
state.status = "uploading"; state.status = "uploading";
@@ -61,6 +58,7 @@ const uploadMigrationChunks = limitFunction(
}); });
await trpc().upload.completeMigrationUpload.mutate({ uploadId }); await trpc().upload.completeMigrationUpload.mutate({ uploadId });
state.status = "uploaded";
}, },
{ concurrency: 1 }, { concurrency: 1 },
); );
@@ -87,18 +85,11 @@ export const requestFileMigration = async (fileInfo: FileInfo) => {
await scheduler.schedule( await scheduler.schedule(
async () => { async () => {
state.status = "download-pending";
state.status = "downloading"; state.status = "downloading";
fileBuffer = await requestFileDownload(fileInfo.id, dataKey, true); fileBuffer = await requestFileDownload(fileInfo.id, dataKey, true);
return fileBuffer.byteLength; return fileBuffer.byteLength;
}, },
async () => { () => requestFileUpload(state, fileInfo.id, fileBuffer!, dataKey),
state.status = "encryption-pending";
await uploadMigrationChunks(state, fileInfo.id, fileBuffer!, dataKey);
state.status = "completed";
},
); );
} catch (e) { } catch (e) {
state.status = "error"; state.status = "error";

View File

@@ -35,7 +35,7 @@ const generateThumbnail = limitFunction(
async (fileId: number, fileBuffer: ArrayBuffer, fileType: string, dataKey: CryptoKey) => { async (fileId: number, fileBuffer: ArrayBuffer, fileType: string, dataKey: CryptoKey) => {
statuses.set(fileId, "generating"); statuses.set(fileId, "generating");
const thumbnail = await doGenerateThumbnail(fileBuffer, fileType); const thumbnail = await doGenerateThumbnail(new Blob([fileBuffer], { type: fileType }));
if (!thumbnail) return null; if (!thumbnail) return null;
const thumbnailBuffer = await thumbnail.arrayBuffer(); const thumbnailBuffer = await thumbnail.arrayBuffer();