feat: implement direct file upload functionality with S3 integration
This commit is contained in:
parent
3519e1307b
commit
b9a522c6d7
3 changed files with 220 additions and 39 deletions
|
|
@ -37,7 +37,7 @@ async function uploadToS3(fileName: string, mediaUrl: string, maxRetry = 3) {
|
|||
}
|
||||
|
||||
const buffer = await response.arrayBuffer();
|
||||
|
||||
|
||||
// 2. Bun S3 Client만 사용하여 쓰기
|
||||
// client.write는 내부적으로 효율적인 스트리밍/버퍼 처리를 수행합니다.
|
||||
await client.write(fileName, buffer, {
|
||||
|
|
@ -67,4 +67,11 @@ async function uploadToS3(fileName: string, mediaUrl: string, maxRetry = 3) {
|
|||
throw lastError;
|
||||
}
|
||||
|
||||
export { makeS3FileName, uploadToS3, client as s3Client };
|
||||
async function writeToS3(fileName: string, data: ArrayBuffer | Blob, contentType: string) {
|
||||
await warmupS3();
|
||||
await client.write(fileName, data, {
|
||||
type: contentType,
|
||||
});
|
||||
}
|
||||
|
||||
export { makeS3FileName, uploadToS3, writeToS3, client as s3Client };
|
||||
|
|
@ -9,7 +9,7 @@ import { normalizeQueryTags, normalizeTags } from "@/lib/tag";
|
|||
import { fetchTweetData } from "@/lib/tweet";
|
||||
import { fetchPixivData } from "@/lib/pixiv";
|
||||
import { checkExistingPostByUrl } from "@/lib/post";
|
||||
import { makeS3FileName, s3Client, uploadToS3 } from "@/lib/s3";
|
||||
import { makeS3FileName, s3Client, uploadToS3, writeToS3 } from "@/lib/s3";
|
||||
|
||||
const inFlightUploads = new Set<string>();
|
||||
|
||||
|
|
@ -685,6 +685,91 @@ export default new Elysia({ prefix: "/post" })
|
|||
})
|
||||
})
|
||||
|
||||
.post("/upload/direct", async ({ body, status, request }) => {
|
||||
const requester = (request as any).requester;
|
||||
if (!requester || (requester.role !== "admin" && requester.role !== "writer")) {
|
||||
return status(401, uploadError("업로드 권한이 없습니다."));
|
||||
}
|
||||
|
||||
const { files, author, tag } = body;
|
||||
const fileList = Array.isArray(files) ? files : [files];
|
||||
|
||||
if (fileList.length === 0) {
|
||||
return status(400, uploadError("업로드할 파일이 없습니다."));
|
||||
}
|
||||
|
||||
let savedCount = 0;
|
||||
let failedCount = 0;
|
||||
const savedIds: string[] = [];
|
||||
const normalizedTags = normalizeTags(tag || ["미분류"]);
|
||||
|
||||
for (const [index, file] of fileList.entries()) {
|
||||
try {
|
||||
const extension = file.name.split(".").pop();
|
||||
const fileName = `direct/${crypto.randomUUID()}.${extension}`;
|
||||
const mediaType = file.type.startsWith("video/") ? "video" : "image";
|
||||
|
||||
await writeToS3(fileName, file, file.type);
|
||||
|
||||
const post = await MediaUpload.create({
|
||||
type: "direct",
|
||||
mediaIndex: index,
|
||||
mediaUrl: `${config.s3.endpoint}/${config.s3.bucket}/${fileName}`,
|
||||
s3Key: fileName,
|
||||
mediaType,
|
||||
tags: normalizedTags,
|
||||
author: author || requester.username || "익명",
|
||||
uploadedBy: {
|
||||
userId: requester.userId,
|
||||
discordId: requester.discordId,
|
||||
username: requester.username,
|
||||
role: requester.role,
|
||||
},
|
||||
});
|
||||
|
||||
if (post) {
|
||||
savedIds.push(post._id.toString());
|
||||
savedCount += 1;
|
||||
}
|
||||
} catch (error) {
|
||||
failedCount += 1;
|
||||
console.error(`[Direct upload failed] name=${file.name}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
if (savedCount > 0) {
|
||||
await saveTags(normalizedTags);
|
||||
await createAuditLog({
|
||||
actor: {
|
||||
userId: requester.userId,
|
||||
discordId: requester.discordId,
|
||||
username: requester.username,
|
||||
role: requester.role,
|
||||
},
|
||||
action: "post.upload.direct",
|
||||
targetType: "post",
|
||||
summary: `${requester.username} uploaded ${savedCount} files directly`,
|
||||
detail: {
|
||||
savedCount,
|
||||
failedCount,
|
||||
ids: savedIds,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return uploadOk("업로드가 완료되었습니다.", {
|
||||
savedCount,
|
||||
failedCount,
|
||||
ids: savedIds,
|
||||
});
|
||||
}, {
|
||||
body: t.Object({
|
||||
files: t.Files(),
|
||||
author: t.Optional(t.String()),
|
||||
tag: t.Optional(t.Array(t.String())),
|
||||
})
|
||||
})
|
||||
|
||||
.post("/bulk-delete", async ({ body, status, jwt, cookie: { mizuki } }) => {
|
||||
const rawToken = mizuki.value;
|
||||
if (typeof rawToken !== "string" || rawToken.length === 0) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue