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 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/server/utils/strategy.ts"],"sourcesContent":["/**\r\n * Smart 上传策略选择器\r\n *\r\n * 服务端根据 totalSize 自动选择 ONE_SHOT 或 MULTIPART:\r\n * - ONE_SHOT: 1 个 part(适合 ≤ ONE_SHOT_MAX 的小文件)\r\n * 客户端只调 1 次 part + 1 次 complete = 2 RTT\r\n * - MULTIPART: N 个 part,根据文件大小动态调整 partSize:\r\n * ≤ 1GB → 1MB / part\r\n * 1 ~ 10GB → 5MB / part\r\n * > 10GB → 10MB / part\r\n *\r\n * 设计约束\r\n * ========\r\n * - 纯函数:无副作用、不读 IO、不访问全局状态,易于单测\r\n * - 0 字节文件合法(partSize=0, totalParts=1)\r\n * - 决策结果携带 reason 字段,便于日志/排障\r\n * - 不与 config 直接耦合:调用方传入 cfg\r\n */\r\n\r\nexport type StrategyKind = 'ONE_SHOT' | 'MULTIPART';\r\n\r\nexport interface StrategyConfig {\r\n /** ≤ 此字节数走 ONE_SHOT(默认 1MB) */\r\n oneShotMax: number;\r\n /** 默认分片大小(默认 1MB) */\r\n defaultPartSize: number;\r\n /** 超过此字节数切到大分片(默认 1GB) */\r\n bigFileThreshold: number;\r\n /** 大文件分片大小(默认 5MB) */\r\n bigFilePartSize: number;\r\n /** 超过此字节数切到超大分片(默认 10GB) */\r\n hugeFileThreshold: number;\r\n /** 超大文件分片大小(默认 10MB) */\r\n hugeFilePartSize: number;\r\n}\r\n\r\nexport interface StrategyResult {\r\n kind: StrategyKind;\r\n /** 实际分片大小(字节) */\r\n partSize: number;\r\n /** 总分片数(ONE_SHOT 固定为 1) */\r\n totalParts: number;\r\n /** 决策原因(便于日志/调试) */\r\n reason: string;\r\n}\r\n\r\n/** 默认策略配置 */\r\nexport const DEFAULT_STRATEGY_CONFIG: StrategyConfig = {\r\n oneShotMax: 1024 * 1024, // 1MB\r\n defaultPartSize: 1024 * 1024, // 1MB\r\n bigFileThreshold: 1024 ** 3, // 1GB\r\n bigFilePartSize: 5 * 1024 * 1024, // 5MB\r\n hugeFileThreshold: 10 * 1024 ** 3, // 10GB\r\n hugeFilePartSize: 10 * 1024 * 1024, // 10MB\r\n};\r\n\r\n/**\r\n * 根据 totalSize 选出上传策略\r\n *\r\n * @throws totalSize 非法(负数 / NaN / 非有限数)\r\n *\r\n * @example\r\n * chooseStrategy(1024) // → ONE_SHOT 1 part, partSize=1024\r\n * chooseStrategy(5 * 1024 * 1024) // → MULTIPART 5 part, partSize=1MB\r\n * chooseStrategy(5 * 1024**3) // → MULTIPART 1024 part, partSize=5MB\r\n * chooseStrategy(50 * 1024**3) // → MULTIPART 5120 part, partSize=10MB\r\n */\r\nexport function chooseStrategy(\r\n totalSize: number,\r\n cfg: StrategyConfig = DEFAULT_STRATEGY_CONFIG\r\n): StrategyResult {\r\n if (!Number.isFinite(totalSize) || totalSize < 0) {\r\n throw new Error(`[strategy] totalSize 非法: ${totalSize}(必须 ≥ 0 的有限数)`);\r\n }\r\n validateConfig(cfg);\r\n\r\n if (totalSize === 0) {\r\n return { kind: 'ONE_SHOT', partSize: 0, totalParts: 1, reason: '0字节' };\r\n }\r\n\r\n if (totalSize <= cfg.oneShotMax) {\r\n return {\r\n kind: 'ONE_SHOT',\r\n partSize: totalSize,\r\n totalParts: 1,\r\n reason: `≤ ${cfg.oneShotMax}B 走 ONE_SHOT(1 part 直传)`,\r\n };\r\n }\r\n\r\n if (totalSize > cfg.hugeFileThreshold) {\r\n const partSize = cfg.hugeFilePartSize;\r\n return {\r\n kind: 'MULTIPART',\r\n partSize,\r\n totalParts: Math.ceil(totalSize / partSize),\r\n reason: `> ${cfg.hugeFileThreshold}B 走 HUGE(${partSize}B/part)`,\r\n };\r\n }\r\n\r\n if (totalSize > cfg.bigFileThreshold) {\r\n const partSize = cfg.bigFilePartSize;\r\n return {\r\n kind: 'MULTIPART',\r\n partSize,\r\n totalParts: Math.ceil(totalSize / partSize),\r\n reason: `> ${cfg.bigFileThreshold}B 走 BIG(${partSize}B/part)`,\r\n };\r\n }\r\n\r\n const partSize = cfg.defaultPartSize;\r\n return {\r\n kind: 'MULTIPART',\r\n partSize,\r\n totalParts: Math.ceil(totalSize / partSize),\r\n reason: `≤ ${cfg.bigFileThreshold}B 走 DEFAULT(${partSize}B/part)`,\r\n };\r\n}\r\n\r\n/**\r\n * 建议的客户端并发上传 part 数\r\n * - ONE_SHOT:固定 1(只传 1 个 part,无需并发)\r\n * - totalParts ≤ 4:用 totalParts(part 太少,无并发必要)\r\n * - totalParts > 4:用 4(服务端限流/带宽考虑)\r\n */\r\nexport function suggestConcurrency(s: StrategyResult): number {\r\n if (s.kind === 'ONE_SHOT') return 1;\r\n if (s.totalParts <= 4) return Math.max(1, s.totalParts);\r\n return 4;\r\n}\r\n\r\nfunction validateConfig(cfg: StrategyConfig): void {\r\n const fields: Array<[keyof StrategyConfig, string]> = [\r\n ['oneShotMax', 'oneShotMax'],\r\n ['defaultPartSize', 'defaultPartSize'],\r\n ['bigFileThreshold', 'bigFileThreshold'],\r\n ['bigFilePartSize', 'bigFilePartSize'],\r\n ['hugeFileThreshold', 'hugeFileThreshold'],\r\n ['hugeFilePartSize', 'hugeFilePartSize'],\r\n ];\r\n for (const [k, name] of fields) {\r\n if (!Number.isFinite(cfg[k]) || cfg[k] <= 0) {\r\n throw new Error(`[strategy] cfg.${name} 非法: ${cfg[k]}(必须 > 0)`);\r\n }\r\n }\r\n if (cfg.oneShotMax > cfg.defaultPartSize) {\r\n throw new Error(\r\n `[strategy] oneShotMax(${cfg.oneShotMax}) 不能 > defaultPartSize(${cfg.defaultPartSize})`\r\n );\r\n }\r\n if (cfg.bigFileThreshold >= cfg.hugeFileThreshold) {\r\n throw new Error(\r\n `[strategy] bigFileThreshold(${cfg.bigFileThreshold}) 必须 < hugeFileThreshold(${cfg.hugeFileThreshold})`\r\n );\r\n }\r\n if (cfg.defaultPartSize > cfg.bigFilePartSize) {\r\n throw new Error(\r\n `[strategy] defaultPartSize(${cfg.defaultPartSize}) 不能 > bigFilePartSize(${cfg.bigFilePartSize})`\r\n );\r\n }\r\n if (cfg.bigFilePartSize > cfg.hugeFilePartSize) {\r\n throw new Error(\r\n `[strategy] bigFilePartSize(${cfg.bigFilePartSize}) 不能 > hugeFilePartSize(${cfg.hugeFilePartSize})`\r\n );\r\n }\r\n}\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA+CO,IAAM,0BAA0C;AAAA,EACrD,YAAY,OAAO;AAAA;AAAA,EACnB,iBAAiB,OAAO;AAAA;AAAA,EACxB,kBAAkB,QAAQ;AAAA;AAAA,EAC1B,iBAAiB,IAAI,OAAO;AAAA;AAAA,EAC5B,mBAAmB,KAAK,QAAQ;AAAA;AAAA,EAChC,kBAAkB,KAAK,OAAO;AAAA;AAChC;AAaO,SAAS,eACd,WACA,MAAsB,yBACN;AAChB,MAAI,CAAC,OAAO,SAAS,SAAS,KAAK,YAAY,GAAG;AAChD,UAAM,IAAI,MAAM,sCAA4B,SAAS,kDAAe;AAAA,EACtE;AACA,iBAAe,GAAG;AAElB,MAAI,cAAc,GAAG;AACnB,WAAO,EAAE,MAAM,YAAY,UAAU,GAAG,YAAY,GAAG,QAAQ,gBAAM;AAAA,EACvE;AAEA,MAAI,aAAa,IAAI,YAAY;AAC/B,WAAO;AAAA,MACL,MAAM;AAAA,MACN,UAAU;AAAA,MACV,YAAY;AAAA,MACZ,QAAQ,UAAK,IAAI,UAAU;AAAA,IAC7B;AAAA,EACF;AAEA,MAAI,YAAY,IAAI,mBAAmB;AACrC,UAAMA,YAAW,IAAI;AACrB,WAAO;AAAA,MACL,MAAM;AAAA,MACN,UAAAA;AAAA,MACA,YAAY,KAAK,KAAK,YAAYA,SAAQ;AAAA,MAC1C,QAAQ,KAAK,IAAI,iBAAiB,iBAAYA,SAAQ;AAAA,IACxD;AAAA,EACF;AAEA,MAAI,YAAY,IAAI,kBAAkB;AACpC,UAAMA,YAAW,IAAI;AACrB,WAAO;AAAA,MACL,MAAM;AAAA,MACN,UAAAA;AAAA,MACA,YAAY,KAAK,KAAK,YAAYA,SAAQ;AAAA,MAC1C,QAAQ,KAAK,IAAI,gBAAgB,gBAAWA,SAAQ;AAAA,IACtD;AAAA,EACF;AAEA,QAAM,WAAW,IAAI;AACrB,SAAO;AAAA,IACL,MAAM;AAAA,IACN;AAAA,IACA,YAAY,KAAK,KAAK,YAAY,QAAQ;AAAA,IAC1C,QAAQ,UAAK,IAAI,gBAAgB,oBAAe,QAAQ;AAAA,EAC1D;AACF;AAQO,SAAS,mBAAmB,GAA2B;AAC5D,MAAI,EAAE,SAAS,WAAY,QAAO;AAClC,MAAI,EAAE,cAAc,EAAG,QAAO,KAAK,IAAI,GAAG,EAAE,UAAU;AACtD,SAAO;AACT;AAEA,SAAS,eAAe,KAA2B;AACjD,QAAM,SAAgD;AAAA,IACpD,CAAC,cAAc,YAAY;AAAA,IAC3B,CAAC,mBAAmB,iBAAiB;AAAA,IACrC,CAAC,oBAAoB,kBAAkB;AAAA,IACvC,CAAC,mBAAmB,iBAAiB;AAAA,IACrC,CAAC,qBAAqB,mBAAmB;AAAA,IACzC,CAAC,oBAAoB,kBAAkB;AAAA,EACzC;AACA,aAAW,CAAC,GAAG,IAAI,KAAK,QAAQ;AAC9B,QAAI,CAAC,OAAO,SAAS,IAAI,CAAC,CAAC,KAAK,IAAI,CAAC,KAAK,GAAG;AAC3C,YAAM,IAAI,MAAM,kBAAkB,IAAI,kBAAQ,IAAI,CAAC,CAAC,oBAAU;AAAA,IAChE;AAAA,EACF;AACA,MAAI,IAAI,aAAa,IAAI,iBAAiB;AACxC,UAAM,IAAI;AAAA,MACR,yBAAyB,IAAI,UAAU,oCAA0B,IAAI,eAAe;AAAA,IACtF;AAAA,EACF;AACA,MAAI,IAAI,oBAAoB,IAAI,mBAAmB;AACjD,UAAM,IAAI;AAAA,MACR,+BAA+B,IAAI,gBAAgB,sCAA4B,IAAI,iBAAiB;AAAA,IACtG;AAAA,EACF;AACA,MAAI,IAAI,kBAAkB,IAAI,iBAAiB;AAC7C,UAAM,IAAI;AAAA,MACR,8BAA8B,IAAI,eAAe,oCAA0B,IAAI,eAAe;AAAA,IAChG;AAAA,EACF;AACA,MAAI,IAAI,kBAAkB,IAAI,kBAAkB;AAC9C,UAAM,IAAI;AAAA,MACR,8BAA8B,IAAI,eAAe,qCAA2B,IAAI,gBAAgB;AAAA,IAClG;AAAA,EACF;AACF;","names":["partSize"]}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// src/server/utils/strategy.ts
|
|
2
|
+
var DEFAULT_STRATEGY_CONFIG = {
|
|
3
|
+
oneShotMax: 1024 * 1024,
|
|
4
|
+
// 1MB
|
|
5
|
+
defaultPartSize: 1024 * 1024,
|
|
6
|
+
// 1MB
|
|
7
|
+
bigFileThreshold: 1024 ** 3,
|
|
8
|
+
// 1GB
|
|
9
|
+
bigFilePartSize: 5 * 1024 * 1024,
|
|
10
|
+
// 5MB
|
|
11
|
+
hugeFileThreshold: 10 * 1024 ** 3,
|
|
12
|
+
// 10GB
|
|
13
|
+
hugeFilePartSize: 10 * 1024 * 1024
|
|
14
|
+
// 10MB
|
|
15
|
+
};
|
|
16
|
+
function chooseStrategy(totalSize, cfg = DEFAULT_STRATEGY_CONFIG) {
|
|
17
|
+
if (!Number.isFinite(totalSize) || totalSize < 0) {
|
|
18
|
+
throw new Error(`[strategy] totalSize \u975E\u6CD5: ${totalSize}(\u5FC5\u987B \u2265 0 \u7684\u6709\u9650\u6570)`);
|
|
19
|
+
}
|
|
20
|
+
validateConfig(cfg);
|
|
21
|
+
if (totalSize === 0) {
|
|
22
|
+
return { kind: "ONE_SHOT", partSize: 0, totalParts: 1, reason: "0\u5B57\u8282" };
|
|
23
|
+
}
|
|
24
|
+
if (totalSize <= cfg.oneShotMax) {
|
|
25
|
+
return {
|
|
26
|
+
kind: "ONE_SHOT",
|
|
27
|
+
partSize: totalSize,
|
|
28
|
+
totalParts: 1,
|
|
29
|
+
reason: `\u2264 ${cfg.oneShotMax}B \u8D70 ONE_SHOT(1 part \u76F4\u4F20)`
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
if (totalSize > cfg.hugeFileThreshold) {
|
|
33
|
+
const partSize2 = cfg.hugeFilePartSize;
|
|
34
|
+
return {
|
|
35
|
+
kind: "MULTIPART",
|
|
36
|
+
partSize: partSize2,
|
|
37
|
+
totalParts: Math.ceil(totalSize / partSize2),
|
|
38
|
+
reason: `> ${cfg.hugeFileThreshold}B \u8D70 HUGE(${partSize2}B/part)`
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
if (totalSize > cfg.bigFileThreshold) {
|
|
42
|
+
const partSize2 = cfg.bigFilePartSize;
|
|
43
|
+
return {
|
|
44
|
+
kind: "MULTIPART",
|
|
45
|
+
partSize: partSize2,
|
|
46
|
+
totalParts: Math.ceil(totalSize / partSize2),
|
|
47
|
+
reason: `> ${cfg.bigFileThreshold}B \u8D70 BIG(${partSize2}B/part)`
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
const partSize = cfg.defaultPartSize;
|
|
51
|
+
return {
|
|
52
|
+
kind: "MULTIPART",
|
|
53
|
+
partSize,
|
|
54
|
+
totalParts: Math.ceil(totalSize / partSize),
|
|
55
|
+
reason: `\u2264 ${cfg.bigFileThreshold}B \u8D70 DEFAULT(${partSize}B/part)`
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function suggestConcurrency(s) {
|
|
59
|
+
if (s.kind === "ONE_SHOT") return 1;
|
|
60
|
+
if (s.totalParts <= 4) return Math.max(1, s.totalParts);
|
|
61
|
+
return 4;
|
|
62
|
+
}
|
|
63
|
+
function validateConfig(cfg) {
|
|
64
|
+
const fields = [
|
|
65
|
+
["oneShotMax", "oneShotMax"],
|
|
66
|
+
["defaultPartSize", "defaultPartSize"],
|
|
67
|
+
["bigFileThreshold", "bigFileThreshold"],
|
|
68
|
+
["bigFilePartSize", "bigFilePartSize"],
|
|
69
|
+
["hugeFileThreshold", "hugeFileThreshold"],
|
|
70
|
+
["hugeFilePartSize", "hugeFilePartSize"]
|
|
71
|
+
];
|
|
72
|
+
for (const [k, name] of fields) {
|
|
73
|
+
if (!Number.isFinite(cfg[k]) || cfg[k] <= 0) {
|
|
74
|
+
throw new Error(`[strategy] cfg.${name} \u975E\u6CD5: ${cfg[k]}(\u5FC5\u987B > 0)`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (cfg.oneShotMax > cfg.defaultPartSize) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
`[strategy] oneShotMax(${cfg.oneShotMax}) \u4E0D\u80FD > defaultPartSize(${cfg.defaultPartSize})`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
if (cfg.bigFileThreshold >= cfg.hugeFileThreshold) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
`[strategy] bigFileThreshold(${cfg.bigFileThreshold}) \u5FC5\u987B < hugeFileThreshold(${cfg.hugeFileThreshold})`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
if (cfg.defaultPartSize > cfg.bigFilePartSize) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`[strategy] defaultPartSize(${cfg.defaultPartSize}) \u4E0D\u80FD > bigFilePartSize(${cfg.bigFilePartSize})`
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
if (cfg.bigFilePartSize > cfg.hugeFilePartSize) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
`[strategy] bigFilePartSize(${cfg.bigFilePartSize}) \u4E0D\u80FD > hugeFilePartSize(${cfg.hugeFilePartSize})`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
export {
|
|
99
|
+
DEFAULT_STRATEGY_CONFIG,
|
|
100
|
+
chooseStrategy,
|
|
101
|
+
suggestConcurrency
|
|
102
|
+
};
|
|
103
|
+
//# sourceMappingURL=strategy.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/server/utils/strategy.ts"],"sourcesContent":["/**\r\n * Smart 上传策略选择器\r\n *\r\n * 服务端根据 totalSize 自动选择 ONE_SHOT 或 MULTIPART:\r\n * - ONE_SHOT: 1 个 part(适合 ≤ ONE_SHOT_MAX 的小文件)\r\n * 客户端只调 1 次 part + 1 次 complete = 2 RTT\r\n * - MULTIPART: N 个 part,根据文件大小动态调整 partSize:\r\n * ≤ 1GB → 1MB / part\r\n * 1 ~ 10GB → 5MB / part\r\n * > 10GB → 10MB / part\r\n *\r\n * 设计约束\r\n * ========\r\n * - 纯函数:无副作用、不读 IO、不访问全局状态,易于单测\r\n * - 0 字节文件合法(partSize=0, totalParts=1)\r\n * - 决策结果携带 reason 字段,便于日志/排障\r\n * - 不与 config 直接耦合:调用方传入 cfg\r\n */\r\n\r\nexport type StrategyKind = 'ONE_SHOT' | 'MULTIPART';\r\n\r\nexport interface StrategyConfig {\r\n /** ≤ 此字节数走 ONE_SHOT(默认 1MB) */\r\n oneShotMax: number;\r\n /** 默认分片大小(默认 1MB) */\r\n defaultPartSize: number;\r\n /** 超过此字节数切到大分片(默认 1GB) */\r\n bigFileThreshold: number;\r\n /** 大文件分片大小(默认 5MB) */\r\n bigFilePartSize: number;\r\n /** 超过此字节数切到超大分片(默认 10GB) */\r\n hugeFileThreshold: number;\r\n /** 超大文件分片大小(默认 10MB) */\r\n hugeFilePartSize: number;\r\n}\r\n\r\nexport interface StrategyResult {\r\n kind: StrategyKind;\r\n /** 实际分片大小(字节) */\r\n partSize: number;\r\n /** 总分片数(ONE_SHOT 固定为 1) */\r\n totalParts: number;\r\n /** 决策原因(便于日志/调试) */\r\n reason: string;\r\n}\r\n\r\n/** 默认策略配置 */\r\nexport const DEFAULT_STRATEGY_CONFIG: StrategyConfig = {\r\n oneShotMax: 1024 * 1024, // 1MB\r\n defaultPartSize: 1024 * 1024, // 1MB\r\n bigFileThreshold: 1024 ** 3, // 1GB\r\n bigFilePartSize: 5 * 1024 * 1024, // 5MB\r\n hugeFileThreshold: 10 * 1024 ** 3, // 10GB\r\n hugeFilePartSize: 10 * 1024 * 1024, // 10MB\r\n};\r\n\r\n/**\r\n * 根据 totalSize 选出上传策略\r\n *\r\n * @throws totalSize 非法(负数 / NaN / 非有限数)\r\n *\r\n * @example\r\n * chooseStrategy(1024) // → ONE_SHOT 1 part, partSize=1024\r\n * chooseStrategy(5 * 1024 * 1024) // → MULTIPART 5 part, partSize=1MB\r\n * chooseStrategy(5 * 1024**3) // → MULTIPART 1024 part, partSize=5MB\r\n * chooseStrategy(50 * 1024**3) // → MULTIPART 5120 part, partSize=10MB\r\n */\r\nexport function chooseStrategy(\r\n totalSize: number,\r\n cfg: StrategyConfig = DEFAULT_STRATEGY_CONFIG\r\n): StrategyResult {\r\n if (!Number.isFinite(totalSize) || totalSize < 0) {\r\n throw new Error(`[strategy] totalSize 非法: ${totalSize}(必须 ≥ 0 的有限数)`);\r\n }\r\n validateConfig(cfg);\r\n\r\n if (totalSize === 0) {\r\n return { kind: 'ONE_SHOT', partSize: 0, totalParts: 1, reason: '0字节' };\r\n }\r\n\r\n if (totalSize <= cfg.oneShotMax) {\r\n return {\r\n kind: 'ONE_SHOT',\r\n partSize: totalSize,\r\n totalParts: 1,\r\n reason: `≤ ${cfg.oneShotMax}B 走 ONE_SHOT(1 part 直传)`,\r\n };\r\n }\r\n\r\n if (totalSize > cfg.hugeFileThreshold) {\r\n const partSize = cfg.hugeFilePartSize;\r\n return {\r\n kind: 'MULTIPART',\r\n partSize,\r\n totalParts: Math.ceil(totalSize / partSize),\r\n reason: `> ${cfg.hugeFileThreshold}B 走 HUGE(${partSize}B/part)`,\r\n };\r\n }\r\n\r\n if (totalSize > cfg.bigFileThreshold) {\r\n const partSize = cfg.bigFilePartSize;\r\n return {\r\n kind: 'MULTIPART',\r\n partSize,\r\n totalParts: Math.ceil(totalSize / partSize),\r\n reason: `> ${cfg.bigFileThreshold}B 走 BIG(${partSize}B/part)`,\r\n };\r\n }\r\n\r\n const partSize = cfg.defaultPartSize;\r\n return {\r\n kind: 'MULTIPART',\r\n partSize,\r\n totalParts: Math.ceil(totalSize / partSize),\r\n reason: `≤ ${cfg.bigFileThreshold}B 走 DEFAULT(${partSize}B/part)`,\r\n };\r\n}\r\n\r\n/**\r\n * 建议的客户端并发上传 part 数\r\n * - ONE_SHOT:固定 1(只传 1 个 part,无需并发)\r\n * - totalParts ≤ 4:用 totalParts(part 太少,无并发必要)\r\n * - totalParts > 4:用 4(服务端限流/带宽考虑)\r\n */\r\nexport function suggestConcurrency(s: StrategyResult): number {\r\n if (s.kind === 'ONE_SHOT') return 1;\r\n if (s.totalParts <= 4) return Math.max(1, s.totalParts);\r\n return 4;\r\n}\r\n\r\nfunction validateConfig(cfg: StrategyConfig): void {\r\n const fields: Array<[keyof StrategyConfig, string]> = [\r\n ['oneShotMax', 'oneShotMax'],\r\n ['defaultPartSize', 'defaultPartSize'],\r\n ['bigFileThreshold', 'bigFileThreshold'],\r\n ['bigFilePartSize', 'bigFilePartSize'],\r\n ['hugeFileThreshold', 'hugeFileThreshold'],\r\n ['hugeFilePartSize', 'hugeFilePartSize'],\r\n ];\r\n for (const [k, name] of fields) {\r\n if (!Number.isFinite(cfg[k]) || cfg[k] <= 0) {\r\n throw new Error(`[strategy] cfg.${name} 非法: ${cfg[k]}(必须 > 0)`);\r\n }\r\n }\r\n if (cfg.oneShotMax > cfg.defaultPartSize) {\r\n throw new Error(\r\n `[strategy] oneShotMax(${cfg.oneShotMax}) 不能 > defaultPartSize(${cfg.defaultPartSize})`\r\n );\r\n }\r\n if (cfg.bigFileThreshold >= cfg.hugeFileThreshold) {\r\n throw new Error(\r\n `[strategy] bigFileThreshold(${cfg.bigFileThreshold}) 必须 < hugeFileThreshold(${cfg.hugeFileThreshold})`\r\n );\r\n }\r\n if (cfg.defaultPartSize > cfg.bigFilePartSize) {\r\n throw new Error(\r\n `[strategy] defaultPartSize(${cfg.defaultPartSize}) 不能 > bigFilePartSize(${cfg.bigFilePartSize})`\r\n );\r\n }\r\n if (cfg.bigFilePartSize > cfg.hugeFilePartSize) {\r\n throw new Error(\r\n `[strategy] bigFilePartSize(${cfg.bigFilePartSize}) 不能 > hugeFilePartSize(${cfg.hugeFilePartSize})`\r\n );\r\n }\r\n}\r\n"],"mappings":"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","names":["partSize"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "secure-upload-fastify-sdk",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Fastify plugin SDK for resumable / multipart / smart uploads. Pluggable storage (Local / S3 / OSS / COS), pluggable repository (Prisma or custom), optional bridge to secure-crypto-top-zod-sdk.",
|
|
5
|
+
"main": "./dist/server/index.js",
|
|
6
|
+
"module": "./dist/server/index.mjs",
|
|
7
|
+
"types": "./dist/server/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./server": {
|
|
15
|
+
"types": "./dist/server/index.d.ts",
|
|
16
|
+
"import": "./dist/server/index.mjs",
|
|
17
|
+
"require": "./dist/server/index.js"
|
|
18
|
+
},
|
|
19
|
+
"./storage": {
|
|
20
|
+
"types": "./dist/server/utils/storage/index.d.ts",
|
|
21
|
+
"import": "./dist/server/utils/storage/index.mjs",
|
|
22
|
+
"require": "./dist/server/utils/storage/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./repository": {
|
|
25
|
+
"types": "./dist/server/utils/repository/index.d.ts",
|
|
26
|
+
"import": "./dist/server/utils/repository/index.mjs",
|
|
27
|
+
"require": "./dist/server/utils/repository/index.js"
|
|
28
|
+
},
|
|
29
|
+
"./errors": {
|
|
30
|
+
"types": "./dist/server/errors.d.ts",
|
|
31
|
+
"import": "./dist/server/errors.mjs",
|
|
32
|
+
"require": "./dist/server/errors.js"
|
|
33
|
+
},
|
|
34
|
+
"./strategy": {
|
|
35
|
+
"types": "./dist/server/utils/strategy.d.ts",
|
|
36
|
+
"import": "./dist/server/utils/strategy.mjs",
|
|
37
|
+
"require": "./dist/server/utils/strategy.js"
|
|
38
|
+
},
|
|
39
|
+
"./prisma/schema": "./prisma/schema.prisma"
|
|
40
|
+
},
|
|
41
|
+
"files": [
|
|
42
|
+
"dist",
|
|
43
|
+
"prisma",
|
|
44
|
+
"README.md"
|
|
45
|
+
],
|
|
46
|
+
"keywords": [
|
|
47
|
+
"fastify",
|
|
48
|
+
"plugin",
|
|
49
|
+
"upload",
|
|
50
|
+
"multipart",
|
|
51
|
+
"resumable",
|
|
52
|
+
"smart-upload",
|
|
53
|
+
"sdk"
|
|
54
|
+
],
|
|
55
|
+
"scripts": {
|
|
56
|
+
"build": "tsup && tsc -p tsconfig.dts.json",
|
|
57
|
+
"build:js": "tsup",
|
|
58
|
+
"build:dts": "tsc -p tsconfig.dts.json",
|
|
59
|
+
"dev": "tsup --watch",
|
|
60
|
+
"typecheck": "tsc --noEmit",
|
|
61
|
+
"login": "npm login --registry=https://registry.npmjs.org/",
|
|
62
|
+
"publish": "npm publish --access public --registry=https://registry.npmjs.org/",
|
|
63
|
+
"test": "node --import tsx --test \"tests/**/*.test.ts\"",
|
|
64
|
+
"test:strategy": "node --import tsx --test tests/strategy.test.ts",
|
|
65
|
+
"test:e2e": "node --import tsx tests/standalone-e2e-cli.mjs"
|
|
66
|
+
},
|
|
67
|
+
"peerDependencies": {
|
|
68
|
+
"fastify": "^4.28.0",
|
|
69
|
+
"fastify-plugin": "^4.5.1",
|
|
70
|
+
"zod": "^3.23.0"
|
|
71
|
+
},
|
|
72
|
+
"peerDependenciesMeta": {
|
|
73
|
+
"secure-crypto-top-zod-sdk": {
|
|
74
|
+
"optional": true
|
|
75
|
+
},
|
|
76
|
+
"@prisma/client": {
|
|
77
|
+
"optional": true
|
|
78
|
+
},
|
|
79
|
+
"ioredis": {
|
|
80
|
+
"optional": true
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
"dependencies": {
|
|
84
|
+
"secure-crypto-top-zod-sdk": "file:../secure-crypto-top-zod-sdk"
|
|
85
|
+
},
|
|
86
|
+
"devDependencies": {
|
|
87
|
+
"@prisma/client": "5.15.0",
|
|
88
|
+
"@types/node": "^20.14.0",
|
|
89
|
+
"fastify": "^4.28.0",
|
|
90
|
+
"fastify-plugin": "^4.5.1",
|
|
91
|
+
"ioredis": "^5.4.1",
|
|
92
|
+
"tsup": "^8.0.2",
|
|
93
|
+
"tsx": "^4.15.0",
|
|
94
|
+
"typescript": "^5.4.5",
|
|
95
|
+
"zod": "^3.23.8"
|
|
96
|
+
}
|
|
97
|
+
}
|