사소한 리팩토링 2

This commit is contained in:
static
2026-01-11 15:54:05 +09:00
parent 83369f83e3
commit 80368c3a29
3 changed files with 46 additions and 112 deletions

View File

@@ -5,16 +5,6 @@ export const digestMessage = async (message: BufferSource) => {
return await crypto.subtle.digest("SHA-256", message); return await crypto.subtle.digest("SHA-256", message);
}; };
export const createStreamingHmac = async (hmacSecret: CryptoKey) => {
const keyBytes = new Uint8Array(await crypto.subtle.exportKey("raw", hmacSecret));
const h = hmac.create(sha256, keyBytes);
return {
update: (data: Uint8Array) => h.update(data),
digest: () => h.digest(),
};
};
export const generateHmacSecret = async () => { export const generateHmacSecret = async () => {
return { return {
hmacSecret: await crypto.subtle.generateKey( hmacSecret: await crypto.subtle.generateKey(
@@ -28,6 +18,10 @@ export const generateHmacSecret = async () => {
}; };
}; };
export const signMessageHmac = async (message: BufferSource, hmacSecret: CryptoKey) => { export const createHmacStream = async (hmacSecret: CryptoKey) => {
return await crypto.subtle.sign("HMAC", hmacSecret, message); const h = hmac.create(sha256, new Uint8Array(await crypto.subtle.exportKey("raw", hmacSecret)));
return {
update: (data: Uint8Array) => h.update(data),
digest: () => h.digest(),
};
}; };

View File

@@ -9,8 +9,7 @@ import {
encryptString, encryptString,
encryptChunk, encryptChunk,
digestMessage, digestMessage,
signMessageHmac, createHmacStream,
createStreamingHmac,
} from "$lib/modules/crypto"; } from "$lib/modules/crypto";
import { Scheduler } from "$lib/modules/scheduler"; import { Scheduler } from "$lib/modules/scheduler";
import { generateThumbnail, generateThumbnailFromFile } from "$lib/modules/thumbnail"; import { generateThumbnail, generateThumbnailFromFile } from "$lib/modules/thumbnail";
@@ -60,27 +59,7 @@ export const clearUploadedFiles = () => {
const requestDuplicateFileScan = limitFunction( const requestDuplicateFileScan = limitFunction(
async (file: File, hmacSecret: HmacSecret, onDuplicate: () => Promise<boolean>) => { async (file: File, hmacSecret: HmacSecret, onDuplicate: () => Promise<boolean>) => {
const fileBuffer = await file.arrayBuffer(); const hmacStream = await createHmacStream(hmacSecret.secret);
const fileSigned = encodeToBase64(await signMessageHmac(fileBuffer, hmacSecret.secret));
const files = await trpc().file.listByHash.query({
hskVersion: hmacSecret.version,
contentHmac: fileSigned,
});
if (files.length === 0 || (await onDuplicate())) {
return { fileBuffer, fileSigned };
} else {
return {};
}
},
{ concurrency: 1 },
);
const isImageFile = (fileType: string) => fileType.startsWith("image/");
const requestDuplicateFileScanStreaming = limitFunction(
async (file: File, hmacSecret: HmacSecret, onDuplicate: () => Promise<boolean>) => {
const hmacStream = await createStreamingHmac(hmacSecret.secret);
const reader = file.stream().getReader(); const reader = file.stream().getReader();
while (true) { while (true) {
@@ -152,16 +131,12 @@ const encryptChunks = async (fileBuffer: ArrayBuffer, dataKey: CryptoKey) => {
return chunksEncrypted; return chunksEncrypted;
}; };
const encryptFile = limitFunction( const encryptImageFile = limitFunction(
async (state: FileUploadState, file: File, fileBuffer: ArrayBuffer, masterKey: MasterKey) => { async (state: FileUploadState, file: File, masterKey: MasterKey) => {
state.status = "encrypting"; state.status = "encrypting";
const fileType = getFileType(file); const fileBuffer = await file.arrayBuffer();
const createdAt = extractExifDateTime(fileBuffer);
let createdAt;
if (fileType.startsWith("image/")) {
createdAt = extractExifDateTime(fileBuffer);
}
const { dataKey, dataKeyVersion } = await generateDataKey(); const { dataKey, dataKeyVersion } = await generateDataKey();
const dataKeyWrapped = await wrapDataKey(dataKey, masterKey.key); const dataKeyWrapped = await wrapDataKey(dataKey, masterKey.key);
@@ -172,7 +147,7 @@ const encryptFile = limitFunction(
createdAt && (await encryptString(createdAt.getTime().toString(), dataKey)); createdAt && (await encryptString(createdAt.getTime().toString(), dataKey));
const lastModifiedAtEncrypted = await encryptString(file.lastModified.toString(), dataKey); const lastModifiedAtEncrypted = await encryptString(file.lastModified.toString(), dataKey);
const thumbnail = await generateThumbnail(fileBuffer, fileType); const thumbnail = await generateThumbnail(fileBuffer, getFileType(file));
const thumbnailBuffer = await thumbnail?.arrayBuffer(); const thumbnailBuffer = await thumbnail?.arrayBuffer();
const thumbnailEncrypted = thumbnailBuffer && (await encryptData(thumbnailBuffer, dataKey)); const thumbnailEncrypted = thumbnailBuffer && (await encryptData(thumbnailBuffer, dataKey));
@@ -181,7 +156,6 @@ const encryptFile = limitFunction(
return { return {
dataKeyWrapped, dataKeyWrapped,
dataKeyVersion, dataKeyVersion,
fileType,
chunksEncrypted, chunksEncrypted,
nameEncrypted, nameEncrypted,
createdAtEncrypted, createdAtEncrypted,
@@ -229,7 +203,7 @@ const uploadThumbnail = async (
await trpc().upload.completeFileThumbnailUpload.mutate({ uploadId }); await trpc().upload.completeFileThumbnailUpload.mutate({ uploadId });
}; };
const requestFileUpload = limitFunction( const requestImageFileUpload = limitFunction(
async ( async (
state: FileUploadState, state: FileUploadState,
metadata: RouterInputs["upload"]["startFileUpload"], metadata: RouterInputs["upload"]["startFileUpload"],
@@ -242,7 +216,6 @@ const requestFileUpload = limitFunction(
const { uploadId } = await trpc().upload.startFileUpload.mutate(metadata); const { uploadId } = await trpc().upload.startFileUpload.mutate(metadata);
// Upload chunks with progress tracking
const totalBytes = chunksEncrypted.reduce((sum, c) => sum + c.chunkEncrypted.byteLength, 0); const totalBytes = chunksEncrypted.reduce((sum, c) => sum + c.chunkEncrypted.byteLength, 0);
let uploadedBytes = 0; let uploadedBytes = 0;
const startTime = Date.now(); const startTime = Date.now();
@@ -265,9 +238,8 @@ const requestFileUpload = limitFunction(
uploadedBytes += chunkEncrypted.byteLength; uploadedBytes += chunkEncrypted.byteLength;
// Calculate progress, rate, estimated const elapsed = (Date.now() - startTime) / 1000;
const elapsed = (Date.now() - startTime) / 1000; // seconds const rate = uploadedBytes / elapsed;
const rate = uploadedBytes / elapsed; // bytes per second
const remaining = totalBytes - uploadedBytes; const remaining = totalBytes - uploadedBytes;
const estimated = rate > 0 ? remaining / rate : undefined; const estimated = rate > 0 ? remaining / rate : undefined;
@@ -276,13 +248,11 @@ const requestFileUpload = limitFunction(
state.estimated = estimated; state.estimated = estimated;
} }
// Complete upload
const { file: fileId } = await trpc().upload.completeFileUpload.mutate({ const { file: fileId } = await trpc().upload.completeFileUpload.mutate({
uploadId, uploadId,
contentHmac: fileSigned, contentHmac: fileSigned,
}); });
// Upload thumbnail if exists
if (thumbnailData) { if (thumbnailData) {
try { try {
await uploadThumbnail(fileId, thumbnailData, dataKeyVersion); await uploadThumbnail(fileId, thumbnailData, dataKeyVersion);
@@ -299,7 +269,7 @@ const requestFileUpload = limitFunction(
{ concurrency: 1 }, { concurrency: 1 },
); );
const uploadFileStreaming = async ( const requestFileUpload = async (
state: FileUploadState, state: FileUploadState,
file: File, file: File,
masterKey: MasterKey, masterKey: MasterKey,
@@ -316,7 +286,6 @@ const uploadFileStreaming = async (
const nameEncrypted = await encryptString(file.name, dataKey); const nameEncrypted = await encryptString(file.name, dataKey);
const lastModifiedAtEncrypted = await encryptString(file.lastModified.toString(), dataKey); const lastModifiedAtEncrypted = await encryptString(file.lastModified.toString(), dataKey);
// Calculate total chunks for metadata
const totalChunks = Math.ceil(file.size / CHUNK_SIZE); const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
const metadata = { const metadata = {
chunks: totalChunks, chunks: totalChunks,
@@ -334,7 +303,6 @@ const uploadFileStreaming = async (
const { uploadId } = await trpc().upload.startFileUpload.mutate(metadata); const { uploadId } = await trpc().upload.startFileUpload.mutate(metadata);
// Stream file, encrypt, and upload with concurrency limit
const reader = file.stream().getReader(); const reader = file.stream().getReader();
const limit = pLimit(4); const limit = pLimit(4);
let buffer = new Uint8Array(0); let buffer = new Uint8Array(0);
@@ -364,7 +332,6 @@ const uploadFileStreaming = async (
throw new Error(`Chunk upload failed: ${response.status} ${response.statusText}`); throw new Error(`Chunk upload failed: ${response.status} ${response.statusText}`);
} }
// Update progress after upload completes
uploadedBytes += originalChunkSize; uploadedBytes += originalChunkSize;
const elapsed = (Date.now() - startTime) / 1000; const elapsed = (Date.now() - startTime) / 1000;
const rate = uploadedBytes / elapsed; const rate = uploadedBytes / elapsed;
@@ -411,7 +378,6 @@ const uploadFileStreaming = async (
contentHmac: fileSigned, contentHmac: fileSigned,
}); });
// Generate and upload thumbnail for video files
if (fileType.startsWith("video/")) { if (fileType.startsWith("video/")) {
try { try {
const thumbnail = await generateThumbnailFromFile(file); const thumbnail = await generateThumbnailFromFile(file);
@@ -446,35 +412,29 @@ export const uploadFile = async (
}); });
const state = uploadingFiles.at(-1)!; const state = uploadingFiles.at(-1)!;
const fileType = getFileType(file);
// Image files: use buffer-based approach (need EXIF + thumbnail)
if (isImageFile(fileType)) {
return await scheduler.schedule(file.size, async () => { return await scheduler.schedule(file.size, async () => {
state.status = "encryption-pending"; state.status = "encryption-pending";
try { try {
const { fileBuffer, fileSigned } = await requestDuplicateFileScan( const { fileSigned } = await requestDuplicateFileScan(file, hmacSecret, onDuplicate);
file, if (!fileSigned) {
hmacSecret,
onDuplicate,
);
if (!fileBuffer || !fileSigned) {
state.status = "canceled"; state.status = "canceled";
uploadingFiles = uploadingFiles.filter((file) => file !== state); uploadingFiles = uploadingFiles.filter((file) => file !== state);
return undefined; return;
} }
const fileType = getFileType(file);
if (fileType.startsWith("image/")) {
const fileBuffer = await file.arrayBuffer();
const { const {
dataKeyWrapped, dataKeyWrapped,
dataKeyVersion, dataKeyVersion,
fileType,
chunksEncrypted, chunksEncrypted,
nameEncrypted, nameEncrypted,
createdAtEncrypted, createdAtEncrypted,
lastModifiedAtEncrypted, lastModifiedAtEncrypted,
thumbnail, thumbnail,
} = await encryptFile(state, file, fileBuffer, masterKey); } = await encryptImageFile(state, file, masterKey);
const metadata = { const metadata = {
chunks: chunksEncrypted.length, chunks: chunksEncrypted.length,
@@ -492,7 +452,7 @@ export const uploadFile = async (
lastModifiedAtIv: lastModifiedAtEncrypted.iv, lastModifiedAtIv: lastModifiedAtEncrypted.iv,
}; };
const { fileId, thumbnailBuffer } = await requestFileUpload( const { fileId, thumbnailBuffer } = await requestImageFileUpload(
state, state,
metadata, metadata,
chunksEncrypted, chunksEncrypted,
@@ -501,28 +461,8 @@ export const uploadFile = async (
dataKeyVersion, dataKeyVersion,
); );
return { fileId, fileBuffer, thumbnailBuffer }; return { fileId, fileBuffer, thumbnailBuffer };
} catch (e) { } else {
state.status = "error"; const { fileId } = await requestFileUpload(
throw e;
}
});
}
// Video and other files: use streaming approach
return await scheduler.schedule(file.size, async () => {
state.status = "encryption-pending";
try {
// 1st pass: streaming HMAC for duplicate check
const { fileSigned } = await requestDuplicateFileScanStreaming(file, hmacSecret, onDuplicate);
if (!fileSigned) {
state.status = "canceled";
uploadingFiles = uploadingFiles.filter((f) => f !== state);
return undefined;
}
// 2nd pass: streaming encrypt + upload
const { fileId } = await uploadFileStreaming(
state, state,
file, file,
masterKey, masterKey,
@@ -530,7 +470,8 @@ export const uploadFile = async (
fileSigned, fileSigned,
parentId, parentId,
); );
return { fileId, fileBuffer: undefined, thumbnailBuffer: undefined }; return { fileId };
}
} catch (e) { } catch (e) {
state.status = "error"; state.status = "error";
throw e; throw e;

View File

@@ -122,13 +122,8 @@ export const generateThumbnail = async (fileBuffer: ArrayBuffer, fileType: strin
} }
}; };
export const getThumbnailUrl = (thumbnailBuffer: ArrayBuffer) => {
return `data:image/webp;base64,${encodeToBase64(thumbnailBuffer)}`;
};
export const generateThumbnailFromFile = async (file: File) => { export const generateThumbnailFromFile = async (file: File) => {
const fileType = file.type || (file.name.endsWith(".heic") ? "image/heic" : ""); if (!file.type.startsWith("video/")) return null;
if (!fileType.startsWith("video/")) return null;
let url; let url;
try { try {
@@ -142,3 +137,7 @@ export const generateThumbnailFromFile = async (file: File) => {
} }
} }
}; };
export const getThumbnailUrl = (thumbnailBuffer: ArrayBuffer) => {
return `data:image/webp;base64,${encodeToBase64(thumbnailBuffer)}`;
};