@zucker-framework/storage 1.0.0 → 1.0.5

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 CHANGED
@@ -31,18 +31,24 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  // src/index.ts
32
32
  var index_exports = {};
33
33
  __export(index_exports, {
34
+ BufferedFileManagerStore: () => BufferedFileManagerStore,
34
35
  DANGEROUS_EXTENSIONS: () => DANGEROUS_EXTENSIONS,
35
36
  FILE_MANAGER: () => FILE_MANAGER,
36
37
  FileLifecycleManager: () => FileLifecycleManager,
37
38
  LocalFileStorage: () => LocalFileStorage,
38
39
  S3Storage: () => S3Storage,
39
40
  StorageProvider: () => StorageProvider,
41
+ StorageTransferError: () => StorageTransferError,
42
+ UnavailableFileManager: () => UnavailableFileManager,
40
43
  WebDavStorage: () => WebDavStorage,
41
44
  ZuckerStorageModule: () => ZuckerStorageModule,
42
45
  generateDateBasedKey: () => generateDateBasedKey,
46
+ hydrateStorageUrls: () => hydrateStorageUrls,
43
47
  isFileDenied: () => isFileDenied,
44
48
  isSafeOriginalFileName: () => isSafeOriginalFileName,
45
- sanitizeFileName: () => sanitizeFileName
49
+ sanitizeFileName: () => sanitizeFileName,
50
+ transferVerifiedObject: () => transferVerifiedObject,
51
+ verifyStoredObject: () => verifyStoredObject
46
52
  });
47
53
  module.exports = __toCommonJS(index_exports);
48
54
 
