mirror of
https://github.com/kmc7468/arkvault.git
synced 2026-02-04 08:06:56 +00:00
파일 업로드 로직 리팩토링 2
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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 = () => {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user