@zucker-framework/storage 1.0.0 → 1.0.2
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.d.mts +100 -1
- package/dist/index.d.ts +100 -1
- package/dist/index.js +206 -2
- package/dist/index.mjs +199 -1
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ReadStream } from 'fs';
|
|
2
2
|
import { OnModuleDestroy, Type, DynamicModule, InjectionToken, OptionalFactoryDependency } from '@nestjs/common';
|
|
3
|
+
import { ReadStream as ReadStream$1 } from 'node:fs';
|
|
3
4
|
|
|
4
5
|
interface UploadOptions {
|
|
5
6
|
contentType?: string;
|
|
@@ -424,10 +425,108 @@ declare class ZuckerStorageModule {
|
|
|
424
425
|
private static createAsyncProviders;
|
|
425
426
|
}
|
|
426
427
|
|
|
428
|
+
interface StorageUrlHydrationOptions {
|
|
429
|
+
/** The application must establish ownership before allowing a key to be signed. */
|
|
430
|
+
allowedStorageKeys: ReadonlySet<string>;
|
|
431
|
+
getDownloadUrl: (key: string) => Promise<string>;
|
|
432
|
+
storageKeyField: string;
|
|
433
|
+
urlFields: readonly string[];
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Clone a JSON-like response graph and resolve only authorized object keys.
|
|
437
|
+
* Dates/class instances pass through. Signatures are deduplicated within this
|
|
438
|
+
* call, never cached between requests. A signing failure rejects the whole call;
|
|
439
|
+
* generated URLs never mutate the source graph that might later be persisted.
|
|
440
|
+
*/
|
|
441
|
+
declare function hydrateStorageUrls<T>(value: T, options: StorageUrlHydrationOptions): Promise<T>;
|
|
442
|
+
|
|
443
|
+
/** Disabled storage preserves startup availability while all data operations remain explicit. */
|
|
444
|
+
declare class UnavailableFileManager implements FileManager {
|
|
445
|
+
readonly providerType: StorageProviderType;
|
|
446
|
+
private readonly message;
|
|
447
|
+
constructor(providerType: StorageProviderType, message?: string);
|
|
448
|
+
isConfigured(): boolean;
|
|
449
|
+
private unavailable;
|
|
450
|
+
upload(_buffer: Buffer, _key: string, _options?: UploadOptions): Promise<UploadResult>;
|
|
451
|
+
uploadStream(_stream: ReadStream$1, _key: string, _options?: UploadOptions): Promise<UploadResult>;
|
|
452
|
+
download(_key: string): Promise<{
|
|
453
|
+
stream: ReadStream$1;
|
|
454
|
+
size: number;
|
|
455
|
+
}>;
|
|
456
|
+
delete(_key: string): Promise<void>;
|
|
457
|
+
exists(_key: string): Promise<boolean>;
|
|
458
|
+
getPublicUrl(_key: string): Promise<string>;
|
|
459
|
+
getDownloadUrl(_key: string): Promise<string>;
|
|
460
|
+
list(_prefix: string): Promise<FileInfo[]>;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
interface DownloadedStorageObject {
|
|
464
|
+
body: Buffer;
|
|
465
|
+
reportedSize: number;
|
|
466
|
+
}
|
|
467
|
+
interface BufferedObjectStore<TMimeType extends string = string> {
|
|
468
|
+
read(key: string): Promise<DownloadedStorageObject>;
|
|
469
|
+
exists(key: string): Promise<boolean>;
|
|
470
|
+
write(key: string, body: Buffer, options: {
|
|
471
|
+
contentType: TMimeType;
|
|
472
|
+
metadata: Record<string, string>;
|
|
473
|
+
private: true;
|
|
474
|
+
}): Promise<{
|
|
475
|
+
size: number;
|
|
476
|
+
}>;
|
|
477
|
+
}
|
|
478
|
+
type StorageTransferErrorCode = 'STORAGE_NOT_CONFIGURED' | 'OBJECT_SIZE_OUT_OF_BOUNDS' | 'SOURCE_SIZE_MISMATCH' | 'TARGET_SIZE_CONFLICT' | 'TARGET_MIME_CONFLICT' | 'TARGET_HASH_CONFLICT' | 'UPLOAD_SIZE_MISMATCH';
|
|
479
|
+
declare class StorageTransferError extends Error {
|
|
480
|
+
readonly code: StorageTransferErrorCode;
|
|
481
|
+
constructor(code: StorageTransferErrorCode);
|
|
482
|
+
}
|
|
483
|
+
type StorageTransferErrorFactory = (code: StorageTransferErrorCode) => Error;
|
|
484
|
+
interface VerifiedStorageMetadata<TMimeType extends string = string> {
|
|
485
|
+
sha256: string;
|
|
486
|
+
size: number;
|
|
487
|
+
mimeType: TMimeType;
|
|
488
|
+
}
|
|
489
|
+
/** Compare every byte via SHA-256, with explicit size and MIME conflict reasons. */
|
|
490
|
+
declare function verifyStoredObject<TMimeType extends string>(object: DownloadedStorageObject, expected: VerifiedStorageMetadata<TMimeType>, detectMimeType: (body: Buffer) => TMimeType, errorFactory?: StorageTransferErrorFactory): void;
|
|
491
|
+
interface VerifiedObjectTransferOptions<TMimeType extends string> {
|
|
492
|
+
source: DownloadedStorageObject;
|
|
493
|
+
target: BufferedObjectStore<TMimeType>;
|
|
494
|
+
destinationKey: string;
|
|
495
|
+
mode: 'dry-run' | 'apply';
|
|
496
|
+
allowUpload: boolean;
|
|
497
|
+
detectMimeType: (body: Buffer) => TMimeType;
|
|
498
|
+
errorFactory?: StorageTransferErrorFactory;
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Copy a bounded source buffer into a private destination, verify existing data
|
|
502
|
+
* before reuse and round-trip every upload before reporting success. Dry-run and
|
|
503
|
+
* disallowed uploads never write. This does not delete sources or commit metadata.
|
|
504
|
+
*/
|
|
505
|
+
declare function transferVerifiedObject<TMimeType extends string>(options: VerifiedObjectTransferOptions<TMimeType>): Promise<VerifiedStorageMetadata<TMimeType> & {
|
|
506
|
+
targetDisposition: 'planned' | 'uploaded' | 'reused';
|
|
507
|
+
}>;
|
|
508
|
+
|
|
509
|
+
/** Bounded in-memory transfer adapter over the existing FileManager contract. */
|
|
510
|
+
declare class BufferedFileManagerStore implements BufferedObjectStore {
|
|
511
|
+
private readonly manager;
|
|
512
|
+
private readonly maxObjectBytes;
|
|
513
|
+
private readonly errorFactory;
|
|
514
|
+
constructor(manager: FileManager, maxObjectBytes: number, errorFactory?: StorageTransferErrorFactory);
|
|
515
|
+
read(key: string): Promise<DownloadedStorageObject>;
|
|
516
|
+
exists(key: string): Promise<boolean>;
|
|
517
|
+
write(key: string, body: Buffer, options: {
|
|
518
|
+
contentType: string;
|
|
519
|
+
metadata: Record<string, string>;
|
|
520
|
+
private: true;
|
|
521
|
+
}): Promise<{
|
|
522
|
+
size: number;
|
|
523
|
+
}>;
|
|
524
|
+
}
|
|
525
|
+
|
|
427
526
|
declare enum StorageProvider {
|
|
428
527
|
LOCAL = "LOCAL",
|
|
429
528
|
S3 = "S3",
|
|
430
529
|
WEBDAV = "WEBDAV"
|
|
431
530
|
}
|
|
432
531
|
|
|
433
|
-
export { type ChunkUploadState, DANGEROUS_EXTENSIONS, 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, type UploadOptions, type UploadResult, WebDavStorage, type WebDavStorageOptions, ZuckerStorageModule, generateDateBasedKey, isFileDenied, isSafeOriginalFileName, sanitizeFileName };
|
|
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 };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ReadStream } from 'fs';
|
|
2
2
|
import { OnModuleDestroy, Type, DynamicModule, InjectionToken, OptionalFactoryDependency } from '@nestjs/common';
|
|
3
|
+
import { ReadStream as ReadStream$1 } from 'node:fs';
|
|
3
4
|
|
|
4
5
|
interface UploadOptions {
|
|
5
6
|
contentType?: string;
|
|
@@ -424,10 +425,108 @@ declare class ZuckerStorageModule {
|
|
|
424
425
|
private static createAsyncProviders;
|
|
425
426
|
}
|
|
426
427
|
|
|
428
|
+
interface StorageUrlHydrationOptions {
|
|
429
|
+
/** The application must establish ownership before allowing a key to be signed. */
|
|
430
|
+
allowedStorageKeys: ReadonlySet<string>;
|
|
431
|
+
getDownloadUrl: (key: string) => Promise<string>;
|
|
432
|
+
storageKeyField: string;
|
|
433
|
+
urlFields: readonly string[];
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Clone a JSON-like response graph and resolve only authorized object keys.
|
|
437
|
+
* Dates/class instances pass through. Signatures are deduplicated within this
|
|
438
|
+
* call, never cached between requests. A signing failure rejects the whole call;
|
|
439
|
+
* generated URLs never mutate the source graph that might later be persisted.
|
|
440
|
+
*/
|
|
441
|
+
declare function hydrateStorageUrls<T>(value: T, options: StorageUrlHydrationOptions): Promise<T>;
|
|
442
|
+
|
|
443
|
+
/** Disabled storage preserves startup availability while all data operations remain explicit. */
|
|
444
|
+
declare class UnavailableFileManager implements FileManager {
|
|
445
|
+
readonly providerType: StorageProviderType;
|
|
446
|
+
private readonly message;
|
|
447
|
+
constructor(providerType: StorageProviderType, message?: string);
|
|
448
|
+
isConfigured(): boolean;
|
|
449
|
+
private unavailable;
|
|
450
|
+
upload(_buffer: Buffer, _key: string, _options?: UploadOptions): Promise<UploadResult>;
|
|
451
|
+
uploadStream(_stream: ReadStream$1, _key: string, _options?: UploadOptions): Promise<UploadResult>;
|
|
452
|
+
download(_key: string): Promise<{
|
|
453
|
+
stream: ReadStream$1;
|
|
454
|
+
size: number;
|
|
455
|
+
}>;
|
|
456
|
+
delete(_key: string): Promise<void>;
|
|
457
|
+
exists(_key: string): Promise<boolean>;
|
|
458
|
+
getPublicUrl(_key: string): Promise<string>;
|
|
459
|
+
getDownloadUrl(_key: string): Promise<string>;
|
|
460
|
+
list(_prefix: string): Promise<FileInfo[]>;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
interface DownloadedStorageObject {
|
|
464
|
+
body: Buffer;
|
|
465
|
+
reportedSize: number;
|
|
466
|
+
}
|
|
467
|
+
interface BufferedObjectStore<TMimeType extends string = string> {
|
|
468
|
+
read(key: string): Promise<DownloadedStorageObject>;
|
|
469
|
+
exists(key: string): Promise<boolean>;
|
|
470
|
+
write(key: string, body: Buffer, options: {
|
|
471
|
+
contentType: TMimeType;
|
|
472
|
+
metadata: Record<string, string>;
|
|
473
|
+
private: true;
|
|
474
|
+
}): Promise<{
|
|
475
|
+
size: number;
|
|
476
|
+
}>;
|
|
477
|
+
}
|
|
478
|
+
type StorageTransferErrorCode = 'STORAGE_NOT_CONFIGURED' | 'OBJECT_SIZE_OUT_OF_BOUNDS' | 'SOURCE_SIZE_MISMATCH' | 'TARGET_SIZE_CONFLICT' | 'TARGET_MIME_CONFLICT' | 'TARGET_HASH_CONFLICT' | 'UPLOAD_SIZE_MISMATCH';
|
|
479
|
+
declare class StorageTransferError extends Error {
|
|
480
|
+
readonly code: StorageTransferErrorCode;
|
|
481
|
+
constructor(code: StorageTransferErrorCode);
|
|
482
|
+
}
|
|
483
|
+
type StorageTransferErrorFactory = (code: StorageTransferErrorCode) => Error;
|
|
484
|
+
interface VerifiedStorageMetadata<TMimeType extends string = string> {
|
|
485
|
+
sha256: string;
|
|
486
|
+
size: number;
|
|
487
|
+
mimeType: TMimeType;
|
|
488
|
+
}
|
|
489
|
+
/** Compare every byte via SHA-256, with explicit size and MIME conflict reasons. */
|
|
490
|
+
declare function verifyStoredObject<TMimeType extends string>(object: DownloadedStorageObject, expected: VerifiedStorageMetadata<TMimeType>, detectMimeType: (body: Buffer) => TMimeType, errorFactory?: StorageTransferErrorFactory): void;
|
|
491
|
+
interface VerifiedObjectTransferOptions<TMimeType extends string> {
|
|
492
|
+
source: DownloadedStorageObject;
|
|
493
|
+
target: BufferedObjectStore<TMimeType>;
|
|
494
|
+
destinationKey: string;
|
|
495
|
+
mode: 'dry-run' | 'apply';
|
|
496
|
+
allowUpload: boolean;
|
|
497
|
+
detectMimeType: (body: Buffer) => TMimeType;
|
|
498
|
+
errorFactory?: StorageTransferErrorFactory;
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Copy a bounded source buffer into a private destination, verify existing data
|
|
502
|
+
* before reuse and round-trip every upload before reporting success. Dry-run and
|
|
503
|
+
* disallowed uploads never write. This does not delete sources or commit metadata.
|
|
504
|
+
*/
|
|
505
|
+
declare function transferVerifiedObject<TMimeType extends string>(options: VerifiedObjectTransferOptions<TMimeType>): Promise<VerifiedStorageMetadata<TMimeType> & {
|
|
506
|
+
targetDisposition: 'planned' | 'uploaded' | 'reused';
|
|
507
|
+
}>;
|
|
508
|
+
|
|
509
|
+
/** Bounded in-memory transfer adapter over the existing FileManager contract. */
|
|
510
|
+
declare class BufferedFileManagerStore implements BufferedObjectStore {
|
|
511
|
+
private readonly manager;
|
|
512
|
+
private readonly maxObjectBytes;
|
|
513
|
+
private readonly errorFactory;
|
|
514
|
+
constructor(manager: FileManager, maxObjectBytes: number, errorFactory?: StorageTransferErrorFactory);
|
|
515
|
+
read(key: string): Promise<DownloadedStorageObject>;
|
|
516
|
+
exists(key: string): Promise<boolean>;
|
|
517
|
+
write(key: string, body: Buffer, options: {
|
|
518
|
+
contentType: string;
|
|
519
|
+
metadata: Record<string, string>;
|
|
520
|
+
private: true;
|
|
521
|
+
}): Promise<{
|
|
522
|
+
size: number;
|
|
523
|
+
}>;
|
|
524
|
+
}
|
|
525
|
+
|
|
427
526
|
declare enum StorageProvider {
|
|
428
527
|
LOCAL = "LOCAL",
|
|
429
528
|
S3 = "S3",
|
|
430
529
|
WEBDAV = "WEBDAV"
|
|
431
530
|
}
|
|
432
531
|
|
|
433
|
-
export { type ChunkUploadState, DANGEROUS_EXTENSIONS, 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, type UploadOptions, type UploadResult, WebDavStorage, type WebDavStorageOptions, ZuckerStorageModule, generateDateBasedKey, isFileDenied, isSafeOriginalFileName, sanitizeFileName };
|
|
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 };
|
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
|
|
|
@@ -1057,6 +1063,198 @@ ZuckerStorageModule = _ts_decorate5([
|
|
|
1057
1063
|
(0, import_common5.Module)({})
|
|
1058
1064
|
], ZuckerStorageModule);
|
|
1059
1065
|
|
|
1066
|
+
// src/storage-url-hydrator.ts
|
|
1067
|
+
function isPlainRecord(value) {
|
|
1068
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
1069
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1070
|
+
return prototype === Object.prototype || prototype === null;
|
|
1071
|
+
}
|
|
1072
|
+
__name(isPlainRecord, "isPlainRecord");
|
|
1073
|
+
async function hydrateStorageUrls(value, options) {
|
|
1074
|
+
const signedUrls = /* @__PURE__ */ new Map();
|
|
1075
|
+
const hydrate = /* @__PURE__ */ __name(async (current) => {
|
|
1076
|
+
if (Array.isArray(current)) return Promise.all(current.map(hydrate));
|
|
1077
|
+
if (!isPlainRecord(current)) return current;
|
|
1078
|
+
const entries = await Promise.all(Object.entries(current).map(async ([key2, nested]) => [
|
|
1079
|
+
key2,
|
|
1080
|
+
await hydrate(nested)
|
|
1081
|
+
]));
|
|
1082
|
+
const result = Object.fromEntries(entries);
|
|
1083
|
+
const key = result[options.storageKeyField];
|
|
1084
|
+
if (typeof key === "string" && key.length > 0 && options.allowedStorageKeys.has(key)) {
|
|
1085
|
+
let signed = signedUrls.get(key);
|
|
1086
|
+
if (!signed) {
|
|
1087
|
+
signed = options.getDownloadUrl(key);
|
|
1088
|
+
signedUrls.set(key, signed);
|
|
1089
|
+
}
|
|
1090
|
+
const url = await signed;
|
|
1091
|
+
for (const field of options.urlFields) {
|
|
1092
|
+
Object.defineProperty(result, field, {
|
|
1093
|
+
value: url,
|
|
1094
|
+
writable: true,
|
|
1095
|
+
enumerable: true,
|
|
1096
|
+
configurable: true
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
return result;
|
|
1101
|
+
}, "hydrate");
|
|
1102
|
+
return hydrate(value);
|
|
1103
|
+
}
|
|
1104
|
+
__name(hydrateStorageUrls, "hydrateStorageUrls");
|
|
1105
|
+
|
|
1106
|
+
// src/unavailable-file-manager.ts
|
|
1107
|
+
var UnavailableFileManager = class {
|
|
1108
|
+
static {
|
|
1109
|
+
__name(this, "UnavailableFileManager");
|
|
1110
|
+
}
|
|
1111
|
+
providerType;
|
|
1112
|
+
message;
|
|
1113
|
+
constructor(providerType, message = `Storage provider ${providerType} is not configured`) {
|
|
1114
|
+
this.providerType = providerType;
|
|
1115
|
+
this.message = message;
|
|
1116
|
+
}
|
|
1117
|
+
isConfigured() {
|
|
1118
|
+
return false;
|
|
1119
|
+
}
|
|
1120
|
+
unavailable() {
|
|
1121
|
+
return new Error(this.message);
|
|
1122
|
+
}
|
|
1123
|
+
upload(_buffer, _key, _options) {
|
|
1124
|
+
return Promise.reject(this.unavailable());
|
|
1125
|
+
}
|
|
1126
|
+
uploadStream(_stream, _key, _options) {
|
|
1127
|
+
return Promise.reject(this.unavailable());
|
|
1128
|
+
}
|
|
1129
|
+
download(_key) {
|
|
1130
|
+
return Promise.reject(this.unavailable());
|
|
1131
|
+
}
|
|
1132
|
+
delete(_key) {
|
|
1133
|
+
return Promise.reject(this.unavailable());
|
|
1134
|
+
}
|
|
1135
|
+
exists(_key) {
|
|
1136
|
+
return Promise.resolve(false);
|
|
1137
|
+
}
|
|
1138
|
+
getPublicUrl(_key) {
|
|
1139
|
+
return Promise.reject(this.unavailable());
|
|
1140
|
+
}
|
|
1141
|
+
getDownloadUrl(_key) {
|
|
1142
|
+
return Promise.reject(this.unavailable());
|
|
1143
|
+
}
|
|
1144
|
+
list(_prefix) {
|
|
1145
|
+
return Promise.resolve([]);
|
|
1146
|
+
}
|
|
1147
|
+
};
|
|
1148
|
+
|
|
1149
|
+
// src/verified-object-transfer.ts
|
|
1150
|
+
var import_node_crypto2 = require("crypto");
|
|
1151
|
+
var StorageTransferError = class extends Error {
|
|
1152
|
+
static {
|
|
1153
|
+
__name(this, "StorageTransferError");
|
|
1154
|
+
}
|
|
1155
|
+
code;
|
|
1156
|
+
constructor(code) {
|
|
1157
|
+
super(code), this.code = code;
|
|
1158
|
+
this.name = "StorageTransferError";
|
|
1159
|
+
}
|
|
1160
|
+
};
|
|
1161
|
+
function verifyStoredObject(object, expected, detectMimeType, errorFactory = (code) => new StorageTransferError(code)) {
|
|
1162
|
+
if (object.reportedSize !== object.body.length || object.body.length !== expected.size) throw errorFactory("TARGET_SIZE_CONFLICT");
|
|
1163
|
+
if (detectMimeType(object.body) !== expected.mimeType) throw errorFactory("TARGET_MIME_CONFLICT");
|
|
1164
|
+
if ((0, import_node_crypto2.createHash)("sha256").update(object.body).digest("hex") !== expected.sha256) throw errorFactory("TARGET_HASH_CONFLICT");
|
|
1165
|
+
}
|
|
1166
|
+
__name(verifyStoredObject, "verifyStoredObject");
|
|
1167
|
+
async function transferVerifiedObject(options) {
|
|
1168
|
+
const fail = options.errorFactory ?? ((code) => new StorageTransferError(code));
|
|
1169
|
+
const body = options.source.body;
|
|
1170
|
+
if (options.source.reportedSize !== body.length || body.length === 0) throw fail("SOURCE_SIZE_MISMATCH");
|
|
1171
|
+
const metadata = {
|
|
1172
|
+
sha256: (0, import_node_crypto2.createHash)("sha256").update(body).digest("hex"),
|
|
1173
|
+
size: body.length,
|
|
1174
|
+
mimeType: options.detectMimeType(body)
|
|
1175
|
+
};
|
|
1176
|
+
if (await options.target.exists(options.destinationKey)) {
|
|
1177
|
+
verifyStoredObject(await options.target.read(options.destinationKey), metadata, options.detectMimeType, fail);
|
|
1178
|
+
return {
|
|
1179
|
+
...metadata,
|
|
1180
|
+
targetDisposition: options.mode === "dry-run" ? "planned" : "reused"
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
if (options.mode === "dry-run" || !options.allowUpload) return {
|
|
1184
|
+
...metadata,
|
|
1185
|
+
targetDisposition: "planned"
|
|
1186
|
+
};
|
|
1187
|
+
const uploaded = await options.target.write(options.destinationKey, body, {
|
|
1188
|
+
contentType: metadata.mimeType,
|
|
1189
|
+
metadata: {
|
|
1190
|
+
sha256: metadata.sha256,
|
|
1191
|
+
size: String(metadata.size),
|
|
1192
|
+
mimeType: metadata.mimeType
|
|
1193
|
+
},
|
|
1194
|
+
private: true
|
|
1195
|
+
});
|
|
1196
|
+
if (uploaded.size !== metadata.size) throw fail("UPLOAD_SIZE_MISMATCH");
|
|
1197
|
+
verifyStoredObject(await options.target.read(options.destinationKey), metadata, options.detectMimeType, fail);
|
|
1198
|
+
return {
|
|
1199
|
+
...metadata,
|
|
1200
|
+
targetDisposition: "uploaded"
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1203
|
+
__name(transferVerifiedObject, "transferVerifiedObject");
|
|
1204
|
+
|
|
1205
|
+
// src/buffered-file-manager-store.ts
|
|
1206
|
+
var BufferedFileManagerStore = class {
|
|
1207
|
+
static {
|
|
1208
|
+
__name(this, "BufferedFileManagerStore");
|
|
1209
|
+
}
|
|
1210
|
+
manager;
|
|
1211
|
+
maxObjectBytes;
|
|
1212
|
+
errorFactory;
|
|
1213
|
+
constructor(manager, maxObjectBytes, errorFactory = (code) => new StorageTransferError(code)) {
|
|
1214
|
+
this.manager = manager;
|
|
1215
|
+
this.maxObjectBytes = maxObjectBytes;
|
|
1216
|
+
this.errorFactory = errorFactory;
|
|
1217
|
+
if (!Number.isSafeInteger(maxObjectBytes) || maxObjectBytes < 1) throw new RangeError("maxObjectBytes must be a positive integer");
|
|
1218
|
+
if (!manager.isConfigured()) throw errorFactory("STORAGE_NOT_CONFIGURED");
|
|
1219
|
+
}
|
|
1220
|
+
async read(key) {
|
|
1221
|
+
const downloaded = await this.manager.download(key);
|
|
1222
|
+
if (downloaded.size < 1 || downloaded.size > this.maxObjectBytes) {
|
|
1223
|
+
downloaded.stream.destroy();
|
|
1224
|
+
throw this.errorFactory("OBJECT_SIZE_OUT_OF_BOUNDS");
|
|
1225
|
+
}
|
|
1226
|
+
const chunks = [];
|
|
1227
|
+
let size = 0;
|
|
1228
|
+
for await (const chunk of downloaded.stream) {
|
|
1229
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
1230
|
+
size += buffer.length;
|
|
1231
|
+
if (size > this.maxObjectBytes) {
|
|
1232
|
+
downloaded.stream.destroy();
|
|
1233
|
+
throw this.errorFactory("OBJECT_SIZE_OUT_OF_BOUNDS");
|
|
1234
|
+
}
|
|
1235
|
+
chunks.push(buffer);
|
|
1236
|
+
}
|
|
1237
|
+
return {
|
|
1238
|
+
body: Buffer.concat(chunks),
|
|
1239
|
+
reportedSize: downloaded.size
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
exists(key) {
|
|
1243
|
+
return this.manager.exists(key);
|
|
1244
|
+
}
|
|
1245
|
+
async write(key, body, options) {
|
|
1246
|
+
if (body.length > this.maxObjectBytes) throw this.errorFactory("OBJECT_SIZE_OUT_OF_BOUNDS");
|
|
1247
|
+
const result = await this.manager.upload(body, key, {
|
|
1248
|
+
contentType: options.contentType,
|
|
1249
|
+
metadata: options.metadata,
|
|
1250
|
+
acl: "private"
|
|
1251
|
+
});
|
|
1252
|
+
return {
|
|
1253
|
+
size: result.size
|
|
1254
|
+
};
|
|
1255
|
+
}
|
|
1256
|
+
};
|
|
1257
|
+
|
|
1060
1258
|
// src/index.ts
|
|
1061
1259
|
var StorageProvider = /* @__PURE__ */ (function(StorageProvider2) {
|
|
1062
1260
|
StorageProvider2["LOCAL"] = "LOCAL";
|
|
@@ -1066,16 +1264,22 @@ var StorageProvider = /* @__PURE__ */ (function(StorageProvider2) {
|
|
|
1066
1264
|
})({});
|
|
1067
1265
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1068
1266
|
0 && (module.exports = {
|
|
1267
|
+
BufferedFileManagerStore,
|
|
1069
1268
|
DANGEROUS_EXTENSIONS,
|
|
1070
1269
|
FILE_MANAGER,
|
|
1071
1270
|
FileLifecycleManager,
|
|
1072
1271
|
LocalFileStorage,
|
|
1073
1272
|
S3Storage,
|
|
1074
1273
|
StorageProvider,
|
|
1274
|
+
StorageTransferError,
|
|
1275
|
+
UnavailableFileManager,
|
|
1075
1276
|
WebDavStorage,
|
|
1076
1277
|
ZuckerStorageModule,
|
|
1077
1278
|
generateDateBasedKey,
|
|
1279
|
+
hydrateStorageUrls,
|
|
1078
1280
|
isFileDenied,
|
|
1079
1281
|
isSafeOriginalFileName,
|
|
1080
|
-
sanitizeFileName
|
|
1282
|
+
sanitizeFileName,
|
|
1283
|
+
transferVerifiedObject,
|
|
1284
|
+
verifyStoredObject
|
|
1081
1285
|
});
|
package/dist/index.mjs
CHANGED
|
@@ -1012,6 +1012,198 @@ ZuckerStorageModule = _ts_decorate5([
|
|
|
1012
1012
|
Module({})
|
|
1013
1013
|
], ZuckerStorageModule);
|
|
1014
1014
|
|
|
1015
|
+
// src/storage-url-hydrator.ts
|
|
1016
|
+
function isPlainRecord(value) {
|
|
1017
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
1018
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1019
|
+
return prototype === Object.prototype || prototype === null;
|
|
1020
|
+
}
|
|
1021
|
+
__name(isPlainRecord, "isPlainRecord");
|
|
1022
|
+
async function hydrateStorageUrls(value, options) {
|
|
1023
|
+
const signedUrls = /* @__PURE__ */ new Map();
|
|
1024
|
+
const hydrate = /* @__PURE__ */ __name(async (current) => {
|
|
1025
|
+
if (Array.isArray(current)) return Promise.all(current.map(hydrate));
|
|
1026
|
+
if (!isPlainRecord(current)) return current;
|
|
1027
|
+
const entries = await Promise.all(Object.entries(current).map(async ([key2, nested]) => [
|
|
1028
|
+
key2,
|
|
1029
|
+
await hydrate(nested)
|
|
1030
|
+
]));
|
|
1031
|
+
const result = Object.fromEntries(entries);
|
|
1032
|
+
const key = result[options.storageKeyField];
|
|
1033
|
+
if (typeof key === "string" && key.length > 0 && options.allowedStorageKeys.has(key)) {
|
|
1034
|
+
let signed = signedUrls.get(key);
|
|
1035
|
+
if (!signed) {
|
|
1036
|
+
signed = options.getDownloadUrl(key);
|
|
1037
|
+
signedUrls.set(key, signed);
|
|
1038
|
+
}
|
|
1039
|
+
const url = await signed;
|
|
1040
|
+
for (const field of options.urlFields) {
|
|
1041
|
+
Object.defineProperty(result, field, {
|
|
1042
|
+
value: url,
|
|
1043
|
+
writable: true,
|
|
1044
|
+
enumerable: true,
|
|
1045
|
+
configurable: true
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
return result;
|
|
1050
|
+
}, "hydrate");
|
|
1051
|
+
return hydrate(value);
|
|
1052
|
+
}
|
|
1053
|
+
__name(hydrateStorageUrls, "hydrateStorageUrls");
|
|
1054
|
+
|
|
1055
|
+
// src/unavailable-file-manager.ts
|
|
1056
|
+
var UnavailableFileManager = class {
|
|
1057
|
+
static {
|
|
1058
|
+
__name(this, "UnavailableFileManager");
|
|
1059
|
+
}
|
|
1060
|
+
providerType;
|
|
1061
|
+
message;
|
|
1062
|
+
constructor(providerType, message = `Storage provider ${providerType} is not configured`) {
|
|
1063
|
+
this.providerType = providerType;
|
|
1064
|
+
this.message = message;
|
|
1065
|
+
}
|
|
1066
|
+
isConfigured() {
|
|
1067
|
+
return false;
|
|
1068
|
+
}
|
|
1069
|
+
unavailable() {
|
|
1070
|
+
return new Error(this.message);
|
|
1071
|
+
}
|
|
1072
|
+
upload(_buffer, _key, _options) {
|
|
1073
|
+
return Promise.reject(this.unavailable());
|
|
1074
|
+
}
|
|
1075
|
+
uploadStream(_stream, _key, _options) {
|
|
1076
|
+
return Promise.reject(this.unavailable());
|
|
1077
|
+
}
|
|
1078
|
+
download(_key) {
|
|
1079
|
+
return Promise.reject(this.unavailable());
|
|
1080
|
+
}
|
|
1081
|
+
delete(_key) {
|
|
1082
|
+
return Promise.reject(this.unavailable());
|
|
1083
|
+
}
|
|
1084
|
+
exists(_key) {
|
|
1085
|
+
return Promise.resolve(false);
|
|
1086
|
+
}
|
|
1087
|
+
getPublicUrl(_key) {
|
|
1088
|
+
return Promise.reject(this.unavailable());
|
|
1089
|
+
}
|
|
1090
|
+
getDownloadUrl(_key) {
|
|
1091
|
+
return Promise.reject(this.unavailable());
|
|
1092
|
+
}
|
|
1093
|
+
list(_prefix) {
|
|
1094
|
+
return Promise.resolve([]);
|
|
1095
|
+
}
|
|
1096
|
+
};
|
|
1097
|
+
|
|
1098
|
+
// src/verified-object-transfer.ts
|
|
1099
|
+
import { createHash } from "crypto";
|
|
1100
|
+
var StorageTransferError = class extends Error {
|
|
1101
|
+
static {
|
|
1102
|
+
__name(this, "StorageTransferError");
|
|
1103
|
+
}
|
|
1104
|
+
code;
|
|
1105
|
+
constructor(code) {
|
|
1106
|
+
super(code), this.code = code;
|
|
1107
|
+
this.name = "StorageTransferError";
|
|
1108
|
+
}
|
|
1109
|
+
};
|
|
1110
|
+
function verifyStoredObject(object, expected, detectMimeType, errorFactory = (code) => new StorageTransferError(code)) {
|
|
1111
|
+
if (object.reportedSize !== object.body.length || object.body.length !== expected.size) throw errorFactory("TARGET_SIZE_CONFLICT");
|
|
1112
|
+
if (detectMimeType(object.body) !== expected.mimeType) throw errorFactory("TARGET_MIME_CONFLICT");
|
|
1113
|
+
if (createHash("sha256").update(object.body).digest("hex") !== expected.sha256) throw errorFactory("TARGET_HASH_CONFLICT");
|
|
1114
|
+
}
|
|
1115
|
+
__name(verifyStoredObject, "verifyStoredObject");
|
|
1116
|
+
async function transferVerifiedObject(options) {
|
|
1117
|
+
const fail = options.errorFactory ?? ((code) => new StorageTransferError(code));
|
|
1118
|
+
const body = options.source.body;
|
|
1119
|
+
if (options.source.reportedSize !== body.length || body.length === 0) throw fail("SOURCE_SIZE_MISMATCH");
|
|
1120
|
+
const metadata = {
|
|
1121
|
+
sha256: createHash("sha256").update(body).digest("hex"),
|
|
1122
|
+
size: body.length,
|
|
1123
|
+
mimeType: options.detectMimeType(body)
|
|
1124
|
+
};
|
|
1125
|
+
if (await options.target.exists(options.destinationKey)) {
|
|
1126
|
+
verifyStoredObject(await options.target.read(options.destinationKey), metadata, options.detectMimeType, fail);
|
|
1127
|
+
return {
|
|
1128
|
+
...metadata,
|
|
1129
|
+
targetDisposition: options.mode === "dry-run" ? "planned" : "reused"
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
if (options.mode === "dry-run" || !options.allowUpload) return {
|
|
1133
|
+
...metadata,
|
|
1134
|
+
targetDisposition: "planned"
|
|
1135
|
+
};
|
|
1136
|
+
const uploaded = await options.target.write(options.destinationKey, body, {
|
|
1137
|
+
contentType: metadata.mimeType,
|
|
1138
|
+
metadata: {
|
|
1139
|
+
sha256: metadata.sha256,
|
|
1140
|
+
size: String(metadata.size),
|
|
1141
|
+
mimeType: metadata.mimeType
|
|
1142
|
+
},
|
|
1143
|
+
private: true
|
|
1144
|
+
});
|
|
1145
|
+
if (uploaded.size !== metadata.size) throw fail("UPLOAD_SIZE_MISMATCH");
|
|
1146
|
+
verifyStoredObject(await options.target.read(options.destinationKey), metadata, options.detectMimeType, fail);
|
|
1147
|
+
return {
|
|
1148
|
+
...metadata,
|
|
1149
|
+
targetDisposition: "uploaded"
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1152
|
+
__name(transferVerifiedObject, "transferVerifiedObject");
|
|
1153
|
+
|
|
1154
|
+
// src/buffered-file-manager-store.ts
|
|
1155
|
+
var BufferedFileManagerStore = class {
|
|
1156
|
+
static {
|
|
1157
|
+
__name(this, "BufferedFileManagerStore");
|
|
1158
|
+
}
|
|
1159
|
+
manager;
|
|
1160
|
+
maxObjectBytes;
|
|
1161
|
+
errorFactory;
|
|
1162
|
+
constructor(manager, maxObjectBytes, errorFactory = (code) => new StorageTransferError(code)) {
|
|
1163
|
+
this.manager = manager;
|
|
1164
|
+
this.maxObjectBytes = maxObjectBytes;
|
|
1165
|
+
this.errorFactory = errorFactory;
|
|
1166
|
+
if (!Number.isSafeInteger(maxObjectBytes) || maxObjectBytes < 1) throw new RangeError("maxObjectBytes must be a positive integer");
|
|
1167
|
+
if (!manager.isConfigured()) throw errorFactory("STORAGE_NOT_CONFIGURED");
|
|
1168
|
+
}
|
|
1169
|
+
async read(key) {
|
|
1170
|
+
const downloaded = await this.manager.download(key);
|
|
1171
|
+
if (downloaded.size < 1 || downloaded.size > this.maxObjectBytes) {
|
|
1172
|
+
downloaded.stream.destroy();
|
|
1173
|
+
throw this.errorFactory("OBJECT_SIZE_OUT_OF_BOUNDS");
|
|
1174
|
+
}
|
|
1175
|
+
const chunks = [];
|
|
1176
|
+
let size = 0;
|
|
1177
|
+
for await (const chunk of downloaded.stream) {
|
|
1178
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
1179
|
+
size += buffer.length;
|
|
1180
|
+
if (size > this.maxObjectBytes) {
|
|
1181
|
+
downloaded.stream.destroy();
|
|
1182
|
+
throw this.errorFactory("OBJECT_SIZE_OUT_OF_BOUNDS");
|
|
1183
|
+
}
|
|
1184
|
+
chunks.push(buffer);
|
|
1185
|
+
}
|
|
1186
|
+
return {
|
|
1187
|
+
body: Buffer.concat(chunks),
|
|
1188
|
+
reportedSize: downloaded.size
|
|
1189
|
+
};
|
|
1190
|
+
}
|
|
1191
|
+
exists(key) {
|
|
1192
|
+
return this.manager.exists(key);
|
|
1193
|
+
}
|
|
1194
|
+
async write(key, body, options) {
|
|
1195
|
+
if (body.length > this.maxObjectBytes) throw this.errorFactory("OBJECT_SIZE_OUT_OF_BOUNDS");
|
|
1196
|
+
const result = await this.manager.upload(body, key, {
|
|
1197
|
+
contentType: options.contentType,
|
|
1198
|
+
metadata: options.metadata,
|
|
1199
|
+
acl: "private"
|
|
1200
|
+
});
|
|
1201
|
+
return {
|
|
1202
|
+
size: result.size
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
1205
|
+
};
|
|
1206
|
+
|
|
1015
1207
|
// src/index.ts
|
|
1016
1208
|
var StorageProvider = /* @__PURE__ */ (function(StorageProvider2) {
|
|
1017
1209
|
StorageProvider2["LOCAL"] = "LOCAL";
|
|
@@ -1020,16 +1212,22 @@ var StorageProvider = /* @__PURE__ */ (function(StorageProvider2) {
|
|
|
1020
1212
|
return StorageProvider2;
|
|
1021
1213
|
})({});
|
|
1022
1214
|
export {
|
|
1215
|
+
BufferedFileManagerStore,
|
|
1023
1216
|
DANGEROUS_EXTENSIONS,
|
|
1024
1217
|
FILE_MANAGER,
|
|
1025
1218
|
FileLifecycleManager,
|
|
1026
1219
|
LocalFileStorage,
|
|
1027
1220
|
S3Storage,
|
|
1028
1221
|
StorageProvider,
|
|
1222
|
+
StorageTransferError,
|
|
1223
|
+
UnavailableFileManager,
|
|
1029
1224
|
WebDavStorage,
|
|
1030
1225
|
ZuckerStorageModule,
|
|
1031
1226
|
generateDateBasedKey,
|
|
1227
|
+
hydrateStorageUrls,
|
|
1032
1228
|
isFileDenied,
|
|
1033
1229
|
isSafeOriginalFileName,
|
|
1034
|
-
sanitizeFileName
|
|
1230
|
+
sanitizeFileName,
|
|
1231
|
+
transferVerifiedObject,
|
|
1232
|
+
verifyStoredObject
|
|
1035
1233
|
};
|