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