@goplusvn/core 0.1.57 → 0.1.58

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,100 @@
1
+ import type { NextRequest } from "next/server";
2
+
3
+ import { safeFileName } from "./keys";
4
+ import { putFile } from "./storage-service";
5
+
6
+ /**
7
+ * Route factory nhận tập tin tải lên.
8
+ *
9
+ * Trả về URL dạng `/api/files/<key>` cho MỌI chế độ kho (S3 hay đĩa local) —
10
+ * cố ý: URL đã lưu trong cột `attachments` không được đổi nghĩa khi admin bật
11
+ * hay tắt S3. Bản cũ ở app ghi thẳng vào `public/uploads` khi chưa có S3, tức
12
+ * chứng từ tài chính và CCCD nằm trong thư mục web đọc công khai không cần
13
+ * phiên; ở đây đĩa local là thư mục PRIVATE và vẫn phải đi qua proxy có quyền.
14
+ *
15
+ * // src/app/api/upload/route.ts
16
+ * export const POST = apiHandler(createUploadHandler())
17
+ */
18
+
19
+ export interface UploadHandlerOptions {
20
+ /** Mặc định 25MB. */
21
+ maxBytes?: number;
22
+ /** Đuôi cho phép (chữ thường, không dấu chấm). */
23
+ allowedExtensions?: string[];
24
+ /** Tiền tố key trong kho. Mặc định `uploads`. */
25
+ prefix?: string;
26
+ /** Tiền tố URL trả về. Mặc định `/api/files`. */
27
+ urlPrefix?: string;
28
+ onError?: (error: unknown, req: NextRequest) => Response | Promise<Response>;
29
+ }
30
+
31
+ const DEFAULT_ALLOWED = [
32
+ "pdf",
33
+ "jpg",
34
+ "jpeg",
35
+ "png",
36
+ "gif",
37
+ "webp",
38
+ "doc",
39
+ "docx",
40
+ "xls",
41
+ "xlsx",
42
+ "csv",
43
+ "txt",
44
+ ];
45
+
46
+ /** Thư mục theo ngày (YYYYMMDD) để một ngày gom về một chỗ. */
47
+ function dateDir(now: Date): string {
48
+ const y = now.getFullYear();
49
+ const m = String(now.getMonth() + 1).padStart(2, "0");
50
+ const d = String(now.getDate()).padStart(2, "0");
51
+ return `${y}${m}${d}`;
52
+ }
53
+
54
+ export function createUploadHandler(options: UploadHandlerOptions = {}) {
55
+ const maxBytes = options.maxBytes ?? 25 * 1024 * 1024;
56
+ const allowed = new Set(options.allowedExtensions ?? DEFAULT_ALLOWED);
57
+ const prefix = (options.prefix ?? "uploads").replace(/^\/|\/$/g, "");
58
+ const urlPrefix = (options.urlPrefix ?? "/api/files").replace(/\/$/, "");
59
+
60
+ return async function handler(req: NextRequest): Promise<Response> {
61
+ try {
62
+ const formData = await req.formData();
63
+ const file = formData.get("file");
64
+ if (!file || typeof file === "string") {
65
+ return Response.json({ error: "No file uploaded" }, { status: 400 });
66
+ }
67
+
68
+ if (file.size > maxBytes) {
69
+ const mb = Math.round(maxBytes / (1024 * 1024));
70
+ return Response.json(
71
+ { error: `Tập tin quá lớn (tối đa ${mb}MB)` },
72
+ { status: 413 },
73
+ );
74
+ }
75
+
76
+ const safeName = safeFileName(file.name || "file");
77
+ const ext = (safeName.split(".").pop() || "").toLowerCase();
78
+ if (!allowed.has(ext)) {
79
+ return Response.json(
80
+ { error: "Định dạng tập tin không được hỗ trợ" },
81
+ { status: 415 },
82
+ );
83
+ }
84
+
85
+ // Chèn timestamp trước phần mở rộng — cùng tên tải lên hai lần không đè nhau.
86
+ const dot = safeName.lastIndexOf(".");
87
+ const base = dot > 0 ? safeName.slice(0, dot) : safeName;
88
+ const key = `${prefix}/${dateDir(new Date())}/${base}_${Date.now()}.${ext}`;
89
+
90
+ const buffer = Buffer.from(await file.arrayBuffer());
91
+ await putFile(key, buffer, file.type || "application/octet-stream");
92
+
93
+ return Response.json({ url: `${urlPrefix}/${key}`, key });
94
+ } catch (error) {
95
+ if (options.onError) return options.onError(error, req);
96
+ console.error("[storage] upload lỗi:", error);
97
+ return Response.json({ error: "Upload failed" }, { status: 500 });
98
+ }
99
+ };
100
+ }