@zucker-framework/storage 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/LICENSE +201 -0
- package/NOTICE +22 -0
- package/THIRD_PARTY_NOTICES +4 -0
- package/dist/index.d.mts +433 -0
- package/dist/index.d.ts +433 -0
- package/dist/index.js +1081 -0
- package/dist/index.mjs +1035 -0
- package/package.json +44 -0
- package/scripts/r2-storage-canary.mjs +84 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1081 @@
|
|
|
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 __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
9
|
+
var __export = (target, all) => {
|
|
10
|
+
for (var name in all)
|
|
11
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
12
|
+
};
|
|
13
|
+
var __copyProps = (to, from, except, desc) => {
|
|
14
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
15
|
+
for (let key of __getOwnPropNames(from))
|
|
16
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
17
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
18
|
+
}
|
|
19
|
+
return to;
|
|
20
|
+
};
|
|
21
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
22
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
23
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
24
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
25
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
26
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
27
|
+
mod
|
|
28
|
+
));
|
|
29
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
30
|
+
|
|
31
|
+
// src/index.ts
|
|
32
|
+
var index_exports = {};
|
|
33
|
+
__export(index_exports, {
|
|
34
|
+
DANGEROUS_EXTENSIONS: () => DANGEROUS_EXTENSIONS,
|
|
35
|
+
FILE_MANAGER: () => FILE_MANAGER,
|
|
36
|
+
FileLifecycleManager: () => FileLifecycleManager,
|
|
37
|
+
LocalFileStorage: () => LocalFileStorage,
|
|
38
|
+
S3Storage: () => S3Storage,
|
|
39
|
+
StorageProvider: () => StorageProvider,
|
|
40
|
+
WebDavStorage: () => WebDavStorage,
|
|
41
|
+
ZuckerStorageModule: () => ZuckerStorageModule,
|
|
42
|
+
generateDateBasedKey: () => generateDateBasedKey,
|
|
43
|
+
isFileDenied: () => isFileDenied,
|
|
44
|
+
isSafeOriginalFileName: () => isSafeOriginalFileName,
|
|
45
|
+
sanitizeFileName: () => sanitizeFileName
|
|
46
|
+
});
|
|
47
|
+
module.exports = __toCommonJS(index_exports);
|
|
48
|
+
|
|
49
|
+
// src/storage.interface.ts
|
|
50
|
+
var DANGEROUS_EXTENSIONS = [
|
|
51
|
+
"exe",
|
|
52
|
+
"bat",
|
|
53
|
+
"cmd",
|
|
54
|
+
"com",
|
|
55
|
+
"msi",
|
|
56
|
+
"scr",
|
|
57
|
+
"pif",
|
|
58
|
+
"vbs",
|
|
59
|
+
"vbe",
|
|
60
|
+
"js",
|
|
61
|
+
"jse",
|
|
62
|
+
"wsf",
|
|
63
|
+
"wsh",
|
|
64
|
+
"ps1",
|
|
65
|
+
"sh",
|
|
66
|
+
"bash",
|
|
67
|
+
"csh",
|
|
68
|
+
"ksh",
|
|
69
|
+
"php",
|
|
70
|
+
"phtml",
|
|
71
|
+
"php3",
|
|
72
|
+
"php4",
|
|
73
|
+
"php5",
|
|
74
|
+
"phps",
|
|
75
|
+
"asp",
|
|
76
|
+
"aspx",
|
|
77
|
+
"jsp",
|
|
78
|
+
"jspx",
|
|
79
|
+
"cgi",
|
|
80
|
+
"pl",
|
|
81
|
+
"py",
|
|
82
|
+
"rb",
|
|
83
|
+
"dll",
|
|
84
|
+
"so",
|
|
85
|
+
"dylib",
|
|
86
|
+
"class",
|
|
87
|
+
"jar",
|
|
88
|
+
"war",
|
|
89
|
+
"htaccess",
|
|
90
|
+
"htpasswd",
|
|
91
|
+
"svg"
|
|
92
|
+
];
|
|
93
|
+
function sanitizeFileName(fileName) {
|
|
94
|
+
let normalized = fileName.normalize("NFKC");
|
|
95
|
+
normalized = normalized.replace(/\\/g, "/");
|
|
96
|
+
const lastSlash = normalized.lastIndexOf("/");
|
|
97
|
+
if (lastSlash >= 0) {
|
|
98
|
+
normalized = normalized.substring(lastSlash + 1);
|
|
99
|
+
}
|
|
100
|
+
normalized = normalized.replace(/[\x00-\x1f\x7f]/g, "");
|
|
101
|
+
if (!normalized) {
|
|
102
|
+
throw new Error("\u975E\u6CD5\u6587\u4EF6\u540D");
|
|
103
|
+
}
|
|
104
|
+
return normalized;
|
|
105
|
+
}
|
|
106
|
+
__name(sanitizeFileName, "sanitizeFileName");
|
|
107
|
+
function isSafeOriginalFileName(name) {
|
|
108
|
+
return /^[a-zA-Z0-9._-]+$/.test(name);
|
|
109
|
+
}
|
|
110
|
+
__name(isSafeOriginalFileName, "isSafeOriginalFileName");
|
|
111
|
+
function isFileDenied(fileName, mimeType, validation, fileSize) {
|
|
112
|
+
const sanitized = sanitizeFileName(fileName);
|
|
113
|
+
const dotIndex = sanitized.lastIndexOf(".");
|
|
114
|
+
const ext = dotIndex >= 0 ? sanitized.substring(dotIndex + 1).toLowerCase() : "";
|
|
115
|
+
if (validation.maxFileSize && validation.maxFileSize > 0 && fileSize !== void 0) {
|
|
116
|
+
if (fileSize > validation.maxFileSize) return true;
|
|
117
|
+
}
|
|
118
|
+
let defaultDeny = false;
|
|
119
|
+
if (validation.denyExtensions?.length) {
|
|
120
|
+
if (validation.denyExtensions.includes(ext)) return true;
|
|
121
|
+
defaultDeny = false;
|
|
122
|
+
}
|
|
123
|
+
if (validation.allowExtensions?.length) {
|
|
124
|
+
if (validation.allowExtensions.includes(ext)) return false;
|
|
125
|
+
defaultDeny = true;
|
|
126
|
+
}
|
|
127
|
+
if (mimeType && validation.denyMimeTypes?.length) {
|
|
128
|
+
if (validation.denyMimeTypes.includes(mimeType)) return true;
|
|
129
|
+
defaultDeny = false;
|
|
130
|
+
}
|
|
131
|
+
if (mimeType && validation.allowMimeTypes?.length) {
|
|
132
|
+
if (validation.allowMimeTypes.includes(mimeType)) return false;
|
|
133
|
+
defaultDeny = true;
|
|
134
|
+
}
|
|
135
|
+
return defaultDeny;
|
|
136
|
+
}
|
|
137
|
+
__name(isFileDenied, "isFileDenied");
|
|
138
|
+
function generateDateBasedKey(originalName, options) {
|
|
139
|
+
const safeName = sanitizeFileName(originalName);
|
|
140
|
+
const now = /* @__PURE__ */ new Date();
|
|
141
|
+
const datePart = [
|
|
142
|
+
now.getFullYear().toString(),
|
|
143
|
+
String(now.getMonth() + 1).padStart(2, "0"),
|
|
144
|
+
String(now.getDate()).padStart(2, "0")
|
|
145
|
+
].join("");
|
|
146
|
+
const dotIndex = safeName.lastIndexOf(".");
|
|
147
|
+
const ext = dotIndex >= 0 ? safeName.substring(dotIndex) : "";
|
|
148
|
+
const prefix = options?.prefix ? `${options.prefix}/` : "";
|
|
149
|
+
if (options?.useOriginalFileName && isSafeOriginalFileName(safeName)) {
|
|
150
|
+
const uniqueDir = `${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
|
|
151
|
+
return `${prefix}${datePart}/${uniqueDir}/${safeName}`;
|
|
152
|
+
}
|
|
153
|
+
const uniqueName = `${Date.now()}-${Math.random().toString(36).substring(2, 8)}${ext}`;
|
|
154
|
+
return `${prefix}${datePart}/${uniqueName}`;
|
|
155
|
+
}
|
|
156
|
+
__name(generateDateBasedKey, "generateDateBasedKey");
|
|
157
|
+
|
|
158
|
+
// src/local-storage.ts
|
|
159
|
+
var import_common = require("@nestjs/common");
|
|
160
|
+
var fs = __toESM(require("fs"));
|
|
161
|
+
var path = __toESM(require("path"));
|
|
162
|
+
var import_node_crypto = require("crypto");
|
|
163
|
+
var import_fs = require("fs");
|
|
164
|
+
function _ts_decorate(decorators, target, key, desc) {
|
|
165
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
166
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
|
|
167
|
+
r = Reflect.decorate(decorators, target, key, desc);
|
|
168
|
+
} else {
|
|
169
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
170
|
+
if (d = decorators[i]) {
|
|
171
|
+
r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
176
|
+
}
|
|
177
|
+
__name(_ts_decorate, "_ts_decorate");
|
|
178
|
+
function _ts_metadata(metadataKey, metadataValue) {
|
|
179
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") {
|
|
180
|
+
return Reflect.metadata(metadataKey, metadataValue);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
__name(_ts_metadata, "_ts_metadata");
|
|
184
|
+
var LocalFileStorage = class _LocalFileStorage {
|
|
185
|
+
static {
|
|
186
|
+
__name(this, "LocalFileStorage");
|
|
187
|
+
}
|
|
188
|
+
logger = new import_common.Logger(_LocalFileStorage.name);
|
|
189
|
+
providerType = "local";
|
|
190
|
+
basePath;
|
|
191
|
+
baseUrl;
|
|
192
|
+
/** Active chunked upload sessions */
|
|
193
|
+
chunkedUploads = /* @__PURE__ */ new Map();
|
|
194
|
+
constructor(options) {
|
|
195
|
+
this.basePath = options.basePath;
|
|
196
|
+
this.baseUrl = options.baseUrl ?? "";
|
|
197
|
+
fs.mkdirSync(this.basePath, {
|
|
198
|
+
recursive: true
|
|
199
|
+
});
|
|
200
|
+
this.logger.log(`\u672C\u5730\u5B58\u50A8\u5DF2\u521D\u59CB\u5316: ${this.basePath}`);
|
|
201
|
+
}
|
|
202
|
+
isConfigured() {
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
async upload(buffer, key, _options) {
|
|
206
|
+
const filePath = this.resolvePath(key);
|
|
207
|
+
fs.mkdirSync(path.dirname(filePath), {
|
|
208
|
+
recursive: true
|
|
209
|
+
});
|
|
210
|
+
fs.writeFileSync(filePath, buffer);
|
|
211
|
+
return {
|
|
212
|
+
key,
|
|
213
|
+
url: this.buildUrl(key),
|
|
214
|
+
size: buffer.length
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
async uploadStream(stream, key, options) {
|
|
218
|
+
const chunks = [];
|
|
219
|
+
for await (const chunk of stream) {
|
|
220
|
+
chunks.push(Buffer.from(chunk));
|
|
221
|
+
}
|
|
222
|
+
return this.upload(Buffer.concat(chunks), key, options);
|
|
223
|
+
}
|
|
224
|
+
async download(key) {
|
|
225
|
+
const filePath = this.resolvePath(key);
|
|
226
|
+
const stat = fs.statSync(filePath);
|
|
227
|
+
return {
|
|
228
|
+
stream: (0, import_fs.createReadStream)(filePath),
|
|
229
|
+
size: stat.size
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
async delete(key) {
|
|
233
|
+
const filePath = this.resolvePath(key);
|
|
234
|
+
if (fs.existsSync(filePath)) {
|
|
235
|
+
fs.unlinkSync(filePath);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
async exists(key) {
|
|
239
|
+
return fs.existsSync(this.resolvePath(key));
|
|
240
|
+
}
|
|
241
|
+
async getPublicUrl(key) {
|
|
242
|
+
return this.buildUrl(key);
|
|
243
|
+
}
|
|
244
|
+
async getDownloadUrl(key) {
|
|
245
|
+
return this.getPublicUrl(key);
|
|
246
|
+
}
|
|
247
|
+
async list(prefix) {
|
|
248
|
+
const dirPath = this.resolvePath(prefix);
|
|
249
|
+
if (!fs.existsSync(dirPath)) return [];
|
|
250
|
+
const entries = fs.readdirSync(dirPath, {
|
|
251
|
+
withFileTypes: true
|
|
252
|
+
});
|
|
253
|
+
return entries.filter((e) => e.isFile()).map((e) => {
|
|
254
|
+
const filePath = path.join(dirPath, e.name);
|
|
255
|
+
const stat = fs.statSync(filePath);
|
|
256
|
+
return {
|
|
257
|
+
key: path.join(prefix, e.name),
|
|
258
|
+
size: stat.size,
|
|
259
|
+
lastModified: stat.mtime
|
|
260
|
+
};
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
async downloadRange(key, position, length) {
|
|
264
|
+
const filePath = this.resolvePath(key);
|
|
265
|
+
const stat = fs.statSync(filePath);
|
|
266
|
+
const end = length !== void 0 ? Math.min(position + length - 1, stat.size - 1) : stat.size - 1;
|
|
267
|
+
const readSize = end - position + 1;
|
|
268
|
+
return {
|
|
269
|
+
stream: (0, import_fs.createReadStream)(filePath, {
|
|
270
|
+
start: position,
|
|
271
|
+
end
|
|
272
|
+
}),
|
|
273
|
+
size: readSize
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
async getFileInfo(key) {
|
|
277
|
+
const filePath = this.resolvePath(key);
|
|
278
|
+
if (!fs.existsSync(filePath)) return void 0;
|
|
279
|
+
const stat = fs.statSync(filePath);
|
|
280
|
+
if (!stat.isFile()) return void 0;
|
|
281
|
+
return {
|
|
282
|
+
key,
|
|
283
|
+
size: stat.size,
|
|
284
|
+
lastModified: stat.mtime
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
async copy(sourceKey, destKey) {
|
|
288
|
+
const src = this.resolvePath(sourceKey);
|
|
289
|
+
const dest = this.resolvePath(destKey);
|
|
290
|
+
fs.mkdirSync(path.dirname(dest), {
|
|
291
|
+
recursive: true
|
|
292
|
+
});
|
|
293
|
+
fs.copyFileSync(src, dest);
|
|
294
|
+
}
|
|
295
|
+
async move(sourceKey, destKey) {
|
|
296
|
+
const src = this.resolvePath(sourceKey);
|
|
297
|
+
const dest = this.resolvePath(destKey);
|
|
298
|
+
fs.mkdirSync(path.dirname(dest), {
|
|
299
|
+
recursive: true
|
|
300
|
+
});
|
|
301
|
+
fs.renameSync(src, dest);
|
|
302
|
+
}
|
|
303
|
+
async initChunkedUpload(key, totalChunks, totalSize) {
|
|
304
|
+
const uploadId = (0, import_node_crypto.randomUUID)();
|
|
305
|
+
const chunksDir = path.join(this.basePath, ".chunks", uploadId);
|
|
306
|
+
fs.mkdirSync(chunksDir, {
|
|
307
|
+
recursive: true
|
|
308
|
+
});
|
|
309
|
+
this.chunkedUploads.set(uploadId, {
|
|
310
|
+
uploadId,
|
|
311
|
+
key,
|
|
312
|
+
totalChunks,
|
|
313
|
+
receivedChunks: [],
|
|
314
|
+
totalSize,
|
|
315
|
+
complete: false
|
|
316
|
+
});
|
|
317
|
+
this.logger.log(`Chunked upload initialized: ${uploadId} for key=${key}, totalChunks=${totalChunks}`);
|
|
318
|
+
return uploadId;
|
|
319
|
+
}
|
|
320
|
+
async uploadChunk(uploadId, chunkIndex, buffer) {
|
|
321
|
+
const state = this.chunkedUploads.get(uploadId);
|
|
322
|
+
if (!state) {
|
|
323
|
+
throw new Error(`Unknown upload session: ${uploadId}`);
|
|
324
|
+
}
|
|
325
|
+
if (chunkIndex < 0 || chunkIndex >= state.totalChunks) {
|
|
326
|
+
throw new Error(`Invalid chunk index ${chunkIndex} (expected 0-${state.totalChunks - 1})`);
|
|
327
|
+
}
|
|
328
|
+
const chunkPath = path.join(this.basePath, ".chunks", uploadId, `chunk-${chunkIndex}`);
|
|
329
|
+
fs.writeFileSync(chunkPath, buffer);
|
|
330
|
+
state.receivedChunks.push(chunkIndex);
|
|
331
|
+
}
|
|
332
|
+
async completeChunkedUpload(uploadId, _options) {
|
|
333
|
+
const state = this.chunkedUploads.get(uploadId);
|
|
334
|
+
if (!state) {
|
|
335
|
+
throw new Error(`Unknown upload session: ${uploadId}`);
|
|
336
|
+
}
|
|
337
|
+
const missing = [];
|
|
338
|
+
for (let i = 0; i < state.totalChunks; i++) {
|
|
339
|
+
if (!state.receivedChunks.includes(i)) {
|
|
340
|
+
missing.push(i);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (missing.length > 0) {
|
|
344
|
+
throw new Error(`Missing chunks: ${missing.join(", ")}`);
|
|
345
|
+
}
|
|
346
|
+
const filePath = this.resolvePath(state.key);
|
|
347
|
+
fs.mkdirSync(path.dirname(filePath), {
|
|
348
|
+
recursive: true
|
|
349
|
+
});
|
|
350
|
+
const writeStream = fs.createWriteStream(filePath);
|
|
351
|
+
for (let i = 0; i < state.totalChunks; i++) {
|
|
352
|
+
const chunkPath = path.join(this.basePath, ".chunks", uploadId, `chunk-${i}`);
|
|
353
|
+
const chunkData = fs.readFileSync(chunkPath);
|
|
354
|
+
writeStream.write(chunkData);
|
|
355
|
+
}
|
|
356
|
+
writeStream.end();
|
|
357
|
+
await new Promise((resolve2, reject) => {
|
|
358
|
+
writeStream.on("finish", resolve2);
|
|
359
|
+
writeStream.on("error", reject);
|
|
360
|
+
});
|
|
361
|
+
const chunksDir = path.join(this.basePath, ".chunks", uploadId);
|
|
362
|
+
fs.rmSync(chunksDir, {
|
|
363
|
+
recursive: true,
|
|
364
|
+
force: true
|
|
365
|
+
});
|
|
366
|
+
state.complete = true;
|
|
367
|
+
this.chunkedUploads.delete(uploadId);
|
|
368
|
+
const stat = fs.statSync(filePath);
|
|
369
|
+
return {
|
|
370
|
+
key: state.key,
|
|
371
|
+
url: this.buildUrl(state.key),
|
|
372
|
+
size: stat.size
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
async abortChunkedUpload(uploadId) {
|
|
376
|
+
const chunksDir = path.join(this.basePath, ".chunks", uploadId);
|
|
377
|
+
if (fs.existsSync(chunksDir)) {
|
|
378
|
+
fs.rmSync(chunksDir, {
|
|
379
|
+
recursive: true,
|
|
380
|
+
force: true
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
this.chunkedUploads.delete(uploadId);
|
|
384
|
+
this.logger.log(`Chunked upload aborted: ${uploadId}`);
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* 便捷文件保存
|
|
388
|
+
* 自动根据文件类型生成日期归档路径并保存。
|
|
389
|
+
*/
|
|
390
|
+
async saveFile(buffer, fileType, options) {
|
|
391
|
+
const normalizedType = fileType.startsWith(".") ? fileType : `.${fileType}`;
|
|
392
|
+
const key = generateDateBasedKey(`_temp${normalizedType}`);
|
|
393
|
+
return this.upload(buffer, key, options);
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* 应用 POSIX 文件权限
|
|
397
|
+
* 使用 fs.chmodSync 设置文件权限,非 POSIX 系统静默忽略错误。
|
|
398
|
+
*/
|
|
399
|
+
async applyFilePermission(key, permissions) {
|
|
400
|
+
const filePath = this.resolvePath(key);
|
|
401
|
+
try {
|
|
402
|
+
fs.chmodSync(filePath, permissions.mode);
|
|
403
|
+
} catch {
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Resolve a storage key to an absolute file path.
|
|
408
|
+
* Validates against path traversal attacks (e.g. "../../../etc/passwd").
|
|
409
|
+
*/
|
|
410
|
+
resolvePath(key) {
|
|
411
|
+
const resolved = path.resolve(this.basePath, key);
|
|
412
|
+
const normalizedBase = path.resolve(this.basePath);
|
|
413
|
+
if (!resolved.startsWith(normalizedBase + path.sep) && resolved !== normalizedBase) {
|
|
414
|
+
throw new Error(`Path traversal detected: key "${key}" resolves outside base path`);
|
|
415
|
+
}
|
|
416
|
+
return resolved;
|
|
417
|
+
}
|
|
418
|
+
buildUrl(key) {
|
|
419
|
+
return this.baseUrl ? `${this.baseUrl}/${key}` : key;
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
LocalFileStorage = _ts_decorate([
|
|
423
|
+
(0, import_common.Injectable)(),
|
|
424
|
+
_ts_metadata("design:type", Function),
|
|
425
|
+
_ts_metadata("design:paramtypes", [
|
|
426
|
+
Object
|
|
427
|
+
])
|
|
428
|
+
], LocalFileStorage);
|
|
429
|
+
|
|
430
|
+
// src/s3-storage.ts
|
|
431
|
+
var import_common2 = require("@nestjs/common");
|
|
432
|
+
var import_client_s3 = require("@aws-sdk/client-s3");
|
|
433
|
+
var import_s3_request_presigner = require("@aws-sdk/s3-request-presigner");
|
|
434
|
+
function _ts_decorate2(decorators, target, key, desc) {
|
|
435
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
436
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
|
|
437
|
+
r = Reflect.decorate(decorators, target, key, desc);
|
|
438
|
+
} else {
|
|
439
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
440
|
+
if (d = decorators[i]) {
|
|
441
|
+
r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
446
|
+
}
|
|
447
|
+
__name(_ts_decorate2, "_ts_decorate");
|
|
448
|
+
function _ts_metadata2(metadataKey, metadataValue) {
|
|
449
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") {
|
|
450
|
+
return Reflect.metadata(metadataKey, metadataValue);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
__name(_ts_metadata2, "_ts_metadata");
|
|
454
|
+
var S3Storage = class _S3Storage {
|
|
455
|
+
static {
|
|
456
|
+
__name(this, "S3Storage");
|
|
457
|
+
}
|
|
458
|
+
logger = new import_common2.Logger(_S3Storage.name);
|
|
459
|
+
client;
|
|
460
|
+
bucket;
|
|
461
|
+
publicUrl;
|
|
462
|
+
objectAclMode;
|
|
463
|
+
uploadUrlMode;
|
|
464
|
+
signedUrlExpiresIn;
|
|
465
|
+
providerType = "s3";
|
|
466
|
+
constructor(options) {
|
|
467
|
+
this.bucket = options.bucket;
|
|
468
|
+
this.publicUrl = options.publicUrl ?? "";
|
|
469
|
+
this.objectAclMode = options.objectAclMode ?? "object-acl";
|
|
470
|
+
this.uploadUrlMode = options.uploadUrlMode ?? "signed";
|
|
471
|
+
this.signedUrlExpiresIn = options.signedUrlExpiresIn ?? 3600;
|
|
472
|
+
this.client = new import_client_s3.S3Client({
|
|
473
|
+
region: options.region ?? "us-east-1",
|
|
474
|
+
endpoint: options.endpoint,
|
|
475
|
+
credentials: {
|
|
476
|
+
accessKeyId: options.accessKeyId,
|
|
477
|
+
secretAccessKey: options.secretAccessKey
|
|
478
|
+
},
|
|
479
|
+
forcePathStyle: options.forcePathStyle ?? Boolean(options.endpoint)
|
|
480
|
+
});
|
|
481
|
+
this.logger.log("S3 storage initialized");
|
|
482
|
+
}
|
|
483
|
+
onModuleDestroy() {
|
|
484
|
+
this.client.destroy();
|
|
485
|
+
this.logger.log("S3 \u5BA2\u6237\u7AEF\u5DF2\u5173\u95ED");
|
|
486
|
+
}
|
|
487
|
+
isConfigured() {
|
|
488
|
+
return true;
|
|
489
|
+
}
|
|
490
|
+
async upload(buffer, key, options) {
|
|
491
|
+
if (this.objectAclMode === "none" && options?.acl === "public-read") {
|
|
492
|
+
throw new Error("Object-level public ACL is disabled for this storage provider");
|
|
493
|
+
}
|
|
494
|
+
const input = {
|
|
495
|
+
Bucket: this.bucket,
|
|
496
|
+
Key: key,
|
|
497
|
+
Body: buffer,
|
|
498
|
+
ContentType: options?.contentType ?? "application/octet-stream",
|
|
499
|
+
Metadata: options?.metadata,
|
|
500
|
+
...this.objectAclMode === "object-acl" && options?.acl ? {
|
|
501
|
+
ACL: options.acl
|
|
502
|
+
} : {}
|
|
503
|
+
};
|
|
504
|
+
await this.client.send(new import_client_s3.PutObjectCommand(input));
|
|
505
|
+
return {
|
|
506
|
+
key,
|
|
507
|
+
url: this.publicUrl ? await this.getPublicUrl(key) : this.uploadUrlMode === "signed" ? await this.getDownloadUrl(key) : "",
|
|
508
|
+
size: buffer.length
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
async uploadStream(stream, key, options) {
|
|
512
|
+
const chunks = [];
|
|
513
|
+
for await (const chunk of stream) {
|
|
514
|
+
chunks.push(Buffer.from(chunk));
|
|
515
|
+
}
|
|
516
|
+
return this.upload(Buffer.concat(chunks), key, options);
|
|
517
|
+
}
|
|
518
|
+
async download(key) {
|
|
519
|
+
const response = await this.client.send(new import_client_s3.GetObjectCommand({
|
|
520
|
+
Bucket: this.bucket,
|
|
521
|
+
Key: key
|
|
522
|
+
}));
|
|
523
|
+
if (!response.Body) {
|
|
524
|
+
throw new Error("Stored object response had no body");
|
|
525
|
+
}
|
|
526
|
+
return {
|
|
527
|
+
stream: response.Body,
|
|
528
|
+
size: response.ContentLength ?? 0
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
async delete(key) {
|
|
532
|
+
await this.client.send(new import_client_s3.DeleteObjectCommand({
|
|
533
|
+
Bucket: this.bucket,
|
|
534
|
+
Key: key
|
|
535
|
+
}));
|
|
536
|
+
}
|
|
537
|
+
async exists(key) {
|
|
538
|
+
try {
|
|
539
|
+
await this.client.send(new import_client_s3.HeadObjectCommand({
|
|
540
|
+
Bucket: this.bucket,
|
|
541
|
+
Key: key
|
|
542
|
+
}));
|
|
543
|
+
return true;
|
|
544
|
+
} catch (error) {
|
|
545
|
+
const s3Err = error;
|
|
546
|
+
if (s3Err.name === "NotFound" || s3Err.$metadata?.httpStatusCode === 404) {
|
|
547
|
+
return false;
|
|
548
|
+
}
|
|
549
|
+
throw error;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
async getPublicUrl(key) {
|
|
553
|
+
if (!this.publicUrl) {
|
|
554
|
+
throw new Error("No public URL is configured for this storage provider");
|
|
555
|
+
}
|
|
556
|
+
return `${this.publicUrl}/${encodeKeyPath(key)}`;
|
|
557
|
+
}
|
|
558
|
+
async getDownloadUrl(key, expiresIn = this.signedUrlExpiresIn) {
|
|
559
|
+
if (this.publicUrl) return this.getPublicUrl(key);
|
|
560
|
+
return (0, import_s3_request_presigner.getSignedUrl)(this.client, new import_client_s3.GetObjectCommand({
|
|
561
|
+
Bucket: this.bucket,
|
|
562
|
+
Key: key
|
|
563
|
+
}), {
|
|
564
|
+
expiresIn
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
async list(prefix) {
|
|
568
|
+
const files = [];
|
|
569
|
+
let continuationToken;
|
|
570
|
+
do {
|
|
571
|
+
const response = await this.client.send(new import_client_s3.ListObjectsV2Command({
|
|
572
|
+
Bucket: this.bucket,
|
|
573
|
+
Prefix: prefix,
|
|
574
|
+
ContinuationToken: continuationToken
|
|
575
|
+
}));
|
|
576
|
+
files.push(...(response.Contents ?? []).map((item) => ({
|
|
577
|
+
key: item.Key ?? "",
|
|
578
|
+
size: item.Size ?? 0,
|
|
579
|
+
lastModified: item.LastModified ?? /* @__PURE__ */ new Date()
|
|
580
|
+
})));
|
|
581
|
+
continuationToken = response.IsTruncated ? response.NextContinuationToken : void 0;
|
|
582
|
+
} while (continuationToken);
|
|
583
|
+
return files;
|
|
584
|
+
}
|
|
585
|
+
async getFileInfo(key) {
|
|
586
|
+
try {
|
|
587
|
+
const response = await this.client.send(new import_client_s3.HeadObjectCommand({
|
|
588
|
+
Bucket: this.bucket,
|
|
589
|
+
Key: key
|
|
590
|
+
}));
|
|
591
|
+
return {
|
|
592
|
+
key,
|
|
593
|
+
size: response.ContentLength ?? 0,
|
|
594
|
+
lastModified: response.LastModified ?? /* @__PURE__ */ new Date(),
|
|
595
|
+
contentType: response.ContentType
|
|
596
|
+
};
|
|
597
|
+
} catch (error) {
|
|
598
|
+
const s3Err = error;
|
|
599
|
+
if (s3Err.name === "NotFound" || s3Err.$metadata?.httpStatusCode === 404) {
|
|
600
|
+
return void 0;
|
|
601
|
+
}
|
|
602
|
+
throw error;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
async copy(sourceKey, destKey) {
|
|
606
|
+
await this.client.send(new import_client_s3.CopyObjectCommand({
|
|
607
|
+
Bucket: this.bucket,
|
|
608
|
+
CopySource: `${this.bucket}/${encodeKeyPath(sourceKey)}`,
|
|
609
|
+
Key: destKey
|
|
610
|
+
}));
|
|
611
|
+
}
|
|
612
|
+
async move(sourceKey, destKey) {
|
|
613
|
+
await this.copy(sourceKey, destKey);
|
|
614
|
+
await this.delete(sourceKey);
|
|
615
|
+
}
|
|
616
|
+
};
|
|
617
|
+
S3Storage = _ts_decorate2([
|
|
618
|
+
(0, import_common2.Injectable)(),
|
|
619
|
+
_ts_metadata2("design:type", Function),
|
|
620
|
+
_ts_metadata2("design:paramtypes", [
|
|
621
|
+
typeof S3StorageOptions === "undefined" ? Object : S3StorageOptions
|
|
622
|
+
])
|
|
623
|
+
], S3Storage);
|
|
624
|
+
function encodeKeyPath(key) {
|
|
625
|
+
return key.split("/").map(encodeURIComponent).join("/");
|
|
626
|
+
}
|
|
627
|
+
__name(encodeKeyPath, "encodeKeyPath");
|
|
628
|
+
|
|
629
|
+
// src/webdav-storage.ts
|
|
630
|
+
var import_common3 = require("@nestjs/common");
|
|
631
|
+
var path2 = __toESM(require("path"));
|
|
632
|
+
var import_stream = require("stream");
|
|
633
|
+
function _ts_decorate3(decorators, target, key, desc) {
|
|
634
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
635
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
|
|
636
|
+
r = Reflect.decorate(decorators, target, key, desc);
|
|
637
|
+
} else {
|
|
638
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
639
|
+
if (d = decorators[i]) {
|
|
640
|
+
r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
645
|
+
}
|
|
646
|
+
__name(_ts_decorate3, "_ts_decorate");
|
|
647
|
+
function _ts_metadata3(metadataKey, metadataValue) {
|
|
648
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") {
|
|
649
|
+
return Reflect.metadata(metadataKey, metadataValue);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
__name(_ts_metadata3, "_ts_metadata");
|
|
653
|
+
var WebDavStorage = class _WebDavStorage {
|
|
654
|
+
static {
|
|
655
|
+
__name(this, "WebDavStorage");
|
|
656
|
+
}
|
|
657
|
+
logger = new import_common3.Logger(_WebDavStorage.name);
|
|
658
|
+
clientPromise;
|
|
659
|
+
publicUrl;
|
|
660
|
+
basePath;
|
|
661
|
+
directoryPromises = /* @__PURE__ */ new Map();
|
|
662
|
+
providerType = "webdav";
|
|
663
|
+
options;
|
|
664
|
+
constructor(options) {
|
|
665
|
+
if (!options.url || !options.username || !options.password) {
|
|
666
|
+
throw new Error("WebDAV url, username and password are required");
|
|
667
|
+
}
|
|
668
|
+
this.options = options;
|
|
669
|
+
this.publicUrl = (options.publicUrl ?? "").replace(/\/$/, "");
|
|
670
|
+
this.basePath = options.basePath?.trim() || "/";
|
|
671
|
+
this.logger.log("WebDAV storage initialized");
|
|
672
|
+
}
|
|
673
|
+
async initClient() {
|
|
674
|
+
const { createClient } = await import("webdav");
|
|
675
|
+
return createClient(this.options.url, {
|
|
676
|
+
username: this.options.username,
|
|
677
|
+
password: this.options.password
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
getClient() {
|
|
681
|
+
this.clientPromise ??= this.initClient();
|
|
682
|
+
return this.clientPromise;
|
|
683
|
+
}
|
|
684
|
+
isConfigured() {
|
|
685
|
+
return true;
|
|
686
|
+
}
|
|
687
|
+
async upload(buffer, key, options) {
|
|
688
|
+
const client = await this.getClient();
|
|
689
|
+
const remotePath = this.resolvePath(key);
|
|
690
|
+
const parentDir = remotePath.substring(0, remotePath.lastIndexOf("/"));
|
|
691
|
+
if (parentDir && parentDir !== "/") {
|
|
692
|
+
await this.ensureDirectory(parentDir);
|
|
693
|
+
}
|
|
694
|
+
await client.putFileContents(remotePath, buffer, {
|
|
695
|
+
contentLength: buffer.length,
|
|
696
|
+
overwrite: true,
|
|
697
|
+
headers: options?.contentType ? {
|
|
698
|
+
"Content-Type": options.contentType
|
|
699
|
+
} : void 0
|
|
700
|
+
});
|
|
701
|
+
return {
|
|
702
|
+
key,
|
|
703
|
+
url: this.buildUrl(key),
|
|
704
|
+
size: buffer.length
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
async uploadStream(stream, key, options) {
|
|
708
|
+
const chunks = [];
|
|
709
|
+
for await (const chunk of stream) {
|
|
710
|
+
chunks.push(Buffer.from(chunk));
|
|
711
|
+
}
|
|
712
|
+
return this.upload(Buffer.concat(chunks), key, options);
|
|
713
|
+
}
|
|
714
|
+
async download(key) {
|
|
715
|
+
const client = await this.getClient();
|
|
716
|
+
const remotePath = this.resolvePath(key);
|
|
717
|
+
const stat = await client.stat(remotePath);
|
|
718
|
+
const buffer = await client.getFileContents(remotePath);
|
|
719
|
+
const readable = import_stream.Readable.from(buffer);
|
|
720
|
+
return {
|
|
721
|
+
stream: readable,
|
|
722
|
+
size: stat.size ?? buffer.length
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
async delete(key) {
|
|
726
|
+
const client = await this.getClient();
|
|
727
|
+
const remotePath = this.resolvePath(key);
|
|
728
|
+
try {
|
|
729
|
+
await client.deleteFile(remotePath);
|
|
730
|
+
} catch (error) {
|
|
731
|
+
const err = error;
|
|
732
|
+
if (err.status === 404) return;
|
|
733
|
+
throw error;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
async exists(key) {
|
|
737
|
+
const client = await this.getClient();
|
|
738
|
+
const remotePath = this.resolvePath(key);
|
|
739
|
+
return client.exists(remotePath);
|
|
740
|
+
}
|
|
741
|
+
async getPublicUrl(key) {
|
|
742
|
+
return this.buildUrl(key);
|
|
743
|
+
}
|
|
744
|
+
async getDownloadUrl(key) {
|
|
745
|
+
return this.getPublicUrl(key);
|
|
746
|
+
}
|
|
747
|
+
async list(prefix) {
|
|
748
|
+
const client = await this.getClient();
|
|
749
|
+
const remotePath = this.resolvePath(prefix);
|
|
750
|
+
try {
|
|
751
|
+
const items = await client.getDirectoryContents(remotePath, {
|
|
752
|
+
deep: true
|
|
753
|
+
});
|
|
754
|
+
return items.filter((item) => item.type === "file").map((item) => ({
|
|
755
|
+
key: this.stripBasePath(item.filename),
|
|
756
|
+
size: item.size ?? 0,
|
|
757
|
+
lastModified: new Date(item.lastmod ?? Date.now()),
|
|
758
|
+
contentType: item.mime
|
|
759
|
+
}));
|
|
760
|
+
} catch (error) {
|
|
761
|
+
const err = error;
|
|
762
|
+
if (err.status === 404) return [];
|
|
763
|
+
throw error;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
async getFileInfo(key) {
|
|
767
|
+
const client = await this.getClient();
|
|
768
|
+
const remotePath = this.resolvePath(key);
|
|
769
|
+
try {
|
|
770
|
+
const stat = await client.stat(remotePath);
|
|
771
|
+
if (stat.type === "directory") return void 0;
|
|
772
|
+
return {
|
|
773
|
+
key,
|
|
774
|
+
size: stat.size ?? 0,
|
|
775
|
+
lastModified: new Date(stat.lastmod ?? Date.now()),
|
|
776
|
+
contentType: stat.mime
|
|
777
|
+
};
|
|
778
|
+
} catch (error) {
|
|
779
|
+
const err = error;
|
|
780
|
+
if (err.status === 404) return void 0;
|
|
781
|
+
throw error;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
async copy(sourceKey, destKey) {
|
|
785
|
+
const client = await this.getClient();
|
|
786
|
+
const src = this.resolvePath(sourceKey);
|
|
787
|
+
const dest = this.resolvePath(destKey);
|
|
788
|
+
const parentDir = dest.substring(0, dest.lastIndexOf("/"));
|
|
789
|
+
if (parentDir && parentDir !== "/") {
|
|
790
|
+
await this.ensureDirectory(parentDir);
|
|
791
|
+
}
|
|
792
|
+
await client.copyFile(src, dest);
|
|
793
|
+
}
|
|
794
|
+
async move(sourceKey, destKey) {
|
|
795
|
+
const client = await this.getClient();
|
|
796
|
+
const src = this.resolvePath(sourceKey);
|
|
797
|
+
const dest = this.resolvePath(destKey);
|
|
798
|
+
const parentDir = dest.substring(0, dest.lastIndexOf("/"));
|
|
799
|
+
if (parentDir && parentDir !== "/") {
|
|
800
|
+
await this.ensureDirectory(parentDir);
|
|
801
|
+
}
|
|
802
|
+
await client.moveFile(src, dest);
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* 递归创建目录
|
|
806
|
+
*/
|
|
807
|
+
async ensureDirectory(dirPath) {
|
|
808
|
+
const inflight = this.directoryPromises.get(dirPath);
|
|
809
|
+
if (inflight) return inflight;
|
|
810
|
+
const creation = this.createDirectoryIfMissing(dirPath);
|
|
811
|
+
this.directoryPromises.set(dirPath, creation);
|
|
812
|
+
try {
|
|
813
|
+
await creation;
|
|
814
|
+
} finally {
|
|
815
|
+
if (this.directoryPromises.get(dirPath) === creation) {
|
|
816
|
+
this.directoryPromises.delete(dirPath);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
async createDirectoryIfMissing(dirPath) {
|
|
821
|
+
const client = await this.getClient();
|
|
822
|
+
if (await client.exists(dirPath)) return;
|
|
823
|
+
const parent = dirPath.substring(0, dirPath.lastIndexOf("/"));
|
|
824
|
+
if (parent && parent !== "/") {
|
|
825
|
+
await this.ensureDirectory(parent);
|
|
826
|
+
}
|
|
827
|
+
try {
|
|
828
|
+
await client.createDirectory(dirPath);
|
|
829
|
+
} catch (error) {
|
|
830
|
+
const status = error.status;
|
|
831
|
+
if ([
|
|
832
|
+
405,
|
|
833
|
+
409
|
|
834
|
+
].includes(status ?? 0) && await client.exists(dirPath)) return;
|
|
835
|
+
throw error;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
resolvePath(key) {
|
|
839
|
+
const base = this.basePath.endsWith("/") ? this.basePath : `${this.basePath}/`;
|
|
840
|
+
const resolved = path2.posix.resolve(base, key);
|
|
841
|
+
const normalizedBase = path2.posix.resolve(base);
|
|
842
|
+
if (!resolved.startsWith(normalizedBase + "/") && resolved !== normalizedBase) {
|
|
843
|
+
throw new Error(`Path traversal detected: key "${key}" resolves outside base path`);
|
|
844
|
+
}
|
|
845
|
+
return resolved;
|
|
846
|
+
}
|
|
847
|
+
stripBasePath(fullPath) {
|
|
848
|
+
const base = this.basePath.endsWith("/") ? this.basePath : `${this.basePath}/`;
|
|
849
|
+
if (fullPath.startsWith(base)) {
|
|
850
|
+
return fullPath.substring(base.length);
|
|
851
|
+
}
|
|
852
|
+
return fullPath;
|
|
853
|
+
}
|
|
854
|
+
buildUrl(key) {
|
|
855
|
+
return this.publicUrl ? `${this.publicUrl}/${key}` : key;
|
|
856
|
+
}
|
|
857
|
+
};
|
|
858
|
+
WebDavStorage = _ts_decorate3([
|
|
859
|
+
(0, import_common3.Injectable)(),
|
|
860
|
+
_ts_metadata3("design:type", Function),
|
|
861
|
+
_ts_metadata3("design:paramtypes", [
|
|
862
|
+
typeof WebDavStorageOptions === "undefined" ? Object : WebDavStorageOptions
|
|
863
|
+
])
|
|
864
|
+
], WebDavStorage);
|
|
865
|
+
|
|
866
|
+
// src/file-lifecycle.ts
|
|
867
|
+
var import_common4 = require("@nestjs/common");
|
|
868
|
+
function _ts_decorate4(decorators, target, key, desc) {
|
|
869
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
870
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
|
|
871
|
+
r = Reflect.decorate(decorators, target, key, desc);
|
|
872
|
+
} else {
|
|
873
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
874
|
+
if (d = decorators[i]) {
|
|
875
|
+
r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
880
|
+
}
|
|
881
|
+
__name(_ts_decorate4, "_ts_decorate");
|
|
882
|
+
var FileLifecycleManager = class _FileLifecycleManager {
|
|
883
|
+
static {
|
|
884
|
+
__name(this, "FileLifecycleManager");
|
|
885
|
+
}
|
|
886
|
+
logger = new import_common4.Logger(_FileLifecycleManager.name);
|
|
887
|
+
policies = [];
|
|
888
|
+
/** 注册一个生命周期策略 */
|
|
889
|
+
registerPolicy(policy) {
|
|
890
|
+
this.policies.push(policy);
|
|
891
|
+
this.logger.log(`File lifecycle policy registered: prefix=${policy.pathPrefix ?? "*"}, maxAge=${policy.maxAge ?? "none"}`);
|
|
892
|
+
}
|
|
893
|
+
/** 获取所有已注册策略 */
|
|
894
|
+
getPolicies() {
|
|
895
|
+
return [
|
|
896
|
+
...this.policies
|
|
897
|
+
];
|
|
898
|
+
}
|
|
899
|
+
/**
|
|
900
|
+
* 评估文件列表,返回需要清理的文件 keys。
|
|
901
|
+
* 调用方负责实际删除操作。
|
|
902
|
+
*/
|
|
903
|
+
evaluate(files, policy) {
|
|
904
|
+
const toDelete = [];
|
|
905
|
+
const activePolicies = policy ? [
|
|
906
|
+
policy
|
|
907
|
+
] : this.policies;
|
|
908
|
+
const now = Date.now();
|
|
909
|
+
for (const p of activePolicies) {
|
|
910
|
+
let scopedFiles = files;
|
|
911
|
+
if (p.pathPrefix) {
|
|
912
|
+
scopedFiles = files.filter((f) => f.key.startsWith(p.pathPrefix));
|
|
913
|
+
}
|
|
914
|
+
const sorted = [
|
|
915
|
+
...scopedFiles
|
|
916
|
+
].sort((a, b) => a.createdAt - b.createdAt);
|
|
917
|
+
if (p.maxAge && p.maxAge > 0) {
|
|
918
|
+
for (const f of sorted) {
|
|
919
|
+
if (now - f.createdAt > p.maxAge) {
|
|
920
|
+
toDelete.push(f.key);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
if (p.maxCount && p.maxCount > 0) {
|
|
925
|
+
const remaining = sorted.filter((f) => !toDelete.includes(f.key));
|
|
926
|
+
if (remaining.length > p.maxCount) {
|
|
927
|
+
const excess = remaining.slice(0, remaining.length - p.maxCount);
|
|
928
|
+
for (const f of excess) {
|
|
929
|
+
toDelete.push(f.key);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
if (p.maxTotalSize && p.maxTotalSize > 0) {
|
|
934
|
+
const remaining = sorted.filter((f) => !toDelete.includes(f.key));
|
|
935
|
+
let totalSize = remaining.reduce((sum, f) => sum + f.size, 0);
|
|
936
|
+
for (const f of remaining) {
|
|
937
|
+
if (totalSize <= p.maxTotalSize) break;
|
|
938
|
+
toDelete.push(f.key);
|
|
939
|
+
totalSize -= f.size;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
return [
|
|
944
|
+
...new Set(toDelete)
|
|
945
|
+
];
|
|
946
|
+
}
|
|
947
|
+
};
|
|
948
|
+
FileLifecycleManager = _ts_decorate4([
|
|
949
|
+
(0, import_common4.Injectable)()
|
|
950
|
+
], FileLifecycleManager);
|
|
951
|
+
|
|
952
|
+
// src/storage.module.ts
|
|
953
|
+
var import_common5 = require("@nestjs/common");
|
|
954
|
+
function _ts_decorate5(decorators, target, key, desc) {
|
|
955
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
956
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
|
|
957
|
+
r = Reflect.decorate(decorators, target, key, desc);
|
|
958
|
+
} else {
|
|
959
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
960
|
+
if (d = decorators[i]) {
|
|
961
|
+
r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
966
|
+
}
|
|
967
|
+
__name(_ts_decorate5, "_ts_decorate");
|
|
968
|
+
var FILE_MANAGER = "FILE_MANAGER";
|
|
969
|
+
var STORAGE_MODULE_OPTIONS = /* @__PURE__ */ Symbol("STORAGE_MODULE_OPTIONS");
|
|
970
|
+
var ZuckerStorageModule = class _ZuckerStorageModule {
|
|
971
|
+
static {
|
|
972
|
+
__name(this, "ZuckerStorageModule");
|
|
973
|
+
}
|
|
974
|
+
static forRoot(options) {
|
|
975
|
+
return {
|
|
976
|
+
module: _ZuckerStorageModule,
|
|
977
|
+
global: true,
|
|
978
|
+
providers: [
|
|
979
|
+
{
|
|
980
|
+
provide: FILE_MANAGER,
|
|
981
|
+
useFactory: /* @__PURE__ */ __name(() => _ZuckerStorageModule.createFileManager(options), "useFactory")
|
|
982
|
+
}
|
|
983
|
+
],
|
|
984
|
+
exports: [
|
|
985
|
+
FILE_MANAGER
|
|
986
|
+
]
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
static forRootAsync(options) {
|
|
990
|
+
const asyncProviders = this.createAsyncProviders(options);
|
|
991
|
+
return {
|
|
992
|
+
module: _ZuckerStorageModule,
|
|
993
|
+
global: true,
|
|
994
|
+
imports: options.imports ?? [],
|
|
995
|
+
providers: [
|
|
996
|
+
...asyncProviders,
|
|
997
|
+
{
|
|
998
|
+
provide: FILE_MANAGER,
|
|
999
|
+
useFactory: /* @__PURE__ */ __name((opts) => _ZuckerStorageModule.createFileManager(opts), "useFactory"),
|
|
1000
|
+
inject: [
|
|
1001
|
+
STORAGE_MODULE_OPTIONS
|
|
1002
|
+
]
|
|
1003
|
+
}
|
|
1004
|
+
],
|
|
1005
|
+
exports: [
|
|
1006
|
+
FILE_MANAGER
|
|
1007
|
+
]
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
static createFileManager(options) {
|
|
1011
|
+
if (options.provider === "local") {
|
|
1012
|
+
return new LocalFileStorage({
|
|
1013
|
+
basePath: options.basePath,
|
|
1014
|
+
baseUrl: options.baseUrl
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
if (options.provider === "webdav") {
|
|
1018
|
+
const { provider: _2, ...webdavOpts } = options;
|
|
1019
|
+
return new WebDavStorage(webdavOpts);
|
|
1020
|
+
}
|
|
1021
|
+
const { provider: _, ...s3Opts } = options;
|
|
1022
|
+
return new S3Storage(s3Opts);
|
|
1023
|
+
}
|
|
1024
|
+
static createAsyncProviders(options) {
|
|
1025
|
+
if (options.useFactory) {
|
|
1026
|
+
return [
|
|
1027
|
+
{
|
|
1028
|
+
provide: STORAGE_MODULE_OPTIONS,
|
|
1029
|
+
useFactory: options.useFactory,
|
|
1030
|
+
inject: options.inject ?? []
|
|
1031
|
+
}
|
|
1032
|
+
];
|
|
1033
|
+
}
|
|
1034
|
+
const optionsProvider = {
|
|
1035
|
+
provide: STORAGE_MODULE_OPTIONS,
|
|
1036
|
+
useFactory: /* @__PURE__ */ __name(async (factory) => factory.createStorageOptions(), "useFactory"),
|
|
1037
|
+
inject: [
|
|
1038
|
+
options.useClass ?? options.useExisting
|
|
1039
|
+
]
|
|
1040
|
+
};
|
|
1041
|
+
if (options.useClass) {
|
|
1042
|
+
return [
|
|
1043
|
+
{
|
|
1044
|
+
provide: options.useClass,
|
|
1045
|
+
useClass: options.useClass
|
|
1046
|
+
},
|
|
1047
|
+
optionsProvider
|
|
1048
|
+
];
|
|
1049
|
+
}
|
|
1050
|
+
return [
|
|
1051
|
+
optionsProvider
|
|
1052
|
+
];
|
|
1053
|
+
}
|
|
1054
|
+
};
|
|
1055
|
+
ZuckerStorageModule = _ts_decorate5([
|
|
1056
|
+
(0, import_common5.Global)(),
|
|
1057
|
+
(0, import_common5.Module)({})
|
|
1058
|
+
], ZuckerStorageModule);
|
|
1059
|
+
|
|
1060
|
+
// src/index.ts
|
|
1061
|
+
var StorageProvider = /* @__PURE__ */ (function(StorageProvider2) {
|
|
1062
|
+
StorageProvider2["LOCAL"] = "LOCAL";
|
|
1063
|
+
StorageProvider2["S3"] = "S3";
|
|
1064
|
+
StorageProvider2["WEBDAV"] = "WEBDAV";
|
|
1065
|
+
return StorageProvider2;
|
|
1066
|
+
})({});
|
|
1067
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1068
|
+
0 && (module.exports = {
|
|
1069
|
+
DANGEROUS_EXTENSIONS,
|
|
1070
|
+
FILE_MANAGER,
|
|
1071
|
+
FileLifecycleManager,
|
|
1072
|
+
LocalFileStorage,
|
|
1073
|
+
S3Storage,
|
|
1074
|
+
StorageProvider,
|
|
1075
|
+
WebDavStorage,
|
|
1076
|
+
ZuckerStorageModule,
|
|
1077
|
+
generateDateBasedKey,
|
|
1078
|
+
isFileDenied,
|
|
1079
|
+
isSafeOriginalFileName,
|
|
1080
|
+
sanitizeFileName
|
|
1081
|
+
});
|