From c778a4fb9e342f7b77bf4be9c8f6d488c5c6ff3b Mon Sep 17 00:00:00 2001 From: static Date: Mon, 12 Jan 2026 16:58:28 +0900 Subject: [PATCH] =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=97=85=EB=A1=9C?= =?UTF-8?q?=EB=93=9C=20=EB=A1=9C=EC=A7=81=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81=202?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/modules/file/upload.svelte.ts | 218 ++++++++++-------- .../settings/migration/+page.svelte | 2 +- .../settings/migration/File.svelte | 5 +- .../settings/migration/service.svelte.ts | 19 +- .../settings/thumbnail/service.ts | 2 +- 5 files changed, 129 insertions(+), 117 deletions(-) diff --git a/src/lib/modules/file/upload.svelte.ts b/src/lib/modules/file/upload.svelte.ts index d066d4f..9bf043a 100644 --- a/src/lib/modules/file/upload.svelte.ts +++ b/src/lib/modules/file/upload.svelte.ts @@ -50,7 +50,14 @@ export const clearUploadedFiles = () => { }; const requestDuplicateFileScan = limitFunction( - async (file: File, hmacSecret: HmacSecret, onDuplicate: () => Promise) => { + async ( + state: FileUploadState, + file: File, + hmacSecret: HmacSecret, + onDuplicate: () => Promise, + ) => { + state.status = "encryption-pending"; + const hmacResult = await signMessageHmac(file, hmacSecret.secret); const fileSigned = encodeToBase64(hmacResult); const files = await trpc().file.listByHash.query({ @@ -98,77 +105,101 @@ const extractExifDateTime = (fileBuffer: ArrayBuffer) => { return new Date(utcDate - offsetMs); }; -const requestFileUpload2 = async ( - state: FileUploadState, - file: Blob, - fileSigned: string, - fileMetadata: { - parentId: "root" | number; - name: string; - createdAt?: Date; - lastModifiedAt: Date; - }, - masterKey: MasterKey, - hmacSecret: HmacSecret, -) => { - state.status = "encrypting"; +interface FileMetadata { + parentId: "root" | number; + name: string; + createdAt?: Date; + lastModifiedAt: Date; +} - const { dataKey, dataKeyVersion } = await generateDataKey(); - const dataKeyWrapped = await wrapDataKey(dataKey, masterKey.key); +const requestFileMetadataEncryption = limitFunction( + async ( + state: FileUploadState, + file: Blob, + fileMetadata: FileMetadata, + masterKey: MasterKey, + hmacSecret: HmacSecret, + ) => { + state.status = "encrypting"; - const [nameEncrypted, createdAtEncrypted, lastModifiedAtEncrypted, thumbnailBuffer] = - await Promise.all([ - 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 { dataKey, dataKeyVersion } = await generateDataKey(); + const dataKeyWrapped = await wrapDataKey(dataKey, masterKey.key); - const { uploadId } = await trpc().upload.startFileUpload.mutate({ - chunks: Math.ceil(file.size / CHUNK_SIZE), - parent: fileMetadata.parentId, - mekVersion: masterKey.version, - dek: dataKeyWrapped, - 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, - }); + const [nameEncrypted, createdAtEncrypted, lastModifiedAtEncrypted, thumbnailBuffer] = + await Promise.all([ + encryptString(fileMetadata.name, dataKey), + fileMetadata.createdAt && + encryptString(fileMetadata.createdAt.getTime().toString(), dataKey), + encryptString(fileMetadata.lastModifiedAt.getTime().toString(), dataKey), + generateThumbnail(file).then((blob) => blob?.arrayBuffer()), + ]); - state.status = "uploading"; - - await uploadBlob(uploadId, file, dataKey, { - onProgress(s) { - state.progress = s.progress; - 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, + const { uploadId } = await trpc().upload.startFileUpload.mutate({ + chunks: Math.ceil(file.size / CHUNK_SIZE), + parent: fileMetadata.parentId, + mekVersion: masterKey.version, + dek: dataKeyWrapped, 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 ( file: File, @@ -185,51 +216,44 @@ export const uploadFile = async ( const state = uploadingFiles.at(-1)!; return await scheduler.schedule(file.size, async () => { - state.status = "encryption-pending"; - try { - const { fileSigned } = await requestDuplicateFileScan(file, hmacSecret, onDuplicate); + const { fileSigned } = await requestDuplicateFileScan(state, file, hmacSecret, onDuplicate); + if (!fileSigned) { state.status = "canceled"; uploadingFiles = uploadingFiles.filter((file) => file !== state); return; } + let fileBuffer; const fileType = getFileType(file); + const fileMetadata: FileMetadata = { + parentId, + name: file.name, + lastModifiedAt: new Date(file.lastModified), + }; + if (fileType.startsWith("image/")) { - const fileBuffer = await file.arrayBuffer(); - const fileCreatedAt = 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 }; + fileBuffer = await file.arrayBuffer(); + fileMetadata.createdAt = extractExifDateTime(fileBuffer); } + + 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) { state.status = "error"; throw e; diff --git a/src/routes/(fullscreen)/settings/migration/+page.svelte b/src/routes/(fullscreen)/settings/migration/+page.svelte index b4bc9cb..4db6a80 100644 --- a/src/routes/(fullscreen)/settings/migration/+page.svelte +++ b/src/routes/(fullscreen)/settings/migration/+page.svelte @@ -18,7 +18,7 @@ info, state: getMigrationState(info.id), })) - .filter((file) => file.state?.status !== "completed"), + .filter((file) => file.state?.status !== "uploaded"), ); const migrateAllFiles = () => { diff --git a/src/routes/(fullscreen)/settings/migration/File.svelte b/src/routes/(fullscreen)/settings/migration/File.svelte index ec9d25b..d16e800 100644 --- a/src/routes/(fullscreen)/settings/migration/File.svelte +++ b/src/routes/(fullscreen)/settings/migration/File.svelte @@ -1,12 +1,9 @@ diff --git a/src/routes/(fullscreen)/settings/migration/service.svelte.ts b/src/routes/(fullscreen)/settings/migration/service.svelte.ts index 9d08db2..67201b0 100644 --- a/src/routes/(fullscreen)/settings/migration/service.svelte.ts +++ b/src/routes/(fullscreen)/settings/migration/service.svelte.ts @@ -9,13 +9,10 @@ import { trpc } from "$trpc/client"; export type MigrationStatus = | "queued" - | "download-pending" | "downloading" - | "encryption-pending" - | "encrypting" | "upload-pending" | "uploading" - | "completed" + | "uploaded" | "error"; export interface MigrationState { @@ -38,13 +35,13 @@ export const getMigrationState = (fileId: number) => { export const clearMigrationStates = () => { for (const [id, state] of states) { - if (state.status === "completed" || state.status === "error") { + if (state.status === "uploaded" || state.status === "error") { states.delete(id); } } }; -const uploadMigrationChunks = limitFunction( +const requestFileUpload = limitFunction( async (state: MigrationState, fileId: number, fileBuffer: ArrayBuffer, dataKey: CryptoKey) => { state.status = "uploading"; @@ -61,6 +58,7 @@ const uploadMigrationChunks = limitFunction( }); await trpc().upload.completeMigrationUpload.mutate({ uploadId }); + state.status = "uploaded"; }, { concurrency: 1 }, ); @@ -87,18 +85,11 @@ export const requestFileMigration = async (fileInfo: FileInfo) => { await scheduler.schedule( async () => { - state.status = "download-pending"; state.status = "downloading"; fileBuffer = await requestFileDownload(fileInfo.id, dataKey, true); return fileBuffer.byteLength; }, - async () => { - state.status = "encryption-pending"; - - await uploadMigrationChunks(state, fileInfo.id, fileBuffer!, dataKey); - - state.status = "completed"; - }, + () => requestFileUpload(state, fileInfo.id, fileBuffer!, dataKey), ); } catch (e) { state.status = "error"; diff --git a/src/routes/(fullscreen)/settings/thumbnail/service.ts b/src/routes/(fullscreen)/settings/thumbnail/service.ts index 314cf5a..381ed53 100644 --- a/src/routes/(fullscreen)/settings/thumbnail/service.ts +++ b/src/routes/(fullscreen)/settings/thumbnail/service.ts @@ -35,7 +35,7 @@ const generateThumbnail = limitFunction( async (fileId: number, fileBuffer: ArrayBuffer, fileType: string, dataKey: CryptoKey) => { statuses.set(fileId, "generating"); - const thumbnail = await doGenerateThumbnail(fileBuffer, fileType); + const thumbnail = await doGenerateThumbnail(new Blob([fileBuffer], { type: fileType })); if (!thumbnail) return null; const thumbnailBuffer = await thumbnail.arrayBuffer();