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,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 数据层仓库接口
|
|
3
|
+
*
|
|
4
|
+
* SDK 不强制绑定 ORM,只依赖这 2 个接口:
|
|
5
|
+
* - MultipartRepository: 分片上传上下文(init/part/complete/abort/expire)
|
|
6
|
+
* - FileRepository: 文件元信息(秒传查询、orphan 扫描)
|
|
7
|
+
*
|
|
8
|
+
* 接入方式:
|
|
9
|
+
* - kind: 'prisma' → SDK 用内置 PrismaMultipartRepository + PrismaFileRepository
|
|
10
|
+
* - kind: 'custom' → 用户注入自己的实现(Drizzle / TypeORM / 内存 / Mongo ...)
|
|
11
|
+
*/
|
|
12
|
+
import type { MultipartStatusT, FileStatusT } from './types-extra';
|
|
13
|
+
export interface MultipartRecord {
|
|
14
|
+
id: string;
|
|
15
|
+
userId: number | null;
|
|
16
|
+
filename: string;
|
|
17
|
+
contentType: string;
|
|
18
|
+
totalSize: bigint;
|
|
19
|
+
partSize: number;
|
|
20
|
+
totalParts: number;
|
|
21
|
+
isPublic: boolean;
|
|
22
|
+
status: MultipartStatusT;
|
|
23
|
+
receivedParts: number[];
|
|
24
|
+
partHashes: Record<string, string>;
|
|
25
|
+
meta: Record<string, unknown> | null;
|
|
26
|
+
expectedHash: string | null;
|
|
27
|
+
fileId: string | null;
|
|
28
|
+
createdAt: Date;
|
|
29
|
+
updatedAt: Date;
|
|
30
|
+
expiresAt: Date;
|
|
31
|
+
completedAt: Date | null;
|
|
32
|
+
abortedAt: Date | null;
|
|
33
|
+
}
|
|
34
|
+
export interface FileRecord {
|
|
35
|
+
id: string;
|
|
36
|
+
userId: number | null;
|
|
37
|
+
filename: string;
|
|
38
|
+
contentType: string;
|
|
39
|
+
size: bigint;
|
|
40
|
+
storageKey: string;
|
|
41
|
+
isPublic: boolean;
|
|
42
|
+
status: FileStatusT;
|
|
43
|
+
uploadId: string | null;
|
|
44
|
+
totalParts: number | null;
|
|
45
|
+
contentHash: string | null;
|
|
46
|
+
meta: Record<string, unknown> | null;
|
|
47
|
+
createdAt: Date;
|
|
48
|
+
updatedAt: Date;
|
|
49
|
+
deletedAt: Date | null;
|
|
50
|
+
}
|
|
51
|
+
export interface CreateInitInput {
|
|
52
|
+
userId: number | null;
|
|
53
|
+
filename: string;
|
|
54
|
+
contentType: string;
|
|
55
|
+
totalSize: number;
|
|
56
|
+
partSize: number;
|
|
57
|
+
totalParts: number;
|
|
58
|
+
isPublic: boolean;
|
|
59
|
+
expectedHash?: string | null;
|
|
60
|
+
meta?: Record<string, unknown> | null;
|
|
61
|
+
ttlSeconds: number;
|
|
62
|
+
}
|
|
63
|
+
export interface CreateFileInput {
|
|
64
|
+
id: string;
|
|
65
|
+
userId: number | null;
|
|
66
|
+
filename: string;
|
|
67
|
+
contentType: string;
|
|
68
|
+
size: number;
|
|
69
|
+
storageKey: string;
|
|
70
|
+
isPublic: boolean;
|
|
71
|
+
uploadId: string;
|
|
72
|
+
totalParts: number;
|
|
73
|
+
contentHash: string;
|
|
74
|
+
meta?: Record<string, unknown> | null;
|
|
75
|
+
}
|
|
76
|
+
export interface AddPartResult {
|
|
77
|
+
updated: true;
|
|
78
|
+
record: MultipartRecord;
|
|
79
|
+
}
|
|
80
|
+
export interface AddPartDuplicate {
|
|
81
|
+
updated: false;
|
|
82
|
+
duplicate: true;
|
|
83
|
+
existingHash: string | null;
|
|
84
|
+
record: MultipartRecord;
|
|
85
|
+
}
|
|
86
|
+
export type AddPartOutcome = AddPartResult | AddPartDuplicate;
|
|
87
|
+
export interface MultipartRepository {
|
|
88
|
+
createInit(input: CreateInitInput): Promise<MultipartRecord>;
|
|
89
|
+
findById(id: string): Promise<MultipartRecord | null>;
|
|
90
|
+
addPart(id: string, partNumber: number, partHash: string | null): Promise<AddPartOutcome>;
|
|
91
|
+
markCompleted(id: string, fileId: string): Promise<MultipartRecord>;
|
|
92
|
+
markAborted(id: string): Promise<MultipartRecord>;
|
|
93
|
+
markExpired(id: string): Promise<void>;
|
|
94
|
+
listExpired(limit: number): Promise<MultipartRecord[]>;
|
|
95
|
+
}
|
|
96
|
+
export interface FileRepository {
|
|
97
|
+
findByHash(hash: string): Promise<FileRecord | null>;
|
|
98
|
+
/**
|
|
99
|
+
* v1.4.2: 严格秒传查询。
|
|
100
|
+
* 必须同时满足: contentHash 相同、未删除、磁盘副本可见性与上传请求一致。
|
|
101
|
+
* - isPublic=true → 命中 isPublic=true 的记录
|
|
102
|
+
* - isPublic=false → 命中 isPublic=false 且 userId 与上传者一致的记录
|
|
103
|
+
* 否则返回 null,调用方应走正常分片上传(新建独立 fileId)。
|
|
104
|
+
*/
|
|
105
|
+
findDedupCandidate(hash: string, isPublic: boolean, userId: number | null): Promise<FileRecord | null>;
|
|
106
|
+
findById(id: string): Promise<FileRecord | null>;
|
|
107
|
+
create(input: CreateFileInput): Promise<FileRecord>;
|
|
108
|
+
softDelete(id: string, metaPatch?: Record<string, unknown>): Promise<void>;
|
|
109
|
+
listOrphans(opts: {
|
|
110
|
+
sinceDays: number;
|
|
111
|
+
limit: number;
|
|
112
|
+
}): Promise<FileRecord[]>;
|
|
113
|
+
}
|
|
114
|
+
export declare const MultipartStatus: {
|
|
115
|
+
readonly INIT: "INIT";
|
|
116
|
+
readonly UPLOADING: "UPLOADING";
|
|
117
|
+
readonly COMPLETED: "COMPLETED";
|
|
118
|
+
readonly ABORTED: "ABORTED";
|
|
119
|
+
readonly EXPIRED: "EXPIRED";
|
|
120
|
+
readonly FAILED: "FAILED";
|
|
121
|
+
};
|
|
122
|
+
export declare const FileStatus: {
|
|
123
|
+
readonly PENDING: "PENDING";
|
|
124
|
+
readonly UPLOADING: "UPLOADING";
|
|
125
|
+
readonly COMPLETED: "COMPLETED";
|
|
126
|
+
readonly FAILED: "FAILED";
|
|
127
|
+
};
|
|
128
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/server/utils/repository/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage 适配器统一导出
|
|
3
|
+
*
|
|
4
|
+
* 用法:
|
|
5
|
+
* import { LocalStorage, InMemoryStorage } from 'secure-upload-fastify-sdk/storage';
|
|
6
|
+
* import type { StorageAdapter, FileMeta } from 'secure-upload-fastify-sdk/storage';
|
|
7
|
+
*/
|
|
8
|
+
export { LocalStorage, type LocalStorageOptions } from './local';
|
|
9
|
+
export { InMemoryStorage } from './memory';
|
|
10
|
+
export type { StorageAdapter, FileMeta, PutResult, MergeResult, } from './types';
|
|
11
|
+
/**
|
|
12
|
+
* 如何扩展 S3 / 阿里云 OSS / 腾讯云 COS
|
|
13
|
+
* ========================================
|
|
14
|
+
*
|
|
15
|
+
* 1. 实现 StorageAdapter 接口(本目录下写一个 s3.ts):
|
|
16
|
+
*
|
|
17
|
+
* import { S3Client, PutObjectCommand, GetObjectCommand,
|
|
18
|
+
* DeleteObjectCommand, HeadObjectCommand } from '@aws-sdk/client-s3';
|
|
19
|
+
* import type { StorageAdapter, FileMeta, MergeResult } from './types';
|
|
20
|
+
*
|
|
21
|
+
* export class S3Storage implements StorageAdapter {
|
|
22
|
+
* constructor(
|
|
23
|
+
* private client: S3Client,
|
|
24
|
+
* private bucket: string,
|
|
25
|
+
* private prefix = 'uploads/'
|
|
26
|
+
* ) {}
|
|
27
|
+
*
|
|
28
|
+
* // 1.1 put
|
|
29
|
+
* async put(data, meta) {
|
|
30
|
+
* const key = `${this.prefix}${Date.now()}/${meta.id}`;
|
|
31
|
+
* await this.client.send(new PutObjectCommand({
|
|
32
|
+
* Bucket: this.bucket, Key: key, Body: data,
|
|
33
|
+
* ContentType: meta.contentType,
|
|
34
|
+
* }));
|
|
35
|
+
* return { key };
|
|
36
|
+
* }
|
|
37
|
+
*
|
|
38
|
+
* // 1.2 get
|
|
39
|
+
* get(key) {
|
|
40
|
+
* const obj = await this.client.send(new GetObjectCommand({...}));
|
|
41
|
+
* return obj.Body as Readable;
|
|
42
|
+
* }
|
|
43
|
+
*
|
|
44
|
+
* // 1.3 delete
|
|
45
|
+
* async delete(key) { await this.client.send(new DeleteObjectCommand({...})); }
|
|
46
|
+
*
|
|
47
|
+
* // 1.4 exists
|
|
48
|
+
* async exists(key) {
|
|
49
|
+
* try { await this.client.send(new HeadObjectCommand({...})); return true; }
|
|
50
|
+
* catch (e) { if (e.$metadata?.httpStatusCode === 404) return false; throw e; }
|
|
51
|
+
* }
|
|
52
|
+
*
|
|
53
|
+
* // 1.5 putPart(list 形式 + S3 multipart upload API)
|
|
54
|
+
* // 这里有两种实现:
|
|
55
|
+
* // a. 简单版:每个 part 当一个独立对象,key = `${uploadId}/part_0001.bin`
|
|
56
|
+
* // b. 进阶版:用 S3 原生 multipart upload(更省 API 调用,但更复杂)
|
|
57
|
+
* // 推荐 (a) MVP 阶段够用,merge 时按 list + get 拼接
|
|
58
|
+
* async putPart(uploadId, partNumber, data) {
|
|
59
|
+
* const key = `${this.prefix}_multipart/${uploadId}/part_${String(partNumber).padStart(4, '0')}.bin`;
|
|
60
|
+
* await this.client.send(new PutObjectCommand({...}));
|
|
61
|
+
* return { size: data.length };
|
|
62
|
+
* }
|
|
63
|
+
*
|
|
64
|
+
* // 1.6 listParts(ListObjectsV2)
|
|
65
|
+
* async listParts(uploadId) {
|
|
66
|
+
* const r = await this.client.send(new ListObjectsV2Command({
|
|
67
|
+
* Bucket: this.bucket, Prefix: `${this.prefix}_multipart/${uploadId}/`
|
|
68
|
+
* }));
|
|
69
|
+
* return (r.Contents ?? [])
|
|
70
|
+
* .map(o => Number(o.Key?.match(/part_(\d+)\.bin$/)?.[1] ?? 0))
|
|
71
|
+
* .filter(n => n > 0)
|
|
72
|
+
* .sort((a, b) => a - b);
|
|
73
|
+
* }
|
|
74
|
+
*
|
|
75
|
+
* // 1.7 mergeParts(GetObject 逐 part 读取 + 写新对象)
|
|
76
|
+
* async mergeParts(uploadId, totalParts, target): Promise<MergeResult> {
|
|
77
|
+
* const key = `${this.prefix}${Date.now()}/${target.id}`;
|
|
78
|
+
* // ... 流式拼接 ...
|
|
79
|
+
* // 推荐用 @aws-sdk/lib-storage 的 Upload 类
|
|
80
|
+
* }
|
|
81
|
+
*
|
|
82
|
+
* // 1.8 computeMergedHash
|
|
83
|
+
* async computeMergedHash(uploadId, totalParts) {
|
|
84
|
+
* const hash = createHash('sha256');
|
|
85
|
+
* for (let i = 1; i <= totalParts; i++) {
|
|
86
|
+
* const obj = await this.client.send(new GetObjectCommand({...}));
|
|
87
|
+
* await pipeline(obj.Body as Readable, async function* (src) {
|
|
88
|
+
* for await (const chunk of src) { hash.update(chunk); yield chunk; }
|
|
89
|
+
* });
|
|
90
|
+
* }
|
|
91
|
+
* return hash.digest('hex');
|
|
92
|
+
* }
|
|
93
|
+
*
|
|
94
|
+
* // 1.9 cleanupMultipart(DeleteObjects 批量)
|
|
95
|
+
* async cleanupMultipart(uploadId) {
|
|
96
|
+
* // 列出所有 part,批量 DeleteObjects
|
|
97
|
+
* }
|
|
98
|
+
* }
|
|
99
|
+
*
|
|
100
|
+
* 2. 接入 SDK:
|
|
101
|
+
*
|
|
102
|
+
* import { S3Client } from '@aws-sdk/client-s3';
|
|
103
|
+
* import { S3Storage } from './s3-storage';
|
|
104
|
+
* import secureUploadPlugin from 'secure-upload-fastify-sdk/server';
|
|
105
|
+
*
|
|
106
|
+
* const s3 = new S3Client({ region: 'us-east-1' });
|
|
107
|
+
* await app.register(secureUploadPlugin, {
|
|
108
|
+
* storage: new S3Storage(s3, 'my-bucket'),
|
|
109
|
+
* repository: { kind: 'prisma', prisma },
|
|
110
|
+
* redis,
|
|
111
|
+
* });
|
|
112
|
+
*
|
|
113
|
+
* 3. 阿里云 OSS / 腾讯云 COS:接口完全一样,只是底层 SDK 替换,实现 StorageAdapter 即可。
|
|
114
|
+
*/
|
|
115
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/server/utils/storage/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,YAAY,EAAE,KAAK,mBAAmB,EAAE,MAAM,SAAS,CAAC;AACjE,OAAO,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAC3C,YAAY,EACV,cAAc,EACd,QAAQ,EACR,SAAS,EACT,WAAW,GACZ,MAAM,SAAS,CAAC;AAEjB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuGG"}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/server/utils/storage/index.ts
|
|
31
|
+
var storage_exports = {};
|
|
32
|
+
__export(storage_exports, {
|
|
33
|
+
InMemoryStorage: () => InMemoryStorage,
|
|
34
|
+
LocalStorage: () => LocalStorage
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(storage_exports);
|
|
37
|
+
|
|
38
|
+
// src/server/utils/storage/local.ts
|
|
39
|
+
var import_node_fs = require("fs");
|
|
40
|
+
var import_node_path = __toESM(require("path"));
|
|
41
|
+
var import_node_crypto = require("crypto");
|
|
42
|
+
var LocalStorage = class {
|
|
43
|
+
baseDir;
|
|
44
|
+
constructor(opts) {
|
|
45
|
+
this.baseDir = typeof opts === "string" ? opts : opts.baseDir;
|
|
46
|
+
}
|
|
47
|
+
// ============================================================
|
|
48
|
+
// 内部工具
|
|
49
|
+
// ============================================================
|
|
50
|
+
inferExt(filename, contentType) {
|
|
51
|
+
const fromName = import_node_path.default.extname(filename).toLowerCase();
|
|
52
|
+
if (fromName) return fromName;
|
|
53
|
+
const map = {
|
|
54
|
+
"image/png": ".png",
|
|
55
|
+
"image/jpeg": ".jpg",
|
|
56
|
+
"image/gif": ".gif",
|
|
57
|
+
"image/webp": ".webp",
|
|
58
|
+
"image/svg+xml": ".svg",
|
|
59
|
+
"application/pdf": ".pdf",
|
|
60
|
+
"text/plain": ".txt",
|
|
61
|
+
"application/json": ".json",
|
|
62
|
+
"application/zip": ".zip",
|
|
63
|
+
"video/mp4": ".mp4"
|
|
64
|
+
};
|
|
65
|
+
return map[contentType] ?? ".bin";
|
|
66
|
+
}
|
|
67
|
+
generateKey(filename, contentType, id) {
|
|
68
|
+
const now = /* @__PURE__ */ new Date();
|
|
69
|
+
const yyyy = now.getFullYear();
|
|
70
|
+
const mm = String(now.getMonth() + 1).padStart(2, "0");
|
|
71
|
+
const dd = String(now.getDate()).padStart(2, "0");
|
|
72
|
+
const ext = this.inferExt(filename, contentType);
|
|
73
|
+
return `${yyyy}/${mm}/${dd}/${id}${ext}`;
|
|
74
|
+
}
|
|
75
|
+
multipartDir(uploadId) {
|
|
76
|
+
return import_node_path.default.resolve(this.baseDir, "_multipart", uploadId);
|
|
77
|
+
}
|
|
78
|
+
partPath(uploadId, partNumber) {
|
|
79
|
+
if (!Number.isInteger(partNumber) || partNumber < 1) {
|
|
80
|
+
throw new Error(`[storage] partNumber \u975E\u6CD5: ${partNumber}`);
|
|
81
|
+
}
|
|
82
|
+
return import_node_path.default.join(this.multipartDir(uploadId), `part_${String(partNumber).padStart(4, "0")}.bin`);
|
|
83
|
+
}
|
|
84
|
+
// ============================================================
|
|
85
|
+
// StorageAdapter 实现
|
|
86
|
+
// ============================================================
|
|
87
|
+
async put(data, meta) {
|
|
88
|
+
const key = this.generateKey(meta.filename, meta.contentType, meta.id);
|
|
89
|
+
const absolutePath = import_node_path.default.resolve(this.baseDir, key);
|
|
90
|
+
await import_node_fs.promises.mkdir(import_node_path.default.dirname(absolutePath), { recursive: true });
|
|
91
|
+
await import_node_fs.promises.writeFile(absolutePath, data);
|
|
92
|
+
return { key, absolutePath };
|
|
93
|
+
}
|
|
94
|
+
get(key) {
|
|
95
|
+
const absolutePath = import_node_path.default.resolve(this.baseDir, key);
|
|
96
|
+
return (0, import_node_fs.createReadStream)(absolutePath);
|
|
97
|
+
}
|
|
98
|
+
async delete(key) {
|
|
99
|
+
const absolutePath = import_node_path.default.resolve(this.baseDir, key);
|
|
100
|
+
try {
|
|
101
|
+
await import_node_fs.promises.unlink(absolutePath);
|
|
102
|
+
} catch (e) {
|
|
103
|
+
if (e.code !== "ENOENT") throw e;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async exists(key) {
|
|
107
|
+
const absolutePath = import_node_path.default.resolve(this.baseDir, key);
|
|
108
|
+
try {
|
|
109
|
+
await import_node_fs.promises.access(absolutePath);
|
|
110
|
+
return true;
|
|
111
|
+
} catch {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async putPart(uploadId, partNumber, data) {
|
|
116
|
+
const dir = this.multipartDir(uploadId);
|
|
117
|
+
await import_node_fs.promises.mkdir(dir, { recursive: true });
|
|
118
|
+
const finalPath = this.partPath(uploadId, partNumber);
|
|
119
|
+
const tmpPath = `${finalPath}.tmp`;
|
|
120
|
+
try {
|
|
121
|
+
await import_node_fs.promises.writeFile(tmpPath, data);
|
|
122
|
+
await import_node_fs.promises.rename(tmpPath, finalPath);
|
|
123
|
+
} catch (e) {
|
|
124
|
+
try {
|
|
125
|
+
await import_node_fs.promises.unlink(tmpPath);
|
|
126
|
+
} catch {
|
|
127
|
+
}
|
|
128
|
+
throw e;
|
|
129
|
+
}
|
|
130
|
+
return { size: data.length, path: finalPath };
|
|
131
|
+
}
|
|
132
|
+
async listParts(uploadId) {
|
|
133
|
+
const dir = this.multipartDir(uploadId);
|
|
134
|
+
try {
|
|
135
|
+
const names = await import_node_fs.promises.readdir(dir);
|
|
136
|
+
const nums = [];
|
|
137
|
+
for (const n of names) {
|
|
138
|
+
const m = n.match(/^part_(\d{4,})\.bin$/);
|
|
139
|
+
if (m) nums.push(parseInt(m[1], 10));
|
|
140
|
+
}
|
|
141
|
+
nums.sort((a, b) => a - b);
|
|
142
|
+
return nums;
|
|
143
|
+
} catch (e) {
|
|
144
|
+
if (e.code === "ENOENT") return [];
|
|
145
|
+
throw e;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
async partSize(uploadId, partNumber) {
|
|
149
|
+
const p = this.partPath(uploadId, partNumber);
|
|
150
|
+
const stat = await import_node_fs.promises.stat(p);
|
|
151
|
+
return stat.size;
|
|
152
|
+
}
|
|
153
|
+
async mergeParts(uploadId, totalParts, target) {
|
|
154
|
+
const key = this.generateKey(target.filename, target.contentType, target.id);
|
|
155
|
+
const absolutePath = import_node_path.default.resolve(this.baseDir, key);
|
|
156
|
+
await import_node_fs.promises.mkdir(import_node_path.default.dirname(absolutePath), { recursive: true });
|
|
157
|
+
const tmpPath = `${absolutePath}.merge.tmp`;
|
|
158
|
+
const out = (0, import_node_fs.createWriteStream)(tmpPath);
|
|
159
|
+
try {
|
|
160
|
+
for (let i = 1; i <= totalParts; i++) {
|
|
161
|
+
const partPath = this.partPath(uploadId, i);
|
|
162
|
+
try {
|
|
163
|
+
await import_node_fs.promises.access(partPath);
|
|
164
|
+
} catch {
|
|
165
|
+
throw new Error(`[storage] \u7F3A\u5206\u7247 part_${i} (uploadId=${uploadId})`);
|
|
166
|
+
}
|
|
167
|
+
await new Promise((resolve, reject) => {
|
|
168
|
+
const rs = (0, import_node_fs.createReadStream)(partPath);
|
|
169
|
+
rs.on("error", reject);
|
|
170
|
+
rs.on("end", resolve);
|
|
171
|
+
rs.pipe(out, { end: false });
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
out.end();
|
|
175
|
+
await new Promise((resolve, reject) => {
|
|
176
|
+
out.on("finish", () => resolve());
|
|
177
|
+
out.on("error", reject);
|
|
178
|
+
});
|
|
179
|
+
const stat = await import_node_fs.promises.stat(tmpPath);
|
|
180
|
+
await import_node_fs.promises.rename(tmpPath, absolutePath);
|
|
181
|
+
return { key, absolutePath, size: stat.size };
|
|
182
|
+
} catch (e) {
|
|
183
|
+
try {
|
|
184
|
+
await import_node_fs.promises.unlink(tmpPath);
|
|
185
|
+
} catch {
|
|
186
|
+
}
|
|
187
|
+
throw e;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
async cleanupMultipart(uploadId) {
|
|
191
|
+
const dir = this.multipartDir(uploadId);
|
|
192
|
+
try {
|
|
193
|
+
await import_node_fs.promises.rm(dir, { recursive: true, force: true });
|
|
194
|
+
} catch (e) {
|
|
195
|
+
if (e.code !== "ENOENT") {
|
|
196
|
+
console.warn(`[storage] cleanup multipart failed: uploadId=${uploadId} err=${e.message}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
async computeMergedHash(uploadId, totalParts) {
|
|
201
|
+
const hash = (0, import_node_crypto.createHash)("sha256");
|
|
202
|
+
for (let i = 1; i <= totalParts; i++) {
|
|
203
|
+
const partPath = this.partPath(uploadId, i);
|
|
204
|
+
const stream = (0, import_node_fs.createReadStream)(partPath);
|
|
205
|
+
await new Promise((resolve, reject) => {
|
|
206
|
+
stream.on("data", (chunk) => hash.update(chunk));
|
|
207
|
+
stream.on("end", resolve);
|
|
208
|
+
stream.on("error", reject);
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
return hash.digest("hex");
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
// src/server/utils/storage/memory.ts
|
|
216
|
+
var import_node_stream = require("stream");
|
|
217
|
+
var import_node_crypto2 = require("crypto");
|
|
218
|
+
var InMemoryStorage = class {
|
|
219
|
+
files = /* @__PURE__ */ new Map();
|
|
220
|
+
parts = /* @__PURE__ */ new Map();
|
|
221
|
+
async put(data, meta) {
|
|
222
|
+
const key = `mem/${meta.id}`;
|
|
223
|
+
this.files.set(key, Buffer.from(data));
|
|
224
|
+
return { key };
|
|
225
|
+
}
|
|
226
|
+
get(key) {
|
|
227
|
+
const buf = this.files.get(key);
|
|
228
|
+
if (!buf) throw new Error(`[memory-storage] not found: ${key}`);
|
|
229
|
+
return import_node_stream.Readable.from(buf);
|
|
230
|
+
}
|
|
231
|
+
async delete(key) {
|
|
232
|
+
this.files.delete(key);
|
|
233
|
+
}
|
|
234
|
+
async exists(key) {
|
|
235
|
+
return this.files.has(key);
|
|
236
|
+
}
|
|
237
|
+
async putPart(uploadId, partNumber, data) {
|
|
238
|
+
let m = this.parts.get(uploadId);
|
|
239
|
+
if (!m) {
|
|
240
|
+
m = /* @__PURE__ */ new Map();
|
|
241
|
+
this.parts.set(uploadId, m);
|
|
242
|
+
}
|
|
243
|
+
m.set(partNumber, Buffer.from(data));
|
|
244
|
+
return { size: data.length };
|
|
245
|
+
}
|
|
246
|
+
async listParts(uploadId) {
|
|
247
|
+
const m = this.parts.get(uploadId);
|
|
248
|
+
if (!m) return [];
|
|
249
|
+
return [...m.keys()].sort((a, b) => a - b);
|
|
250
|
+
}
|
|
251
|
+
async mergeParts(uploadId, totalParts, target) {
|
|
252
|
+
const m = this.parts.get(uploadId);
|
|
253
|
+
if (!m) throw new Error(`[memory-storage] no parts for ${uploadId}`);
|
|
254
|
+
const parts = [];
|
|
255
|
+
for (let i = 1; i <= totalParts; i++) {
|
|
256
|
+
const p = m.get(i);
|
|
257
|
+
if (!p) throw new Error(`[memory-storage] missing part ${i}`);
|
|
258
|
+
parts.push(p);
|
|
259
|
+
}
|
|
260
|
+
const merged = Buffer.concat(parts);
|
|
261
|
+
const key = `mem/${target.id}`;
|
|
262
|
+
this.files.set(key, merged);
|
|
263
|
+
return { key, size: merged.length };
|
|
264
|
+
}
|
|
265
|
+
async computeMergedHash(uploadId, totalParts) {
|
|
266
|
+
const hash = (0, import_node_crypto2.createHash)("sha256");
|
|
267
|
+
const m = this.parts.get(uploadId);
|
|
268
|
+
if (!m) throw new Error(`[memory-storage] no parts for ${uploadId}`);
|
|
269
|
+
for (let i = 1; i <= totalParts; i++) {
|
|
270
|
+
const p = m.get(i);
|
|
271
|
+
if (!p) throw new Error(`[memory-storage] missing part ${i}`);
|
|
272
|
+
hash.update(p);
|
|
273
|
+
}
|
|
274
|
+
return hash.digest("hex");
|
|
275
|
+
}
|
|
276
|
+
async cleanupMultipart(uploadId) {
|
|
277
|
+
this.parts.delete(uploadId);
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
281
|
+
0 && (module.exports = {
|
|
282
|
+
InMemoryStorage,
|
|
283
|
+
LocalStorage
|
|
284
|
+
});
|
|
285
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/server/utils/storage/index.ts","../../../../src/server/utils/storage/local.ts","../../../../src/server/utils/storage/memory.ts"],"sourcesContent":["/**\r\n * Storage 适配器统一导出\r\n *\r\n * 用法:\r\n * import { LocalStorage, InMemoryStorage } from 'secure-upload-fastify-sdk/storage';\r\n * import type { StorageAdapter, FileMeta } from 'secure-upload-fastify-sdk/storage';\r\n */\r\n\r\nexport { LocalStorage, type LocalStorageOptions } from './local';\r\nexport { InMemoryStorage } from './memory';\r\nexport type {\r\n StorageAdapter,\r\n FileMeta,\r\n PutResult,\r\n MergeResult,\r\n} from './types';\r\n\r\n/**\r\n * 如何扩展 S3 / 阿里云 OSS / 腾讯云 COS\r\n * ========================================\r\n *\r\n * 1. 实现 StorageAdapter 接口(本目录下写一个 s3.ts):\r\n *\r\n * import { S3Client, PutObjectCommand, GetObjectCommand,\r\n * DeleteObjectCommand, HeadObjectCommand } from '@aws-sdk/client-s3';\r\n * import type { StorageAdapter, FileMeta, MergeResult } from './types';\r\n *\r\n * export class S3Storage implements StorageAdapter {\r\n * constructor(\r\n * private client: S3Client,\r\n * private bucket: string,\r\n * private prefix = 'uploads/'\r\n * ) {}\r\n *\r\n * // 1.1 put\r\n * async put(data, meta) {\r\n * const key = `${this.prefix}${Date.now()}/${meta.id}`;\r\n * await this.client.send(new PutObjectCommand({\r\n * Bucket: this.bucket, Key: key, Body: data,\r\n * ContentType: meta.contentType,\r\n * }));\r\n * return { key };\r\n * }\r\n *\r\n * // 1.2 get\r\n * get(key) {\r\n * const obj = await this.client.send(new GetObjectCommand({...}));\r\n * return obj.Body as Readable;\r\n * }\r\n *\r\n * // 1.3 delete\r\n * async delete(key) { await this.client.send(new DeleteObjectCommand({...})); }\r\n *\r\n * // 1.4 exists\r\n * async exists(key) {\r\n * try { await this.client.send(new HeadObjectCommand({...})); return true; }\r\n * catch (e) { if (e.$metadata?.httpStatusCode === 404) return false; throw e; }\r\n * }\r\n *\r\n * // 1.5 putPart(list 形式 + S3 multipart upload API)\r\n * // 这里有两种实现:\r\n * // a. 简单版:每个 part 当一个独立对象,key = `${uploadId}/part_0001.bin`\r\n * // b. 进阶版:用 S3 原生 multipart upload(更省 API 调用,但更复杂)\r\n * // 推荐 (a) MVP 阶段够用,merge 时按 list + get 拼接\r\n * async putPart(uploadId, partNumber, data) {\r\n * const key = `${this.prefix}_multipart/${uploadId}/part_${String(partNumber).padStart(4, '0')}.bin`;\r\n * await this.client.send(new PutObjectCommand({...}));\r\n * return { size: data.length };\r\n * }\r\n *\r\n * // 1.6 listParts(ListObjectsV2)\r\n * async listParts(uploadId) {\r\n * const r = await this.client.send(new ListObjectsV2Command({\r\n * Bucket: this.bucket, Prefix: `${this.prefix}_multipart/${uploadId}/`\r\n * }));\r\n * return (r.Contents ?? [])\r\n * .map(o => Number(o.Key?.match(/part_(\\d+)\\.bin$/)?.[1] ?? 0))\r\n * .filter(n => n > 0)\r\n * .sort((a, b) => a - b);\r\n * }\r\n *\r\n * // 1.7 mergeParts(GetObject 逐 part 读取 + 写新对象)\r\n * async mergeParts(uploadId, totalParts, target): Promise<MergeResult> {\r\n * const key = `${this.prefix}${Date.now()}/${target.id}`;\r\n * // ... 流式拼接 ...\r\n * // 推荐用 @aws-sdk/lib-storage 的 Upload 类\r\n * }\r\n *\r\n * // 1.8 computeMergedHash\r\n * async computeMergedHash(uploadId, totalParts) {\r\n * const hash = createHash('sha256');\r\n * for (let i = 1; i <= totalParts; i++) {\r\n * const obj = await this.client.send(new GetObjectCommand({...}));\r\n * await pipeline(obj.Body as Readable, async function* (src) {\r\n * for await (const chunk of src) { hash.update(chunk); yield chunk; }\r\n * });\r\n * }\r\n * return hash.digest('hex');\r\n * }\r\n *\r\n * // 1.9 cleanupMultipart(DeleteObjects 批量)\r\n * async cleanupMultipart(uploadId) {\r\n * // 列出所有 part,批量 DeleteObjects\r\n * }\r\n * }\r\n *\r\n * 2. 接入 SDK:\r\n *\r\n * import { S3Client } from '@aws-sdk/client-s3';\r\n * import { S3Storage } from './s3-storage';\r\n * import secureUploadPlugin from 'secure-upload-fastify-sdk/server';\r\n *\r\n * const s3 = new S3Client({ region: 'us-east-1' });\r\n * await app.register(secureUploadPlugin, {\r\n * storage: new S3Storage(s3, 'my-bucket'),\r\n * repository: { kind: 'prisma', prisma },\r\n * redis,\r\n * });\r\n *\r\n * 3. 阿里云 OSS / 腾讯云 COS:接口完全一样,只是底层 SDK 替换,实现 StorageAdapter 即可。\r\n */\r\n","/**\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":"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","names":["path","fs","import_node_crypto"]}
|