@@ -431,6 +437,7 @@ LocalFileStorage = _ts_decorate([
431
437
  var import_common2 = require("@nestjs/common");
432
438
  var import_client_s3 = require("@aws-sdk/client-s3");
433
439
  var import_s3_request_presigner = require("@aws-sdk/s3-request-presigner");
440
+ var import_node_stream = require("stream");
434
441
  function _ts_decorate2(decorators, target, key, desc) {
435
442
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
436
443
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
@@ -523,6 +530,9 @@ var S3Storage = class _S3Storage {
523
530
  if (!response.Body) {
524
531
  throw new Error("Stored object response had no body");
525
532
  }
533
+ if (!(response.Body instanceof import_node_stream.Readable)) {
534
+ throw new Error("Stored object response must be a Node readable stream");
535
+ }
526
536
  return {
527
537
  stream: response.Body,
528
538
  size: response.ContentLength ?? 0
@@ -629,7 +639,8 @@ __name(encodeKeyPath, "encodeKeyPath");
629
639
  // src/webdav-storage.ts
630
640
  var import_common3 = require("@nestjs/common");
631
641
  var path2 = __toESM(require("path"));
632
- var import_stream = require("stream");
642
+ var import_node_stream2 = require("stream");
643
+ var import_promises = require("stream/promises");
633
644
  function _ts_decorate3(decorators, target, key, desc) {
634
645
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
635
646
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
@@ -662,7 +673,7 @@ var WebDavStorage = class _WebDavStorage {
662
673
  providerType = "webdav";
663
674
  options;
664
675
  constructor(options) {
665
- if (!options.url || !options.username || !options.password) {
676
+ if (!options.url || !options.allowAnonymous && (!options.username || !options.password)) {
666
677
  throw new Error("WebDAV url, username and password are required");
667
678
  }
668
679
  this.options = options;
@@ -689,7 +700,7 @@ var WebDavStorage = class _WebDavStorage {
689
700
  const remotePath = this.resolvePath(key);
690
701
  const parentDir = remotePath.substring(0, remotePath.lastIndexOf("/"));
691
702
  if (parentDir && parentDir !== "/") {
692
- await this.ensureDirectory(parentDir);
703
+ await this.ensureRemoteDirectory(parentDir);
693
704
  }
694
705
  await client.putFileContents(remotePath, buffer, {
695
706
  contentLength: buffer.length,
@@ -705,21 +716,87 @@ var WebDavStorage = class _WebDavStorage {
705
716
  };
706
717
  }
707
718
  async uploadStream(stream, key, options) {
708
- const chunks = [];
709
- for await (const chunk of stream) {
710
- chunks.push(Buffer.from(chunk));
719
+ const controller = new AbortController();
720
+ const abort = /* @__PURE__ */ __name(() => controller.abort(options?.signal?.reason), "abort");
721
+ options?.signal?.addEventListener("abort", abort, {
722
+ once: true
723
+ });
724
+ if (options?.signal?.aborted) abort();
725
+ let size = 0;
726
+ const counter = new import_node_stream2.Transform({
727
+ transform(chunk, _encoding, callback) {
728
+ size += chunk.length;
729
+ callback(null, chunk);
730
+ }
731
+ });
732
+ let transfer;
733
+ let pumping;
734
+ const sourceError = /* @__PURE__ */ __name((error) => controller.abort(error), "sourceError");
735
+ stream.on("error", sourceError);
736
+ (0, import_node_stream2.addAbortSignal)(controller.signal, stream);
737
+ try {
738
+ controller.signal.throwIfAborted();
739
+ const client = await this.getClient();
740
+ const remotePath = this.resolvePath(key);
741
+ await this.ensureRemoteDirectory(path2.posix.dirname(remotePath), controller.signal);
742
+ controller.signal.throwIfAborted();
743
+ transfer = client.putFileContents(remotePath, counter, {
744
+ overwrite: true,
745
+ contentLength: false,
746
+ signal: controller.signal,
747
+ headers: options?.contentType ? {
748
+ "Content-Type": options.contentType
749
+ } : void 0
750
+ });
751
+ pumping = (0, import_promises.pipeline)(stream, counter, {
752
+ signal: controller.signal
753
+ });
754
+ await Promise.all([
755
+ transfer,
756
+ pumping
757
+ ]);
758
+ return {
759
+ key,
760
+ url: this.buildUrl(key),
761
+ size
762
+ };
763
+ } catch (error) {
764
+ controller.abort(error);
765
+ stream.destroy(error instanceof Error ? error : new Error("Storage upload failed"));
766
+ counter.destroy();
767
+ await Promise.allSettled([
768
+ transfer,
769
+ pumping,
770
+ (0, import_promises.finished)(stream, {
771
+ cleanup: true
772
+ })
773
+ ]);
774
+ throw error;
775
+ } finally {
776
+ options?.signal?.removeEventListener("abort", abort);
777
+ stream.off("error", sourceError);
711
778
  }
712
- return this.upload(Buffer.concat(chunks), key, options);
713
779
  }
714
- async download(key) {
780
+ async download(key, options) {
781
+ options?.signal?.throwIfAborted();
715
782
  const client = await this.getClient();
716
783
  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);
784
+ const stat = await client.stat(remotePath, ...options?.signal ? [
785
+ {
786
+ signal: options.signal
787
+ }
788
+ ] : []);
789
+ if (stat.type !== "file") throw new Error("Remote path is not a file");
790
+ options?.signal?.throwIfAborted();
791
+ const stream = client.createReadStream(remotePath, ...options?.signal ? [
792
+ {
793
+ signal: options.signal
794
+ }
795
+ ] : []);
796
+ if (options?.signal) (0, import_node_stream2.addAbortSignal)(options.signal, stream);
720
797
  return {
721
- stream: readable,
722
- size: stat.size ?? buffer.length
798
+ stream,
799
+ size: stat.size
723
800
  };
724
801
  }
725
802
  async delete(key) {
@@ -744,22 +821,26 @@ var WebDavStorage = class _WebDavStorage {
744
821
  async getDownloadUrl(key) {
745
822
  return this.getPublicUrl(key);
746
823
  }
747
- async list(prefix) {
824
+ async list(prefix, options = {}) {
748
825
  const client = await this.getClient();
749
826
  const remotePath = this.resolvePath(prefix);
750
827
  try {
751
828
  const items = await client.getDirectoryContents(remotePath, {
752
- deep: true
829
+ deep: options.deep ?? true
753
830
  });
754
- return items.filter((item) => item.type === "file").map((item) => ({
831
+ return items.filter((item) => item.type === "file" || options.includeDirectories).map((item) => ({
755
832
  key: this.stripBasePath(item.filename),
833
+ type: item.type,
834
+ filename: item.filename,
835
+ basename: item.basename,
836
+ lastmod: item.lastmod,
756
837
  size: item.size ?? 0,
757
838
  lastModified: new Date(item.lastmod ?? Date.now()),
758
839
  contentType: item.mime
759
840
  }));
760
841
  } catch (error) {
761
842
  const err = error;
762
- if (err.status === 404) return [];
843
+ if (err.status === 404 && options.missing !== "throw") return [];
763
844
  throw error;
764
845
  }
765
846
  }
@@ -804,7 +885,12 @@ var WebDavStorage = class _WebDavStorage {
804
885
  /**
805
886
  * 递归创建目录
806
887
  */
807
- async ensureDirectory(dirPath) {
888
+ /** Accepts a storage key, with the same traversal protection as upload. */
889
+ async ensureDirectory(key) {
890
+ return this.ensureRemoteDirectory(this.resolvePath(key));
891
+ }
892
+ async ensureRemoteDirectory(dirPath, signal) {
893
+ if (signal) return this.createDirectoryIfMissing(dirPath, signal);
808
894
  const inflight = this.directoryPromises.get(dirPath);
809
895
  if (inflight) return inflight;
810
896
  const creation = this.createDirectoryIfMissing(dirPath);
@@ -817,21 +903,35 @@ var WebDavStorage = class _WebDavStorage {
817
903
  }
818
904
  }
819
905
  }
820
- async createDirectoryIfMissing(dirPath) {
906
+ async createDirectoryIfMissing(dirPath, signal) {
907
+ signal?.throwIfAborted();
821
908
  const client = await this.getClient();
822
- if (await client.exists(dirPath)) return;
909
+ if (await client.exists(dirPath, ...signal ? [
910
+ {
911
+ signal
912
+ }
913
+ ] : [])) return;
823
914
  const parent = dirPath.substring(0, dirPath.lastIndexOf("/"));
824
915
  if (parent && parent !== "/") {
825
- await this.ensureDirectory(parent);
916
+ await this.ensureRemoteDirectory(parent, signal);
826
917
  }
827
918
  try {
828
- await client.createDirectory(dirPath);
919
+ signal?.throwIfAborted();
920
+ await client.createDirectory(dirPath, ...signal ? [
921
+ {
922
+ signal
923
+ }
924
+ ] : []);
829
925
  } catch (error) {
830
926
  const status = error.status;
831
927
  if ([
832
928
  405,
833
929
  409
834
- ].includes(status ?? 0) && await client.exists(dirPath)) return;
930
+ ].includes(status ?? 0) && await client.exists(dirPath, ...signal ? [
931
+ {
932
+ signal
933
+ }
934
+ ] : [])) return;
835
935
  throw error;
836
936
  }
837
937
  }
@@ -1057,6 +1157,198 @@ ZuckerStorageModule = _ts_decorate5([
1057
1157
  (0, import_common5.Module)({})
1058
1158
  ], ZuckerStorageModule);
1059
1159
 
1160
+ // src/storage-url-hydrator.ts
1161
+ function isPlainRecord(value) {
1162
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
1163
+ const prototype = Object.getPrototypeOf(value);
1164
+ return prototype === Object.prototype || prototype === null;
1165
+ }
1166
+ __name(isPlainRecord, "isPlainRecord");
1167
+ async function hydrateStorageUrls(value, options) {
1168
+ const signedUrls = /* @__PURE__ */ new Map();
1169
+ const hydrate = /* @__PURE__ */ __name(async (current) => {
1170
+ if (Array.isArray(current)) return Promise.all(current.map(hydrate));
1171
+ if (!isPlainRecord(current)) return current;
1172
+ const entries = await Promise.all(Object.entries(current).map(async ([key2, nested]) => [
1173
+ key2,
1174
+ await hydrate(nested)
1175
+ ]));
1176
+ const result = Object.fromEntries(entries);
1177
+ const key = result[options.storageKeyField];
1178
+ if (typeof key === "string" && key.length > 0 && options.allowedStorageKeys.has(key)) {
1179
+ let signed = signedUrls.get(key);
1180
+ if (!signed) {
1181
+ signed = options.getDownloadUrl(key);
1182
+ signedUrls.set(key, signed);
1183
+ }
1184
+ const url = await signed;
1185
+ for (const field of options.urlFields) {
1186
+ Object.defineProperty(result, field, {
1187
+ value: url,
1188
+ writable: true,
1189
+ enumerable: true,
1190
+ configurable: true
1191
+ });
1192
+ }
1193
+ }
1194
+ return result;
1195
+ }, "hydrate");
1196
+ return hydrate(value);
1197
+ }
1198
+ __name(hydrateStorageUrls, "hydrateStorageUrls");
1199
+
1200
+ // src/unavailable-file-manager.ts
1201
+ var UnavailableFileManager = class {
1202
+ static {
1203
+ __name(this, "UnavailableFileManager");
1204
+ }
1205
+ providerType;
1206
+ message;
1207
+ constructor(providerType, message = `Storage provider ${providerType} is not configured`) {
1208
+ this.providerType = providerType;
1209
+ this.message = message;
1210
+ }
1211
+ isConfigured() {
1212
+ return false;
1213
+ }
1214
+ unavailable() {
1215
+ return new Error(this.message);
1216
+ }
1217
+ upload(_buffer, _key, _options) {
1218
+ return Promise.reject(this.unavailable());
1219
+ }
1220
+ uploadStream(_stream, _key, _options) {
1221
+ return Promise.reject(this.unavailable());
1222
+ }
1223
+ download(_key) {
1224
+ return Promise.reject(this.unavailable());
1225
+ }
1226
+ delete(_key) {
1227
+ return Promise.reject(this.unavailable());
1228
+ }
1229
+ exists(_key) {
1230
+ return Promise.resolve(false);
1231
+ }
1232
+ getPublicUrl(_key) {
1233
+ return Promise.reject(this.unavailable());
1234
+ }
1235
+ getDownloadUrl(_key) {
1236
+ return Promise.reject(this.unavailable());
1237
+ }
1238
+ list(_prefix) {
1239
+ return Promise.resolve([]);
1240
+ }
1241
+ };
1242
+
1243
+ // src/verified-object-transfer.ts
1244
+ var import_node_crypto2 = require("crypto");
1245
+ var StorageTransferError = class extends Error {
1246
+ static {
1247
+ __name(this, "StorageTransferError");
1248
+ }
1249
+ code;
1250
+ constructor(code) {
1251
+ super(code), this.code = code;
1252
+ this.name = "StorageTransferError";
1253
+ }
1254
+ };
1255
+ function verifyStoredObject(object, expected, detectMimeType, errorFactory = (code) => new StorageTransferError(code)) {
1256
+ if (object.reportedSize !== object.body.length || object.body.length !== expected.size) throw errorFactory("TARGET_SIZE_CONFLICT");
1257
+ if (detectMimeType(object.body) !== expected.mimeType) throw errorFactory("TARGET_MIME_CONFLICT");
1258
+ if ((0, import_node_crypto2.createHash)("sha256").update(object.body).digest("hex") !== expected.sha256) throw errorFactory("TARGET_HASH_CONFLICT");
1259
+ }
1260
+ __name(verifyStoredObject, "verifyStoredObject");
1261
+ async function transferVerifiedObject(options) {
1262
+ const fail = options.errorFactory ?? ((code) => new StorageTransferError(code));
1263
+ const body = options.source.body;
1264
+ if (options.source.reportedSize !== body.length || body.length === 0) throw fail("SOURCE_SIZE_MISMATCH");
1265
+ const metadata = {
1266
+ sha256: (0, import_node_crypto2.createHash)("sha256").update(body).digest("hex"),
1267
+ size: body.length,
1268
+ mimeType: options.detectMimeType(body)
1269
+ };
1270
+ if (await options.target.exists(options.destinationKey)) {
1271
+ verifyStoredObject(await options.target.read(options.destinationKey), metadata, options.detectMimeType, fail);
1272
+ return {
1273
+ ...metadata,
1274
+ targetDisposition: options.mode === "dry-run" ? "planned" : "reused"
1275
+ };
1276
+ }
1277
+ if (options.mode === "dry-run" || !options.allowUpload) return {
1278
+ ...metadata,
1279
+ targetDisposition: "planned"
1280
+ };
1281
+ const uploaded = await options.target.write(options.destinationKey, body, {
1282
+ contentType: metadata.mimeType,
1283
+ metadata: {
1284
+ sha256: metadata.sha256,
1285
+ size: String(metadata.size),
1286
+ mimeType: metadata.mimeType
1287
+ },
1288
+ private: true
1289
+ });
1290
+ if (uploaded.size !== metadata.size) throw fail("UPLOAD_SIZE_MISMATCH");
1291
+ verifyStoredObject(await options.target.read(options.destinationKey), metadata, options.detectMimeType, fail);
1292
+ return {
1293
+ ...metadata,
1294
+ targetDisposition: "uploaded"
1295
+ };
1296
+ }
1297
+ __name(transferVerifiedObject, "transferVerifiedObject");
1298
+
1299
+ // src/buffered-file-manager-store.ts
1300
+ var BufferedFileManagerStore = class {
1301
+ static {
1302
+ __name(this, "BufferedFileManagerStore");
1303
+ }
1304
+ manager;
1305
+ maxObjectBytes;
1306
+ errorFactory;
1307
+ constructor(manager, maxObjectBytes, errorFactory = (code) => new StorageTransferError(code)) {
1308
+ this.manager = manager;
1309
+ this.maxObjectBytes = maxObjectBytes;
1310
+ this.errorFactory = errorFactory;
1311
+ if (!Number.isSafeInteger(maxObjectBytes) || maxObjectBytes < 1) throw new RangeError("maxObjectBytes must be a positive integer");
1312
+ if (!manager.isConfigured()) throw errorFactory("STORAGE_NOT_CONFIGURED");
1313
+ }
1314
+ async read(key) {
1315
+ const downloaded = await this.manager.download(key);
1316
+ if (downloaded.size < 1 || downloaded.size > this.maxObjectBytes) {
1317
+ downloaded.stream.destroy();
1318
+ throw this.errorFactory("OBJECT_SIZE_OUT_OF_BOUNDS");
1319
+ }
1320
+ const chunks = [];
1321
+ let size = 0;
1322
+ for await (const chunk of downloaded.stream) {
1323
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
1324
+ size += buffer.length;
1325
+ if (size > this.maxObjectBytes) {
1326
+ downloaded.stream.destroy();
1327
+ throw this.errorFactory("OBJECT_SIZE_OUT_OF_BOUNDS");
1328
+ }
1329
+ chunks.push(buffer);
1330
+ }
1331
+ return {
1332
+ body: Buffer.concat(chunks),
1333
+ reportedSize: downloaded.size
1334
+ };
1335
+ }
1336
+ exists(key) {
1337
+ return this.manager.exists(key);
1338
+ }
1339
+ async write(key, body, options) {
1340
+ if (body.length > this.maxObjectBytes) throw this.errorFactory("OBJECT_SIZE_OUT_OF_BOUNDS");
1341
+ const result = await this.manager.upload(body, key, {
1342
+ contentType: options.contentType,
1343
+ metadata: options.metadata,
1344
+ acl: "private"
1345
+ });
1346
+ return {
1347
+ size: result.size
1348
+ };
1349
+ }
1350
+ };
1351
+
1060
1352
  // src/index.ts
1061
1353
  var StorageProvider = /* @__PURE__ */ (function(StorageProvider2) {
1062
1354
  StorageProvider2["LOCAL"] = "LOCAL";
@@ -1066,16 +1358,22 @@ var StorageProvider = /* @__PURE__ */ (function(StorageProvider2) {
1066
1358
  })({});
1067
1359
  // Annotate the CommonJS export names for ESM import in node:
1068
1360
  0 && (module.exports = {
1361
+ BufferedFileManagerStore,
1069
1362
  DANGEROUS_EXTENSIONS,
1070
1363
  FILE_MANAGER,
1071
1364
  FileLifecycleManager,
1072
1365
  LocalFileStorage,
1073
1366
  S3Storage,
1074
1367
  StorageProvider,
1368
+ StorageTransferError,
1369
+ UnavailableFileManager,
1075
1370
  WebDavStorage,
1076
1371
  ZuckerStorageModule,
1077
1372
  generateDateBasedKey,
1373
+ hydrateStorageUrls,
1078
1374
  isFileDenied,
1079
1375
  isSafeOriginalFileName,
1080
- sanitizeFileName
1376
+ sanitizeFileName,
1377
+ transferVerifiedObject,
1378
+ verifyStoredObject
1081
1379
  });