@zucker-framework/storage 1.0.2 → 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/README.md ADDED
@@ -0,0 +1,19 @@
1
+ # Storage
2
+
3
+ ## WebDAV streaming and directory APIs (unreleased)
4
+
5
+ `WebDavStorage.uploadStream(readable, key, { contentType?, signal? })` streams through a bounded Node Transform to the WebDAV SDK without collecting the file. It counts bytes, awaits both the source pipeline and remote response, and aborts/destroys the streams on source failure, remote failure, setup failure or cancellation. Failed remote writes may leave a partial remote object: the provider does not promise atomic overwrite or delete a previous object on failure. Consumers retain recovery/commit policy.
6
+
7
+ `download(key, { signal? })` checks stat is a file and returns `{ stream: Readable, size }` using the SDK's real read stream (no buffer conversion). Abort destroys the stream. Callers must consume or destroy returned streams and handle stream errors; a resolved download call does not mean all bytes have arrived.
8
+
9
+ **Type compatibility:** `FileManager.download().stream` is now honestly typed as Node `Readable`, not an `fs.ReadStream` (WebDAV/S3 streams have no file descriptor/path). `FileManager.uploadStream` accepts `Readable`. Consumers using filesystem-specific stream properties must migrate. This is a public type-contract breaking correction; release/version it accordingly. Local and S3 streaming implementations are not changed by this WebDAV work.
10
+
11
+ `list(prefix, { deep: false, includeDirectories: true, missing: 'throw' })` requests shallow entries including directories and propagates missing-directory errors for host fallback. Defaults preserve recursive files-only enumeration and empty-on-404 behavior. `WebDavEntry` includes the original `filename`, `basename`, `lastmod`, `type`, as well as generic `key`, `size`, `lastModified`, `contentType`.
12
+
13
+ `ensureDirectory(key)` is public, recursively creates missing parents and coalesces concurrent uncancelled creation. All public keys retain base-path traversal protection. Cancellation-owned upload directory operations use their own SDK signal rather than sharing another caller's cancellation.
14
+
15
+ Credentials remain required by default. Explicit `allowAnonymous: true` permits missing credentials for hosts that deliberately allow unauthenticated WebDAV endpoints; URL remains mandatory. This opt-in does not read environment variables or change host authentication policy. Storage logs never include credentials or endpoint URLs.
16
+
17
+ The host owns URL-to-key normalization, public URL settings, tenant ownership, disk fallback/recovery paths and error disclosure. No in-memory fallback or product environment configuration is introduced.
18
+
19
+ Verification includes SDK mocks and the actual installed WebDAV SDK against a local synthetic HTTP server. Neither demonstrates real infrastructure permissions, durability or remote cancellation cleanup.
package/dist/index.d.mts CHANGED
@@ -1,6 +1,6 @@
1
+ import { Readable } from 'node:stream';
1
2
  import { ReadStream } from 'fs';
2
3
  import { OnModuleDestroy, Type, DynamicModule, InjectionToken, OptionalFactoryDependency } from '@nestjs/common';
3
- import { ReadStream as ReadStream$1 } from 'node:fs';
4
4
 
