@modern-admin/feature-upload 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/graphql.d.ts +36 -0
- package/dist/graphql.d.ts.map +1 -0
- package/dist/graphql.js +141 -0
- package/dist/graphql.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/nest/index.d.ts +3 -0
- package/dist/nest/index.d.ts.map +1 -0
- package/dist/nest/index.js +4 -0
- package/dist/nest/index.js.map +1 -0
- package/dist/nest/upload-sweeper.service.d.ts +32 -0
- package/dist/nest/upload-sweeper.service.d.ts.map +1 -0
- package/dist/nest/upload-sweeper.service.js +68 -0
- package/dist/nest/upload-sweeper.service.js.map +1 -0
- package/dist/nest/upload.controller.d.ts +51 -0
- package/dist/nest/upload.controller.d.ts.map +1 -0
- package/dist/nest/upload.controller.js +220 -0
- package/dist/nest/upload.controller.js.map +1 -0
- package/dist/nest/upload.module.d.ts +28 -0
- package/dist/nest/upload.module.d.ts.map +1 -0
- package/dist/nest/upload.module.js +51 -0
- package/dist/nest/upload.module.js.map +1 -0
- package/dist/nest/upload.tokens.d.ts +23 -0
- package/dist/nest/upload.tokens.d.ts.map +1 -0
- package/dist/nest/upload.tokens.js +6 -0
- package/dist/nest/upload.tokens.js.map +1 -0
- package/dist/pending-registry.d.ts +60 -0
- package/dist/pending-registry.d.ts.map +1 -0
- package/dist/pending-registry.js +106 -0
- package/dist/pending-registry.js.map +1 -0
- package/dist/providers/local.d.ts +36 -0
- package/dist/providers/local.d.ts.map +1 -0
- package/dist/providers/local.js +44 -0
- package/dist/providers/local.js.map +1 -0
- package/dist/providers/s3.d.ts +96 -0
- package/dist/providers/s3.d.ts.map +1 -0
- package/dist/providers/s3.js +164 -0
- package/dist/providers/s3.js.map +1 -0
- package/dist/registry.d.ts +27 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +25 -0
- package/dist/registry.js.map +1 -0
- package/dist/types.d.ts +130 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +9 -0
- package/dist/types.js.map +1 -0
- package/dist/upload-feature.d.ts +31 -0
- package/dist/upload-feature.d.ts.map +1 -0
- package/dist/upload-feature.js +170 -0
- package/dist/upload-feature.js.map +1 -0
- package/package.json +94 -0
- package/src/graphql.ts +185 -0
- package/src/index.ts +22 -0
- package/src/nest/index.ts +4 -0
- package/src/nest/upload-sweeper.service.ts +52 -0
- package/src/nest/upload.controller.ts +228 -0
- package/src/nest/upload.module.ts +44 -0
- package/src/nest/upload.tokens.ts +24 -0
- package/src/pending-registry.ts +115 -0
- package/src/providers/local.ts +63 -0
- package/src/providers/s3.ts +226 -0
- package/src/registry.ts +39 -0
- package/src/types.ts +137 -0
- package/src/upload-feature.ts +208 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ModernAdminUploadModule — registers `UploadController` plus the sweeper
|
|
3
|
+
* background task that purges orphaned pending uploads.
|
|
4
|
+
*
|
|
5
|
+
* Import alongside `ModernAdminModule.forRoot()` in the host application:
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* @Module({
|
|
9
|
+
* imports: [
|
|
10
|
+
* ModernAdminModule.forRoot({ global: true, ... }),
|
|
11
|
+
* ModernAdminUploadModule.forRoot({ pendingTtlMs: 60 * 60 * 1000 }),
|
|
12
|
+
* // feature modules that use uploadFeature() ...
|
|
13
|
+
* ],
|
|
14
|
+
* })
|
|
15
|
+
* export class AdminModule {}
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* The module depends on `MODERN_ADMIN` and `ModernAdminAuthGuard` being
|
|
19
|
+
* available in the DI tree, which is satisfied when `ModernAdminModule.forRoot`
|
|
20
|
+
* is registered with `global: true` (the recommended default).
|
|
21
|
+
*/
|
|
22
|
+
import { type DynamicModule } from '@nestjs/common';
|
|
23
|
+
import { type ModernAdminUploadModuleOptions } from './upload.tokens.js';
|
|
24
|
+
export type { ModernAdminUploadModuleOptions } from './upload.tokens.js';
|
|
25
|
+
export declare class ModernAdminUploadModule {
|
|
26
|
+
static forRoot(options?: ModernAdminUploadModuleOptions): DynamicModule;
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=upload.module.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"upload.module.d.ts","sourceRoot":"","sources":["../../src/nest/upload.module.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,KAAK,aAAa,EAAU,MAAM,gBAAgB,CAAA;AAG3D,OAAO,EAAyB,KAAK,8BAA8B,EAAE,MAAM,oBAAoB,CAAA;AAE/F,YAAY,EAAE,8BAA8B,EAAE,MAAM,oBAAoB,CAAA;AAExE,qBACa,uBAAuB;IAClC,MAAM,CAAC,OAAO,CAAC,OAAO,GAAE,8BAAmC,GAAG,aAAa;CAY5E"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ModernAdminUploadModule — registers `UploadController` plus the sweeper
|
|
3
|
+
* background task that purges orphaned pending uploads.
|
|
4
|
+
*
|
|
5
|
+
* Import alongside `ModernAdminModule.forRoot()` in the host application:
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* @Module({
|
|
9
|
+
* imports: [
|
|
10
|
+
* ModernAdminModule.forRoot({ global: true, ... }),
|
|
11
|
+
* ModernAdminUploadModule.forRoot({ pendingTtlMs: 60 * 60 * 1000 }),
|
|
12
|
+
* // feature modules that use uploadFeature() ...
|
|
13
|
+
* ],
|
|
14
|
+
* })
|
|
15
|
+
* export class AdminModule {}
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* The module depends on `MODERN_ADMIN` and `ModernAdminAuthGuard` being
|
|
19
|
+
* available in the DI tree, which is satisfied when `ModernAdminModule.forRoot`
|
|
20
|
+
* is registered with `global: true` (the recommended default).
|
|
21
|
+
*/
|
|
22
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
23
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
24
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
25
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
26
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
27
|
+
};
|
|
28
|
+
var ModernAdminUploadModule_1;
|
|
29
|
+
import { Module } from '@nestjs/common';
|
|
30
|
+
import { UploadController } from './upload.controller.js';
|
|
31
|
+
import { UploadSweeperService } from './upload-sweeper.service.js';
|
|
32
|
+
import { UPLOAD_MODULE_OPTIONS } from './upload.tokens.js';
|
|
33
|
+
let ModernAdminUploadModule = ModernAdminUploadModule_1 = class ModernAdminUploadModule {
|
|
34
|
+
static forRoot(options = {}) {
|
|
35
|
+
return {
|
|
36
|
+
module: ModernAdminUploadModule_1,
|
|
37
|
+
global: options.global ?? false,
|
|
38
|
+
controllers: [UploadController],
|
|
39
|
+
providers: [
|
|
40
|
+
{ provide: UPLOAD_MODULE_OPTIONS, useValue: options },
|
|
41
|
+
UploadSweeperService,
|
|
42
|
+
],
|
|
43
|
+
exports: [UPLOAD_MODULE_OPTIONS],
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
ModernAdminUploadModule = ModernAdminUploadModule_1 = __decorate([
|
|
48
|
+
Module({})
|
|
49
|
+
], ModernAdminUploadModule);
|
|
50
|
+
export { ModernAdminUploadModule };
|
|
51
|
+
//# sourceMappingURL=upload.module.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"upload.module.js","sourceRoot":"","sources":["../../src/nest/upload.module.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;;;;;;;;AAEH,OAAO,EAAsB,MAAM,EAAE,MAAM,gBAAgB,CAAA;AAC3D,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AACzD,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAA;AAClE,OAAO,EAAE,qBAAqB,EAAuC,MAAM,oBAAoB,CAAA;AAKxF,IAAM,uBAAuB,+BAA7B,MAAM,uBAAuB;IAClC,MAAM,CAAC,OAAO,CAAC,UAA0C,EAAE;QACzD,OAAO;YACL,MAAM,EAAE,yBAAuB;YAC/B,MAAM,EAAE,OAAO,CAAC,MAAM,IAAI,KAAK;YAC/B,WAAW,EAAE,CAAC,gBAAgB,CAAC;YAC/B,SAAS,EAAE;gBACT,EAAE,OAAO,EAAE,qBAAqB,EAAE,QAAQ,EAAE,OAAO,EAAE;gBACrD,oBAAoB;aACrB;YACD,OAAO,EAAE,CAAC,qBAAqB,CAAC;SACjC,CAAA;IACH,CAAC;CACF,CAAA;AAbY,uBAAuB;IADnC,MAAM,CAAC,EAAE,CAAC;GACE,uBAAuB,CAanC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DI tokens + option types shared between `ModernAdminUploadModule`,
|
|
3
|
+
* `UploadController`, and the sweeper service.
|
|
4
|
+
*/
|
|
5
|
+
export declare const UPLOAD_MODULE_OPTIONS: unique symbol;
|
|
6
|
+
export interface ModernAdminUploadModuleOptions {
|
|
7
|
+
/** Mark this module as global. Defaults to false. */
|
|
8
|
+
global?: boolean;
|
|
9
|
+
/**
|
|
10
|
+
* TTL applied to freshly uploaded files before the sweeper deletes them.
|
|
11
|
+
* Default: 1 hour. Files are confirmed (TTL becomes irrelevant) as soon
|
|
12
|
+
* as the parent record is saved, via the action hooks installed by
|
|
13
|
+
* `uploadFeature`.
|
|
14
|
+
*/
|
|
15
|
+
pendingTtlMs?: number;
|
|
16
|
+
/**
|
|
17
|
+
* Interval at which the sweeper runs. Default: 5 minutes. Set to `0` to
|
|
18
|
+
* disable the periodic sweeper entirely (the registry still works — entries
|
|
19
|
+
* just never expire).
|
|
20
|
+
*/
|
|
21
|
+
sweepIntervalMs?: number;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=upload.tokens.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"upload.tokens.d.ts","sourceRoot":"","sources":["../../src/nest/upload.tokens.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,eAAO,MAAM,qBAAqB,eAAmD,CAAA;AAErF,MAAM,WAAW,8BAA8B;IAC7C,qDAAqD;IACrD,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAA;CACzB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"upload.tokens.js","sourceRoot":"","sources":["../../src/nest/upload.tokens.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,CAAC,MAAM,qBAAqB,GAAG,MAAM,CAAC,GAAG,CAAC,oCAAoC,CAAC,CAAA"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PendingUploadsRegistry — tracks files that were uploaded to storage but have
|
|
3
|
+
* not yet been "confirmed" by saving the parent record.
|
|
4
|
+
*
|
|
5
|
+
* Why
|
|
6
|
+
* ---
|
|
7
|
+
* Uploads happen *before* the form is submitted (the user picks a file, the
|
|
8
|
+
* editor calls `POST /upload`, gets back a key, stores it in form state). If
|
|
9
|
+
* the user then abandons the form (closes the tab, navigates away, server
|
|
10
|
+
* rejects the create payload, etc.), the file ends up orphaned in storage.
|
|
11
|
+
*
|
|
12
|
+
* How
|
|
13
|
+
* ---
|
|
14
|
+
* - The controller calls `track(key, providerId, ttlMs)` after every successful
|
|
15
|
+
* upload, recording an expiry timestamp.
|
|
16
|
+
* - The action hooks installed by `uploadFeature` call `confirm(keys)` from
|
|
17
|
+
* `new.after` / `edit.after` once the record is saved, removing the keys
|
|
18
|
+
* from the pending set so they will not be swept.
|
|
19
|
+
* - The user-initiated `DELETE /upload?…` endpoint calls `cancel(key)` to
|
|
20
|
+
* immediately remove the file from storage when the user removes a freshly
|
|
21
|
+
* uploaded file *before* saving.
|
|
22
|
+
* - A periodic sweeper started by `ModernAdminUploadModule.forRoot()` calls
|
|
23
|
+
* `sweep()` to delete files whose pending entry has expired. This handles
|
|
24
|
+
* abandoned forms, browser crashes, and any other path that bypasses
|
|
25
|
+
* client-side cleanup.
|
|
26
|
+
*
|
|
27
|
+
* The registry is a process-level singleton (same pattern as
|
|
28
|
+
* `UploadProviderRegistry`).
|
|
29
|
+
*
|
|
30
|
+
* TODO(roadmap): swap the in-process `Map` for a Redis-backed store and move
|
|
31
|
+
* the sweeper driver (`UploadSweeperService`) onto BullMQ. This is required
|
|
32
|
+
* for multi-instance deployments where one Nest replica serves the upload
|
|
33
|
+
* request and a different replica processes the form submission — without a
|
|
34
|
+
* shared store the second replica would not see the pending entry, the
|
|
35
|
+
* confirm hook would no-op, and the file would be swept on the originator.
|
|
36
|
+
* BullMQ also gives us crash-safe scheduling and retries.
|
|
37
|
+
*/
|
|
38
|
+
export declare const PendingUploadsRegistry: {
|
|
39
|
+
/** Mark `key` as a freshly-uploaded, unconfirmed file. */
|
|
40
|
+
track(key: string, providerId: string, ttlMs: number): void;
|
|
41
|
+
/** Whether `key` is currently in the pending set (helper for tests / cancel). */
|
|
42
|
+
has(key: string): boolean;
|
|
43
|
+
/** Confirm one or more keys — they leave pending without being deleted. */
|
|
44
|
+
confirm(keys: ReadonlyArray<string>): void;
|
|
45
|
+
/**
|
|
46
|
+
* Cancel a single pending key — deletes the file from storage and removes
|
|
47
|
+
* the entry. No-op if the key is not pending (already confirmed or unknown).
|
|
48
|
+
*/
|
|
49
|
+
cancel(key: string): Promise<boolean>;
|
|
50
|
+
/**
|
|
51
|
+
* Sweep expired entries — delete each from storage and from the registry.
|
|
52
|
+
* Returns the number of files swept.
|
|
53
|
+
*/
|
|
54
|
+
sweep(now?: number): Promise<number>;
|
|
55
|
+
/** For test cleanup. */
|
|
56
|
+
clear(): void;
|
|
57
|
+
/** Number of currently-pending entries (for tests / introspection). */
|
|
58
|
+
size(): number;
|
|
59
|
+
};
|
|
60
|
+
//# sourceMappingURL=pending-registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pending-registry.d.ts","sourceRoot":"","sources":["../src/pending-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAaH,eAAO,MAAM,sBAAsB;IACjC,0DAA0D;eAC/C,MAAM,cAAc,MAAM,SAAS,MAAM,GAAG,IAAI;IAI3D,iFAAiF;aACxE,MAAM,GAAG,OAAO;IAIzB,2EAA2E;kBAC7D,aAAa,CAAC,MAAM,CAAC,GAAG,IAAI;IAI1C;;;OAGG;gBACe,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAc3C;;;OAGG;gBACc,MAAM,GAAgB,OAAO,CAAC,MAAM,CAAC;IAkBtD,wBAAwB;aACf,IAAI;IAIb,uEAAuE;YAC/D,MAAM;CAGf,CAAA"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PendingUploadsRegistry — tracks files that were uploaded to storage but have
|
|
3
|
+
* not yet been "confirmed" by saving the parent record.
|
|
4
|
+
*
|
|
5
|
+
* Why
|
|
6
|
+
* ---
|
|
7
|
+
* Uploads happen *before* the form is submitted (the user picks a file, the
|
|
8
|
+
* editor calls `POST /upload`, gets back a key, stores it in form state). If
|
|
9
|
+
* the user then abandons the form (closes the tab, navigates away, server
|
|
10
|
+
* rejects the create payload, etc.), the file ends up orphaned in storage.
|
|
11
|
+
*
|
|
12
|
+
* How
|
|
13
|
+
* ---
|
|
14
|
+
* - The controller calls `track(key, providerId, ttlMs)` after every successful
|
|
15
|
+
* upload, recording an expiry timestamp.
|
|
16
|
+
* - The action hooks installed by `uploadFeature` call `confirm(keys)` from
|
|
17
|
+
* `new.after` / `edit.after` once the record is saved, removing the keys
|
|
18
|
+
* from the pending set so they will not be swept.
|
|
19
|
+
* - The user-initiated `DELETE /upload?…` endpoint calls `cancel(key)` to
|
|
20
|
+
* immediately remove the file from storage when the user removes a freshly
|
|
21
|
+
* uploaded file *before* saving.
|
|
22
|
+
* - A periodic sweeper started by `ModernAdminUploadModule.forRoot()` calls
|
|
23
|
+
* `sweep()` to delete files whose pending entry has expired. This handles
|
|
24
|
+
* abandoned forms, browser crashes, and any other path that bypasses
|
|
25
|
+
* client-side cleanup.
|
|
26
|
+
*
|
|
27
|
+
* The registry is a process-level singleton (same pattern as
|
|
28
|
+
* `UploadProviderRegistry`).
|
|
29
|
+
*
|
|
30
|
+
* TODO(roadmap): swap the in-process `Map` for a Redis-backed store and move
|
|
31
|
+
* the sweeper driver (`UploadSweeperService`) onto BullMQ. This is required
|
|
32
|
+
* for multi-instance deployments where one Nest replica serves the upload
|
|
33
|
+
* request and a different replica processes the form submission — without a
|
|
34
|
+
* shared store the second replica would not see the pending entry, the
|
|
35
|
+
* confirm hook would no-op, and the file would be swept on the originator.
|
|
36
|
+
* BullMQ also gives us crash-safe scheduling and retries.
|
|
37
|
+
*/
|
|
38
|
+
import { UploadProviderRegistry } from './registry.js';
|
|
39
|
+
const _pending = new Map();
|
|
40
|
+
export const PendingUploadsRegistry = {
|
|
41
|
+
/** Mark `key` as a freshly-uploaded, unconfirmed file. */
|
|
42
|
+
track(key, providerId, ttlMs) {
|
|
43
|
+
_pending.set(key, { providerId, expiresAt: Date.now() + ttlMs });
|
|
44
|
+
},
|
|
45
|
+
/** Whether `key` is currently in the pending set (helper for tests / cancel). */
|
|
46
|
+
has(key) {
|
|
47
|
+
return _pending.has(key);
|
|
48
|
+
},
|
|
49
|
+
/** Confirm one or more keys — they leave pending without being deleted. */
|
|
50
|
+
confirm(keys) {
|
|
51
|
+
for (const k of keys)
|
|
52
|
+
_pending.delete(k);
|
|
53
|
+
},
|
|
54
|
+
/**
|
|
55
|
+
* Cancel a single pending key — deletes the file from storage and removes
|
|
56
|
+
* the entry. No-op if the key is not pending (already confirmed or unknown).
|
|
57
|
+
*/
|
|
58
|
+
async cancel(key) {
|
|
59
|
+
const entry = _pending.get(key);
|
|
60
|
+
if (!entry)
|
|
61
|
+
return false;
|
|
62
|
+
_pending.delete(key);
|
|
63
|
+
const cfg = UploadProviderRegistry.get(entry.providerId);
|
|
64
|
+
if (!cfg)
|
|
65
|
+
return false;
|
|
66
|
+
try {
|
|
67
|
+
await cfg.provider.delete(key);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// Non-fatal — best-effort cleanup.
|
|
71
|
+
}
|
|
72
|
+
return true;
|
|
73
|
+
},
|
|
74
|
+
/**
|
|
75
|
+
* Sweep expired entries — delete each from storage and from the registry.
|
|
76
|
+
* Returns the number of files swept.
|
|
77
|
+
*/
|
|
78
|
+
async sweep(now = Date.now()) {
|
|
79
|
+
let swept = 0;
|
|
80
|
+
for (const [key, entry] of _pending) {
|
|
81
|
+
if (entry.expiresAt <= now) {
|
|
82
|
+
_pending.delete(key);
|
|
83
|
+
const cfg = UploadProviderRegistry.get(entry.providerId);
|
|
84
|
+
if (!cfg)
|
|
85
|
+
continue;
|
|
86
|
+
try {
|
|
87
|
+
await cfg.provider.delete(key);
|
|
88
|
+
swept++;
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// Non-fatal.
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return swept;
|
|
96
|
+
},
|
|
97
|
+
/** For test cleanup. */
|
|
98
|
+
clear() {
|
|
99
|
+
_pending.clear();
|
|
100
|
+
},
|
|
101
|
+
/** Number of currently-pending entries (for tests / introspection). */
|
|
102
|
+
size() {
|
|
103
|
+
return _pending.size;
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
//# sourceMappingURL=pending-registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pending-registry.js","sourceRoot":"","sources":["../src/pending-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,OAAO,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAA;AAStD,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAwB,CAAA;AAEhD,MAAM,CAAC,MAAM,sBAAsB,GAAG;IACpC,0DAA0D;IAC1D,KAAK,CAAC,GAAW,EAAE,UAAkB,EAAE,KAAa;QAClD,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,UAAU,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,EAAE,CAAC,CAAA;IAClE,CAAC;IAED,iFAAiF;IACjF,GAAG,CAAC,GAAW;QACb,OAAO,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;IAC1B,CAAC;IAED,2EAA2E;IAC3E,OAAO,CAAC,IAA2B;QACjC,KAAK,MAAM,CAAC,IAAI,IAAI;YAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAA;IAC1C,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,MAAM,CAAC,GAAW;QACtB,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QAC/B,IAAI,CAAC,KAAK;YAAE,OAAO,KAAK,CAAA;QACxB,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;QACpB,MAAM,GAAG,GAAG,sBAAsB,CAAC,GAAG,CAAC,KAAK,CAAC,UAAU,CAAC,CAAA;QACxD,IAAI,CAAC,GAAG;YAAE,OAAO,KAAK,CAAA;QACtB,IAAI,CAAC;YACH,MAAM,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;QAChC,CAAC;QAAC,MAAM,CAAC;YACP,mCAAmC;QACrC,CAAC;QACD,OAAO,IAAI,CAAA;IACb,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,KAAK,CAAC,MAAc,IAAI,CAAC,GAAG,EAAE;QAClC,IAAI,KAAK,GAAG,CAAC,CAAA;QACb,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,QAAQ,EAAE,CAAC;YACpC,IAAI,KAAK,CAAC,SAAS,IAAI,GAAG,EAAE,CAAC;gBAC3B,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;gBACpB,MAAM,GAAG,GAAG,sBAAsB,CAAC,GAAG,CAAC,KAAK,CAAC,UAAU,CAAC,CAAA;gBACxD,IAAI,CAAC,GAAG;oBAAE,SAAQ;gBAClB,IAAI,CAAC;oBACH,MAAM,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;oBAC9B,KAAK,EAAE,CAAA;gBACT,CAAC;gBAAC,MAAM,CAAC;oBACP,aAAa;gBACf,CAAC;YACH,CAAC;QACH,CAAC;QACD,OAAO,KAAK,CAAA;IACd,CAAC;IAED,wBAAwB;IACxB,KAAK;QACH,QAAQ,CAAC,KAAK,EAAE,CAAA;IAClB,CAAC;IAED,uEAAuE;IACvE,IAAI;QACF,OAAO,QAAQ,CAAC,IAAI,CAAA;IACtB,CAAC;CACF,CAAA"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LocalUploadProvider — stores files on the local filesystem.
|
|
3
|
+
*
|
|
4
|
+
* Files are written to `uploadDir` with a UUID-based filename to avoid
|
|
5
|
+
* collisions. Set `baseUrl` to the public URL prefix where the directory is
|
|
6
|
+
* served as static files (e.g. `'/uploads'` or `'http://localhost:3000/uploads'`).
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* new LocalUploadProvider({ uploadDir: './public/uploads', baseUrl: '/uploads' })
|
|
10
|
+
*/
|
|
11
|
+
import type { IUploadProvider, UploadedFile } from '../types.js';
|
|
12
|
+
export interface LocalUploadOptions {
|
|
13
|
+
/**
|
|
14
|
+
* Absolute or process-relative path to the directory where files are stored.
|
|
15
|
+
* Created automatically on first upload.
|
|
16
|
+
*/
|
|
17
|
+
uploadDir: string;
|
|
18
|
+
/**
|
|
19
|
+
* Public URL prefix (without trailing slash) where uploaded files can be
|
|
20
|
+
* accessed. Used to construct the `url` returned by the upload endpoint and
|
|
21
|
+
* the `urlTemplate` for the frontend.
|
|
22
|
+
*
|
|
23
|
+
* @example '/uploads'
|
|
24
|
+
* @example 'https://static.example.com/uploads'
|
|
25
|
+
*/
|
|
26
|
+
baseUrl?: string;
|
|
27
|
+
}
|
|
28
|
+
export declare class LocalUploadProvider implements IUploadProvider {
|
|
29
|
+
private readonly options;
|
|
30
|
+
constructor(options: LocalUploadOptions);
|
|
31
|
+
upload(file: UploadedFile, key?: string): Promise<string>;
|
|
32
|
+
getUrl(key: string): string;
|
|
33
|
+
delete(key: string): Promise<void>;
|
|
34
|
+
urlTemplate(): string;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=local.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local.d.ts","sourceRoot":"","sources":["../../src/providers/local.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAKH,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAEhE,MAAM,WAAW,kBAAkB;IACjC;;;OAGG;IACH,SAAS,EAAE,MAAM,CAAA;IACjB;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,qBAAa,mBAAoB,YAAW,eAAe;IAC7C,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,kBAAkB;IAElD,MAAM,CAAC,IAAI,EAAE,YAAY,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAS/D,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM;IAKrB,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQxC,WAAW,IAAI,MAAM;CAItB"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LocalUploadProvider — stores files on the local filesystem.
|
|
3
|
+
*
|
|
4
|
+
* Files are written to `uploadDir` with a UUID-based filename to avoid
|
|
5
|
+
* collisions. Set `baseUrl` to the public URL prefix where the directory is
|
|
6
|
+
* served as static files (e.g. `'/uploads'` or `'http://localhost:3000/uploads'`).
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* new LocalUploadProvider({ uploadDir: './public/uploads', baseUrl: '/uploads' })
|
|
10
|
+
*/
|
|
11
|
+
import { mkdir, writeFile, unlink } from 'node:fs/promises';
|
|
12
|
+
import { join, extname, dirname } from 'node:path';
|
|
13
|
+
import { uuidv7 } from '@modern-admin/core';
|
|
14
|
+
export class LocalUploadProvider {
|
|
15
|
+
constructor(options) {
|
|
16
|
+
this.options = options;
|
|
17
|
+
}
|
|
18
|
+
async upload(file, key) {
|
|
19
|
+
const resolvedKey = key ?? `${uuidv7()}${extname(file.originalName)}`;
|
|
20
|
+
const dest = join(this.options.uploadDir, resolvedKey);
|
|
21
|
+
// Create the full directory tree (handles nested keys like 'avatars/2024/01/uuid.jpg').
|
|
22
|
+
await mkdir(dirname(dest), { recursive: true });
|
|
23
|
+
await writeFile(dest, file.buffer);
|
|
24
|
+
return resolvedKey;
|
|
25
|
+
}
|
|
26
|
+
getUrl(key) {
|
|
27
|
+
const base = this.options.baseUrl ?? '/uploads';
|
|
28
|
+
return `${base}/${key}`;
|
|
29
|
+
}
|
|
30
|
+
async delete(key) {
|
|
31
|
+
try {
|
|
32
|
+
await unlink(join(this.options.uploadDir, key));
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
if (err.code !== 'ENOENT')
|
|
36
|
+
throw err;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
urlTemplate() {
|
|
40
|
+
const base = this.options.baseUrl ?? '/uploads';
|
|
41
|
+
return `${base}/{key}`;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=local.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local.js","sourceRoot":"","sources":["../../src/providers/local.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AAC3D,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAClD,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAA;AAoB3C,MAAM,OAAO,mBAAmB;IAC9B,YAA6B,OAA2B;QAA3B,YAAO,GAAP,OAAO,CAAoB;IAAG,CAAC;IAE5D,KAAK,CAAC,MAAM,CAAC,IAAkB,EAAE,GAAY;QAC3C,MAAM,WAAW,GAAG,GAAG,IAAI,GAAG,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,YAAY,CAAC,EAAE,CAAA;QACrE,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,WAAW,CAAC,CAAA;QACtD,wFAAwF;QACxF,MAAM,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;QAC/C,MAAM,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,CAAA;QAClC,OAAO,WAAW,CAAA;IACpB,CAAC;IAED,MAAM,CAAC,GAAW;QAChB,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,IAAI,UAAU,CAAA;QAC/C,OAAO,GAAG,IAAI,IAAI,GAAG,EAAE,CAAA;IACzB,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,GAAW;QACtB,IAAI,CAAC;YACH,MAAM,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC,CAAA;QACjD,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAK,GAA6B,CAAC,IAAI,KAAK,QAAQ;gBAAE,MAAM,GAAG,CAAA;QACjE,CAAC;IACH,CAAC;IAED,WAAW;QACT,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,IAAI,UAAU,CAAA;QAC/C,OAAO,GAAG,IAAI,QAAQ,CAAA;IACxB,CAAC;CACF"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S3UploadProvider — stores files in an AWS S3 bucket (or any S3-compatible
|
|
3
|
+
* service such as MinIO, Cloudflare R2, DigitalOcean Spaces, etc.).
|
|
4
|
+
*
|
|
5
|
+
* Requires `@aws-sdk/client-s3` to be installed in the host project.
|
|
6
|
+
* For streaming multipart upload of large files, also install `@aws-sdk/lib-storage`.
|
|
7
|
+
* For pre-signed URLs (private buckets), install `@aws-sdk/s3-request-presigner`.
|
|
8
|
+
*
|
|
9
|
+
* @example AWS S3 — public bucket
|
|
10
|
+
* new S3UploadProvider({ bucket: 'my-bucket', region: 'us-east-1', acl: 'public-read' })
|
|
11
|
+
*
|
|
12
|
+
* @example AWS S3 — private bucket with pre-signed URLs (60 min expiry)
|
|
13
|
+
* new S3UploadProvider({ bucket: 'my-bucket', region: 'us-east-1', signed: { expiresIn: 3600 } })
|
|
14
|
+
*
|
|
15
|
+
* @example MinIO / custom endpoint
|
|
16
|
+
* new S3UploadProvider({
|
|
17
|
+
* bucket: 'my-bucket',
|
|
18
|
+
* region: 'us-east-1',
|
|
19
|
+
* endpoint: 'http://localhost:9000',
|
|
20
|
+
* forcePathStyle: true,
|
|
21
|
+
* publicBaseUrl: 'http://localhost:9000/my-bucket',
|
|
22
|
+
* })
|
|
23
|
+
*
|
|
24
|
+
* @example Inject a pre-configured S3Client (share across providers)
|
|
25
|
+
* const s3 = new S3Client({ region: 'us-east-1' })
|
|
26
|
+
* new S3UploadProvider({ bucket: 'my-bucket', region: 'us-east-1' }, s3)
|
|
27
|
+
*/
|
|
28
|
+
import type { IUploadProvider, UploadedFile } from '../types.js';
|
|
29
|
+
export interface S3UploadOptions {
|
|
30
|
+
/** S3 bucket name. */
|
|
31
|
+
bucket: string;
|
|
32
|
+
/** AWS region (e.g. `'us-east-1'`). */
|
|
33
|
+
region: string;
|
|
34
|
+
/** AWS access key id. Falls back to environment / credential chain. */
|
|
35
|
+
accessKeyId?: string;
|
|
36
|
+
/** AWS secret access key. Falls back to environment / credential chain. */
|
|
37
|
+
secretAccessKey?: string;
|
|
38
|
+
/**
|
|
39
|
+
* Custom endpoint for S3-compatible services.
|
|
40
|
+
* @example 'http://localhost:9000'
|
|
41
|
+
* @example 'https://nyc3.digitaloceanspaces.com'
|
|
42
|
+
*/
|
|
43
|
+
endpoint?: string;
|
|
44
|
+
/** Force path-style URLs (required for some MinIO / custom endpoint setups). */
|
|
45
|
+
forcePathStyle?: boolean;
|
|
46
|
+
/** Optional key prefix / "folder". Without trailing slash. */
|
|
47
|
+
prefix?: string;
|
|
48
|
+
/**
|
|
49
|
+
* Canned ACL applied to every uploaded object, e.g. `'public-read'`.
|
|
50
|
+
* Omit for private buckets — use `signed` instead.
|
|
51
|
+
*/
|
|
52
|
+
acl?: string;
|
|
53
|
+
/**
|
|
54
|
+
* Override the public base URL used for `getUrl()` and `urlTemplate()`.
|
|
55
|
+
* Useful when files are served via CloudFront or a custom CDN.
|
|
56
|
+
* Without trailing slash.
|
|
57
|
+
* @example 'https://cdn.example.com'
|
|
58
|
+
*/
|
|
59
|
+
publicBaseUrl?: string;
|
|
60
|
+
/**
|
|
61
|
+
* Generate pre-signed URLs instead of public URLs.
|
|
62
|
+
* Required for private S3 buckets. Needs `@aws-sdk/s3-request-presigner`.
|
|
63
|
+
*
|
|
64
|
+
* - `true` uses the default expiry (3600 s / 1 hour).
|
|
65
|
+
* - Pass `{ expiresIn: seconds }` to customise.
|
|
66
|
+
*
|
|
67
|
+
* When `signed` is set, `urlTemplate()` is not implemented (each URL is
|
|
68
|
+
* unique and time-limited) and `getUrl()` is async.
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* signed: true // 1-hour pre-signed URLs
|
|
72
|
+
* signed: { expiresIn: 60 * 60 * 24 } // 24-hour pre-signed URLs
|
|
73
|
+
*/
|
|
74
|
+
signed?: boolean | {
|
|
75
|
+
expiresIn?: number;
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
export declare class S3UploadProvider implements IUploadProvider {
|
|
79
|
+
private readonly options;
|
|
80
|
+
private readonly injectedClient?;
|
|
81
|
+
private _client;
|
|
82
|
+
constructor(options: S3UploadOptions, injectedClient?: any | undefined);
|
|
83
|
+
private client;
|
|
84
|
+
upload(file: UploadedFile, key?: string): Promise<string>;
|
|
85
|
+
getUrl(key: string): Promise<string>;
|
|
86
|
+
delete(key: string): Promise<void>;
|
|
87
|
+
/**
|
|
88
|
+
* URL template for the frontend. Only available when `signed` is NOT set
|
|
89
|
+
* (public buckets). For private buckets each URL is unique + time-limited,
|
|
90
|
+
* so the frontend must call the upload endpoint to get a fresh URL.
|
|
91
|
+
*/
|
|
92
|
+
urlTemplate(): string | undefined;
|
|
93
|
+
private signedUrl;
|
|
94
|
+
private publicBaseUrl;
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=s3.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"s3.d.ts","sourceRoot":"","sources":["../../src/providers/s3.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAIH,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAEhE,MAAM,WAAW,eAAe;IAC9B,sBAAsB;IACtB,MAAM,EAAE,MAAM,CAAA;IACd,uCAAuC;IACvC,MAAM,EAAE,MAAM,CAAA;IACd,uEAAuE;IACvE,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,2EAA2E;IAC3E,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,gFAAgF;IAChF,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,8DAA8D;IAC9D,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;;;;;;;;;;;;OAaG;IACH,MAAM,CAAC,EAAE,OAAO,GAAG;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;CAC1C;AAED,qBAAa,gBAAiB,YAAW,eAAe;IAKpD,OAAO,CAAC,QAAQ,CAAC,OAAO;IAIxB,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC;IAPlC,OAAO,CAAC,OAAO,CAAY;gBAGR,OAAO,EAAE,eAAe,EAIxB,cAAc,CAAC,EAAE,GAAG,YAAA;YAIzB,MAAM;IA4Bd,MAAM,CAAC,IAAI,EAAE,YAAY,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IA2CzD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAOpC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAaxC;;;;OAIG;IACH,WAAW,IAAI,MAAM,GAAG,SAAS;YAKnB,SAAS;IA0BvB,OAAO,CAAC,aAAa;CAKtB"}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S3UploadProvider — stores files in an AWS S3 bucket (or any S3-compatible
|
|
3
|
+
* service such as MinIO, Cloudflare R2, DigitalOcean Spaces, etc.).
|
|
4
|
+
*
|
|
5
|
+
* Requires `@aws-sdk/client-s3` to be installed in the host project.
|
|
6
|
+
* For streaming multipart upload of large files, also install `@aws-sdk/lib-storage`.
|
|
7
|
+
* For pre-signed URLs (private buckets), install `@aws-sdk/s3-request-presigner`.
|
|
8
|
+
*
|
|
9
|
+
* @example AWS S3 — public bucket
|
|
10
|
+
* new S3UploadProvider({ bucket: 'my-bucket', region: 'us-east-1', acl: 'public-read' })
|
|
11
|
+
*
|
|
12
|
+
* @example AWS S3 — private bucket with pre-signed URLs (60 min expiry)
|
|
13
|
+
* new S3UploadProvider({ bucket: 'my-bucket', region: 'us-east-1', signed: { expiresIn: 3600 } })
|
|
14
|
+
*
|
|
15
|
+
* @example MinIO / custom endpoint
|
|
16
|
+
* new S3UploadProvider({
|
|
17
|
+
* bucket: 'my-bucket',
|
|
18
|
+
* region: 'us-east-1',
|
|
19
|
+
* endpoint: 'http://localhost:9000',
|
|
20
|
+
* forcePathStyle: true,
|
|
21
|
+
* publicBaseUrl: 'http://localhost:9000/my-bucket',
|
|
22
|
+
* })
|
|
23
|
+
*
|
|
24
|
+
* @example Inject a pre-configured S3Client (share across providers)
|
|
25
|
+
* const s3 = new S3Client({ region: 'us-east-1' })
|
|
26
|
+
* new S3UploadProvider({ bucket: 'my-bucket', region: 'us-east-1' }, s3)
|
|
27
|
+
*/
|
|
28
|
+
import { extname } from 'node:path';
|
|
29
|
+
import { uuidv7 } from '@modern-admin/core';
|
|
30
|
+
export class S3UploadProvider {
|
|
31
|
+
constructor(options,
|
|
32
|
+
// Accept a pre-configured S3Client instance (share across providers /
|
|
33
|
+
// inject in tests). When omitted the provider creates its own client.
|
|
34
|
+
injectedClient) {
|
|
35
|
+
this.options = options;
|
|
36
|
+
this.injectedClient = injectedClient;
|
|
37
|
+
this._client = null;
|
|
38
|
+
}
|
|
39
|
+
async client() {
|
|
40
|
+
if (this.injectedClient)
|
|
41
|
+
return this.injectedClient;
|
|
42
|
+
if (this._client)
|
|
43
|
+
return this._client;
|
|
44
|
+
let S3Client;
|
|
45
|
+
try {
|
|
46
|
+
const mod = await import('@aws-sdk/client-s3');
|
|
47
|
+
S3Client = mod.S3Client;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
throw new Error('[modern-admin/feature-upload] S3UploadProvider requires @aws-sdk/client-s3. ' +
|
|
51
|
+
'Install it: bun add @aws-sdk/client-s3');
|
|
52
|
+
}
|
|
53
|
+
const cfg = {
|
|
54
|
+
region: this.options.region,
|
|
55
|
+
...(this.options.endpoint ? { endpoint: this.options.endpoint } : {}),
|
|
56
|
+
...(this.options.forcePathStyle ? { forcePathStyle: true } : {}),
|
|
57
|
+
};
|
|
58
|
+
if (this.options.accessKeyId && this.options.secretAccessKey) {
|
|
59
|
+
cfg.credentials = {
|
|
60
|
+
accessKeyId: this.options.accessKeyId,
|
|
61
|
+
secretAccessKey: this.options.secretAccessKey,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
this._client = new S3Client(cfg);
|
|
65
|
+
return this._client;
|
|
66
|
+
}
|
|
67
|
+
async upload(file, key) {
|
|
68
|
+
const c = await this.client();
|
|
69
|
+
const ext = extname(file.originalName);
|
|
70
|
+
const prefix = this.options.prefix ? `${this.options.prefix}/` : '';
|
|
71
|
+
const resolvedKey = key ?? `${prefix}${uuidv7()}${ext}`;
|
|
72
|
+
// Prefer @aws-sdk/lib-storage for streaming multipart uploads (large files).
|
|
73
|
+
// Fall back to PutObjectCommand if lib-storage is not installed.
|
|
74
|
+
try {
|
|
75
|
+
const libStorage = await import('@aws-sdk/lib-storage');
|
|
76
|
+
const Upload = libStorage.Upload;
|
|
77
|
+
const input = {
|
|
78
|
+
Bucket: this.options.bucket,
|
|
79
|
+
Key: resolvedKey,
|
|
80
|
+
Body: file.buffer,
|
|
81
|
+
ContentType: file.mimeType,
|
|
82
|
+
};
|
|
83
|
+
if (this.options.acl)
|
|
84
|
+
input.ACL = this.options.acl;
|
|
85
|
+
const uploader = new Upload({ client: c, params: input });
|
|
86
|
+
await uploader.done();
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
// lib-storage not installed — fall back to PutObjectCommand.
|
|
90
|
+
if (err.code === 'ERR_MODULE_NOT_FOUND' ||
|
|
91
|
+
String(err).includes('Cannot find module')) {
|
|
92
|
+
const sdk = await import('@aws-sdk/client-s3');
|
|
93
|
+
const PutObjectCommand = sdk.PutObjectCommand;
|
|
94
|
+
const input = {
|
|
95
|
+
Bucket: this.options.bucket,
|
|
96
|
+
Key: resolvedKey,
|
|
97
|
+
Body: file.buffer,
|
|
98
|
+
ContentType: file.mimeType,
|
|
99
|
+
ContentLength: file.size,
|
|
100
|
+
};
|
|
101
|
+
if (this.options.acl)
|
|
102
|
+
input.ACL = this.options.acl;
|
|
103
|
+
await c.send(new PutObjectCommand(input));
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
throw err;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return resolvedKey;
|
|
110
|
+
}
|
|
111
|
+
async getUrl(key) {
|
|
112
|
+
if (this.options.signed) {
|
|
113
|
+
return this.signedUrl(key);
|
|
114
|
+
}
|
|
115
|
+
return `${this.publicBaseUrl()}/${key}`;
|
|
116
|
+
}
|
|
117
|
+
async delete(key) {
|
|
118
|
+
try {
|
|
119
|
+
const sdk = await import('@aws-sdk/client-s3');
|
|
120
|
+
const c = await this.client();
|
|
121
|
+
const DeleteObjectCommand = sdk.DeleteObjectCommand;
|
|
122
|
+
await c.send(new DeleteObjectCommand({ Bucket: this.options.bucket, Key: key }));
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
// Ignore — file may not exist.
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* URL template for the frontend. Only available when `signed` is NOT set
|
|
130
|
+
* (public buckets). For private buckets each URL is unique + time-limited,
|
|
131
|
+
* so the frontend must call the upload endpoint to get a fresh URL.
|
|
132
|
+
*/
|
|
133
|
+
urlTemplate() {
|
|
134
|
+
if (this.options.signed)
|
|
135
|
+
return undefined;
|
|
136
|
+
return `${this.publicBaseUrl()}/{key}`;
|
|
137
|
+
}
|
|
138
|
+
async signedUrl(key) {
|
|
139
|
+
let getSignedUrl;
|
|
140
|
+
try {
|
|
141
|
+
const mod = await import('@aws-sdk/s3-request-presigner');
|
|
142
|
+
getSignedUrl = mod.getSignedUrl;
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
throw new Error('[modern-admin/feature-upload] Signed URLs require @aws-sdk/s3-request-presigner. ' +
|
|
146
|
+
'Install it: bun add @aws-sdk/s3-request-presigner');
|
|
147
|
+
}
|
|
148
|
+
const sdk = await import('@aws-sdk/client-s3');
|
|
149
|
+
const c = await this.client();
|
|
150
|
+
const GetObjectCommand = sdk.GetObjectCommand;
|
|
151
|
+
const expiresIn = typeof this.options.signed === 'object'
|
|
152
|
+
? (this.options.signed.expiresIn ?? 3600)
|
|
153
|
+
: 3600;
|
|
154
|
+
return getSignedUrl(c, new GetObjectCommand({ Bucket: this.options.bucket, Key: key }), { expiresIn });
|
|
155
|
+
}
|
|
156
|
+
publicBaseUrl() {
|
|
157
|
+
if (this.options.publicBaseUrl)
|
|
158
|
+
return this.options.publicBaseUrl;
|
|
159
|
+
if (this.options.endpoint)
|
|
160
|
+
return `${this.options.endpoint}/${this.options.bucket}`;
|
|
161
|
+
return `https://${this.options.bucket}.s3.${this.options.region}.amazonaws.com`;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
//# sourceMappingURL=s3.js.map
|