@zucker-framework/storage 1.0.2 → 1.0.6
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 +19 -0
- package/dist/index.d.mts +39 -17
- package/dist/index.d.ts +39 -17
- package/dist/index.js +117 -23
- package/dist/index.mjs +117 -23
- package/package.json +1 -1
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:
|
|
134
|
+
uploadStream(stream: Readable, key: string, options?: UploadOptions): Promise<UploadResult>;
|
|
131
135
|
download(key: string): Promise<{
|
|
132
|
-
stream:
|
|
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:
|
|
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:
|
|
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:
|
|
302
|
+
uploadStream(stream: Readable, key: string, options?: UploadOptions): Promise<UploadResult>;
|
|
299
303
|
download(key: string): Promise<{
|
|
300
|
-
stream:
|
|
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
|
|
317
|
-
password
|
|
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:
|
|
337
|
-
|
|
338
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
473
|
+
uploadStream(_stream: Readable, _key: string, _options?: UploadOptions): Promise<UploadResult>;
|
|
452
474
|
download(_key: string): Promise<{
|
|
453
|
-
stream:
|
|
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:
|
|
134
|
+
uploadStream(stream: Readable, key: string, options?: UploadOptions): Promise<UploadResult>;
|
|
131
135
|
download(key: string): Promise<{
|
|
132
|
-
stream:
|
|
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:
|
|
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:
|
|
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:
|
|
302
|
+
uploadStream(stream: Readable, key: string, options?: UploadOptions): Promise<UploadResult>;
|
|
299
303
|
download(key: string): Promise<{
|
|
300
|
-
stream:
|
|
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
|
|
317
|
-
password
|
|
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:
|
|
337
|
-
|
|
338
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
473
|
+
uploadStream(_stream: Readable, _key: string, _options?: UploadOptions): Promise<UploadResult>;
|
|
452
474
|
download(_key: string): Promise<{
|
|
453
|
-
stream:
|
|
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
|
|
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.
|
|
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
|
|
715
|
-
|
|
716
|
-
|
|
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
|
-
|
|
725
|
-
|
|
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
|
|
728
|
-
size: stat.size
|
|
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
|
-
|
|
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
|
|
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.
|
|
916
|
+
await this.ensureRemoteDirectory(parent, signal);
|
|
832
917
|
}
|
|
833
918
|
try {
|
|
834
|
-
|
|
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
|
|
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 {
|
|
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.
|
|
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
|
|
664
|
-
|
|
665
|
-
|
|
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
|
-
|
|
674
|
-
|
|
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
|
|
677
|
-
size: stat.size
|
|
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
|
-
|
|
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
|
|
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.
|
|
865
|
+
await this.ensureRemoteDirectory(parent, signal);
|
|
781
866
|
}
|
|
782
867
|
try {
|
|
783
|
-
|
|
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
|
|
879
|
+
].includes(status ?? 0) && await client.exists(dirPath, ...signal ? [
|
|
880
|
+
{
|
|
881
|
+
signal
|
|
882
|
+
}
|
|
883
|
+
] : [])) return;
|
|
790
884
|
throw error;
|
|
791
885
|
}
|
|
792
886
|
}
|