mirror of
https://github.com/kmc7468/arkvault.git
synced 2026-02-04 08:06:56 +00:00
사소한 리팩토링 2
This commit is contained in:
@@ -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(),
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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)}`;
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user