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