5
5
  interface UploadOptions {
6
6
  contentType?: string;
@@ -19,6 +19,10 @@ interface UploadResult {
19
19
  }
20
20
  interface FileInfo {
21
21
  key: string;
22
+ /** Available when the provider exposes filesystem entries (e.g. WebDAV). */
23
+ type?: 'file' | 'directory';
24
+ filename?: string;
25
+ basename?: string;
22
26
  size: number;
23
27
  lastModified: Date;
24
28
  contentType?: string;
@@ -127,9 +131,9 @@ interface FileManager {
127
131
  readonly providerType: StorageProviderType;
128
132
  isConfigured(): boolean;
129
133
  upload(buffer: Buffer, key: string, options?: UploadOptions): Promise<UploadResult>;
130
- uploadStream(stream: ReadStream, key: string, options?: UploadOptions): Promise<UploadResult>;
134
+ uploadStream(stream: Readable, key: string, options?: UploadOptions): Promise<UploadResult>;
131
135
  download(key: string): Promise<{
132
- stream: ReadStream;
136
+ stream: Readable;
133
137
  size: number;
134
138
  }>;
135
139
  delete(key: string): Promise<void>;
@@ -150,7 +154,7 @@ interface FileManager {
150
154
  * @param position 起始偏移量(字节)
151
155
  */
152
156
  downloadRange?(key: string, position: number, length?: number): Promise<{
153
- stream: ReadStream;
157
+ stream: Readable;
154
158
  size: number;
155
159
  }>;
156
160
  /**
@@ -229,7 +233,7 @@ declare class LocalFileStorage implements FileManager {
229
233
  });
230
234
  isConfigured(): boolean;
231
235
  upload(buffer: Buffer, key: string, _options?: UploadOptions): Promise<UploadResult>;
232
- uploadStream(stream: ReadStream, key: string, options?: UploadOptions): Promise<UploadResult>;
236
+ uploadStream(stream: Readable, key: string, options?: UploadOptions): Promise<UploadResult>;
233
237
  download(key: string): Promise<{
234
238
  stream: ReadStream;
235
239
  size: number;
@@ -295,9 +299,9 @@ declare class S3Storage implements FileManager, OnModuleDestroy {
295
299
  onModuleDestroy(): void;
296
300
  isConfigured(): boolean;
297
301
  upload(buffer: Buffer, key: string, options?: UploadOptions): Promise<UploadResult>;
298
- uploadStream(stream: ReadStream, key: string, options?: UploadOptions): Promise<UploadResult>;
302
+ uploadStream(stream: Readable, key: string, options?: UploadOptions): Promise<UploadResult>;
299
303
  download(key: string): Promise<{
300
- stream: ReadStream;
304
+ stream: Readable;
301
305
  size: number;
302
306
  }>;
303
307
  delete(key: string): Promise<void>;
@@ -310,11 +314,19 @@ declare class S3Storage implements FileManager, OnModuleDestroy {
310
314
  move(sourceKey: string, destKey: string): Promise<void>;
311
315
  }
312
316
 
317
+ interface WebDavEntry extends FileInfo {
318
+ type: 'file' | 'directory';
319
+ filename: string;
320
+ basename: string;
321
+ lastmod: string;
322
+ }
313
323
  interface WebDavStorageOptions {
314
324
  /** WebDAV 服务地址,如 https://nextcloud.example.com/remote.php/dav/files/user */
315
325
  url: string;
316
- username: string;
317
- password: string;
326
+ username?: string;
327
+ password?: string;
328
+ /** Explicit opt-in for endpoints/consumer policies allowing missing credentials. Default false. */
329
+ allowAnonymous?: boolean;
318
330
  /** 公开访问 URL 前缀(可选) */
319
331
  publicUrl?: string;
320
332
  /** 基础路径前缀(可选,默认 '/') */
@@ -333,23 +345,33 @@ declare class WebDavStorage implements FileManager {
333
345
  private getClient;
334
346
  isConfigured(): boolean;
335
347
  upload(buffer: Buffer, key: string, options?: UploadOptions): Promise<UploadResult>;
336
- uploadStream(stream: ReadStream, key: string, options?: UploadOptions): Promise<UploadResult>;
337
- download(key: string): Promise<{
338
- stream: ReadStream;
348
+ uploadStream(stream: Readable, key: string, options?: UploadOptions & {
349
+ signal?: AbortSignal;
350
+ }): Promise<UploadResult>;
351
+ download(key: string, options?: {
352
+ signal?: AbortSignal;
353
+ }): Promise<{
354
+ stream: Readable;
339
355
  size: number;
340
356
  }>;
341
357
  delete(key: string): Promise<void>;
342
358
  exists(key: string): Promise<boolean>;
343
359
  getPublicUrl(key: string): Promise<string>;
344
360
  getDownloadUrl(key: string): Promise<string>;
345
- list(prefix: string): Promise<FileInfo[]>;
361
+ list(prefix: string, options?: {
362
+ deep?: boolean;
363
+ includeDirectories?: boolean;
364
+ missing?: 'empty' | 'throw';
365
+ }): Promise<WebDavEntry[]>;
346
366
  getFileInfo(key: string): Promise<FileInfo | undefined>;
347
367
  copy(sourceKey: string, destKey: string): Promise<void>;
348
368
  move(sourceKey: string, destKey: string): Promise<void>;
349
369
  /**
350
370
  * 递归创建目录
351
371
  */
352
- private ensureDirectory;
372
+ /** Accepts a storage key, with the same traversal protection as upload. */
373
+ ensureDirectory(key: string): Promise<void>;
374
+ private ensureRemoteDirectory;
353
375
  private createDirectoryIfMissing;
354
376
  private resolvePath;
355
377
  private stripBasePath;
@@ -448,9 +470,9 @@ declare class UnavailableFileManager implements FileManager {
448
470
  isConfigured(): boolean;
449
471
  private unavailable;
450
472
  upload(_buffer: Buffer, _key: string, _options?: UploadOptions): Promise<UploadResult>;
451
- uploadStream(_stream: ReadStream$1, _key: string, _options?: UploadOptions): Promise<UploadResult>;
473
+ uploadStream(_stream: Readable, _key: string, _options?: UploadOptions): Promise<UploadResult>;
452
474
  download(_key: string): Promise<{
453
- stream: ReadStream$1;
475
+ stream: Readable;
454
476
  size: number;
455
477
  }>;
456
478
  delete(_key: string): Promise<void>;
@@ -529,4 +551,4 @@ declare enum StorageProvider {
529
551
  WEBDAV = "WEBDAV"
530
552
  }
531
553
 
532
- export { BufferedFileManagerStore, type BufferedObjectStore, type ChunkUploadState, DANGEROUS_EXTENSIONS, type DownloadedStorageObject, FILE_MANAGER, type FileInfo, FileLifecycleManager, type FileLifecyclePolicy, type FileManager, type FileMetadata, type FileUploadValidation, type LifecycleResult, LocalFileStorage, type PosixFilePermissions, type S3ObjectAclMode, S3Storage, type S3StorageOptions, type S3UploadUrlMode, type StorageModuleAsyncOptions, type StorageModuleOptions, type StorageModuleOptionsFactory, StorageProvider, type StorageProviderType, StorageTransferError, type StorageTransferErrorCode, type StorageTransferErrorFactory, type StorageUrlHydrationOptions, UnavailableFileManager, type UploadOptions, type UploadResult, type VerifiedObjectTransferOptions, type VerifiedStorageMetadata, WebDavStorage, type WebDavStorageOptions, ZuckerStorageModule, generateDateBasedKey, hydrateStorageUrls, isFileDenied, isSafeOriginalFileName, sanitizeFileName, transferVerifiedObject, verifyStoredObject };
554
+ export { BufferedFileManagerStore, type BufferedObjectStore, type ChunkUploadState, DANGEROUS_EXTENSIONS, type DownloadedStorageObject, FILE_MANAGER, type FileInfo, FileLifecycleManager, type FileLifecyclePolicy, type FileManager, type FileMetadata, type FileUploadValidation, type LifecycleResult, LocalFileStorage, type PosixFilePermissions, type S3ObjectAclMode, S3Storage, type S3StorageOptions, type S3UploadUrlMode, type StorageModuleAsyncOptions, type StorageModuleOptions, type StorageModuleOptionsFactory, StorageProvider, type StorageProviderType, StorageTransferError, type StorageTransferErrorCode, type StorageTransferErrorFactory, type StorageUrlHydrationOptions, UnavailableFileManager, type UploadOptions, type UploadResult, type VerifiedObjectTransferOptions, type VerifiedStorageMetadata, type WebDavEntry, WebDavStorage, type WebDavStorageOptions, ZuckerStorageModule, generateDateBasedKey, hydrateStorageUrls, isFileDenied, isSafeOriginalFileName, sanitizeFileName, transferVerifiedObject, verifyStoredObject };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
+ import { Readable } from 'node:stream';
1
2
  import { ReadStream } from 'fs';
2
3
  import { OnModuleDestroy, Type, DynamicModule, InjectionToken, OptionalFactoryDependency } from '@nestjs/common';
3
- import { ReadStream as ReadStream$1 } from 'node:fs';
4
4
 
5
5
  interface UploadOptions {
6
6
  contentType?: string;
@@ -19,6 +19,10 @@ interface UploadResult {
19
19
  }
20
20
  interface FileInfo {
21
21
  key: string;
22
+ /** Available when the provider exposes filesystem entries (e.g. WebDAV). */
23
+ type?: 'file' | 'directory';
24
+ filename?: string;
25
+ basename?: string;
22
26
  size: number;
23
27
  lastModified: Date;
24
28
  contentType?: string;
@@ -127,9 +131,9 @@ interface FileManager {
127
131
  readonly providerType: StorageProviderType;
128
132
  isConfigured(): boolean;
129
133
  upload(buffer: Buffer, key: string, options?: UploadOptions): Promise<UploadResult>;
130
- uploadStream(stream: ReadStream, key: string, options?: UploadOptions): Promise<UploadResult>;
134
+ uploadStream(stream: Readable, key: string, options?: UploadOptions): Promise<UploadResult>;
131
135
  download(key: string): Promise<{
132
- stream: ReadStream;
136
+ stream: Readable;
133
137
  size: number;
134
138
  }>;
135
139
  delete(key: string): Promise<void>;
@@ -150,7 +154,7 @@ interface FileManager {
150
154
  * @param position 起始偏移量(字节)
151
155
  */
152
156
  downloadRange?(key: string, position: number, length?: number): Promise<{
153
- stream: ReadStream;
157
+ stream: Readable;
154
158
  size: number;
155
159
  }>;
156
160
  /**
@@ -229,7 +233,7 @@ declare class LocalFileStorage implements FileManager {
229
233
  });
230
234
  isConfigured(): boolean;
231
235
  upload(buffer: Buffer, key: string, _options?: UploadOptions): Promise<UploadResult>;
232
- uploadStream(stream: ReadStream, key: string, options?: UploadOptions): Promise<UploadResult>;
236
+ uploadStream(stream: Readable, key: string, options?: UploadOptions): Promise<UploadResult>;
233
237
  download(key: string): Promise<{
234
238
  stream: ReadStream;
235
239
  size: number;
@@ -295,9 +299,9 @@ declare class S3Storage implements FileManager, OnModuleDestroy {
295
299
  onModuleDestroy(): void;
296
300
  isConfigured(): boolean;
297
301
  upload(buffer: Buffer, key: string, options?: UploadOptions): Promise<UploadResult>;
298
- uploadStream(stream: ReadStream, key: string, options?: UploadOptions): Promise<UploadResult>;
302
+ uploadStream(stream: Readable, key: string, options?: UploadOptions): Promise<UploadResult>;
299
303
  download(key: string): Promise<{
300
- stream: ReadStream;
304
+ stream: Readable;
301
305
  size: number;
302
306
  }>;
303
307
  delete(key: string): Promise<void>;
@@ -310,11 +314,19 @@ declare class S3Storage implements FileManager, OnModuleDestroy {
310
314
  move(sourceKey: string, destKey: string): Promise<void>;
311
315
  }
312
316
 
317
+ interface WebDavEntry extends FileInfo {
318
+ type: 'file' | 'directory';
319
+ filename: string;
320
+ basename: string;
321
+ lastmod: string;
322
+ }
313
323
  interface WebDavStorageOptions {
314
324
  /** WebDAV 服务地址,如 https://nextcloud.example.com/remote.php/dav/files/user */
315
325
  url: string;
316
- username: string;
317
- password: string;
326
+ username?: string;
327
+ password?: string;
328
+ /** Explicit opt-in for endpoints/consumer policies allowing missing credentials. Default false. */
329
+ allowAnonymous?: boolean;
318
330
  /** 公开访问 URL 前缀(可选) */
319
331
  publicUrl?: string;
320
332
  /** 基础路径前缀(可选,默认 '/') */
@@ -333,23 +345,33 @@ declare class WebDavStorage implements FileManager {
333
345
  private getClient;
334
346
  isConfigured(): boolean;
335
347
  upload(buffer: Buffer, key: string, options?: UploadOptions): Promise<UploadResult>;
336
- uploadStream(stream: ReadStream, key: string, options?: UploadOptions): Promise<UploadResult>;
337
- download(key: string): Promise<{
338
- stream: ReadStream;
348
+ uploadStream(stream: Readable, key: string, options?: UploadOptions & {
349
+ signal?: AbortSignal;
350
+ }): Promise<UploadResult>;
351
+ download(key: string, options?: {
352
+ signal?: AbortSignal;
353
+ }): Promise<{
354
+ stream: Readable;
339
355
  size: number;
340
356
  }>;
341
357
  delete(key: string): Promise<void>;
342
358
  exists(key: string): Promise<boolean>;
343
359
  getPublicUrl(key: string): Promise<string>;
344
360
  getDownloadUrl(key: string): Promise<string>;
345
- list(prefix: string): Promise<FileInfo[]>;
361
+ list(prefix: string, options?: {
362
+ deep?: boolean;
363
+ includeDirectories?: boolean;
364
+ missing?: 'empty' | 'throw';
365
+ }): Promise<WebDavEntry[]>;
346
366
  getFileInfo(key: string): Promise<FileInfo | undefined>;
347
367
  copy(sourceKey: string, destKey: string): Promise<void>;
348
368
  move(sourceKey: string, destKey: string): Promise<void>;
349
369
  /**
350
370
  * 递归创建目录
351
371
  */
352
- private ensureDirectory;
372
+ /** Accepts a storage key, with the same traversal protection as upload. */
373
+ ensureDirectory(key: string): Promise<void>;
374
+ private ensureRemoteDirectory;
353
375
  private createDirectoryIfMissing;
354
376
  private resolvePath;
355
377
  private stripBasePath;
@@ -448,9 +470,9 @@ declare class UnavailableFileManager implements FileManager {
448
470
  isConfigured(): boolean;
449
471
  private unavailable;
450
472
  upload(_buffer: Buffer, _key: string, _options?: UploadOptions): Promise<UploadResult>;
451
- uploadStream(_stream: ReadStream$1, _key: string, _options?: UploadOptions): Promise<UploadResult>;
473
+ uploadStream(_stream: Readable, _key: string, _options?: UploadOptions): Promise<UploadResult>;
452
474
  download(_key: string): Promise<{
453
- stream: ReadStream$1;
475
+ stream: Readable;
454
476
  size: number;
455
477
  }>;
456
478
  delete(_key: string): Promise<void>;
@@ -529,4 +551,4 @@ declare enum StorageProvider {
529
551
  WEBDAV = "WEBDAV"
530
552
  }
531
553
 
532
- export { BufferedFileManagerStore, type BufferedObjectStore, type ChunkUploadState, DANGEROUS_EXTENSIONS, type DownloadedStorageObject, FILE_MANAGER, type FileInfo, FileLifecycleManager, type FileLifecyclePolicy, type FileManager, type FileMetadata, type FileUploadValidation, type LifecycleResult, LocalFileStorage, type PosixFilePermissions, type S3ObjectAclMode, S3Storage, type S3StorageOptions, type S3UploadUrlMode, type StorageModuleAsyncOptions, type StorageModuleOptions, type StorageModuleOptionsFactory, StorageProvider, type StorageProviderType, StorageTransferError, type StorageTransferErrorCode, type StorageTransferErrorFactory, type StorageUrlHydrationOptions, UnavailableFileManager, type UploadOptions, type UploadResult, type VerifiedObjectTransferOptions, type VerifiedStorageMetadata, WebDavStorage, type WebDavStorageOptions, ZuckerStorageModule, generateDateBasedKey, hydrateStorageUrls, isFileDenied, isSafeOriginalFileName, sanitizeFileName, transferVerifiedObject, verifyStoredObject };
554
+ export { BufferedFileManagerStore, type BufferedObjectStore, type ChunkUploadState, DANGEROUS_EXTENSIONS, type DownloadedStorageObject, FILE_MANAGER, type FileInfo, FileLifecycleManager, type FileLifecyclePolicy, type FileManager, type FileMetadata, type FileUploadValidation, type LifecycleResult, LocalFileStorage, type PosixFilePermissions, type S3ObjectAclMode, S3Storage, type S3StorageOptions, type S3UploadUrlMode, type StorageModuleAsyncOptions, type StorageModuleOptions, type StorageModuleOptionsFactory, StorageProvider, type StorageProviderType, StorageTransferError, type StorageTransferErrorCode, type StorageTransferErrorFactory, type StorageUrlHydrationOptions, UnavailableFileManager, type UploadOptions, type UploadResult, type VerifiedObjectTransferOptions, type VerifiedStorageMetadata, type WebDavEntry, WebDavStorage, type WebDavStorageOptions, ZuckerStorageModule, generateDateBasedKey, hydrateStorageUrls, isFileDenied, isSafeOriginalFileName, sanitizeFileName, transferVerifiedObject, verifyStoredObject };
package/dist/index.js CHANGED
@@ -437,6 +437,7 @@ LocalFileStorage = _ts_decorate([
437
437
  var import_common2 = require("@nestjs/common");
438
438
  var import_client_s3 = require("@aws-sdk/client-s3");
439
439
  var import_s3_request_presigner = require("@aws-sdk/s3-request-presigner");
440
+ var import_node_stream = require("stream");
440
441
  function _ts_decorate2(decorators, target, key, desc) {
441
442
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
442
443
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
@@ -529,6 +530,9 @@ var S3Storage = class _S3Storage {
529
530
  if (!response.Body) {
530
531
  throw new Error("Stored object response had no body");
531
532
  }
533
+ if (!(response.Body instanceof import_node_stream.Readable)) {
534
+ throw new Error("Stored object response must be a Node readable stream");
535
+ }
532
536
  return {
533
537
  stream: response.Body,
534
538
  size: response.ContentLength ?? 0
@@ -635,7 +639,8 @@ __name(encodeKeyPath, "encodeKeyPath");
635
639
  // src/webdav-storage.ts
636
640
  var import_common3 = require("@nestjs/common");
637
641
  var path2 = __toESM(require("path"));
638
- var import_stream = require("stream");
642
+ var import_node_stream2 = require("stream");
643
+ var import_promises = require("stream/promises");
639
644
  function _ts_decorate3(decorators, target, key, desc) {
640
645
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
641
646
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
@@ -668,7 +673,7 @@ var WebDavStorage = class _WebDavStorage {
668
673
  providerType = "webdav";
669
674
  options;
670
675
  constructor(options) {
671
- if (!options.url || !options.username || !options.password) {
676
+ if (!options.url || !options.allowAnonymous && (!options.username || !options.password)) {
672
677
  throw new Error("WebDAV url, username and password are required");
673
678
  }
674
679
  this.options = options;
@@ -695,7 +700,7 @@ var WebDavStorage = class _WebDavStorage {
695
700
  const remotePath = this.resolvePath(key);
696
701
  const parentDir = remotePath.substring(0, remotePath.lastIndexOf("/"));
697
702
  if (parentDir && parentDir !== "/") {
698
- await this.ensureDirectory(parentDir);
703
+ await this.ensureRemoteDirectory(parentDir);
699
704
  }
700
705
  await client.putFileContents(remotePath, buffer, {
701
706
  contentLength: buffer.length,
@@ -711,21 +716,87 @@ var WebDavStorage = class _WebDavStorage {
711
716
  };
712
717
  }
713
718
  async uploadStream(stream, key, options) {
714
- const chunks = [];
715
- for await (const chunk of stream) {
716
- 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);
717
778
  }
718
- return this.upload(Buffer.concat(chunks), key, options);
719
779
  }
720
- async download(key) {
780
+ async download(key, options) {
781
+ options?.signal?.throwIfAborted();
721
782
  const client = await this.getClient();
722
783
  const remotePath = this.resolvePath(key);
723
- const stat = await client.stat(remotePath);
724
- const buffer = await client.getFileContents(remotePath);
725
- 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);
726
797
  return {
727
- stream: readable,
728
- size: stat.size ?? buffer.length
798
+ stream,
799
+ size: stat.size
729
800
  };
730
801
  }
731
802
  async delete(key) {
@@ -750,22 +821,26 @@ var WebDavStorage = class _WebDavStorage {
750
821
  async getDownloadUrl(key) {
751
822
  return this.getPublicUrl(key);
752
823
  }
753
- async list(prefix) {
824
+ async list(prefix, options = {}) {
754
825
  const client = await this.getClient();
755
826
  const remotePath = this.resolvePath(prefix);
756
827
  try {
757
828
  const items = await client.getDirectoryContents(remotePath, {
758
- deep: true
829
+ deep: options.deep ?? true
759
830
  });
760
- return items.filter((item) => item.type === "file").map((item) => ({
831
+ return items.filter((item) => item.type === "file" || options.includeDirectories).map((item) => ({
761
832
  key: this.stripBasePath(item.filename),
833
+ type: item.type,
834
+ filename: item.filename,
835
+ basename: item.basename,
836
+ lastmod: item.lastmod,
762
837
  size: item.size ?? 0,
763
838
  lastModified: new Date(item.lastmod ?? Date.now()),
764
839
  contentType: item.mime
765
840
  }));
766
841
  } catch (error) {
767
842
  const err = error;
768
- if (err.status === 404) return [];
843
+ if (err.status === 404 && options.missing !== "throw") return [];
769
844
  throw error;
770
845
  }
771
846
  }
@@ -810,7 +885,12 @@ var WebDavStorage = class _WebDavStorage {
810
885
  /**
811
886
  * 递归创建目录
812
887
  */
813
- 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);
814
894
  const inflight = this.directoryPromises.get(dirPath);
815
895
  if (inflight) return inflight;
816
896
  const creation = this.createDirectoryIfMissing(dirPath);
@@ -823,21 +903,35 @@ var WebDavStorage = class _WebDavStorage {
823
903
  }
824
904
  }
825
905
  }
826
- async createDirectoryIfMissing(dirPath) {
906
+ async createDirectoryIfMissing(dirPath, signal) {
907
+ signal?.throwIfAborted();
827
908
  const client = await this.getClient();
828
- if (await client.exists(dirPath)) return;
909
+ if (await client.exists(dirPath, ...signal ? [
910
+ {
911
+ signal
912
+ }
913
+ ] : [])) return;
829
914
  const parent = dirPath.substring(0, dirPath.lastIndexOf("/"));
830
915
  if (parent && parent !== "/") {
831
- await this.ensureDirectory(parent);
916
+ await this.ensureRemoteDirectory(parent, signal);
832
917
  }
833
918
  try {
834
- await client.createDirectory(dirPath);
919
+ signal?.throwIfAborted();
920
+ await client.createDirectory(dirPath, ...signal ? [
921
+ {
922
+ signal
923
+ }
924
+ ] : []);
835
925
  } catch (error) {
836
926
  const status = error.status;
837
927
  if ([
838
928
  405,
839
929
  409
840
- ].includes(status ?? 0) && await client.exists(dirPath)) return;
930
+ ].includes(status ?? 0) && await client.exists(dirPath, ...signal ? [
931
+ {
932
+ signal
933
+ }
934
+ ] : [])) return;
841
935
  throw error;
842
936
  }
843
937
  }
package/dist/index.mjs CHANGED
@@ -386,6 +386,7 @@ LocalFileStorage = _ts_decorate([
386
386
  import { Injectable as Injectable2, Logger as Logger2 } from "@nestjs/common";
387
387
  import { S3Client, PutObjectCommand, GetObjectCommand, DeleteObjectCommand, HeadObjectCommand, ListObjectsV2Command, CopyObjectCommand } from "@aws-sdk/client-s3";
388
388
  import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
389
+ import { Readable } from "stream";
389
390
  function _ts_decorate2(decorators, target, key, desc) {
390
391
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
391
392
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
@@ -478,6 +479,9 @@ var S3Storage = class _S3Storage {
478
479
  if (!response.Body) {
479
480
  throw new Error("Stored object response had no body");
480
481
  }
482
+ if (!(response.Body instanceof Readable)) {
483
+ throw new Error("Stored object response must be a Node readable stream");
484
+ }
481
485
  return {
482
486
  stream: response.Body,
483
487
  size: response.ContentLength ?? 0
@@ -584,7 +588,8 @@ __name(encodeKeyPath, "encodeKeyPath");
584
588
  // src/webdav-storage.ts
585
589
  import { Injectable as Injectable3, Logger as Logger3 } from "@nestjs/common";
586
590
  import * as path2 from "path";
587
- import { Readable } from "stream";
591
+ import { Transform, addAbortSignal } from "stream";
592
+ import { pipeline, finished } from "stream/promises";
588
593
  function _ts_decorate3(decorators, target, key, desc) {
589
594
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
590
595
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
@@ -617,7 +622,7 @@ var WebDavStorage = class _WebDavStorage {
617
622
  providerType = "webdav";
618
623
  options;
619
624
  constructor(options) {
620
- if (!options.url || !options.username || !options.password) {
625
+ if (!options.url || !options.allowAnonymous && (!options.username || !options.password)) {
621
626
  throw new Error("WebDAV url, username and password are required");
622
627
  }
623
628
  this.options = options;
@@ -644,7 +649,7 @@ var WebDavStorage = class _WebDavStorage {
644
649
  const remotePath = this.resolvePath(key);
645
650
  const parentDir = remotePath.substring(0, remotePath.lastIndexOf("/"));
646
651
  if (parentDir && parentDir !== "/") {
647
- await this.ensureDirectory(parentDir);
652
+ await this.ensureRemoteDirectory(parentDir);
648
653
  }
649
654
  await client.putFileContents(remotePath, buffer, {
650
655
  contentLength: buffer.length,
@@ -660,21 +665,87 @@ var WebDavStorage = class _WebDavStorage {
660
665
  };
661
666
  }
662
667
  async uploadStream(stream, key, options) {
663
- const chunks = [];
664
- for await (const chunk of stream) {
665
- chunks.push(Buffer.from(chunk));
668
+ const controller = new AbortController();
669
+ const abort = /* @__PURE__ */ __name(() => controller.abort(options?.signal?.reason), "abort");
670
+ options?.signal?.addEventListener("abort", abort, {
671
+ once: true
672
+ });
673
+ if (options?.signal?.aborted) abort();
674
+ let size = 0;
675
+ const counter = new Transform({
676
+ transform(chunk, _encoding, callback) {
677
+ size += chunk.length;
678
+ callback(null, chunk);
679
+ }
680
+ });
681
+ let transfer;
682
+ let pumping;
683
+ const sourceError = /* @__PURE__ */ __name((error) => controller.abort(error), "sourceError");
684
+ stream.on("error", sourceError);
685
+ addAbortSignal(controller.signal, stream);
686
+ try {
687
+ controller.signal.throwIfAborted();
688
+ const client = await this.getClient();
689
+ const remotePath = this.resolvePath(key);
690
+ await this.ensureRemoteDirectory(path2.posix.dirname(remotePath), controller.signal);
691
+ controller.signal.throwIfAborted();
692
+ transfer = client.putFileContents(remotePath, counter, {
693
+ overwrite: true,
694
+ contentLength: false,
695
+ signal: controller.signal,
696
+ headers: options?.contentType ? {
697
+ "Content-Type": options.contentType
698
+ } : void 0
699
+ });
700
+ pumping = pipeline(stream, counter, {
701
+ signal: controller.signal
702
+ });
703
+ await Promise.all([
704
+ transfer,
705
+ pumping
706
+ ]);
707
+ return {
708
+ key,
709
+ url: this.buildUrl(key),
710
+ size
711
+ };
712
+ } catch (error) {
713
+ controller.abort(error);
714
+ stream.destroy(error instanceof Error ? error : new Error("Storage upload failed"));
715
+ counter.destroy();
716
+ await Promise.allSettled([
717
+ transfer,
718
+ pumping,
719
+ finished(stream, {
720
+ cleanup: true
721
+ })
722
+ ]);
723
+ throw error;
724
+ } finally {
725
+ options?.signal?.removeEventListener("abort", abort);
726
+ stream.off("error", sourceError);
666
727
  }
667
- return this.upload(Buffer.concat(chunks), key, options);
668
728
  }
669
- async download(key) {
729
+ async download(key, options) {
730
+ options?.signal?.throwIfAborted();
670
731
  const client = await this.getClient();
671
732
  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);
733
+ const stat = await client.stat(remotePath, ...options?.signal ? [
734
+ {
735
+ signal: options.signal
736
+ }
737
+ ] : []);
738
+ if (stat.type !== "file") throw new Error("Remote path is not a file");
739
+ options?.signal?.throwIfAborted();
740
+ const stream = client.createReadStream(remotePath, ...options?.signal ? [
741
+ {
742
+ signal: options.signal
743
+ }
744
+ ] : []);
745
+ if (options?.signal) addAbortSignal(options.signal, stream);
675
746
  return {
676
- stream: readable,
677
- size: stat.size ?? buffer.length
747
+ stream,
748
+ size: stat.size
678
749
  };
679
750
  }
680
751
  async delete(key) {
@@ -699,22 +770,26 @@ var WebDavStorage = class _WebDavStorage {
699
770
  async getDownloadUrl(key) {
700
771
  return this.getPublicUrl(key);
701
772
  }
702
- async list(prefix) {
773
+ async list(prefix, options = {}) {
703
774
  const client = await this.getClient();
704
775
  const remotePath = this.resolvePath(prefix);
705
776
  try {
706
777
  const items = await client.getDirectoryContents(remotePath, {
707
- deep: true
778
+ deep: options.deep ?? true
708
779
  });
709
- return items.filter((item) => item.type === "file").map((item) => ({
780
+ return items.filter((item) => item.type === "file" || options.includeDirectories).map((item) => ({
710
781
  key: this.stripBasePath(item.filename),
782
+ type: item.type,
783
+ filename: item.filename,
784
+ basename: item.basename,
785
+ lastmod: item.lastmod,
711
786
  size: item.size ?? 0,
712
787
  lastModified: new Date(item.lastmod ?? Date.now()),
713
788
  contentType: item.mime
714
789
  }));
715
790
  } catch (error) {
716
791
  const err = error;
717
- if (err.status === 404) return [];
792
+ if (err.status === 404 && options.missing !== "throw") return [];
718
793
  throw error;
719
794
  }
720
795
  }
@@ -759,7 +834,12 @@ var WebDavStorage = class _WebDavStorage {
759
834
  /**
760
835
  * 递归创建目录
761
836
  */
762
- async ensureDirectory(dirPath) {
837
+ /** Accepts a storage key, with the same traversal protection as upload. */
838
+ async ensureDirectory(key) {
839
+ return this.ensureRemoteDirectory(this.resolvePath(key));
840
+ }
841
+ async ensureRemoteDirectory(dirPath, signal) {
842
+ if (signal) return this.createDirectoryIfMissing(dirPath, signal);
763
843
  const inflight = this.directoryPromises.get(dirPath);
764
844
  if (inflight) return inflight;
765
845
  const creation = this.createDirectoryIfMissing(dirPath);
@@ -772,21 +852,35 @@ var WebDavStorage = class _WebDavStorage {
772
852
  }
773
853
  }
774
854
  }
775
- async createDirectoryIfMissing(dirPath) {
855
+ async createDirectoryIfMissing(dirPath, signal) {
856
+ signal?.throwIfAborted();
776
857
  const client = await this.getClient();
777
- if (await client.exists(dirPath)) return;
858
+ if (await client.exists(dirPath, ...signal ? [
859
+ {
860
+ signal
861
+ }
862
+ ] : [])) return;
778
863
  const parent = dirPath.substring(0, dirPath.lastIndexOf("/"));
779
864
  if (parent && parent !== "/") {
780
- await this.ensureDirectory(parent);
865
+ await this.ensureRemoteDirectory(parent, signal);
781
866
  }
782
867
  try {
783
- await client.createDirectory(dirPath);
868
+ signal?.throwIfAborted();
869
+ await client.createDirectory(dirPath, ...signal ? [
870
+ {
871
+ signal
872
+ }
873
+ ] : []);
784
874
  } catch (error) {
785
875
  const status = error.status;
786
876
  if ([
787
877
  405,
788
878
  409
789
- ].includes(status ?? 0) && await client.exists(dirPath)) return;
879
+ ].includes(status ?? 0) && await client.exists(dirPath, ...signal ? [
880
+ {
881
+ signal
882
+ }
883
+ ] : [])) return;
790
884
  throw error;
791
885
  }
792
886
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zucker-framework/storage",
3
- "version": "1.0.2",
3
+ "version": "1.0.5",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.mjs",
6
6
  "types": "dist/index.d.ts",