mirror of
https://github.com/kmc7468/arkvault.git
synced 2025-12-14 22:08:45 +00:00
썸네일 업로드 구현
This commit is contained in:
@@ -10,6 +10,7 @@ node_modules
|
|||||||
/build
|
/build
|
||||||
/data
|
/data
|
||||||
/library
|
/library
|
||||||
|
/thumbnails
|
||||||
|
|
||||||
# OS
|
# OS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -9,6 +9,7 @@ node_modules
|
|||||||
/build
|
/build
|
||||||
/data
|
/data
|
||||||
/library
|
/library
|
||||||
|
/thumbnails
|
||||||
|
|
||||||
# OS
|
# OS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
@@ -11,9 +11,11 @@ import {
|
|||||||
digestMessage,
|
digestMessage,
|
||||||
signMessageHmac,
|
signMessageHmac,
|
||||||
} from "$lib/modules/crypto";
|
} from "$lib/modules/crypto";
|
||||||
|
import { generateImageThumbnail, generateVideoThumbnail } from "$lib/modules/thumbnail";
|
||||||
import type {
|
import type {
|
||||||
DuplicateFileScanRequest,
|
DuplicateFileScanRequest,
|
||||||
DuplicateFileScanResponse,
|
DuplicateFileScanResponse,
|
||||||
|
FileThumbnailUploadRequest,
|
||||||
FileUploadRequest,
|
FileUploadRequest,
|
||||||
FileUploadResponse,
|
FileUploadResponse,
|
||||||
} from "$lib/server/schemas";
|
} from "$lib/server/schemas";
|
||||||
@@ -76,6 +78,24 @@ const extractExifDateTime = (fileBuffer: ArrayBuffer) => {
|
|||||||
return new Date(utcDate - offsetMs);
|
return new Date(utcDate - offsetMs);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const generateThumbnail = async (file: File, fileType: string) => {
|
||||||
|
let url;
|
||||||
|
try {
|
||||||
|
if (fileType.startsWith("image/")) {
|
||||||
|
url = URL.createObjectURL(file);
|
||||||
|
return await generateImageThumbnail(url);
|
||||||
|
} else if (fileType.startsWith("video/")) {
|
||||||
|
url = URL.createObjectURL(file);
|
||||||
|
return await generateVideoThumbnail(url);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
if (url) {
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const encryptFile = limitFunction(
|
const encryptFile = limitFunction(
|
||||||
async (
|
async (
|
||||||
status: Writable<FileUploadStatus>,
|
status: Writable<FileUploadStatus>,
|
||||||
@@ -106,6 +126,11 @@ 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(file, fileType);
|
||||||
|
const thumbnailEncrypted = thumbnail
|
||||||
|
? await encryptData(await thumbnail.arrayBuffer(), dataKey)
|
||||||
|
: null;
|
||||||
|
|
||||||
status.update((value) => {
|
status.update((value) => {
|
||||||
value.status = "upload-pending";
|
value.status = "upload-pending";
|
||||||
return value;
|
return value;
|
||||||
@@ -120,13 +145,14 @@ const encryptFile = limitFunction(
|
|||||||
nameEncrypted,
|
nameEncrypted,
|
||||||
createdAtEncrypted,
|
createdAtEncrypted,
|
||||||
lastModifiedAtEncrypted,
|
lastModifiedAtEncrypted,
|
||||||
|
thumbnailEncrypted,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
{ concurrency: 4 },
|
{ concurrency: 4 },
|
||||||
);
|
);
|
||||||
|
|
||||||
const requestFileUpload = limitFunction(
|
const requestFileUpload = limitFunction(
|
||||||
async (status: Writable<FileUploadStatus>, form: FormData) => {
|
async (status: Writable<FileUploadStatus>, form: FormData, thumbnailForm: FormData | null) => {
|
||||||
status.update((value) => {
|
status.update((value) => {
|
||||||
value.status = "uploading";
|
value.status = "uploading";
|
||||||
return value;
|
return value;
|
||||||
@@ -144,6 +170,15 @@ const requestFileUpload = limitFunction(
|
|||||||
});
|
});
|
||||||
const { file }: FileUploadResponse = res.data;
|
const { file }: FileUploadResponse = res.data;
|
||||||
|
|
||||||
|
if (thumbnailForm) {
|
||||||
|
try {
|
||||||
|
await axios.post(`/api/file/${file}/thumbnail/upload`, thumbnailForm);
|
||||||
|
} catch (e) {
|
||||||
|
// TODO
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
status.update((value) => {
|
status.update((value) => {
|
||||||
value.status = "uploaded";
|
value.status = "uploaded";
|
||||||
return value;
|
return value;
|
||||||
@@ -198,6 +233,7 @@ export const uploadFile = async (
|
|||||||
nameEncrypted,
|
nameEncrypted,
|
||||||
createdAtEncrypted,
|
createdAtEncrypted,
|
||||||
lastModifiedAtEncrypted,
|
lastModifiedAtEncrypted,
|
||||||
|
thumbnailEncrypted,
|
||||||
} = await encryptFile(status, file, fileBuffer, masterKey);
|
} = await encryptFile(status, file, fileBuffer, masterKey);
|
||||||
|
|
||||||
const form = new FormData();
|
const form = new FormData();
|
||||||
@@ -223,7 +259,20 @@ export const uploadFile = async (
|
|||||||
form.set("content", new Blob([fileEncrypted.ciphertext]));
|
form.set("content", new Blob([fileEncrypted.ciphertext]));
|
||||||
form.set("checksum", fileEncryptedHash);
|
form.set("checksum", fileEncryptedHash);
|
||||||
|
|
||||||
const { fileId } = await requestFileUpload(status, form);
|
let thumbnailForm = null;
|
||||||
|
if (thumbnailEncrypted) {
|
||||||
|
thumbnailForm = new FormData();
|
||||||
|
thumbnailForm.set(
|
||||||
|
"metadata",
|
||||||
|
JSON.stringify({
|
||||||
|
dekVersion: dataKeyVersion.toISOString(),
|
||||||
|
contentIv: thumbnailEncrypted.iv,
|
||||||
|
} as FileThumbnailUploadRequest),
|
||||||
|
);
|
||||||
|
thumbnailForm.set("content", new Blob([thumbnailEncrypted.ciphertext]));
|
||||||
|
}
|
||||||
|
|
||||||
|
const { fileId } = await requestFileUpload(status, form, thumbnailForm);
|
||||||
return { fileId, fileBuffer };
|
return { fileId, fileBuffer };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
status.update((value) => {
|
status.update((value) => {
|
||||||
|
|||||||
@@ -32,13 +32,13 @@ export type FileRenameRequest = z.infer<typeof fileRenameRequest>;
|
|||||||
|
|
||||||
export const fileThumbnailInfoResponse = z.object({
|
export const fileThumbnailInfoResponse = z.object({
|
||||||
updatedAt: z.string().datetime(),
|
updatedAt: z.string().datetime(),
|
||||||
encContentIv: z.string().base64().nonempty(),
|
contentIv: z.string().base64().nonempty(),
|
||||||
});
|
});
|
||||||
export type FileThumbnailInfoResponse = z.infer<typeof fileThumbnailInfoResponse>;
|
export type FileThumbnailInfoResponse = z.infer<typeof fileThumbnailInfoResponse>;
|
||||||
|
|
||||||
export const fileThumbnailUploadRequest = z.object({
|
export const fileThumbnailUploadRequest = z.object({
|
||||||
dekVersion: z.string().datetime(),
|
dekVersion: z.string().datetime(),
|
||||||
encContentIv: z.string().base64().nonempty(),
|
contentIv: z.string().base64().nonempty(),
|
||||||
});
|
});
|
||||||
export type FileThumbnailUploadRequest = z.infer<typeof fileThumbnailUploadRequest>;
|
export type FileThumbnailUploadRequest = z.infer<typeof fileThumbnailUploadRequest>;
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export const GET: RequestHandler = async ({ locals, params }) => {
|
|||||||
return json(
|
return json(
|
||||||
fileThumbnailInfoResponse.parse({
|
fileThumbnailInfoResponse.parse({
|
||||||
updatedAt: updatedAt.toISOString(),
|
updatedAt: updatedAt.toISOString(),
|
||||||
encContentIv,
|
contentIv: encContentIv,
|
||||||
} satisfies FileThumbnailInfoResponse),
|
} satisfies FileThumbnailInfoResponse),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -39,7 +39,9 @@ export const POST: RequestHandler = async ({ locals, params, request }) => {
|
|||||||
if (fieldname === "metadata") {
|
if (fieldname === "metadata") {
|
||||||
// Ignore subsequent metadata fields
|
// Ignore subsequent metadata fields
|
||||||
if (!metadata) {
|
if (!metadata) {
|
||||||
metadata = fileThumbnailUploadRequest.parse(val);
|
const zodRes = fileThumbnailUploadRequest.safeParse(JSON.parse(val));
|
||||||
|
if (!zodRes.success) error(400, "Invalid request body");
|
||||||
|
metadata = zodRes.data;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
error(400, "Invalid request body");
|
error(400, "Invalid request body");
|
||||||
@@ -57,7 +59,7 @@ export const POST: RequestHandler = async ({ locals, params, request }) => {
|
|||||||
userId,
|
userId,
|
||||||
id,
|
id,
|
||||||
new Date(metadata.dekVersion),
|
new Date(metadata.dekVersion),
|
||||||
metadata.encContentIv,
|
metadata.contentIv,
|
||||||
content,
|
content,
|
||||||
);
|
);
|
||||||
resolve(text("Thumbnail uploaded", { headers: { "Content-Type": "text/plain" } }));
|
resolve(text("Thumbnail uploaded", { headers: { "Content-Type": "text/plain" } }));
|
||||||
|
|||||||
Reference in New Issue
Block a user