@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/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
+ });