@endora-commerce/mod-assets-library 0.0.0-stage → 0.100.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/LICENSE +21 -0
- package/README.md +61 -2
- package/dist/admin/api/assets-library-client.d.ts +29 -0
- package/dist/admin/api/assets-library-client.d.ts.map +1 -0
- package/dist/admin/api/assets-library-client.js +70 -0
- package/dist/admin/api/assets-library-client.js.map +1 -0
- package/dist/admin/components/AssetDetailDrawer.d.ts +8 -0
- package/dist/admin/components/AssetDetailDrawer.d.ts.map +1 -0
- package/dist/admin/components/AssetDetailDrawer.js +96 -0
- package/dist/admin/components/AssetDetailDrawer.js.map +1 -0
- package/dist/admin/components/FolderTree.d.ts +13 -0
- package/dist/admin/components/FolderTree.d.ts.map +1 -0
- package/dist/admin/components/FolderTree.js +71 -0
- package/dist/admin/components/FolderTree.js.map +1 -0
- package/dist/admin/index.d.ts +44 -0
- package/dist/admin/index.d.ts.map +1 -0
- package/dist/admin/index.js +46 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/admin/pages/LibraryPage.d.ts +9 -0
- package/dist/admin/pages/LibraryPage.d.ts.map +1 -0
- package/dist/admin/pages/LibraryPage.js +163 -0
- package/dist/admin/pages/LibraryPage.js.map +1 -0
- package/dist/backend/entities/asset-folder.entity.d.ts +16 -0
- package/dist/backend/entities/asset-folder.entity.d.ts.map +1 -0
- package/dist/backend/entities/asset-folder.entity.js +57 -0
- package/dist/backend/entities/asset-folder.entity.js.map +1 -0
- package/dist/backend/entities/asset.entity.d.ts +38 -0
- package/dist/backend/entities/asset.entity.d.ts.map +1 -0
- package/dist/backend/entities/asset.entity.js +128 -0
- package/dist/backend/entities/asset.entity.js.map +1 -0
- package/dist/backend/index.d.ts +57 -0
- package/dist/backend/index.d.ts.map +1 -0
- package/dist/backend/index.js +150 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/backend/jobs/hard-delete-asset.job.d.ts +18 -0
- package/dist/backend/jobs/hard-delete-asset.job.d.ts.map +1 -0
- package/dist/backend/jobs/hard-delete-asset.job.js +91 -0
- package/dist/backend/jobs/hard-delete-asset.job.js.map +1 -0
- package/dist/backend/plugin.d.ts +47 -0
- package/dist/backend/plugin.d.ts.map +1 -0
- package/dist/backend/plugin.js +96 -0
- package/dist/backend/plugin.js.map +1 -0
- package/dist/backend/routes.admin.d.ts +15 -0
- package/dist/backend/routes.admin.d.ts.map +1 -0
- package/dist/backend/routes.admin.js +189 -0
- package/dist/backend/routes.admin.js.map +1 -0
- package/dist/backend/routes.public.d.ts +14 -0
- package/dist/backend/routes.public.d.ts.map +1 -0
- package/dist/backend/routes.public.js +99 -0
- package/dist/backend/routes.public.js.map +1 -0
- package/dist/backend/services/asset-read-port.d.ts +103 -0
- package/dist/backend/services/asset-read-port.d.ts.map +1 -0
- package/dist/backend/services/asset-read-port.js +167 -0
- package/dist/backend/services/asset-read-port.js.map +1 -0
- package/dist/backend/services/assets-library.service.d.ts +76 -0
- package/dist/backend/services/assets-library.service.d.ts.map +1 -0
- package/dist/backend/services/assets-library.service.js +261 -0
- package/dist/backend/services/assets-library.service.js.map +1 -0
- package/dist/backend/services/cms-body-resolver.d.ts +6 -0
- package/dist/backend/services/cms-body-resolver.d.ts.map +1 -0
- package/dist/backend/services/cms-body-resolver.js +44 -0
- package/dist/backend/services/cms-body-resolver.js.map +1 -0
- package/dist/backend/services/folders.service.d.ts +29 -0
- package/dist/backend/services/folders.service.d.ts.map +1 -0
- package/dist/backend/services/folders.service.js +231 -0
- package/dist/backend/services/folders.service.js.map +1 -0
- package/dist/backend/services/hmac.d.ts +28 -0
- package/dist/backend/services/hmac.d.ts.map +1 -0
- package/dist/backend/services/hmac.js +50 -0
- package/dist/backend/services/hmac.js.map +1 -0
- package/dist/backend/services/reference-registry.d.ts +36 -0
- package/dist/backend/services/reference-registry.d.ts.map +1 -0
- package/dist/backend/services/reference-registry.js +63 -0
- package/dist/backend/services/reference-registry.js.map +1 -0
- package/dist/backend/services/storage/adapter-registry.d.ts +77 -0
- package/dist/backend/services/storage/adapter-registry.d.ts.map +1 -0
- package/dist/backend/services/storage/adapter-registry.js +95 -0
- package/dist/backend/services/storage/adapter-registry.js.map +1 -0
- package/dist/backend/services/storage/errors.d.ts +18 -0
- package/dist/backend/services/storage/errors.d.ts.map +1 -0
- package/dist/backend/services/storage/errors.js +30 -0
- package/dist/backend/services/storage/errors.js.map +1 -0
- package/dist/backend/services/storage/gcs-adapter.d.ts +42 -0
- package/dist/backend/services/storage/gcs-adapter.d.ts.map +1 -0
- package/dist/backend/services/storage/gcs-adapter.js +106 -0
- package/dist/backend/services/storage/gcs-adapter.js.map +1 -0
- package/dist/backend/services/storage/legacy-resolver.d.ts +15 -0
- package/dist/backend/services/storage/legacy-resolver.d.ts.map +1 -0
- package/dist/backend/services/storage/legacy-resolver.js +32 -0
- package/dist/backend/services/storage/legacy-resolver.js.map +1 -0
- package/dist/backend/services/storage/local-fs-adapter.d.ts +59 -0
- package/dist/backend/services/storage/local-fs-adapter.d.ts.map +1 -0
- package/dist/backend/services/storage/local-fs-adapter.js +153 -0
- package/dist/backend/services/storage/local-fs-adapter.js.map +1 -0
- package/dist/backend/services/storage/locator.d.ts +5 -0
- package/dist/backend/services/storage/locator.d.ts.map +1 -0
- package/dist/backend/services/storage/locator.js +16 -0
- package/dist/backend/services/storage/locator.js.map +1 -0
- package/dist/backend/services/storage/object-storage-port.d.ts +46 -0
- package/dist/backend/services/storage/object-storage-port.d.ts.map +1 -0
- package/dist/backend/services/storage/object-storage-port.js +78 -0
- package/dist/backend/services/storage/object-storage-port.js.map +1 -0
- package/dist/backend/services/storage/public-url-base.d.ts +50 -0
- package/dist/backend/services/storage/public-url-base.d.ts.map +1 -0
- package/dist/backend/services/storage/public-url-base.js +70 -0
- package/dist/backend/services/storage/public-url-base.js.map +1 -0
- package/dist/backend/services/storage/s3-adapter.d.ts +50 -0
- package/dist/backend/services/storage/s3-adapter.d.ts.map +1 -0
- package/dist/backend/services/storage/s3-adapter.js +110 -0
- package/dist/backend/services/storage/s3-adapter.js.map +1 -0
- package/dist/backend/services/storage/settings-view.d.ts +4 -0
- package/dist/backend/services/storage/settings-view.d.ts.map +1 -0
- package/dist/backend/services/storage/settings-view.js +73 -0
- package/dist/backend/services/storage/settings-view.js.map +1 -0
- package/dist/backend/services/storage/storage-adapter.d.ts +62 -0
- package/dist/backend/services/storage/storage-adapter.d.ts.map +1 -0
- package/dist/backend/services/storage/storage-adapter.js +10 -0
- package/dist/backend/services/storage/storage-adapter.js.map +1 -0
- package/dist/backend/services/upload-pipeline.d.ts +41 -0
- package/dist/backend/services/upload-pipeline.d.ts.map +1 -0
- package/dist/backend/services/upload-pipeline.js +231 -0
- package/dist/backend/services/upload-pipeline.js.map +1 -0
- package/dist/manifest.d.ts +185 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +229 -0
- package/dist/manifest.js.map +1 -0
- package/dist/migrations/20260505T102206_assets_library_init.d.ts +35 -0
- package/dist/migrations/20260505T102206_assets_library_init.d.ts.map +1 -0
- package/dist/migrations/20260505T102206_assets_library_init.js +132 -0
- package/dist/migrations/20260505T102206_assets_library_init.js.map +1 -0
- package/dist/migrations/index.d.ts +27 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +29 -0
- package/dist/migrations/index.js.map +1 -0
- package/docs/assets-library/index.md +125 -0
- package/i18n/en.json +54 -0
- package/i18n/pl.json +54 -0
- package/package.json +99 -3
- package/tailwind.css +14 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// S3 storage adapter — feature 013 / US3 / T075.
|
|
2
|
+
// Wraps @aws-sdk/client-s3 + @aws-sdk/s3-request-presigner. Storage layout
|
|
3
|
+
// matches the local-FS adapter (sharded path scheme); private URLs use V4
|
|
4
|
+
// pre-signed GetObject.
|
|
5
|
+
import { S3Client, PutObjectCommand, GetObjectCommand, HeadBucketCommand, DeleteObjectCommand, PutObjectAclCommand, } from '@aws-sdk/client-s3';
|
|
6
|
+
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
|
7
|
+
import { computeLocator } from './locator.js';
|
|
8
|
+
import { resolvePublicUrlBase } from './public-url-base.js';
|
|
9
|
+
import { BackendUnavailableError, ConfigurationError, LocatorMissingError, } from './errors.js';
|
|
10
|
+
export class S3StorageAdapter {
|
|
11
|
+
cfg;
|
|
12
|
+
code = 's3';
|
|
13
|
+
client;
|
|
14
|
+
/** The absolute base public object URLs are built on — decided once (D-223). */
|
|
15
|
+
publicBase;
|
|
16
|
+
constructor(cfg) {
|
|
17
|
+
this.cfg = cfg;
|
|
18
|
+
if (!cfg.bucket)
|
|
19
|
+
throw new ConfigurationError('S3StorageAdapter: bucket is required');
|
|
20
|
+
if (!cfg.region)
|
|
21
|
+
throw new ConfigurationError('S3StorageAdapter: region is required');
|
|
22
|
+
const configured = (cfg.publicBaseUrl ?? '').trim();
|
|
23
|
+
this.publicBase = resolvePublicUrlBase(configured.length > 0
|
|
24
|
+
? configured
|
|
25
|
+
: `https://${cfg.bucket}.s3.${cfg.region}.amazonaws.com`, cfg.publicApiBaseUrl);
|
|
26
|
+
this.client = new S3Client({
|
|
27
|
+
region: cfg.region,
|
|
28
|
+
...(cfg.endpoint ? { endpoint: cfg.endpoint, forcePathStyle: true } : {}),
|
|
29
|
+
...(cfg.accessKeyId
|
|
30
|
+
? {
|
|
31
|
+
credentials: {
|
|
32
|
+
accessKeyId: cfg.accessKeyId,
|
|
33
|
+
secretAccessKey: cfg.secretAccessKey,
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
: {}),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
async selfCheck() {
|
|
40
|
+
try {
|
|
41
|
+
await this.client.send(new HeadBucketCommand({ Bucket: this.cfg.bucket }));
|
|
42
|
+
return { ok: true };
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
46
|
+
return { ok: false, reason };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
newLocator(input) {
|
|
50
|
+
const tail = computeLocator(input);
|
|
51
|
+
const prefix = (this.cfg.prefix ?? '').replace(/^\/+|\/+$/g, '');
|
|
52
|
+
return prefix.length > 0 ? `${prefix}/${tail}` : tail;
|
|
53
|
+
}
|
|
54
|
+
async put(input) {
|
|
55
|
+
if (!input.locator)
|
|
56
|
+
throw new LocatorMissingError('S3StorageAdapter.put: empty locator');
|
|
57
|
+
try {
|
|
58
|
+
await this.client.send(new PutObjectCommand({
|
|
59
|
+
Bucket: this.cfg.bucket,
|
|
60
|
+
Key: input.locator,
|
|
61
|
+
Body: input.stream,
|
|
62
|
+
ContentType: input.mimeType,
|
|
63
|
+
ContentLength: input.sizeBytes > 0 ? input.sizeBytes : undefined,
|
|
64
|
+
ACL: input.visibility === 'public' ? 'public-read' : 'private',
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
throw new BackendUnavailableError(`S3StorageAdapter.put: failed to persist ${input.locator}`, e);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async resolveUrl(input) {
|
|
72
|
+
if (input.visibility === 'public') {
|
|
73
|
+
return { url: `${this.publicBase}/${input.locator}`, expiresAt: null };
|
|
74
|
+
}
|
|
75
|
+
const ttl = input.ttlSec ?? this.cfg.privateUrlTtlSec;
|
|
76
|
+
if (!Number.isInteger(ttl) || ttl <= 0) {
|
|
77
|
+
throw new ConfigurationError(`S3StorageAdapter.resolveUrl: invalid TTL ${ttl}`);
|
|
78
|
+
}
|
|
79
|
+
const url = await getSignedUrl(this.client, new GetObjectCommand({ Bucket: this.cfg.bucket, Key: input.locator }), { expiresIn: ttl });
|
|
80
|
+
return { url, expiresAt: new Date(Date.now() + ttl * 1000) };
|
|
81
|
+
}
|
|
82
|
+
async open(input) {
|
|
83
|
+
const out = await this.client.send(new GetObjectCommand({ Bucket: this.cfg.bucket, Key: input.locator }));
|
|
84
|
+
if (!out.Body) {
|
|
85
|
+
throw new BackendUnavailableError(`S3StorageAdapter.open: no body for ${input.locator}`);
|
|
86
|
+
}
|
|
87
|
+
return out.Body;
|
|
88
|
+
}
|
|
89
|
+
async delete(input) {
|
|
90
|
+
try {
|
|
91
|
+
await this.client.send(new DeleteObjectCommand({ Bucket: this.cfg.bucket, Key: input.locator }));
|
|
92
|
+
}
|
|
93
|
+
catch (e) {
|
|
94
|
+
throw new BackendUnavailableError(`S3StorageAdapter.delete: failed to remove ${input.locator}`, e);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
async setVisibility(input) {
|
|
98
|
+
try {
|
|
99
|
+
await this.client.send(new PutObjectAclCommand({
|
|
100
|
+
Bucket: this.cfg.bucket,
|
|
101
|
+
Key: input.locator,
|
|
102
|
+
ACL: input.visibility === 'public' ? 'public-read' : 'private',
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
catch (e) {
|
|
106
|
+
throw new BackendUnavailableError(`S3StorageAdapter.setVisibility: failed on ${input.locator}`, e);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=s3-adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"s3-adapter.js","sourceRoot":"","sources":["../../../../src/backend/services/storage/s3-adapter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settings-view.d.ts","sourceRoot":"","sources":["../../../../src/backend/services/storage/settings-view.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAG3D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAIjE,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,MAAM,aAAa,GAC7B,mBAAmB,CA6DrB"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// AdapterSettingsView impl that reads platform-global Library settings
|
|
2
|
+
// directly from the `settings` table (no per-channel scope, no cache). The
|
|
3
|
+
// reads are infrequent (once per upload / once per resolveUrl miss), so a
|
|
4
|
+
// direct DB hit is fine.
|
|
5
|
+
//
|
|
6
|
+
// AdapterRegistry consumes this view; we keep it separate to make the
|
|
7
|
+
// adapter side trivially mockable in tests.
|
|
8
|
+
import { Setting } from '@endora-commerce/platform/kernel';
|
|
9
|
+
import { SettingValue } from '@endora-commerce/platform/kernel';
|
|
10
|
+
const ADAPTER_CODES = ['local', 's3', 'gcs'];
|
|
11
|
+
export function createSettingsView(emFactory) {
|
|
12
|
+
async function readString(code) {
|
|
13
|
+
const em = emFactory();
|
|
14
|
+
const setting = await em.findOne(Setting, { code });
|
|
15
|
+
if (!setting) {
|
|
16
|
+
throw new Error(`Assets Library settings: missing setting "${code}".`);
|
|
17
|
+
}
|
|
18
|
+
// Platform-global settings have no per-channel override in v1; the
|
|
19
|
+
// setting_values table is empty for these codes. Returning defaultValue
|
|
20
|
+
// is the correct behaviour.
|
|
21
|
+
const override = await em.findOne(SettingValue, { setting: setting });
|
|
22
|
+
const raw = override ? override.value : setting.defaultValue;
|
|
23
|
+
return typeof raw === 'string' ? raw : String(raw ?? '');
|
|
24
|
+
}
|
|
25
|
+
async function readNumber(code, fallback) {
|
|
26
|
+
const em = emFactory();
|
|
27
|
+
const setting = await em.findOne(Setting, { code });
|
|
28
|
+
if (!setting)
|
|
29
|
+
return fallback;
|
|
30
|
+
const override = await em.findOne(SettingValue, { setting: setting });
|
|
31
|
+
const raw = override ? override.value : setting.defaultValue;
|
|
32
|
+
const n = typeof raw === 'number' ? raw : Number(raw);
|
|
33
|
+
return Number.isFinite(n) ? n : fallback;
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
async activeAdapter() {
|
|
37
|
+
const v = (await readString('assets.storage.adapter')).trim();
|
|
38
|
+
if (ADAPTER_CODES.includes(v)) {
|
|
39
|
+
return v;
|
|
40
|
+
}
|
|
41
|
+
return 'local';
|
|
42
|
+
},
|
|
43
|
+
localBaseDir() {
|
|
44
|
+
return readString('assets.local.base_dir');
|
|
45
|
+
},
|
|
46
|
+
localPublicUrlBase() {
|
|
47
|
+
return readString('assets.local.public_url_base');
|
|
48
|
+
},
|
|
49
|
+
privateUrlTtlSec() {
|
|
50
|
+
return readNumber('assets.private_url_ttl_sec', 300);
|
|
51
|
+
},
|
|
52
|
+
async s3Config() {
|
|
53
|
+
return {
|
|
54
|
+
bucket: await readString('assets.s3.bucket'),
|
|
55
|
+
region: await readString('assets.s3.region'),
|
|
56
|
+
accessKeyId: await readString('assets.s3.access_key_id'),
|
|
57
|
+
secretAccessKey: await readString('assets.s3.secret_access_key'),
|
|
58
|
+
endpoint: await readString('assets.s3.endpoint'),
|
|
59
|
+
prefix: await readString('assets.s3.prefix'),
|
|
60
|
+
publicBaseUrl: await readString('assets.s3.public_base_url'),
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
async gcsConfig() {
|
|
64
|
+
return {
|
|
65
|
+
bucket: await readString('assets.gcs.bucket'),
|
|
66
|
+
serviceAccountJson: await readString('assets.gcs.service_account_json'),
|
|
67
|
+
prefix: await readString('assets.gcs.prefix'),
|
|
68
|
+
publicBaseUrl: await readString('assets.gcs.public_base_url'),
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=settings-view.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settings-view.js","sourceRoot":"","sources":["../../../../src/backend/services/storage/settings-view.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { AssetVisibility, StorageBackendCode } from '@endora-commerce/contracts';
|
|
2
|
+
export type { AssetVisibility, StorageBackendCode };
|
|
3
|
+
export interface StorageAdapterSelfCheck {
|
|
4
|
+
ok: boolean;
|
|
5
|
+
/** Admin-displayable when ok=false; left undefined on ok=true. */
|
|
6
|
+
reason?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface StoragePutInput {
|
|
9
|
+
/** Locator the adapter must use; produced by the adapter via `newLocator(...)`. */
|
|
10
|
+
locator: string;
|
|
11
|
+
mimeType: string;
|
|
12
|
+
visibility: AssetVisibility;
|
|
13
|
+
/** Streaming source — must be consumed exactly once. */
|
|
14
|
+
stream: NodeJS.ReadableStream;
|
|
15
|
+
sizeBytes: number;
|
|
16
|
+
}
|
|
17
|
+
export interface StorageResolveUrlInput {
|
|
18
|
+
locator: string;
|
|
19
|
+
visibility: AssetVisibility;
|
|
20
|
+
/** TTL for private signed URLs. Ignored for public assets. */
|
|
21
|
+
ttlSec?: number;
|
|
22
|
+
}
|
|
23
|
+
export interface StorageResolveUrlOutput {
|
|
24
|
+
url: string;
|
|
25
|
+
/** Absolute expiry for signed URLs; null when the URL is stable (public). */
|
|
26
|
+
expiresAt: Date | null;
|
|
27
|
+
}
|
|
28
|
+
export interface StorageNewLocatorInput {
|
|
29
|
+
assetId: string;
|
|
30
|
+
originalFilename: string;
|
|
31
|
+
}
|
|
32
|
+
export interface StorageAdapter {
|
|
33
|
+
readonly code: StorageBackendCode;
|
|
34
|
+
/** Cheap probe (HEAD bucket / stat dir / ListBuckets). Returns ok+reason. */
|
|
35
|
+
selfCheck(): Promise<StorageAdapterSelfCheck>;
|
|
36
|
+
/** Compute a fresh, never-colliding locator for an upload of the given asset. */
|
|
37
|
+
newLocator(input: StorageNewLocatorInput): string;
|
|
38
|
+
/** Atomically persist the byte stream at `locator`. Throws on any failure. */
|
|
39
|
+
put(input: StoragePutInput): Promise<void>;
|
|
40
|
+
/** Resolve a URL for serving. See research R6 for per-backend semantics. */
|
|
41
|
+
resolveUrl(input: StorageResolveUrlInput): Promise<StorageResolveUrlOutput>;
|
|
42
|
+
/** Stream the bytes for backend-proxied private serving (used by local-FS public route). */
|
|
43
|
+
open(input: {
|
|
44
|
+
locator: string;
|
|
45
|
+
}): Promise<NodeJS.ReadableStream>;
|
|
46
|
+
/**
|
|
47
|
+
* Remove the underlying object. MUST throw if the backend cannot confirm
|
|
48
|
+
* removal — the worker relies on this for FR-031 block-then-retry.
|
|
49
|
+
*/
|
|
50
|
+
delete(input: {
|
|
51
|
+
locator: string;
|
|
52
|
+
}): Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* Optional. Adapters whose visibility is enforced by per-request URL
|
|
55
|
+
* resolution (local FS) MAY no-op; cloud adapters update the object ACL.
|
|
56
|
+
*/
|
|
57
|
+
setVisibility?(input: {
|
|
58
|
+
locator: string;
|
|
59
|
+
visibility: AssetVisibility;
|
|
60
|
+
}): Promise<void>;
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=storage-adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storage-adapter.d.ts","sourceRoot":"","sources":["../../../../src/backend/services/storage/storage-adapter.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AACtF,YAAY,EAAE,eAAe,EAAE,kBAAkB,EAAE,CAAC;AAEpD,MAAM,WAAW,uBAAuB;IACtC,EAAE,EAAE,OAAO,CAAC;IACZ,kEAAkE;IAClE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,eAAe;IAC9B,mFAAmF;IACnF,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,eAAe,CAAC;IAC5B,wDAAwD;IACxD,MAAM,EAAE,MAAM,CAAC,cAAc,CAAC;IAC9B,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,eAAe,CAAC;IAC5B,8DAA8D;IAC9D,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,uBAAuB;IACtC,GAAG,EAAE,MAAM,CAAC;IACZ,6EAA6E;IAC7E,SAAS,EAAE,IAAI,GAAG,IAAI,CAAC;CACxB;AAED,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,kBAAkB,CAAC;IAElC,6EAA6E;IAC7E,SAAS,IAAI,OAAO,CAAC,uBAAuB,CAAC,CAAC;IAE9C,iFAAiF;IACjF,UAAU,CAAC,KAAK,EAAE,sBAAsB,GAAG,MAAM,CAAC;IAElD,8EAA8E;IAC9E,GAAG,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE3C,4EAA4E;IAC5E,UAAU,CAAC,KAAK,EAAE,sBAAsB,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAAC;IAE5E,4FAA4F;IAC5F,IAAI,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC;IAEjE;;;OAGG;IACH,MAAM,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAElD;;;OAGG;IACH,aAAa,CAAC,CAAC,KAAK,EAAE;QACpB,OAAO,EAAE,MAAM,CAAC;QAChB,UAAU,EAAE,eAAe,CAAC;KAC7B,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnB"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// In-process Storage Adapter SPI — feature 013 / research.md R1.
|
|
2
|
+
// Each backend (local FS, S3, GCS) implements this exactly once. Consumers of
|
|
3
|
+
// the Library never know which backend is active; they go through
|
|
4
|
+
// AdapterRegistry.getActive() (or the per-asset dispatch in AssetsLibraryService
|
|
5
|
+
// which routes to the backend that originally stored the bytes).
|
|
6
|
+
//
|
|
7
|
+
// This is a TypeScript-only contract — the interface is never serialized over
|
|
8
|
+
// the wire, so it lives here rather than in @endora-commerce/contracts.
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=storage-adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storage-adapter.js","sourceRoot":"","sources":["../../../../src/backend/services/storage/storage-adapter.ts"],"names":[],"mappings":"AAAA,iEAAiE;AACjE,8EAA8E;AAC9E,kEAAkE;AAClE,iFAAiF;AACjF,iEAAiE;AACjE,EAAE;AACF,8EAA8E;AAC9E,wEAAwE"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
2
|
+
import { Asset } from '../entities/asset.entity.js';
|
|
3
|
+
import type { AuditPort } from '@endora-commerce/platform/kernel';
|
|
4
|
+
import type { AdapterRegistry } from './storage/adapter-registry.js';
|
|
5
|
+
import type { AssetVisibility } from './storage/storage-adapter.js';
|
|
6
|
+
export interface UploadPolicy {
|
|
7
|
+
/** From `assets.allowed_file_types` setting; `['*']` ⇒ unrestricted. */
|
|
8
|
+
allowedTypes: string[];
|
|
9
|
+
/** From `assets.max_file_size_mb` setting; `0` ⇒ no cap. */
|
|
10
|
+
maxFileSizeMb: number;
|
|
11
|
+
}
|
|
12
|
+
export interface UploadInput {
|
|
13
|
+
/** Original filename from the multipart part. Used for extension + display. */
|
|
14
|
+
filename: string;
|
|
15
|
+
/** MIME from the multipart Content-Type header (may be wrong; sniffed below). */
|
|
16
|
+
declaredMime: string;
|
|
17
|
+
/** Streaming source — must be consumed exactly once. */
|
|
18
|
+
stream: NodeJS.ReadableStream;
|
|
19
|
+
/** Best-effort byte count from headers; `0` when unknown. */
|
|
20
|
+
declaredSize: number;
|
|
21
|
+
/** Folder id chosen by the admin, or null for "Unsorted". */
|
|
22
|
+
folderId: string | null;
|
|
23
|
+
/** Optional storefront-visible label. */
|
|
24
|
+
label: string | null;
|
|
25
|
+
/** Visibility on creation; defaults to `public`. */
|
|
26
|
+
visibility: AssetVisibility;
|
|
27
|
+
}
|
|
28
|
+
export interface UploadPipelineDeps {
|
|
29
|
+
emFactory: () => EntityManager;
|
|
30
|
+
adapters: AdapterRegistry;
|
|
31
|
+
/** Loader called once per upload; lets routes pre-resolve from settings. */
|
|
32
|
+
loadPolicy: () => Promise<UploadPolicy>;
|
|
33
|
+
/** Feature 054 — audits the asset insert co-transactionally when provided. */
|
|
34
|
+
auditLog?: AuditPort;
|
|
35
|
+
}
|
|
36
|
+
export declare class UploadPipeline {
|
|
37
|
+
private readonly deps;
|
|
38
|
+
constructor(deps: UploadPipelineDeps);
|
|
39
|
+
run(input: UploadInput): Promise<Asset>;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=upload-pipeline.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"upload-pipeline.d.ts","sourceRoot":"","sources":["../../../src/backend/services/upload-pipeline.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAM3D,OAAO,EAAE,KAAK,EAAE,MAAM,6BAA6B,CAAC;AAGpD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAC;AAClE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AACrE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAEpE,MAAM,WAAW,YAAY;IAC3B,wEAAwE;IACxE,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,4DAA4D;IAC5D,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,WAAW;IAC1B,+EAA+E;IAC/E,QAAQ,EAAE,MAAM,CAAC;IACjB,iFAAiF;IACjF,YAAY,EAAE,MAAM,CAAC;IACrB,wDAAwD;IACxD,MAAM,EAAE,MAAM,CAAC,cAAc,CAAC;IAC9B,6DAA6D;IAC7D,YAAY,EAAE,MAAM,CAAC;IACrB,6DAA6D;IAC7D,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,yCAAyC;IACzC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,oDAAoD;IACpD,UAAU,EAAE,eAAe,CAAC;CAC7B;AAED,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,aAAa,CAAC;IAC/B,QAAQ,EAAE,eAAe,CAAC;IAC1B,4EAA4E;IAC5E,UAAU,EAAE,MAAM,OAAO,CAAC,YAAY,CAAC,CAAC;IACxC,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAqCD,qBAAa,cAAc;IACb,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE,kBAAkB;IAE/C,GAAG,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,KAAK,CAAC;CAyI9C"}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
// Upload pipeline — feature 013 / US1 / research.md R3, R8.
|
|
2
|
+
//
|
|
3
|
+
// Flow:
|
|
4
|
+
// 1. Pre-flight: filename / MIME / size against the configured policies.
|
|
5
|
+
// 2. Allocate a UUID, ask the active adapter for a sharded locator.
|
|
6
|
+
// 3. Sniff the actual MIME from the first bytes via `file-type/stream`.
|
|
7
|
+
// 4. Stream into adapter.put(); throw on any failure (no Asset row yet).
|
|
8
|
+
// 5. Insert the Asset row in one transaction. On insert failure, call
|
|
9
|
+
// adapter.delete() to compensate (the orphan-sweep worker is the
|
|
10
|
+
// backstop for this rare path).
|
|
11
|
+
//
|
|
12
|
+
// Atomicity contract per FR-010: if the function throws, neither a row nor
|
|
13
|
+
// a file survives. The compensating delete in step 5 closes the only
|
|
14
|
+
// race-y window (post-put, pre-commit).
|
|
15
|
+
import { fileTypeFromBuffer } from 'file-type';
|
|
16
|
+
import { randomUUID } from 'node:crypto';
|
|
17
|
+
import { Readable, Transform } from 'node:stream';
|
|
18
|
+
import { ERROR_CODES } from '@endora-commerce/contracts';
|
|
19
|
+
import { Asset } from '../entities/asset.entity.js';
|
|
20
|
+
import { HttpError } from '@endora-commerce/platform/http';
|
|
21
|
+
import { recordAuditFromContext } from '@endora-commerce/platform/commands';
|
|
22
|
+
const KIND_BY_MIME_PREFIX = [
|
|
23
|
+
['image/', 'image'],
|
|
24
|
+
['video/', 'video'],
|
|
25
|
+
['application/pdf', 'pdf'],
|
|
26
|
+
];
|
|
27
|
+
function mimeToKind(mime) {
|
|
28
|
+
for (const [prefix, kind] of KIND_BY_MIME_PREFIX) {
|
|
29
|
+
if (mime.startsWith(prefix))
|
|
30
|
+
return kind;
|
|
31
|
+
}
|
|
32
|
+
return 'other';
|
|
33
|
+
}
|
|
34
|
+
function extensionOf(filename) {
|
|
35
|
+
const m = filename.toLowerCase().match(/\.([^./\\]+)$/);
|
|
36
|
+
return m ? m[1] : '';
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Allow-list match: extension OR MIME (case-insensitive). The setting may
|
|
40
|
+
* carry either form. `'*'` anywhere in the list disables the gate.
|
|
41
|
+
*/
|
|
42
|
+
function isAllowed(allowed, mime, ext) {
|
|
43
|
+
if (allowed.length === 0)
|
|
44
|
+
return true;
|
|
45
|
+
for (const entry of allowed) {
|
|
46
|
+
const e = entry.trim().toLowerCase();
|
|
47
|
+
if (e === '' || e === '*')
|
|
48
|
+
return true;
|
|
49
|
+
if (e === mime.toLowerCase())
|
|
50
|
+
return true;
|
|
51
|
+
if (e === `.${ext}` || e === ext)
|
|
52
|
+
return true;
|
|
53
|
+
// Wildcard MIME like `image/*` matches any `image/...`
|
|
54
|
+
if (e.endsWith('/*') && mime.toLowerCase().startsWith(e.slice(0, -1)))
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
export class UploadPipeline {
|
|
60
|
+
deps;
|
|
61
|
+
constructor(deps) {
|
|
62
|
+
this.deps = deps;
|
|
63
|
+
}
|
|
64
|
+
async run(input) {
|
|
65
|
+
const policy = await this.deps.loadPolicy();
|
|
66
|
+
const ext = extensionOf(input.filename);
|
|
67
|
+
// — Pre-flight (cheap rejections before touching the adapter) —
|
|
68
|
+
if (policy.maxFileSizeMb > 0 &&
|
|
69
|
+
input.declaredSize > 0 &&
|
|
70
|
+
input.declaredSize > policy.maxFileSizeMb * 1024 * 1024) {
|
|
71
|
+
throw new HttpError(413, ERROR_CODES.ASSET_UPLOAD_TOO_LARGE, `File exceeds the configured maximum of ${policy.maxFileSizeMb} MB.`);
|
|
72
|
+
}
|
|
73
|
+
if (!isAllowed(policy.allowedTypes, input.declaredMime, ext)) {
|
|
74
|
+
throw new HttpError(415, ERROR_CODES.ASSET_UPLOAD_TYPE_NOT_ALLOWED, `File type "${input.declaredMime || ext || 'unknown'}" is not allowed by the active policy. Allowed: ${policy.allowedTypes.join(', ') || '(none)'}.`);
|
|
75
|
+
}
|
|
76
|
+
// — MIME sniff via file-type/stream —
|
|
77
|
+
// The library returns a wrapped stream we consume in place of the original;
|
|
78
|
+
// the wrapper replays the head bytes so the adapter still gets the full file.
|
|
79
|
+
const { fileType, stream: sniffedStream } = await sniffMime(input.stream);
|
|
80
|
+
const sniffedMime = fileType?.mime ?? input.declaredMime ?? 'application/octet-stream';
|
|
81
|
+
// Re-check allow-list using the *sniffed* MIME (defends against spoofed
|
|
82
|
+
// Content-Type) — the stricter check decides per spec edge case.
|
|
83
|
+
if (sniffedMime !== input.declaredMime &&
|
|
84
|
+
!isAllowed(policy.allowedTypes, sniffedMime, ext)) {
|
|
85
|
+
throw new HttpError(415, ERROR_CODES.ASSET_UPLOAD_TYPE_NOT_ALLOWED, `File content reports type "${sniffedMime}", which is not allowed by the active policy. Allowed: ${policy.allowedTypes.join(', ') || '(none)'}.`);
|
|
86
|
+
}
|
|
87
|
+
// — Allocate id, locator —
|
|
88
|
+
const assetId = randomUUID();
|
|
89
|
+
const adapter = await this.deps.adapters.getActive();
|
|
90
|
+
const locator = adapter.newLocator({
|
|
91
|
+
assetId,
|
|
92
|
+
originalFilename: input.filename,
|
|
93
|
+
});
|
|
94
|
+
// — Stream into the adapter (storage-first commit) —
|
|
95
|
+
// Multipart parts frequently carry no per-part Content-Length, so
|
|
96
|
+
// `declaredSize` is often 0. Count the bytes as they flow to the adapter so
|
|
97
|
+
// the persisted `sizeBytes` is accurate; the file-serving route sends this
|
|
98
|
+
// value as `Content-Length`, and a wrong 0 makes browsers render an empty
|
|
99
|
+
// image (broken preview in admin and missing placeholder on the storefront).
|
|
100
|
+
let observedSize = 0;
|
|
101
|
+
const countingStream = sniffedStream.pipe(new Transform({
|
|
102
|
+
transform(chunk, _enc, cb) {
|
|
103
|
+
observedSize += chunk.length;
|
|
104
|
+
cb(null, chunk);
|
|
105
|
+
},
|
|
106
|
+
}));
|
|
107
|
+
try {
|
|
108
|
+
await adapter.put({
|
|
109
|
+
locator,
|
|
110
|
+
mimeType: sniffedMime,
|
|
111
|
+
visibility: input.visibility,
|
|
112
|
+
stream: countingStream,
|
|
113
|
+
sizeBytes: input.declaredSize,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
118
|
+
throw new HttpError(503, ERROR_CODES.ASSET_STORAGE_UNAVAILABLE, `Storage adapter failed to persist the upload: ${reason}`);
|
|
119
|
+
}
|
|
120
|
+
// — Resolve the *public* URL form once, for back-compat with code paths
|
|
121
|
+
// (notably storefront catalog responses) that still read `storage_url`
|
|
122
|
+
// directly. The local-FS public route ignores the URL and gates on
|
|
123
|
+
// current `visibility` per request, so storing the public form is safe
|
|
124
|
+
// even for `private` local assets. For S3/GCS this URL only works
|
|
125
|
+
// when the asset is actually public; visibility-flipped cloud assets
|
|
126
|
+
// need refresh via `assetsLibrary.resolveUrl(id)` (catalog-query
|
|
127
|
+
// rewire is a Phase 5 follow-up).
|
|
128
|
+
const publicForm = await adapter.resolveUrl({
|
|
129
|
+
locator,
|
|
130
|
+
visibility: 'public',
|
|
131
|
+
});
|
|
132
|
+
// — DB insert (compensating-delete on failure) —
|
|
133
|
+
const em = this.deps.emFactory();
|
|
134
|
+
try {
|
|
135
|
+
const asset = em.create(Asset, {
|
|
136
|
+
id: assetId,
|
|
137
|
+
kind: mimeToKind(sniffedMime),
|
|
138
|
+
filename: input.filename,
|
|
139
|
+
mimeType: sniffedMime,
|
|
140
|
+
sizeBytes: String(observedSize > 0 ? observedSize : input.declaredSize),
|
|
141
|
+
storageUrl: publicForm.url,
|
|
142
|
+
storageLocator: locator,
|
|
143
|
+
storageBackend: adapter.code,
|
|
144
|
+
visibility: input.visibility,
|
|
145
|
+
...(input.folderId ? { folderId: input.folderId } : {}),
|
|
146
|
+
...(input.label ? { label: input.label } : {}),
|
|
147
|
+
});
|
|
148
|
+
em.persist(asset);
|
|
149
|
+
if (this.deps.auditLog) {
|
|
150
|
+
recordAuditFromContext(this.deps.auditLog, em, {
|
|
151
|
+
action: 'asset.upload',
|
|
152
|
+
objectType: 'asset',
|
|
153
|
+
objectId: asset.id,
|
|
154
|
+
stateBefore: null,
|
|
155
|
+
stateAfter: { filename: asset.filename, kind: asset.kind, visibility: asset.visibility },
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
await em.flush();
|
|
159
|
+
return asset;
|
|
160
|
+
}
|
|
161
|
+
catch (insertErr) {
|
|
162
|
+
// Compensating delete on storage. Best-effort; failure here is logged
|
|
163
|
+
// and surfaced via `pendingCleanup` on the next worker tick.
|
|
164
|
+
try {
|
|
165
|
+
await adapter.delete({ locator });
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
/* swallowed — compensating-delete is best-effort */
|
|
169
|
+
}
|
|
170
|
+
throw insertErr;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Read the first 4100 bytes of a Node stream into a Buffer, sniff the MIME
|
|
176
|
+
* via `fileTypeFromBuffer`, and return a new readable that yields the
|
|
177
|
+
* buffered prefix followed by the rest of the original stream. This avoids
|
|
178
|
+
* loading the whole upload into memory while still letting file-type
|
|
179
|
+
* inspect the magic bytes.
|
|
180
|
+
*
|
|
181
|
+
* The 4100-byte head matches the longest prefix file-type's heuristics need
|
|
182
|
+
* for any of its supported formats; bigger prefixes don't improve detection.
|
|
183
|
+
*/
|
|
184
|
+
async function sniffMime(source) {
|
|
185
|
+
const HEAD_BYTES = 4100;
|
|
186
|
+
const headChunks = [];
|
|
187
|
+
let collected = 0;
|
|
188
|
+
const onIter = source[Symbol.asyncIterator]
|
|
189
|
+
? source
|
|
190
|
+
: Readable.from(source);
|
|
191
|
+
const iter = onIter[Symbol.asyncIterator]();
|
|
192
|
+
let leftover;
|
|
193
|
+
while (collected < HEAD_BYTES) {
|
|
194
|
+
const next = await iter.next();
|
|
195
|
+
if (next.done)
|
|
196
|
+
break;
|
|
197
|
+
const chunk = Buffer.isBuffer(next.value) ? next.value : Buffer.from(next.value);
|
|
198
|
+
if (collected + chunk.length <= HEAD_BYTES) {
|
|
199
|
+
headChunks.push(chunk);
|
|
200
|
+
collected += chunk.length;
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
const want = HEAD_BYTES - collected;
|
|
204
|
+
headChunks.push(chunk.subarray(0, want));
|
|
205
|
+
leftover = chunk.subarray(want);
|
|
206
|
+
collected = HEAD_BYTES;
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const head = Buffer.concat(headChunks);
|
|
211
|
+
const sniffed = head.length > 0 ? await fileTypeFromBuffer(head) : undefined;
|
|
212
|
+
// Replay: yield the head, then the leftover (if any), then drain the rest of
|
|
213
|
+
// the iterator. We do not buffer beyond the head; the rest streams normally.
|
|
214
|
+
async function* replay() {
|
|
215
|
+
if (head.length > 0)
|
|
216
|
+
yield head;
|
|
217
|
+
if (leftover && leftover.length > 0)
|
|
218
|
+
yield leftover;
|
|
219
|
+
while (true) {
|
|
220
|
+
const next = await iter.next();
|
|
221
|
+
if (next.done)
|
|
222
|
+
return;
|
|
223
|
+
yield Buffer.isBuffer(next.value) ? next.value : Buffer.from(next.value);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
fileType: sniffed ? { mime: sniffed.mime, ext: sniffed.ext } : undefined,
|
|
228
|
+
stream: Readable.from(replay()),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
//# sourceMappingURL=upload-pipeline.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"upload-pipeline.js","sourceRoot":"","sources":["../../../src/backend/services/upload-pipeline.ts"],"names":[],"mappings":"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"}
|