secure-upload-fastify-sdk 1.0.0
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.
- package/README.md +467 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2069 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2023 -0
- package/dist/index.mjs.map +1 -0
- package/dist/server/adapters/secure-crypto.d.ts +28 -0
- package/dist/server/adapters/secure-crypto.d.ts.map +1 -0
- package/dist/server/errors.d.ts +48 -0
- package/dist/server/errors.d.ts.map +1 -0
- package/dist/server/errors.js +112 -0
- package/dist/server/errors.js.map +1 -0
- package/dist/server/errors.mjs +85 -0
- package/dist/server/errors.mjs.map +1 -0
- package/dist/server/fastify-plugin.d.ts +38 -0
- package/dist/server/fastify-plugin.d.ts.map +1 -0
- package/dist/server/index.d.ts +17 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +2070 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/index.mjs +2024 -0
- package/dist/server/index.mjs.map +1 -0
- package/dist/server/routes/_internal.d.ts +42 -0
- package/dist/server/routes/_internal.d.ts.map +1 -0
- package/dist/server/routes/multipart.d.ts +22 -0
- package/dist/server/routes/multipart.d.ts.map +1 -0
- package/dist/server/routes/smart.d.ts +16 -0
- package/dist/server/routes/smart.d.ts.map +1 -0
- package/dist/server/session.d.ts +19 -0
- package/dist/server/session.d.ts.map +1 -0
- package/dist/server/types.d.ts +233 -0
- package/dist/server/types.d.ts.map +1 -0
- package/dist/server/utils/cleaner.d.ts +20 -0
- package/dist/server/utils/cleaner.d.ts.map +1 -0
- package/dist/server/utils/memory-rate-limit.d.ts +29 -0
- package/dist/server/utils/memory-rate-limit.d.ts.map +1 -0
- package/dist/server/utils/orphan-scanner.d.ts +34 -0
- package/dist/server/utils/orphan-scanner.d.ts.map +1 -0
- package/dist/server/utils/redis-helpers.d.ts +25 -0
- package/dist/server/utils/redis-helpers.d.ts.map +1 -0
- package/dist/server/utils/repository/index.d.ts +27 -0
- package/dist/server/utils/repository/index.d.ts.map +1 -0
- package/dist/server/utils/repository/index.js +501 -0
- package/dist/server/utils/repository/index.js.map +1 -0
- package/dist/server/utils/repository/index.mjs +469 -0
- package/dist/server/utils/repository/index.mjs.map +1 -0
- package/dist/server/utils/repository/memory.d.ts +33 -0
- package/dist/server/utils/repository/memory.d.ts.map +1 -0
- package/dist/server/utils/repository/prisma.d.ts +49 -0
- package/dist/server/utils/repository/prisma.d.ts.map +1 -0
- package/dist/server/utils/repository/types-extra.d.ts +9 -0
- package/dist/server/utils/repository/types-extra.d.ts.map +1 -0
- package/dist/server/utils/repository/types.d.ts +128 -0
- package/dist/server/utils/repository/types.d.ts.map +1 -0
- package/dist/server/utils/storage/index.d.ts +115 -0
- package/dist/server/utils/storage/index.d.ts.map +1 -0
- package/dist/server/utils/storage/index.js +285 -0
- package/dist/server/utils/storage/index.js.map +1 -0
- package/dist/server/utils/storage/index.mjs +247 -0
- package/dist/server/utils/storage/index.mjs.map +1 -0
- package/dist/server/utils/storage/local.d.ts +46 -0
- package/dist/server/utils/storage/local.d.ts.map +1 -0
- package/dist/server/utils/storage/memory.d.ts +34 -0
- package/dist/server/utils/storage/memory.d.ts.map +1 -0
- package/dist/server/utils/storage/types.d.ts +78 -0
- package/dist/server/utils/storage/types.d.ts.map +1 -0
- package/dist/server/utils/strategy.d.ts +64 -0
- package/dist/server/utils/strategy.d.ts.map +1 -0
- package/dist/server/utils/strategy.js +130 -0
- package/dist/server/utils/strategy.js.map +1 -0
- package/dist/server/utils/strategy.mjs +103 -0
- package/dist/server/utils/strategy.mjs.map +1 -0
- package/package.json +97 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
// src/server/utils/storage/local.ts
|
|
2
|
+
import { promises as fs, createReadStream, createWriteStream } from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { createHash } from "crypto";
|
|
5
|
+
var LocalStorage = class {
|
|
6
|
+
baseDir;
|
|
7
|
+
constructor(opts) {
|
|
8
|
+
this.baseDir = typeof opts === "string" ? opts : opts.baseDir;
|
|
9
|
+
}
|
|
10
|
+
// ============================================================
|
|
11
|
+
// 内部工具
|
|
12
|
+
// ============================================================
|
|
13
|
+
inferExt(filename, contentType) {
|
|
14
|
+
const fromName = path.extname(filename).toLowerCase();
|
|
15
|
+
if (fromName) return fromName;
|
|
16
|
+
const map = {
|
|
17
|
+
"image/png": ".png",
|
|
18
|
+
"image/jpeg": ".jpg",
|
|
19
|
+
"image/gif": ".gif",
|
|
20
|
+
"image/webp": ".webp",
|
|
21
|
+
"image/svg+xml": ".svg",
|
|
22
|
+
"application/pdf": ".pdf",
|
|
23
|
+
"text/plain": ".txt",
|
|
24
|
+
"application/json": ".json",
|
|
25
|
+
"application/zip": ".zip",
|
|
26
|
+
"video/mp4": ".mp4"
|
|
27
|
+
};
|
|
28
|
+
return map[contentType] ?? ".bin";
|
|
29
|
+
}
|
|
30
|
+
generateKey(filename, contentType, id) {
|
|
31
|
+
const now = /* @__PURE__ */ new Date();
|
|
32
|
+
const yyyy = now.getFullYear();
|
|
33
|
+
const mm = String(now.getMonth() + 1).padStart(2, "0");
|
|
34
|
+
const dd = String(now.getDate()).padStart(2, "0");
|
|
35
|
+
const ext = this.inferExt(filename, contentType);
|
|
36
|
+
return `${yyyy}/${mm}/${dd}/${id}${ext}`;
|
|
37
|
+
}
|
|
38
|
+
multipartDir(uploadId) {
|
|
39
|
+
return path.resolve(this.baseDir, "_multipart", uploadId);
|
|
40
|
+
}
|
|
41
|
+
partPath(uploadId, partNumber) {
|
|
42
|
+
if (!Number.isInteger(partNumber) || partNumber < 1) {
|
|
43
|
+
throw new Error(`[storage] partNumber \u975E\u6CD5: ${partNumber}`);
|
|
44
|
+
}
|
|
45
|
+
return path.join(this.multipartDir(uploadId), `part_${String(partNumber).padStart(4, "0")}.bin`);
|
|
46
|
+
}
|
|
47
|
+
// ============================================================
|
|
48
|
+
// StorageAdapter 实现
|
|
49
|
+
// ============================================================
|
|
50
|
+
async put(data, meta) {
|
|
51
|
+
const key = this.generateKey(meta.filename, meta.contentType, meta.id);
|
|
52
|
+
const absolutePath = path.resolve(this.baseDir, key);
|
|
53
|
+
await fs.mkdir(path.dirname(absolutePath), { recursive: true });
|
|
54
|
+
await fs.writeFile(absolutePath, data);
|
|
55
|
+
return { key, absolutePath };
|
|
56
|
+
}
|
|
57
|
+
get(key) {
|
|
58
|
+
const absolutePath = path.resolve(this.baseDir, key);
|
|
59
|
+
return createReadStream(absolutePath);
|
|
60
|
+
}
|
|
61
|
+
async delete(key) {
|
|
62
|
+
const absolutePath = path.resolve(this.baseDir, key);
|
|
63
|
+
try {
|
|
64
|
+
await fs.unlink(absolutePath);
|
|
65
|
+
} catch (e) {
|
|
66
|
+
if (e.code !== "ENOENT") throw e;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async exists(key) {
|
|
70
|
+
const absolutePath = path.resolve(this.baseDir, key);
|
|
71
|
+
try {
|
|
72
|
+
await fs.access(absolutePath);
|
|
73
|
+
return true;
|
|
74
|
+
} catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async putPart(uploadId, partNumber, data) {
|
|
79
|
+
const dir = this.multipartDir(uploadId);
|
|
80
|
+
await fs.mkdir(dir, { recursive: true });
|
|
81
|
+
const finalPath = this.partPath(uploadId, partNumber);
|
|
82
|
+
const tmpPath = `${finalPath}.tmp`;
|
|
83
|
+
try {
|
|
84
|
+
await fs.writeFile(tmpPath, data);
|
|
85
|
+
await fs.rename(tmpPath, finalPath);
|
|
86
|
+
} catch (e) {
|
|
87
|
+
try {
|
|
88
|
+
await fs.unlink(tmpPath);
|
|
89
|
+
} catch {
|
|
90
|
+
}
|
|
91
|
+
throw e;
|
|
92
|
+
}
|
|
93
|
+
return { size: data.length, path: finalPath };
|
|
94
|
+
}
|
|
95
|
+
async listParts(uploadId) {
|
|
96
|
+
const dir = this.multipartDir(uploadId);
|
|
97
|
+
try {
|
|
98
|
+
const names = await fs.readdir(dir);
|
|
99
|
+
const nums = [];
|
|
100
|
+
for (const n of names) {
|
|
101
|
+
const m = n.match(/^part_(\d{4,})\.bin$/);
|
|
102
|
+
if (m) nums.push(parseInt(m[1], 10));
|
|
103
|
+
}
|
|
104
|
+
nums.sort((a, b) => a - b);
|
|
105
|
+
return nums;
|
|
106
|
+
} catch (e) {
|
|
107
|
+
if (e.code === "ENOENT") return [];
|
|
108
|
+
throw e;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async partSize(uploadId, partNumber) {
|
|
112
|
+
const p = this.partPath(uploadId, partNumber);
|
|
113
|
+
const stat = await fs.stat(p);
|
|
114
|
+
return stat.size;
|
|
115
|
+
}
|
|
116
|
+
async mergeParts(uploadId, totalParts, target) {
|
|
117
|
+
const key = this.generateKey(target.filename, target.contentType, target.id);
|
|
118
|
+
const absolutePath = path.resolve(this.baseDir, key);
|
|
119
|
+
await fs.mkdir(path.dirname(absolutePath), { recursive: true });
|
|
120
|
+
const tmpPath = `${absolutePath}.merge.tmp`;
|
|
121
|
+
const out = createWriteStream(tmpPath);
|
|
122
|
+
try {
|
|
123
|
+
for (let i = 1; i <= totalParts; i++) {
|
|
124
|
+
const partPath = this.partPath(uploadId, i);
|
|
125
|
+
try {
|
|
126
|
+
await fs.access(partPath);
|
|
127
|
+
} catch {
|
|
128
|
+
throw new Error(`[storage] \u7F3A\u5206\u7247 part_${i} (uploadId=${uploadId})`);
|
|
129
|
+
}
|
|
130
|
+
await new Promise((resolve, reject) => {
|
|
131
|
+
const rs = createReadStream(partPath);
|
|
132
|
+
rs.on("error", reject);
|
|
133
|
+
rs.on("end", resolve);
|
|
134
|
+
rs.pipe(out, { end: false });
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
out.end();
|
|
138
|
+
await new Promise((resolve, reject) => {
|
|
139
|
+
out.on("finish", () => resolve());
|
|
140
|
+
out.on("error", reject);
|
|
141
|
+
});
|
|
142
|
+
const stat = await fs.stat(tmpPath);
|
|
143
|
+
await fs.rename(tmpPath, absolutePath);
|
|
144
|
+
return { key, absolutePath, size: stat.size };
|
|
145
|
+
} catch (e) {
|
|
146
|
+
try {
|
|
147
|
+
await fs.unlink(tmpPath);
|
|
148
|
+
} catch {
|
|
149
|
+
}
|
|
150
|
+
throw e;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
async cleanupMultipart(uploadId) {
|
|
154
|
+
const dir = this.multipartDir(uploadId);
|
|
155
|
+
try {
|
|
156
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
157
|
+
} catch (e) {
|
|
158
|
+
if (e.code !== "ENOENT") {
|
|
159
|
+
console.warn(`[storage] cleanup multipart failed: uploadId=${uploadId} err=${e.message}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
async computeMergedHash(uploadId, totalParts) {
|
|
164
|
+
const hash = createHash("sha256");
|
|
165
|
+
for (let i = 1; i <= totalParts; i++) {
|
|
166
|
+
const partPath = this.partPath(uploadId, i);
|
|
167
|
+
const stream = createReadStream(partPath);
|
|
168
|
+
await new Promise((resolve, reject) => {
|
|
169
|
+
stream.on("data", (chunk) => hash.update(chunk));
|
|
170
|
+
stream.on("end", resolve);
|
|
171
|
+
stream.on("error", reject);
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
return hash.digest("hex");
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// src/server/utils/storage/memory.ts
|
|
179
|
+
import { Readable } from "stream";
|
|
180
|
+
import { createHash as createHash2 } from "crypto";
|
|
181
|
+
var InMemoryStorage = class {
|
|
182
|
+
files = /* @__PURE__ */ new Map();
|
|
183
|
+
parts = /* @__PURE__ */ new Map();
|
|
184
|
+
async put(data, meta) {
|
|
185
|
+
const key = `mem/${meta.id}`;
|
|
186
|
+
this.files.set(key, Buffer.from(data));
|
|
187
|
+
return { key };
|
|
188
|
+
}
|
|
189
|
+
get(key) {
|
|
190
|
+
const buf = this.files.get(key);
|
|
191
|
+
if (!buf) throw new Error(`[memory-storage] not found: ${key}`);
|
|
192
|
+
return Readable.from(buf);
|
|
193
|
+
}
|
|
194
|
+
async delete(key) {
|
|
195
|
+
this.files.delete(key);
|
|
196
|
+
}
|
|
197
|
+
async exists(key) {
|
|
198
|
+
return this.files.has(key);
|
|
199
|
+
}
|
|
200
|
+
async putPart(uploadId, partNumber, data) {
|
|
201
|
+
let m = this.parts.get(uploadId);
|
|
202
|
+
if (!m) {
|
|
203
|
+
m = /* @__PURE__ */ new Map();
|
|
204
|
+
this.parts.set(uploadId, m);
|
|
205
|
+
}
|
|
206
|
+
m.set(partNumber, Buffer.from(data));
|
|
207
|
+
return { size: data.length };
|
|
208
|
+
}
|
|
209
|
+
async listParts(uploadId) {
|
|
210
|
+
const m = this.parts.get(uploadId);
|
|
211
|
+
if (!m) return [];
|
|
212
|
+
return [...m.keys()].sort((a, b) => a - b);
|
|
213
|
+
}
|
|
214
|
+
async mergeParts(uploadId, totalParts, target) {
|
|
215
|
+
const m = this.parts.get(uploadId);
|
|
216
|
+
if (!m) throw new Error(`[memory-storage] no parts for ${uploadId}`);
|
|
217
|
+
const parts = [];
|
|
218
|
+
for (let i = 1; i <= totalParts; i++) {
|
|
219
|
+
const p = m.get(i);
|
|
220
|
+
if (!p) throw new Error(`[memory-storage] missing part ${i}`);
|
|
221
|
+
parts.push(p);
|
|
222
|
+
}
|
|
223
|
+
const merged = Buffer.concat(parts);
|
|
224
|
+
const key = `mem/${target.id}`;
|
|
225
|
+
this.files.set(key, merged);
|
|
226
|
+
return { key, size: merged.length };
|
|
227
|
+
}
|
|
228
|
+
async computeMergedHash(uploadId, totalParts) {
|
|
229
|
+
const hash = createHash2("sha256");
|
|
230
|
+
const m = this.parts.get(uploadId);
|
|
231
|
+
if (!m) throw new Error(`[memory-storage] no parts for ${uploadId}`);
|
|
232
|
+
for (let i = 1; i <= totalParts; i++) {
|
|
233
|
+
const p = m.get(i);
|
|
234
|
+
if (!p) throw new Error(`[memory-storage] missing part ${i}`);
|
|
235
|
+
hash.update(p);
|
|
236
|
+
}
|
|
237
|
+
return hash.digest("hex");
|
|
238
|
+
}
|
|
239
|
+
async cleanupMultipart(uploadId) {
|
|
240
|
+
this.parts.delete(uploadId);
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
export {
|
|
244
|
+
InMemoryStorage,
|
|
245
|
+
LocalStorage
|
|
246
|
+
};
|
|
247
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/server/utils/storage/local.ts","../../../../src/server/utils/storage/memory.ts"],"sourcesContent":["/**\r\n * 本地磁盘存储实现(默认)\r\n *\r\n * 目录布局:\r\n * <baseDir>/yyyy/mm/dd/<id>.<ext> <- 合并后的正式文件\r\n * <baseDir>/_multipart/<uploadId>/part_0001.bin\r\n * part_0002.bin\r\n * ...\r\n *\r\n * 兼容性: 实现 StorageAdapter 接口,可被 SDK 接受。\r\n * 后续可写 S3Storage(实现同一接口)直接替换,业务代码零改动。\r\n */\r\n\r\nimport { promises as fs, createReadStream, createWriteStream } from 'node:fs';\r\nimport path from 'node:path';\r\nimport { createHash } from 'node:crypto';\r\nimport type { ReadStream } from 'node:fs';\r\nimport type {\r\n StorageAdapter,\r\n FileMeta,\r\n PutResult,\r\n MergeResult,\r\n} from './types';\r\n\r\nexport interface LocalStorageOptions {\r\n /** 存储根目录(绝对路径或相对 cwd) */\r\n baseDir: string;\r\n}\r\n\r\nexport class LocalStorage implements StorageAdapter {\r\n private baseDir: string;\r\n\r\n constructor(opts: LocalStorageOptions | string) {\r\n this.baseDir = typeof opts === 'string' ? opts : opts.baseDir;\r\n }\r\n\r\n // ============================================================\r\n // 内部工具\r\n // ============================================================\r\n\r\n private inferExt(filename: string, contentType: string): string {\r\n const fromName = path.extname(filename).toLowerCase();\r\n if (fromName) return fromName;\r\n const map: Record<string, string> = {\r\n 'image/png': '.png',\r\n 'image/jpeg': '.jpg',\r\n 'image/gif': '.gif',\r\n 'image/webp': '.webp',\r\n 'image/svg+xml': '.svg',\r\n 'application/pdf': '.pdf',\r\n 'text/plain': '.txt',\r\n 'application/json': '.json',\r\n 'application/zip': '.zip',\r\n 'video/mp4': '.mp4',\r\n };\r\n return map[contentType] ?? '.bin';\r\n }\r\n\r\n private generateKey(filename: string, contentType: string, id: string): string {\r\n const now = new Date();\r\n const yyyy = now.getFullYear();\r\n const mm = String(now.getMonth() + 1).padStart(2, '0');\r\n const dd = String(now.getDate()).padStart(2, '0');\r\n const ext = this.inferExt(filename, contentType);\r\n return `${yyyy}/${mm}/${dd}/${id}${ext}`;\r\n }\r\n\r\n private multipartDir(uploadId: string): string {\r\n return path.resolve(this.baseDir, '_multipart', uploadId);\r\n }\r\n\r\n private partPath(uploadId: string, partNumber: number): string {\r\n if (!Number.isInteger(partNumber) || partNumber < 1) {\r\n throw new Error(`[storage] partNumber 非法: ${partNumber}`);\r\n }\r\n return path.join(this.multipartDir(uploadId), `part_${String(partNumber).padStart(4, '0')}.bin`);\r\n }\r\n\r\n // ============================================================\r\n // StorageAdapter 实现\r\n // ============================================================\r\n\r\n async put(data: Buffer, meta: FileMeta & { id: string }): Promise<PutResult> {\r\n const key = this.generateKey(meta.filename, meta.contentType, meta.id);\r\n const absolutePath = path.resolve(this.baseDir, key);\r\n await fs.mkdir(path.dirname(absolutePath), { recursive: true });\r\n await fs.writeFile(absolutePath, data);\r\n return { key, absolutePath };\r\n }\r\n\r\n get(key: string): ReadStream {\r\n const absolutePath = path.resolve(this.baseDir, key);\r\n return createReadStream(absolutePath);\r\n }\r\n\r\n async delete(key: string): Promise<void> {\r\n const absolutePath = path.resolve(this.baseDir, key);\r\n try {\r\n await fs.unlink(absolutePath);\r\n } catch (e: any) {\r\n if (e.code !== 'ENOENT') throw e;\r\n }\r\n }\r\n\r\n async exists(key: string): Promise<boolean> {\r\n const absolutePath = path.resolve(this.baseDir, key);\r\n try {\r\n await fs.access(absolutePath);\r\n return true;\r\n } catch {\r\n return false;\r\n }\r\n }\r\n\r\n async putPart(\r\n uploadId: string,\r\n partNumber: number,\r\n data: Buffer\r\n ): Promise<{ size: number; path: string }> {\r\n const dir = this.multipartDir(uploadId);\r\n await fs.mkdir(dir, { recursive: true });\r\n const finalPath = this.partPath(uploadId, partNumber);\r\n const tmpPath = `${finalPath}.tmp`;\r\n try {\r\n await fs.writeFile(tmpPath, data);\r\n await fs.rename(tmpPath, finalPath);\r\n } catch (e) {\r\n try { await fs.unlink(tmpPath); } catch { /* ignore */ }\r\n throw e;\r\n }\r\n return { size: data.length, path: finalPath };\r\n }\r\n\r\n async listParts(uploadId: string): Promise<number[]> {\r\n const dir = this.multipartDir(uploadId);\r\n try {\r\n const names = await fs.readdir(dir);\r\n const nums: number[] = [];\r\n for (const n of names) {\r\n const m = n.match(/^part_(\\d{4,})\\.bin$/);\r\n if (m) nums.push(parseInt(m[1], 10));\r\n }\r\n nums.sort((a, b) => a - b);\r\n return nums;\r\n } catch (e: any) {\r\n if (e.code === 'ENOENT') return [];\r\n throw e;\r\n }\r\n }\r\n\r\n async partSize(uploadId: string, partNumber: number): Promise<number> {\r\n const p = this.partPath(uploadId, partNumber);\r\n const stat = await fs.stat(p);\r\n return stat.size;\r\n }\r\n\r\n async mergeParts(\r\n uploadId: string,\r\n totalParts: number,\r\n target: { id: string; filename: string; contentType: string }\r\n ): Promise<MergeResult> {\r\n const key = this.generateKey(target.filename, target.contentType, target.id);\r\n const absolutePath = path.resolve(this.baseDir, key);\r\n await fs.mkdir(path.dirname(absolutePath), { recursive: true });\r\n const tmpPath = `${absolutePath}.merge.tmp`;\r\n\r\n const out = createWriteStream(tmpPath);\r\n try {\r\n for (let i = 1; i <= totalParts; i++) {\r\n const partPath = this.partPath(uploadId, i);\r\n try {\r\n await fs.access(partPath);\r\n } catch {\r\n throw new Error(`[storage] 缺分片 part_${i} (uploadId=${uploadId})`);\r\n }\r\n await new Promise<void>((resolve, reject) => {\r\n const rs = createReadStream(partPath);\r\n rs.on('error', reject);\r\n rs.on('end', resolve);\r\n rs.pipe(out, { end: false });\r\n });\r\n }\r\n out.end();\r\n await new Promise<void>((resolve, reject) => {\r\n out.on('finish', () => resolve());\r\n out.on('error', reject);\r\n });\r\n const stat = await fs.stat(tmpPath);\r\n await fs.rename(tmpPath, absolutePath);\r\n return { key, absolutePath, size: stat.size };\r\n } catch (e) {\r\n try { await fs.unlink(tmpPath); } catch { /* ignore */ }\r\n throw e;\r\n }\r\n }\r\n\r\n async cleanupMultipart(uploadId: string): Promise<void> {\r\n const dir = this.multipartDir(uploadId);\r\n try {\r\n await fs.rm(dir, { recursive: true, force: true });\r\n } catch (e: any) {\r\n if (e.code !== 'ENOENT') {\r\n console.warn(`[storage] cleanup multipart failed: uploadId=${uploadId} err=${e.message}`);\r\n }\r\n }\r\n }\r\n\r\n async computeMergedHash(uploadId: string, totalParts: number): Promise<string> {\r\n const hash = createHash('sha256');\r\n for (let i = 1; i <= totalParts; i++) {\r\n const partPath = this.partPath(uploadId, i);\r\n const stream = createReadStream(partPath);\r\n await new Promise<void>((resolve, reject) => {\r\n stream.on('data', (chunk) => hash.update(chunk as Buffer));\r\n stream.on('end', resolve);\r\n stream.on('error', reject);\r\n });\r\n }\r\n return hash.digest('hex');\r\n }\r\n}\r\n","/**\r\n * 内存存储实现(测试 / POC 场景)\r\n *\r\n * 用 Map 模拟:\r\n * - files: key -> Buffer (正式文件)\r\n * - parts: uploadId -> Map<partNumber, Buffer> (分片)\r\n * - partSizes: uploadId -> Map<partNumber, number>\r\n *\r\n * 注意: 内存存储在重启时全部丢失,只用于测试。\r\n * 生产请使用 LocalStorage / S3Storage / OSSStorage。\r\n */\r\n\r\nimport { Readable } from 'node:stream';\r\nimport { createHash } from 'node:crypto';\r\nimport type {\r\n StorageAdapter,\r\n FileMeta,\r\n PutResult,\r\n MergeResult,\r\n} from './types';\r\n\r\nexport class InMemoryStorage implements StorageAdapter {\r\n private files = new Map<string, Buffer>();\r\n private parts = new Map<string, Map<number, Buffer>>();\r\n\r\n async put(data: Buffer, meta: FileMeta & { id: string }): Promise<PutResult> {\r\n const key = `mem/${meta.id}`;\r\n this.files.set(key, Buffer.from(data));\r\n return { key };\r\n }\r\n\r\n get(key: string): NodeJS.ReadableStream {\r\n const buf = this.files.get(key);\r\n if (!buf) throw new Error(`[memory-storage] not found: ${key}`);\r\n return Readable.from(buf);\r\n }\r\n\r\n async delete(key: string): Promise<void> {\r\n this.files.delete(key);\r\n }\r\n\r\n async exists(key: string): Promise<boolean> {\r\n return this.files.has(key);\r\n }\r\n\r\n async putPart(\r\n uploadId: string,\r\n partNumber: number,\r\n data: Buffer\r\n ): Promise<{ size: number }> {\r\n let m = this.parts.get(uploadId);\r\n if (!m) {\r\n m = new Map();\r\n this.parts.set(uploadId, m);\r\n }\r\n m.set(partNumber, Buffer.from(data));\r\n return { size: data.length };\r\n }\r\n\r\n async listParts(uploadId: string): Promise<number[]> {\r\n const m = this.parts.get(uploadId);\r\n if (!m) return [];\r\n return [...m.keys()].sort((a, b) => a - b);\r\n }\r\n\r\n async mergeParts(\r\n uploadId: string,\r\n totalParts: number,\r\n target: { id: string; filename: string; contentType: string }\r\n ): Promise<MergeResult> {\r\n const m = this.parts.get(uploadId);\r\n if (!m) throw new Error(`[memory-storage] no parts for ${uploadId}`);\r\n const parts: Buffer[] = [];\r\n for (let i = 1; i <= totalParts; i++) {\r\n const p = m.get(i);\r\n if (!p) throw new Error(`[memory-storage] missing part ${i}`);\r\n parts.push(p);\r\n }\r\n const merged = Buffer.concat(parts);\r\n const key = `mem/${target.id}`;\r\n this.files.set(key, merged);\r\n return { key, size: merged.length };\r\n }\r\n\r\n async computeMergedHash(uploadId: string, totalParts: number): Promise<string> {\r\n const hash = createHash('sha256');\r\n const m = this.parts.get(uploadId);\r\n if (!m) throw new Error(`[memory-storage] no parts for ${uploadId}`);\r\n for (let i = 1; i <= totalParts; i++) {\r\n const p = m.get(i);\r\n if (!p) throw new Error(`[memory-storage] missing part ${i}`);\r\n hash.update(p);\r\n }\r\n return hash.digest('hex');\r\n }\r\n\r\n async cleanupMultipart(uploadId: string): Promise<void> {\r\n this.parts.delete(uploadId);\r\n }\r\n}\r\n"],"mappings":";AAaA,SAAS,YAAY,IAAI,kBAAkB,yBAAyB;AACpE,OAAO,UAAU;AACjB,SAAS,kBAAkB;AAcpB,IAAM,eAAN,MAA6C;AAAA,EAC1C;AAAA,EAER,YAAY,MAAoC;AAC9C,SAAK,UAAU,OAAO,SAAS,WAAW,OAAO,KAAK;AAAA,EACxD;AAAA;AAAA;AAAA;AAAA,EAMQ,SAAS,UAAkB,aAA6B;AAC9D,UAAM,WAAW,KAAK,QAAQ,QAAQ,EAAE,YAAY;AACpD,QAAI,SAAU,QAAO;AACrB,UAAM,MAA8B;AAAA,MAClC,aAAa;AAAA,MACb,cAAc;AAAA,MACd,aAAa;AAAA,MACb,cAAc;AAAA,MACd,iBAAiB;AAAA,MACjB,mBAAmB;AAAA,MACnB,cAAc;AAAA,MACd,oBAAoB;AAAA,MACpB,mBAAmB;AAAA,MACnB,aAAa;AAAA,IACf;AACA,WAAO,IAAI,WAAW,KAAK;AAAA,EAC7B;AAAA,EAEQ,YAAY,UAAkB,aAAqB,IAAoB;AAC7E,UAAM,MAAM,oBAAI,KAAK;AACrB,UAAM,OAAO,IAAI,YAAY;AAC7B,UAAM,KAAK,OAAO,IAAI,SAAS,IAAI,CAAC,EAAE,SAAS,GAAG,GAAG;AACrD,UAAM,KAAK,OAAO,IAAI,QAAQ,CAAC,EAAE,SAAS,GAAG,GAAG;AAChD,UAAM,MAAM,KAAK,SAAS,UAAU,WAAW;AAC/C,WAAO,GAAG,IAAI,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,GAAG;AAAA,EACxC;AAAA,EAEQ,aAAa,UAA0B;AAC7C,WAAO,KAAK,QAAQ,KAAK,SAAS,cAAc,QAAQ;AAAA,EAC1D;AAAA,EAEQ,SAAS,UAAkB,YAA4B;AAC7D,QAAI,CAAC,OAAO,UAAU,UAAU,KAAK,aAAa,GAAG;AACnD,YAAM,IAAI,MAAM,sCAA4B,UAAU,EAAE;AAAA,IAC1D;AACA,WAAO,KAAK,KAAK,KAAK,aAAa,QAAQ,GAAG,QAAQ,OAAO,UAAU,EAAE,SAAS,GAAG,GAAG,CAAC,MAAM;AAAA,EACjG;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,IAAI,MAAc,MAAqD;AAC3E,UAAM,MAAM,KAAK,YAAY,KAAK,UAAU,KAAK,aAAa,KAAK,EAAE;AACrE,UAAM,eAAe,KAAK,QAAQ,KAAK,SAAS,GAAG;AACnD,UAAM,GAAG,MAAM,KAAK,QAAQ,YAAY,GAAG,EAAE,WAAW,KAAK,CAAC;AAC9D,UAAM,GAAG,UAAU,cAAc,IAAI;AACrC,WAAO,EAAE,KAAK,aAAa;AAAA,EAC7B;AAAA,EAEA,IAAI,KAAyB;AAC3B,UAAM,eAAe,KAAK,QAAQ,KAAK,SAAS,GAAG;AACnD,WAAO,iBAAiB,YAAY;AAAA,EACtC;AAAA,EAEA,MAAM,OAAO,KAA4B;AACvC,UAAM,eAAe,KAAK,QAAQ,KAAK,SAAS,GAAG;AACnD,QAAI;AACF,YAAM,GAAG,OAAO,YAAY;AAAA,IAC9B,SAAS,GAAQ;AACf,UAAI,EAAE,SAAS,SAAU,OAAM;AAAA,IACjC;AAAA,EACF;AAAA,EAEA,MAAM,OAAO,KAA+B;AAC1C,UAAM,eAAe,KAAK,QAAQ,KAAK,SAAS,GAAG;AACnD,QAAI;AACF,YAAM,GAAG,OAAO,YAAY;AAC5B,aAAO;AAAA,IACT,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA,EAEA,MAAM,QACJ,UACA,YACA,MACyC;AACzC,UAAM,MAAM,KAAK,aAAa,QAAQ;AACtC,UAAM,GAAG,MAAM,KAAK,EAAE,WAAW,KAAK,CAAC;AACvC,UAAM,YAAY,KAAK,SAAS,UAAU,UAAU;AACpD,UAAM,UAAU,GAAG,SAAS;AAC5B,QAAI;AACF,YAAM,GAAG,UAAU,SAAS,IAAI;AAChC,YAAM,GAAG,OAAO,SAAS,SAAS;AAAA,IACpC,SAAS,GAAG;AACV,UAAI;AAAE,cAAM,GAAG,OAAO,OAAO;AAAA,MAAG,QAAQ;AAAA,MAAe;AACvD,YAAM;AAAA,IACR;AACA,WAAO,EAAE,MAAM,KAAK,QAAQ,MAAM,UAAU;AAAA,EAC9C;AAAA,EAEA,MAAM,UAAU,UAAqC;AACnD,UAAM,MAAM,KAAK,aAAa,QAAQ;AACtC,QAAI;AACF,YAAM,QAAQ,MAAM,GAAG,QAAQ,GAAG;AAClC,YAAM,OAAiB,CAAC;AACxB,iBAAW,KAAK,OAAO;AACrB,cAAM,IAAI,EAAE,MAAM,sBAAsB;AACxC,YAAI,EAAG,MAAK,KAAK,SAAS,EAAE,CAAC,GAAG,EAAE,CAAC;AAAA,MACrC;AACA,WAAK,KAAK,CAAC,GAAG,MAAM,IAAI,CAAC;AACzB,aAAO;AAAA,IACT,SAAS,GAAQ;AACf,UAAI,EAAE,SAAS,SAAU,QAAO,CAAC;AACjC,YAAM;AAAA,IACR;AAAA,EACF;AAAA,EAEA,MAAM,SAAS,UAAkB,YAAqC;AACpE,UAAM,IAAI,KAAK,SAAS,UAAU,UAAU;AAC5C,UAAM,OAAO,MAAM,GAAG,KAAK,CAAC;AAC5B,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,MAAM,WACJ,UACA,YACA,QACsB;AACtB,UAAM,MAAM,KAAK,YAAY,OAAO,UAAU,OAAO,aAAa,OAAO,EAAE;AAC3E,UAAM,eAAe,KAAK,QAAQ,KAAK,SAAS,GAAG;AACnD,UAAM,GAAG,MAAM,KAAK,QAAQ,YAAY,GAAG,EAAE,WAAW,KAAK,CAAC;AAC9D,UAAM,UAAU,GAAG,YAAY;AAE/B,UAAM,MAAM,kBAAkB,OAAO;AACrC,QAAI;AACF,eAAS,IAAI,GAAG,KAAK,YAAY,KAAK;AACpC,cAAM,WAAW,KAAK,SAAS,UAAU,CAAC;AAC1C,YAAI;AACF,gBAAM,GAAG,OAAO,QAAQ;AAAA,QAC1B,QAAQ;AACN,gBAAM,IAAI,MAAM,qCAAsB,CAAC,cAAc,QAAQ,GAAG;AAAA,QAClE;AACA,cAAM,IAAI,QAAc,CAAC,SAAS,WAAW;AAC3C,gBAAM,KAAK,iBAAiB,QAAQ;AACpC,aAAG,GAAG,SAAS,MAAM;AACrB,aAAG,GAAG,OAAO,OAAO;AACpB,aAAG,KAAK,KAAK,EAAE,KAAK,MAAM,CAAC;AAAA,QAC7B,CAAC;AAAA,MACH;AACA,UAAI,IAAI;AACR,YAAM,IAAI,QAAc,CAAC,SAAS,WAAW;AAC3C,YAAI,GAAG,UAAU,MAAM,QAAQ,CAAC;AAChC,YAAI,GAAG,SAAS,MAAM;AAAA,MACxB,CAAC;AACD,YAAM,OAAO,MAAM,GAAG,KAAK,OAAO;AAClC,YAAM,GAAG,OAAO,SAAS,YAAY;AACrC,aAAO,EAAE,KAAK,cAAc,MAAM,KAAK,KAAK;AAAA,IAC9C,SAAS,GAAG;AACV,UAAI;AAAE,cAAM,GAAG,OAAO,OAAO;AAAA,MAAG,QAAQ;AAAA,MAAe;AACvD,YAAM;AAAA,IACR;AAAA,EACF;AAAA,EAEA,MAAM,iBAAiB,UAAiC;AACtD,UAAM,MAAM,KAAK,aAAa,QAAQ;AACtC,QAAI;AACF,YAAM,GAAG,GAAG,KAAK,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAAA,IACnD,SAAS,GAAQ;AACf,UAAI,EAAE,SAAS,UAAU;AACvB,gBAAQ,KAAK,gDAAgD,QAAQ,QAAQ,EAAE,OAAO,EAAE;AAAA,MAC1F;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,kBAAkB,UAAkB,YAAqC;AAC7E,UAAM,OAAO,WAAW,QAAQ;AAChC,aAAS,IAAI,GAAG,KAAK,YAAY,KAAK;AACpC,YAAM,WAAW,KAAK,SAAS,UAAU,CAAC;AAC1C,YAAM,SAAS,iBAAiB,QAAQ;AACxC,YAAM,IAAI,QAAc,CAAC,SAAS,WAAW;AAC3C,eAAO,GAAG,QAAQ,CAAC,UAAU,KAAK,OAAO,KAAe,CAAC;AACzD,eAAO,GAAG,OAAO,OAAO;AACxB,eAAO,GAAG,SAAS,MAAM;AAAA,MAC3B,CAAC;AAAA,IACH;AACA,WAAO,KAAK,OAAO,KAAK;AAAA,EAC1B;AACF;;;AChNA,SAAS,gBAAgB;AACzB,SAAS,cAAAA,mBAAkB;AAQpB,IAAM,kBAAN,MAAgD;AAAA,EAC7C,QAAQ,oBAAI,IAAoB;AAAA,EAChC,QAAQ,oBAAI,IAAiC;AAAA,EAErD,MAAM,IAAI,MAAc,MAAqD;AAC3E,UAAM,MAAM,OAAO,KAAK,EAAE;AAC1B,SAAK,MAAM,IAAI,KAAK,OAAO,KAAK,IAAI,CAAC;AACrC,WAAO,EAAE,IAAI;AAAA,EACf;AAAA,EAEA,IAAI,KAAoC;AACtC,UAAM,MAAM,KAAK,MAAM,IAAI,GAAG;AAC9B,QAAI,CAAC,IAAK,OAAM,IAAI,MAAM,+BAA+B,GAAG,EAAE;AAC9D,WAAO,SAAS,KAAK,GAAG;AAAA,EAC1B;AAAA,EAEA,MAAM,OAAO,KAA4B;AACvC,SAAK,MAAM,OAAO,GAAG;AAAA,EACvB;AAAA,EAEA,MAAM,OAAO,KAA+B;AAC1C,WAAO,KAAK,MAAM,IAAI,GAAG;AAAA,EAC3B;AAAA,EAEA,MAAM,QACJ,UACA,YACA,MAC2B;AAC3B,QAAI,IAAI,KAAK,MAAM,IAAI,QAAQ;AAC/B,QAAI,CAAC,GAAG;AACN,UAAI,oBAAI,IAAI;AACZ,WAAK,MAAM,IAAI,UAAU,CAAC;AAAA,IAC5B;AACA,MAAE,IAAI,YAAY,OAAO,KAAK,IAAI,CAAC;AACnC,WAAO,EAAE,MAAM,KAAK,OAAO;AAAA,EAC7B;AAAA,EAEA,MAAM,UAAU,UAAqC;AACnD,UAAM,IAAI,KAAK,MAAM,IAAI,QAAQ;AACjC,QAAI,CAAC,EAAG,QAAO,CAAC;AAChB,WAAO,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,KAAK,CAAC,GAAG,MAAM,IAAI,CAAC;AAAA,EAC3C;AAAA,EAEA,MAAM,WACJ,UACA,YACA,QACsB;AACtB,UAAM,IAAI,KAAK,MAAM,IAAI,QAAQ;AACjC,QAAI,CAAC,EAAG,OAAM,IAAI,MAAM,iCAAiC,QAAQ,EAAE;AACnE,UAAM,QAAkB,CAAC;AACzB,aAAS,IAAI,GAAG,KAAK,YAAY,KAAK;AACpC,YAAM,IAAI,EAAE,IAAI,CAAC;AACjB,UAAI,CAAC,EAAG,OAAM,IAAI,MAAM,iCAAiC,CAAC,EAAE;AAC5D,YAAM,KAAK,CAAC;AAAA,IACd;AACA,UAAM,SAAS,OAAO,OAAO,KAAK;AAClC,UAAM,MAAM,OAAO,OAAO,EAAE;AAC5B,SAAK,MAAM,IAAI,KAAK,MAAM;AAC1B,WAAO,EAAE,KAAK,MAAM,OAAO,OAAO;AAAA,EACpC;AAAA,EAEA,MAAM,kBAAkB,UAAkB,YAAqC;AAC7E,UAAM,OAAOA,YAAW,QAAQ;AAChC,UAAM,IAAI,KAAK,MAAM,IAAI,QAAQ;AACjC,QAAI,CAAC,EAAG,OAAM,IAAI,MAAM,iCAAiC,QAAQ,EAAE;AACnE,aAAS,IAAI,GAAG,KAAK,YAAY,KAAK;AACpC,YAAM,IAAI,EAAE,IAAI,CAAC;AACjB,UAAI,CAAC,EAAG,OAAM,IAAI,MAAM,iCAAiC,CAAC,EAAE;AAC5D,WAAK,OAAO,CAAC;AAAA,IACf;AACA,WAAO,KAAK,OAAO,KAAK;AAAA,EAC1B;AAAA,EAEA,MAAM,iBAAiB,UAAiC;AACtD,SAAK,MAAM,OAAO,QAAQ;AAAA,EAC5B;AACF;","names":["createHash"]}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 本地磁盘存储实现(默认)
|
|
3
|
+
*
|
|
4
|
+
* 目录布局:
|
|
5
|
+
* <baseDir>/yyyy/mm/dd/<id>.<ext> <- 合并后的正式文件
|
|
6
|
+
* <baseDir>/_multipart/<uploadId>/part_0001.bin
|
|
7
|
+
* part_0002.bin
|
|
8
|
+
* ...
|
|
9
|
+
*
|
|
10
|
+
* 兼容性: 实现 StorageAdapter 接口,可被 SDK 接受。
|
|
11
|
+
* 后续可写 S3Storage(实现同一接口)直接替换,业务代码零改动。
|
|
12
|
+
*/
|
|
13
|
+
import type { ReadStream } from 'node:fs';
|
|
14
|
+
import type { StorageAdapter, FileMeta, PutResult, MergeResult } from './types';
|
|
15
|
+
export interface LocalStorageOptions {
|
|
16
|
+
/** 存储根目录(绝对路径或相对 cwd) */
|
|
17
|
+
baseDir: string;
|
|
18
|
+
}
|
|
19
|
+
export declare class LocalStorage implements StorageAdapter {
|
|
20
|
+
private baseDir;
|
|
21
|
+
constructor(opts: LocalStorageOptions | string);
|
|
22
|
+
private inferExt;
|
|
23
|
+
private generateKey;
|
|
24
|
+
private multipartDir;
|
|
25
|
+
private partPath;
|
|
26
|
+
put(data: Buffer, meta: FileMeta & {
|
|
27
|
+
id: string;
|
|
28
|
+
}): Promise<PutResult>;
|
|
29
|
+
get(key: string): ReadStream;
|
|
30
|
+
delete(key: string): Promise<void>;
|
|
31
|
+
exists(key: string): Promise<boolean>;
|
|
32
|
+
putPart(uploadId: string, partNumber: number, data: Buffer): Promise<{
|
|
33
|
+
size: number;
|
|
34
|
+
path: string;
|
|
35
|
+
}>;
|
|
36
|
+
listParts(uploadId: string): Promise<number[]>;
|
|
37
|
+
partSize(uploadId: string, partNumber: number): Promise<number>;
|
|
38
|
+
mergeParts(uploadId: string, totalParts: number, target: {
|
|
39
|
+
id: string;
|
|
40
|
+
filename: string;
|
|
41
|
+
contentType: string;
|
|
42
|
+
}): Promise<MergeResult>;
|
|
43
|
+
cleanupMultipart(uploadId: string): Promise<void>;
|
|
44
|
+
computeMergedHash(uploadId: string, totalParts: number): Promise<string>;
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=local.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local.d.ts","sourceRoot":"","sources":["../../../../src/server/utils/storage/local.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAC1C,OAAO,KAAK,EACV,cAAc,EACd,QAAQ,EACR,SAAS,EACT,WAAW,EACZ,MAAM,SAAS,CAAC;AAEjB,MAAM,WAAW,mBAAmB;IAClC,yBAAyB;IACzB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,qBAAa,YAAa,YAAW,cAAc;IACjD,OAAO,CAAC,OAAO,CAAS;gBAEZ,IAAI,EAAE,mBAAmB,GAAG,MAAM;IAQ9C,OAAO,CAAC,QAAQ;IAkBhB,OAAO,CAAC,WAAW;IASnB,OAAO,CAAC,YAAY;IAIpB,OAAO,CAAC,QAAQ;IAWV,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,GAAG;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,SAAS,CAAC;IAQ5E,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU;IAKtB,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IASlC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAUrC,OAAO,CACX,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAepC,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAiB9C,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAM/D,UAAU,CACd,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,GAC5D,OAAO,CAAC,WAAW,CAAC;IAoCjB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAWjD,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CAa/E"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 内存存储实现(测试 / POC 场景)
|
|
3
|
+
*
|
|
4
|
+
* 用 Map 模拟:
|
|
5
|
+
* - files: key -> Buffer (正式文件)
|
|
6
|
+
* - parts: uploadId -> Map<partNumber, Buffer> (分片)
|
|
7
|
+
* - partSizes: uploadId -> Map<partNumber, number>
|
|
8
|
+
*
|
|
9
|
+
* 注意: 内存存储在重启时全部丢失,只用于测试。
|
|
10
|
+
* 生产请使用 LocalStorage / S3Storage / OSSStorage。
|
|
11
|
+
*/
|
|
12
|
+
import type { StorageAdapter, FileMeta, PutResult, MergeResult } from './types';
|
|
13
|
+
export declare class InMemoryStorage implements StorageAdapter {
|
|
14
|
+
private files;
|
|
15
|
+
private parts;
|
|
16
|
+
put(data: Buffer, meta: FileMeta & {
|
|
17
|
+
id: string;
|
|
18
|
+
}): Promise<PutResult>;
|
|
19
|
+
get(key: string): NodeJS.ReadableStream;
|
|
20
|
+
delete(key: string): Promise<void>;
|
|
21
|
+
exists(key: string): Promise<boolean>;
|
|
22
|
+
putPart(uploadId: string, partNumber: number, data: Buffer): Promise<{
|
|
23
|
+
size: number;
|
|
24
|
+
}>;
|
|
25
|
+
listParts(uploadId: string): Promise<number[]>;
|
|
26
|
+
mergeParts(uploadId: string, totalParts: number, target: {
|
|
27
|
+
id: string;
|
|
28
|
+
filename: string;
|
|
29
|
+
contentType: string;
|
|
30
|
+
}): Promise<MergeResult>;
|
|
31
|
+
computeMergedHash(uploadId: string, totalParts: number): Promise<string>;
|
|
32
|
+
cleanupMultipart(uploadId: string): Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=memory.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../../../../src/server/utils/storage/memory.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,OAAO,KAAK,EACV,cAAc,EACd,QAAQ,EACR,SAAS,EACT,WAAW,EACZ,MAAM,SAAS,CAAC;AAEjB,qBAAa,eAAgB,YAAW,cAAc;IACpD,OAAO,CAAC,KAAK,CAA6B;IAC1C,OAAO,CAAC,KAAK,CAA0C;IAEjD,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,GAAG;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,SAAS,CAAC;IAM5E,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC,cAAc;IAMjC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIlC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAIrC,OAAO,CACX,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAUtB,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAM9C,UAAU,CACd,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,GAC5D,OAAO,CAAC,WAAW,CAAC;IAejB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAYxE,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAGxD"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 存储适配器接口
|
|
3
|
+
*
|
|
4
|
+
* 任何实现这个接口的对象都可以被 SDK 接受:
|
|
5
|
+
* - 内置: LocalStorage
|
|
6
|
+
* - 用户实现: S3Storage / AliyunOSSStorage / TencentCOSStorage / 内存 / 任何外部存储
|
|
7
|
+
*
|
|
8
|
+
* 设计目标: 不与具体存储服务耦合,业务层只依赖 StorageAdapter 接口
|
|
9
|
+
*/
|
|
10
|
+
import type { ReadStream } from 'node:fs';
|
|
11
|
+
export interface FileMeta {
|
|
12
|
+
filename: string;
|
|
13
|
+
contentType: string;
|
|
14
|
+
size: number;
|
|
15
|
+
isPublic: boolean;
|
|
16
|
+
userId?: number;
|
|
17
|
+
}
|
|
18
|
+
export interface PutResult {
|
|
19
|
+
key: string;
|
|
20
|
+
/** 本地存储的绝对路径(S3/OSS 这类云存储没有,可以是 null) */
|
|
21
|
+
absolutePath?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface MergeResult {
|
|
24
|
+
key: string;
|
|
25
|
+
absolutePath?: string;
|
|
26
|
+
size: number;
|
|
27
|
+
}
|
|
28
|
+
export interface StorageAdapter {
|
|
29
|
+
/** 写入完整文件(单文件直传场景,MVP 保留) */
|
|
30
|
+
put(data: Buffer, meta: FileMeta & {
|
|
31
|
+
id: string;
|
|
32
|
+
}): Promise<PutResult>;
|
|
33
|
+
/** 读取文件流(用于下载 / 代理) */
|
|
34
|
+
get(key: string): ReadStream | NodeJS.ReadableStream;
|
|
35
|
+
/** 删除文件(用于秒传孤儿清理 / abort) */
|
|
36
|
+
delete(key: string): Promise<void>;
|
|
37
|
+
/** 校验 key 在存储上是否存在(用于秒传二次校验) */
|
|
38
|
+
exists(key: string): Promise<boolean>;
|
|
39
|
+
/**
|
|
40
|
+
* 写入单个分片(原子)
|
|
41
|
+
* - 写入失败自动清理临时文件
|
|
42
|
+
* - 返回该分片写入后的实际字节数
|
|
43
|
+
*/
|
|
44
|
+
putPart(uploadId: string, partNumber: number, data: Buffer): Promise<{
|
|
45
|
+
size: number;
|
|
46
|
+
path?: string;
|
|
47
|
+
}>;
|
|
48
|
+
/**
|
|
49
|
+
* 列出已收到的 partNumber(数字升序)
|
|
50
|
+
* - 用于断点续传
|
|
51
|
+
* - 目录不存在时返回空数组
|
|
52
|
+
*/
|
|
53
|
+
listParts(uploadId: string): Promise<number[]>;
|
|
54
|
+
/**
|
|
55
|
+
* 按 partNumber 顺序合并所有分片到目标 key
|
|
56
|
+
* - 必须按 1 → 2 → ... → totalParts 顺序拼接
|
|
57
|
+
* - 失败抛错,需要上层 catch + cleanup
|
|
58
|
+
* - 临时文件路径可以由实现自定义
|
|
59
|
+
*/
|
|
60
|
+
mergeParts(uploadId: string, totalParts: number, target: {
|
|
61
|
+
id: string;
|
|
62
|
+
filename: string;
|
|
63
|
+
contentType: string;
|
|
64
|
+
}): Promise<MergeResult>;
|
|
65
|
+
/**
|
|
66
|
+
* 计算整个 uploadId 下所有分片合并后的 SHA-256(hex)
|
|
67
|
+
* - 用于 complete 阶段最终 hash 校验
|
|
68
|
+
* - 流式计算,不一次性加载整个文件
|
|
69
|
+
*/
|
|
70
|
+
computeMergedHash(uploadId: string, totalParts: number): Promise<string>;
|
|
71
|
+
/**
|
|
72
|
+
* 清理整个 uploadId 的临时分片目录
|
|
73
|
+
* - 用于:用户主动 abort、TTL 到期、合并成功后
|
|
74
|
+
* - 幂等:目录/对象不存在不抛错
|
|
75
|
+
*/
|
|
76
|
+
cleanupMultipart(uploadId: string): Promise<void>;
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/server/utils/storage/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAE1C,MAAM,WAAW,QAAQ;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,yCAAyC;IACzC,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,cAAc;IAK7B,6BAA6B;IAC7B,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,GAAG;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;IAEvE,uBAAuB;IACvB,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,GAAG,MAAM,CAAC,cAAc,CAAC;IAErD,6BAA6B;IAC7B,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEnC,gCAAgC;IAChC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAMtC;;;;OAIG;IACH,OAAO,CACL,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAE5C;;;;OAIG;IACH,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAE/C;;;;;OAKG;IACH,UAAU,CACR,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,GAC5D,OAAO,CAAC,WAAW,CAAC,CAAC;IAExB;;;;OAIG;IACH,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAEzE;;;;OAIG;IACH,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnD"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smart 上传策略选择器
|
|
3
|
+
*
|
|
4
|
+
* 服务端根据 totalSize 自动选择 ONE_SHOT 或 MULTIPART:
|
|
5
|
+
* - ONE_SHOT: 1 个 part(适合 ≤ ONE_SHOT_MAX 的小文件)
|
|
6
|
+
* 客户端只调 1 次 part + 1 次 complete = 2 RTT
|
|
7
|
+
* - MULTIPART: N 个 part,根据文件大小动态调整 partSize:
|
|
8
|
+
* ≤ 1GB → 1MB / part
|
|
9
|
+
* 1 ~ 10GB → 5MB / part
|
|
10
|
+
* > 10GB → 10MB / part
|
|
11
|
+
*
|
|
12
|
+
* 设计约束
|
|
13
|
+
* ========
|
|
14
|
+
* - 纯函数:无副作用、不读 IO、不访问全局状态,易于单测
|
|
15
|
+
* - 0 字节文件合法(partSize=0, totalParts=1)
|
|
16
|
+
* - 决策结果携带 reason 字段,便于日志/排障
|
|
17
|
+
* - 不与 config 直接耦合:调用方传入 cfg
|
|
18
|
+
*/
|
|
19
|
+
export type StrategyKind = 'ONE_SHOT' | 'MULTIPART';
|
|
20
|
+
export interface StrategyConfig {
|
|
21
|
+
/** ≤ 此字节数走 ONE_SHOT(默认 1MB) */
|
|
22
|
+
oneShotMax: number;
|
|
23
|
+
/** 默认分片大小(默认 1MB) */
|
|
24
|
+
defaultPartSize: number;
|
|
25
|
+
/** 超过此字节数切到大分片(默认 1GB) */
|
|
26
|
+
bigFileThreshold: number;
|
|
27
|
+
/** 大文件分片大小(默认 5MB) */
|
|
28
|
+
bigFilePartSize: number;
|
|
29
|
+
/** 超过此字节数切到超大分片(默认 10GB) */
|
|
30
|
+
hugeFileThreshold: number;
|
|
31
|
+
/** 超大文件分片大小(默认 10MB) */
|
|
32
|
+
hugeFilePartSize: number;
|
|
33
|
+
}
|
|
34
|
+
export interface StrategyResult {
|
|
35
|
+
kind: StrategyKind;
|
|
36
|
+
/** 实际分片大小(字节) */
|
|
37
|
+
partSize: number;
|
|
38
|
+
/** 总分片数(ONE_SHOT 固定为 1) */
|
|
39
|
+
totalParts: number;
|
|
40
|
+
/** 决策原因(便于日志/调试) */
|
|
41
|
+
reason: string;
|
|
42
|
+
}
|
|
43
|
+
/** 默认策略配置 */
|
|
44
|
+
export declare const DEFAULT_STRATEGY_CONFIG: StrategyConfig;
|
|
45
|
+
/**
|
|
46
|
+
* 根据 totalSize 选出上传策略
|
|
47
|
+
*
|
|
48
|
+
* @throws totalSize 非法(负数 / NaN / 非有限数)
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* chooseStrategy(1024) // → ONE_SHOT 1 part, partSize=1024
|
|
52
|
+
* chooseStrategy(5 * 1024 * 1024) // → MULTIPART 5 part, partSize=1MB
|
|
53
|
+
* chooseStrategy(5 * 1024**3) // → MULTIPART 1024 part, partSize=5MB
|
|
54
|
+
* chooseStrategy(50 * 1024**3) // → MULTIPART 5120 part, partSize=10MB
|
|
55
|
+
*/
|
|
56
|
+
export declare function chooseStrategy(totalSize: number, cfg?: StrategyConfig): StrategyResult;
|
|
57
|
+
/**
|
|
58
|
+
* 建议的客户端并发上传 part 数
|
|
59
|
+
* - ONE_SHOT:固定 1(只传 1 个 part,无需并发)
|
|
60
|
+
* - totalParts ≤ 4:用 totalParts(part 太少,无并发必要)
|
|
61
|
+
* - totalParts > 4:用 4(服务端限流/带宽考虑)
|
|
62
|
+
*/
|
|
63
|
+
export declare function suggestConcurrency(s: StrategyResult): number;
|
|
64
|
+
//# sourceMappingURL=strategy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"strategy.d.ts","sourceRoot":"","sources":["../../../src/server/utils/strategy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,MAAM,MAAM,YAAY,GAAG,UAAU,GAAG,WAAW,CAAC;AAEpD,MAAM,WAAW,cAAc;IAC7B,+BAA+B;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,qBAAqB;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,0BAA0B;IAC1B,gBAAgB,EAAE,MAAM,CAAC;IACzB,sBAAsB;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,4BAA4B;IAC5B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,wBAAwB;IACxB,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,YAAY,CAAC;IACnB,iBAAiB;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,2BAA2B;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,oBAAoB;IACpB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,aAAa;AACb,eAAO,MAAM,uBAAuB,EAAE,cAOrC,CAAC;AAEF;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAC5B,SAAS,EAAE,MAAM,EACjB,GAAG,GAAE,cAAwC,GAC5C,cAAc,CA8ChB;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,cAAc,GAAG,MAAM,CAI5D"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/server/utils/strategy.ts
|
|
21
|
+
var strategy_exports = {};
|
|
22
|
+
__export(strategy_exports, {
|
|
23
|
+
DEFAULT_STRATEGY_CONFIG: () => DEFAULT_STRATEGY_CONFIG,
|
|
24
|
+
chooseStrategy: () => chooseStrategy,
|
|
25
|
+
suggestConcurrency: () => suggestConcurrency
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(strategy_exports);
|
|
28
|
+
var DEFAULT_STRATEGY_CONFIG = {
|
|
29
|
+
oneShotMax: 1024 * 1024,
|
|
30
|
+
// 1MB
|
|
31
|
+
defaultPartSize: 1024 * 1024,
|
|
32
|
+
// 1MB
|
|
33
|
+
bigFileThreshold: 1024 ** 3,
|
|
34
|
+
// 1GB
|
|
35
|
+
bigFilePartSize: 5 * 1024 * 1024,
|
|
36
|
+
// 5MB
|
|
37
|
+
hugeFileThreshold: 10 * 1024 ** 3,
|
|
38
|
+
// 10GB
|
|
39
|
+
hugeFilePartSize: 10 * 1024 * 1024
|
|
40
|
+
// 10MB
|
|
41
|
+
};
|
|
42
|
+
function chooseStrategy(totalSize, cfg = DEFAULT_STRATEGY_CONFIG) {
|
|
43
|
+
if (!Number.isFinite(totalSize) || totalSize < 0) {
|
|
44
|
+
throw new Error(`[strategy] totalSize \u975E\u6CD5: ${totalSize}(\u5FC5\u987B \u2265 0 \u7684\u6709\u9650\u6570)`);
|
|
45
|
+
}
|
|
46
|
+
validateConfig(cfg);
|
|
47
|
+
if (totalSize === 0) {
|
|
48
|
+
return { kind: "ONE_SHOT", partSize: 0, totalParts: 1, reason: "0\u5B57\u8282" };
|
|
49
|
+
}
|
|
50
|
+
if (totalSize <= cfg.oneShotMax) {
|
|
51
|
+
return {
|
|
52
|
+
kind: "ONE_SHOT",
|
|
53
|
+
partSize: totalSize,
|
|
54
|
+
totalParts: 1,
|
|
55
|
+
reason: `\u2264 ${cfg.oneShotMax}B \u8D70 ONE_SHOT(1 part \u76F4\u4F20)`
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
if (totalSize > cfg.hugeFileThreshold) {
|
|
59
|
+
const partSize2 = cfg.hugeFilePartSize;
|
|
60
|
+
return {
|
|
61
|
+
kind: "MULTIPART",
|
|
62
|
+
partSize: partSize2,
|
|
63
|
+
totalParts: Math.ceil(totalSize / partSize2),
|
|
64
|
+
reason: `> ${cfg.hugeFileThreshold}B \u8D70 HUGE(${partSize2}B/part)`
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
if (totalSize > cfg.bigFileThreshold) {
|
|
68
|
+
const partSize2 = cfg.bigFilePartSize;
|
|
69
|
+
return {
|
|
70
|
+
kind: "MULTIPART",
|
|
71
|
+
partSize: partSize2,
|
|
72
|
+
totalParts: Math.ceil(totalSize / partSize2),
|
|
73
|
+
reason: `> ${cfg.bigFileThreshold}B \u8D70 BIG(${partSize2}B/part)`
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
const partSize = cfg.defaultPartSize;
|
|
77
|
+
return {
|
|
78
|
+
kind: "MULTIPART",
|
|
79
|
+
partSize,
|
|
80
|
+
totalParts: Math.ceil(totalSize / partSize),
|
|
81
|
+
reason: `\u2264 ${cfg.bigFileThreshold}B \u8D70 DEFAULT(${partSize}B/part)`
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function suggestConcurrency(s) {
|
|
85
|
+
if (s.kind === "ONE_SHOT") return 1;
|
|
86
|
+
if (s.totalParts <= 4) return Math.max(1, s.totalParts);
|
|
87
|
+
return 4;
|
|
88
|
+
}
|
|
89
|
+
function validateConfig(cfg) {
|
|
90
|
+
const fields = [
|
|
91
|
+
["oneShotMax", "oneShotMax"],
|
|
92
|
+
["defaultPartSize", "defaultPartSize"],
|
|
93
|
+
["bigFileThreshold", "bigFileThreshold"],
|
|
94
|
+
["bigFilePartSize", "bigFilePartSize"],
|
|
95
|
+
["hugeFileThreshold", "hugeFileThreshold"],
|
|
96
|
+
["hugeFilePartSize", "hugeFilePartSize"]
|
|
97
|
+
];
|
|
98
|
+
for (const [k, name] of fields) {
|
|
99
|
+
if (!Number.isFinite(cfg[k]) || cfg[k] <= 0) {
|
|
100
|
+
throw new Error(`[strategy] cfg.${name} \u975E\u6CD5: ${cfg[k]}(\u5FC5\u987B > 0)`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (cfg.oneShotMax > cfg.defaultPartSize) {
|
|
104
|
+
throw new Error(
|
|
105
|
+
`[strategy] oneShotMax(${cfg.oneShotMax}) \u4E0D\u80FD > defaultPartSize(${cfg.defaultPartSize})`
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
if (cfg.bigFileThreshold >= cfg.hugeFileThreshold) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
`[strategy] bigFileThreshold(${cfg.bigFileThreshold}) \u5FC5\u987B < hugeFileThreshold(${cfg.hugeFileThreshold})`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
if (cfg.defaultPartSize > cfg.bigFilePartSize) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
`[strategy] defaultPartSize(${cfg.defaultPartSize}) \u4E0D\u80FD > bigFilePartSize(${cfg.bigFilePartSize})`
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
if (cfg.bigFilePartSize > cfg.hugeFilePartSize) {
|
|
119
|
+
throw new Error(
|
|
120
|
+
`[strategy] bigFilePartSize(${cfg.bigFilePartSize}) \u4E0D\u80FD > hugeFilePartSize(${cfg.hugeFilePartSize})`
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
125
|
+
0 && (module.exports = {
|
|
126
|
+
DEFAULT_STRATEGY_CONFIG,
|
|
127
|
+
chooseStrategy,
|
|
128
|
+
suggestConcurrency
|
|
129
|
+
});
|
|
130
|
+
//# sourceMappingURL=strategy.js.map
|