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.
Files changed (74) hide show
  1. package/README.md +467 -0
  2. package/dist/index.d.ts +14 -0
  3. package/dist/index.d.ts.map +1 -0
  4. package/dist/index.js +2069 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/index.mjs +2023 -0
  7. package/dist/index.mjs.map +1 -0
  8. package/dist/server/adapters/secure-crypto.d.ts +28 -0
  9. package/dist/server/adapters/secure-crypto.d.ts.map +1 -0
  10. package/dist/server/errors.d.ts +48 -0
  11. package/dist/server/errors.d.ts.map +1 -0
  12. package/dist/server/errors.js +112 -0
  13. package/dist/server/errors.js.map +1 -0
  14. package/dist/server/errors.mjs +85 -0
  15. package/dist/server/errors.mjs.map +1 -0
  16. package/dist/server/fastify-plugin.d.ts +38 -0
  17. package/dist/server/fastify-plugin.d.ts.map +1 -0
  18. package/dist/server/index.d.ts +17 -0
  19. package/dist/server/index.d.ts.map +1 -0
  20. package/dist/server/index.js +2070 -0
  21. package/dist/server/index.js.map +1 -0
  22. package/dist/server/index.mjs +2024 -0
  23. package/dist/server/index.mjs.map +1 -0
  24. package/dist/server/routes/_internal.d.ts +42 -0
  25. package/dist/server/routes/_internal.d.ts.map +1 -0
  26. package/dist/server/routes/multipart.d.ts +22 -0
  27. package/dist/server/routes/multipart.d.ts.map +1 -0
  28. package/dist/server/routes/smart.d.ts +16 -0
  29. package/dist/server/routes/smart.d.ts.map +1 -0
  30. package/dist/server/session.d.ts +19 -0
  31. package/dist/server/session.d.ts.map +1 -0
  32. package/dist/server/types.d.ts +233 -0
  33. package/dist/server/types.d.ts.map +1 -0
  34. package/dist/server/utils/cleaner.d.ts +20 -0
  35. package/dist/server/utils/cleaner.d.ts.map +1 -0
  36. package/dist/server/utils/memory-rate-limit.d.ts +29 -0
  37. package/dist/server/utils/memory-rate-limit.d.ts.map +1 -0
  38. package/dist/server/utils/orphan-scanner.d.ts +34 -0
  39. package/dist/server/utils/orphan-scanner.d.ts.map +1 -0
  40. package/dist/server/utils/redis-helpers.d.ts +25 -0
  41. package/dist/server/utils/redis-helpers.d.ts.map +1 -0
  42. package/dist/server/utils/repository/index.d.ts +27 -0
  43. package/dist/server/utils/repository/index.d.ts.map +1 -0
  44. package/dist/server/utils/repository/index.js +501 -0
  45. package/dist/server/utils/repository/index.js.map +1 -0
  46. package/dist/server/utils/repository/index.mjs +469 -0
  47. package/dist/server/utils/repository/index.mjs.map +1 -0
  48. package/dist/server/utils/repository/memory.d.ts +33 -0
  49. package/dist/server/utils/repository/memory.d.ts.map +1 -0
  50. package/dist/server/utils/repository/prisma.d.ts +49 -0
  51. package/dist/server/utils/repository/prisma.d.ts.map +1 -0
  52. package/dist/server/utils/repository/types-extra.d.ts +9 -0
  53. package/dist/server/utils/repository/types-extra.d.ts.map +1 -0
  54. package/dist/server/utils/repository/types.d.ts +128 -0
  55. package/dist/server/utils/repository/types.d.ts.map +1 -0
  56. package/dist/server/utils/storage/index.d.ts +115 -0
  57. package/dist/server/utils/storage/index.d.ts.map +1 -0
  58. package/dist/server/utils/storage/index.js +285 -0
  59. package/dist/server/utils/storage/index.js.map +1 -0
  60. package/dist/server/utils/storage/index.mjs +247 -0
  61. package/dist/server/utils/storage/index.mjs.map +1 -0
  62. package/dist/server/utils/storage/local.d.ts +46 -0
  63. package/dist/server/utils/storage/local.d.ts.map +1 -0
  64. package/dist/server/utils/storage/memory.d.ts +34 -0
  65. package/dist/server/utils/storage/memory.d.ts.map +1 -0
  66. package/dist/server/utils/storage/types.d.ts +78 -0
  67. package/dist/server/utils/storage/types.d.ts.map +1 -0
  68. package/dist/server/utils/strategy.d.ts +64 -0
  69. package/dist/server/utils/strategy.d.ts.map +1 -0
  70. package/dist/server/utils/strategy.js +130 -0
  71. package/dist/server/utils/strategy.js.map +1 -0
  72. package/dist/server/utils/strategy.mjs +103 -0
  73. package/dist/server/utils/strategy.mjs.map +1 -0
  74. 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":"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","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
+ }