서버 코드를 Router, Service, Repository 계층으로 분리하고 오류 처리 및 파일 삭제 로직 개선

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
staticandClaude Opus 5.5 committed 2026-10-12 00:00:21 +09:00
1 parent ed732be999
commit 6a6aa1765b
72 files changed
+1928 -1509

No files matched your search

+66
View File
@@ -1,5 +1,8 @@
import { createHash } from "crypto";
import { Readable } from "stream";
import { beforeEach, describe, expect, it } from "vitest";
import { UploadRepo } from "#lib/server/db/index.js";
import { FileService, UploadService } from "#lib/server/services/index.js";
import { isChunkUploaded } from "#lib/server/services/upload.js";
import { ciphertext, createUser, newEntryParams, resetDatabase } from "../helpers/database";
@@ -33,3 +36,66 @@ describe("upload session bitmap", () => {
}
});
});
describe("upload completion", () => {
it("registers the file only once when completed concurrently", async () => {
const { userId } = await createUser();
const { uploadId } = await UploadService.startFileUpload({
...newEntryParams(userId, "root"),
totalChunks: 1,
hskVersion: null,
contentType: "image/png",
encCreatedAt: null,
encLastModifiedAt: ciphertext(),
});
const chunk = Buffer.alloc(100, 1);
await UploadService.uploadChunk(userId, uploadId, 1, Readable.from([chunk]));
const hash = createHash("sha256").update(chunk).digest("base64");
const results = await Promise.allSettled([
UploadService.completeFileUpload(userId, uploadId, null, hash),
UploadService.completeFileUpload(userId, uploadId, null, hash),
]);
expect(results.filter(({ status }) => status === "fulfilled")).toHaveLength(1);
expect(results.find(({ status }) => status === "rejected")).toMatchObject({
reason: { code: "NOT_FOUND" },
});
expect(await FileService.getAllFileIds(userId)).toHaveLength(1);
});
it("discards corrupted uploads", async () => {
const { userId } = await createUser();
const { uploadId } = await UploadService.startFileUpload({
...newEntryParams(userId, "root"),
totalChunks: 1,
hskVersion: null,
contentType: "image/png",
encCreatedAt: null,
encLastModifiedAt: ciphertext(),
});
await UploadService.uploadChunk(userId, uploadId, 1, Readable.from([Buffer.alloc(100)]));
await expect(
UploadService.completeFileUpload(userId, uploadId, null, "invalid"),
).rejects.toMatchObject({ code: "CONFLICT" });
expect(await UploadRepo.getUploadSession(uploadId, userId)).toBeNull();
});
it("rejects chunks of invalid sizes", async () => {
const { userId } = await createUser();
const { uploadId } = await UploadService.startFileUpload({
...newEntryParams(userId, "root"),
totalChunks: 2,
hskVersion: null,
contentType: "image/png",
encCreatedAt: null,
encLastModifiedAt: ciphertext(),
});
await expect(
UploadService.uploadChunk(userId, uploadId, 1, Readable.from([Buffer.alloc(100)])),
).rejects.toMatchObject({ code: "BAD_REQUEST" });
});
});