@venizia/ignis-docs 0.0.1-5 → 0.0.1-6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -4
- package/wiki/public/logo.svg +1 -0
- package/wiki/references/base/models.md +319 -1
- package/wiki/references/components/index.md +3 -1
- package/wiki/references/components/static-asset.md +1289 -0
- package/wiki/references/helpers/storage.md +538 -11
- package/wiki/references/utilities/index.md +1 -1
- package/wiki/references/utilities/request.md +150 -0
|
@@ -0,0 +1,1289 @@
|
|
|
1
|
+
# Static Asset Component
|
|
2
|
+
|
|
3
|
+
The Static Asset Component provides a flexible, extensible file management system with support for multiple storage backends through a unified interface.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
| Feature | Description |
|
|
8
|
+
|---------|-------------|
|
|
9
|
+
| **Component** | `StaticAssetComponent` |
|
|
10
|
+
| **Architecture** | Factory-based controller generation with unified storage interface |
|
|
11
|
+
| **Storage Types** | `DiskHelper` (local filesystem), `MinioHelper` (S3-compatible) |
|
|
12
|
+
| **Extensibility** | Easy to add new storage backends (S3, Azure Blob, Google Cloud Storage) |
|
|
13
|
+
| **Dependencies** | Node.js `fs`, `path`, `stream`; MinIO client (optional) |
|
|
14
|
+
|
|
15
|
+
## Key Features
|
|
16
|
+
|
|
17
|
+
✅ **Unified Storage Interface** - Single API for all storage types
|
|
18
|
+
✅ **Multiple Storage Instances** - Configure multiple storage backends simultaneously
|
|
19
|
+
✅ **Factory Pattern** - Dynamic controller generation
|
|
20
|
+
✅ **Built-in Security** - Comprehensive name validation, path traversal protection
|
|
21
|
+
✅ **Type-Safe** - Full TypeScript support with strict interfaces
|
|
22
|
+
✅ **Flexible Configuration** - Environment-based, production-ready setup
|
|
23
|
+
✅ **Database Tracking (MetaLink)** - Optional database-backed file tracking with metadata
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Architecture
|
|
28
|
+
|
|
29
|
+
### Storage Helper Hierarchy
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
IStorageHelper (interface)
|
|
33
|
+
↓
|
|
34
|
+
BaseStorageHelper (abstract class)
|
|
35
|
+
↓
|
|
36
|
+
├── DiskHelper (local filesystem)
|
|
37
|
+
└── MinioHelper (S3-compatible)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Component Flow
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
Application Configuration
|
|
44
|
+
↓
|
|
45
|
+
StaticAssetComponent
|
|
46
|
+
↓
|
|
47
|
+
AssetControllerFactory
|
|
48
|
+
↓
|
|
49
|
+
Dynamic Controller(s) ← uses → IStorageHelper
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Installation & Setup
|
|
55
|
+
|
|
56
|
+
### Complete Setup Example
|
|
57
|
+
|
|
58
|
+
Here's a real-world example from the Vert application showing how to configure storage backends:
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
import {
|
|
62
|
+
applicationEnvironment,
|
|
63
|
+
BaseApplication,
|
|
64
|
+
DiskHelper,
|
|
65
|
+
int,
|
|
66
|
+
MinioHelper,
|
|
67
|
+
StaticAssetComponent,
|
|
68
|
+
StaticAssetComponentBindingKeys,
|
|
69
|
+
StaticAssetStorageTypes,
|
|
70
|
+
TStaticAssetsComponentOptions,
|
|
71
|
+
ValueOrPromise,
|
|
72
|
+
} from '@venizia/ignis';
|
|
73
|
+
import { EnvironmentKeys } from './common/environments';
|
|
74
|
+
|
|
75
|
+
export class Application extends BaseApplication {
|
|
76
|
+
configureComponents(): void {
|
|
77
|
+
// Configure Static Asset Component
|
|
78
|
+
this.bind<TStaticAssetsComponentOptions>({
|
|
79
|
+
key: StaticAssetComponentBindingKeys.STATIC_ASSET_COMPONENT_OPTIONS,
|
|
80
|
+
}).toValue({
|
|
81
|
+
// MinIO storage for user uploads and media
|
|
82
|
+
staticAsset: {
|
|
83
|
+
controller: {
|
|
84
|
+
name: 'AssetController',
|
|
85
|
+
basePath: '/assets',
|
|
86
|
+
isStrict: true,
|
|
87
|
+
},
|
|
88
|
+
storage: StaticAssetStorageTypes.MINIO,
|
|
89
|
+
helper: new MinioHelper({
|
|
90
|
+
endPoint: applicationEnvironment.get(EnvironmentKeys.APP_ENV_MINIO_HOST),
|
|
91
|
+
port: int(applicationEnvironment.get(EnvironmentKeys.APP_ENV_MINIO_API_PORT)),
|
|
92
|
+
accessKey: applicationEnvironment.get(EnvironmentKeys.APP_ENV_MINIO_ACCESS_KEY),
|
|
93
|
+
secretKey: applicationEnvironment.get(EnvironmentKeys.APP_ENV_MINIO_SECRET_KEY),
|
|
94
|
+
useSSL: false,
|
|
95
|
+
}),
|
|
96
|
+
extra: {
|
|
97
|
+
parseMultipartBody: {
|
|
98
|
+
storage: 'memory',
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
// Local disk storage for temporary files and cache
|
|
103
|
+
staticResource: {
|
|
104
|
+
controller: {
|
|
105
|
+
name: 'ResourceController',
|
|
106
|
+
basePath: '/resources',
|
|
107
|
+
isStrict: true,
|
|
108
|
+
},
|
|
109
|
+
storage: StaticAssetStorageTypes.DISK,
|
|
110
|
+
helper: new DiskHelper({
|
|
111
|
+
basePath: './app_data/resources',
|
|
112
|
+
}),
|
|
113
|
+
extra: {
|
|
114
|
+
parseMultipartBody: {
|
|
115
|
+
storage: 'memory',
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Register the component
|
|
122
|
+
this.component(StaticAssetComponent);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
preConfigure() {
|
|
126
|
+
this.configureComponents();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**Key Configuration Elements:**
|
|
132
|
+
- Each storage backend gets a unique key (`staticAsset`, `staticResource`)
|
|
133
|
+
- Each backend has its own controller configuration (name, basePath)
|
|
134
|
+
- Storage type is explicitly set using `StaticAssetStorageTypes`
|
|
135
|
+
- Helper instances are created with environment variables
|
|
136
|
+
- Extra options configure multipart body parsing
|
|
137
|
+
|
|
138
|
+
### Environment Variables
|
|
139
|
+
|
|
140
|
+
Add these to your `.env` file:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
# MinIO Configuration
|
|
144
|
+
APP_ENV_MINIO_HOST=localhost
|
|
145
|
+
APP_ENV_MINIO_API_PORT=9000
|
|
146
|
+
APP_ENV_MINIO_ACCESS_KEY=minioadmin
|
|
147
|
+
APP_ENV_MINIO_SECRET_KEY=minioadmin
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Environment Keys Configuration
|
|
151
|
+
|
|
152
|
+
Define the environment keys in your application:
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
// src/common/environments.ts
|
|
156
|
+
import { EnvironmentKeys as BaseEnv } from '@venizia/ignis';
|
|
157
|
+
|
|
158
|
+
export class EnvironmentKeys extends BaseEnv {
|
|
159
|
+
// MinIO Configuration Keys
|
|
160
|
+
static readonly APP_ENV_MINIO_HOST = 'APP_ENV_MINIO_HOST';
|
|
161
|
+
static readonly APP_ENV_MINIO_API_PORT = 'APP_ENV_MINIO_API_PORT';
|
|
162
|
+
static readonly APP_ENV_MINIO_ACCESS_KEY = 'APP_ENV_MINIO_ACCESS_KEY';
|
|
163
|
+
static readonly APP_ENV_MINIO_SECRET_KEY = 'APP_ENV_MINIO_SECRET_KEY';
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Configuration Options
|
|
168
|
+
|
|
169
|
+
#### `TStaticAssetsComponentOptions`
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
type TStaticAssetsComponentOptions = {
|
|
173
|
+
[key: string]: {
|
|
174
|
+
// Controller configuration
|
|
175
|
+
controller: {
|
|
176
|
+
name: string; // Controller class name
|
|
177
|
+
basePath: string; // Base URL path (e.g., '/assets')
|
|
178
|
+
isStrict?: boolean; // Strict routing mode (default: true)
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// Storage configuration
|
|
182
|
+
storage: 'disk' | 'minio'; // Storage type
|
|
183
|
+
helper: IStorageHelper; // Storage helper instance
|
|
184
|
+
|
|
185
|
+
// Extra options
|
|
186
|
+
extra?: {
|
|
187
|
+
parseMultipartBody?: {
|
|
188
|
+
storage?: 'memory' | 'disk';
|
|
189
|
+
uploadDir?: string;
|
|
190
|
+
};
|
|
191
|
+
normalizeNameFn?: (opts: { originalName: string; folderPath?: string }) => string;
|
|
192
|
+
normalizeLinkFn?: (opts: { bucketName: string; normalizeName: string }) => string;
|
|
193
|
+
};
|
|
194
|
+
} & (
|
|
195
|
+
// MetaLink configuration (optional)
|
|
196
|
+
| { useMetaLink?: false }
|
|
197
|
+
| { useMetaLink: true; metaLink: TMetaLinkConfig }
|
|
198
|
+
);
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
type TMetaLinkConfig<Schema extends TMetaLinkSchema = TMetaLinkSchema> = {
|
|
202
|
+
model: typeof BaseEntity<Schema>; // MetaLink model class
|
|
203
|
+
repository: DefaultCRUDRepository<Schema>; // MetaLink repository instance
|
|
204
|
+
};
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Quick Start Options
|
|
208
|
+
|
|
209
|
+
**Option 1: MinIO Only**
|
|
210
|
+
```typescript
|
|
211
|
+
this.bind({
|
|
212
|
+
key: StaticAssetComponentBindingKeys.STATIC_ASSET_COMPONENT_OPTIONS,
|
|
213
|
+
}).toValue({
|
|
214
|
+
cloudStorage: {
|
|
215
|
+
controller: { name: 'CloudController', basePath: '/cloud' },
|
|
216
|
+
storage: StaticAssetStorageTypes.MINIO,
|
|
217
|
+
helper: new MinioHelper({ /* ... */ }),
|
|
218
|
+
extra: { parseMultipartBody: { storage: 'memory' } },
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
this.component(StaticAssetComponent);
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
**Option 2: Local Disk Only**
|
|
225
|
+
```typescript
|
|
226
|
+
this.bind({
|
|
227
|
+
key: StaticAssetComponentBindingKeys.STATIC_ASSET_COMPONENT_OPTIONS,
|
|
228
|
+
}).toValue({
|
|
229
|
+
localStorage: {
|
|
230
|
+
controller: { name: 'LocalController', basePath: '/files' },
|
|
231
|
+
storage: StaticAssetStorageTypes.DISK,
|
|
232
|
+
helper: new DiskHelper({ basePath: './uploads' }),
|
|
233
|
+
extra: { parseMultipartBody: { storage: 'disk' } },
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
this.component(StaticAssetComponent);
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
**Option 3: Multiple Storage Backends (Recommended)**
|
|
240
|
+
```typescript
|
|
241
|
+
// Use different storage types for different purposes
|
|
242
|
+
this.bind({
|
|
243
|
+
key: StaticAssetComponentBindingKeys.STATIC_ASSET_COMPONENT_OPTIONS,
|
|
244
|
+
}).toValue({
|
|
245
|
+
userUploads: {
|
|
246
|
+
controller: { name: 'UploadsController', basePath: '/uploads' },
|
|
247
|
+
storage: StaticAssetStorageTypes.MINIO,
|
|
248
|
+
helper: new MinioHelper({ /* ... */ }),
|
|
249
|
+
extra: {},
|
|
250
|
+
},
|
|
251
|
+
tempFiles: {
|
|
252
|
+
controller: { name: 'TempController', basePath: '/temp' },
|
|
253
|
+
storage: StaticAssetStorageTypes.DISK,
|
|
254
|
+
helper: new DiskHelper({ basePath: './temp' }),
|
|
255
|
+
extra: {},
|
|
256
|
+
},
|
|
257
|
+
publicAssets: {
|
|
258
|
+
controller: { name: 'PublicController', basePath: '/public' },
|
|
259
|
+
storage: StaticAssetStorageTypes.DISK,
|
|
260
|
+
helper: new DiskHelper({ basePath: './public' }),
|
|
261
|
+
extra: {},
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
this.component(StaticAssetComponent);
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## MetaLink: Database File Tracking
|
|
270
|
+
|
|
271
|
+
MetaLink is an optional feature that tracks uploaded files in a database, enabling advanced file management, querying, and metadata storage.
|
|
272
|
+
|
|
273
|
+
### What is MetaLink?
|
|
274
|
+
|
|
275
|
+
MetaLink creates a database record for every uploaded file, storing:
|
|
276
|
+
- File location (bucket, object name, access link)
|
|
277
|
+
- File metadata (mimetype, size, etag)
|
|
278
|
+
- Storage type (disk or minio)
|
|
279
|
+
- Timestamps (created, modified)
|
|
280
|
+
- Custom metadata (JSONB field)
|
|
281
|
+
|
|
282
|
+
### Benefits
|
|
283
|
+
|
|
284
|
+
✅ **Query uploaded files** - Find files by bucket, name, mimetype, etc.
|
|
285
|
+
✅ **Track file history** - Know when files were uploaded
|
|
286
|
+
✅ **Store metadata** - Keep custom information about files
|
|
287
|
+
✅ **Database integration** - Associate files with other entities
|
|
288
|
+
✅ **Audit trail** - Track what was uploaded and when
|
|
289
|
+
✅ **Graceful errors** - Upload succeeds even if MetaLink creation fails
|
|
290
|
+
|
|
291
|
+
### Database Schema
|
|
292
|
+
|
|
293
|
+
**Table:** `MetaLink`
|
|
294
|
+
|
|
295
|
+
```sql
|
|
296
|
+
CREATE TABLE "MetaLink" (
|
|
297
|
+
id TEXT PRIMARY KEY,
|
|
298
|
+
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
|
299
|
+
modified_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
|
300
|
+
bucket_name TEXT NOT NULL,
|
|
301
|
+
object_name TEXT NOT NULL,
|
|
302
|
+
link TEXT NOT NULL,
|
|
303
|
+
mimetype TEXT NOT NULL,
|
|
304
|
+
size INTEGER NOT NULL,
|
|
305
|
+
etag TEXT,
|
|
306
|
+
metadata JSONB,
|
|
307
|
+
storage_type TEXT NOT NULL,
|
|
308
|
+
is_synced BOOLEAN NOT NULL DEFAULT false
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
CREATE INDEX "IDX_MetaLink_bucketName" ON "MetaLink"(bucket_name);
|
|
312
|
+
CREATE INDEX "IDX_MetaLink_objectName" ON "MetaLink"(object_name);
|
|
313
|
+
CREATE INDEX "IDX_MetaLink_storageType" ON "MetaLink"(storage_type);
|
|
314
|
+
CREATE INDEX "IDX_MetaLink_isSynced" ON "MetaLink"(is_synced);
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
**Schema Fields:**
|
|
318
|
+
|
|
319
|
+
| Field | Type | Description |
|
|
320
|
+
|-------|------|-------------|
|
|
321
|
+
| `id` | TEXT | Primary key (UUID) |
|
|
322
|
+
| `created_at` | TIMESTAMP | When record was created |
|
|
323
|
+
| `modified_at` | TIMESTAMP | When record was last updated |
|
|
324
|
+
| `bucket_name` | TEXT | Storage bucket name |
|
|
325
|
+
| `object_name` | TEXT | File object name |
|
|
326
|
+
| `link` | TEXT | Access URL to the file |
|
|
327
|
+
| `mimetype` | TEXT | File MIME type |
|
|
328
|
+
| `size` | INTEGER | File size in bytes |
|
|
329
|
+
| `etag` | TEXT | Entity tag for versioning |
|
|
330
|
+
| `metadata` | JSONB | Additional file metadata |
|
|
331
|
+
| `storage_type` | TEXT | Storage type ('disk' or 'minio') |
|
|
332
|
+
| `is_synced` | BOOLEAN | Whether MetaLink is synchronized with storage (default: false) |
|
|
333
|
+
|
|
334
|
+
### Setup
|
|
335
|
+
|
|
336
|
+
#### Step 1: Create Model
|
|
337
|
+
|
|
338
|
+
```typescript
|
|
339
|
+
import { BaseMetaLinkModel } from '@venizia/ignis';
|
|
340
|
+
import { model } from '@venizia/ignis';
|
|
341
|
+
|
|
342
|
+
@model({ type: 'entity' })
|
|
343
|
+
export class FileMetaLinkModel extends BaseMetaLinkModel {
|
|
344
|
+
// Inherits all fields from BaseMetaLinkModel
|
|
345
|
+
}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
#### Step 2: Create Repository
|
|
349
|
+
|
|
350
|
+
```typescript
|
|
351
|
+
import { BaseMetaLinkRepository } from '@venizia/ignis';
|
|
352
|
+
import { repository, inject } from '@venizia/ignis';
|
|
353
|
+
import { IDataSource } from '@venizia/ignis';
|
|
354
|
+
|
|
355
|
+
@repository({})
|
|
356
|
+
export class FileMetaLinkRepository extends BaseMetaLinkRepository {
|
|
357
|
+
constructor(@inject({ key: 'datasources.postgres' }) dataSource: IDataSource) {
|
|
358
|
+
super({
|
|
359
|
+
entityClass: FileMetaLinkModel,
|
|
360
|
+
relations: {},
|
|
361
|
+
dataSource,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
#### Step 3: Create Database Table
|
|
368
|
+
|
|
369
|
+
The model has `skipMigrate: true`, so you need to create the table manually:
|
|
370
|
+
|
|
371
|
+
```sql
|
|
372
|
+
-- Run this in your database
|
|
373
|
+
CREATE TABLE "MetaLink" (
|
|
374
|
+
id TEXT PRIMARY KEY,
|
|
375
|
+
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
|
376
|
+
modified_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
|
377
|
+
bucket_name TEXT NOT NULL,
|
|
378
|
+
object_name TEXT NOT NULL,
|
|
379
|
+
link TEXT NOT NULL,
|
|
380
|
+
mimetype TEXT NOT NULL,
|
|
381
|
+
size INTEGER NOT NULL,
|
|
382
|
+
etag TEXT,
|
|
383
|
+
metadata JSONB,
|
|
384
|
+
storage_type TEXT NOT NULL,
|
|
385
|
+
is_synced BOOLEAN NOT NULL DEFAULT false
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
CREATE INDEX "IDX_MetaLink_bucketName" ON "MetaLink"(bucket_name);
|
|
389
|
+
CREATE INDEX "IDX_MetaLink_objectName" ON "MetaLink"(object_name);
|
|
390
|
+
CREATE INDEX "IDX_MetaLink_storageType" ON "MetaLink"(storage_type);
|
|
391
|
+
CREATE INDEX "IDX_MetaLink_isSynced" ON "MetaLink"(is_synced);
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
#### Step 4: Configure Component
|
|
395
|
+
|
|
396
|
+
```typescript
|
|
397
|
+
import { FileMetaLinkModel, FileMetaLinkRepository } from './your-models';
|
|
398
|
+
|
|
399
|
+
export class Application extends BaseApplication {
|
|
400
|
+
configureComponents(): void {
|
|
401
|
+
// Register repository
|
|
402
|
+
this.repository(FileMetaLinkRepository);
|
|
403
|
+
|
|
404
|
+
// Configure Static Asset Component with MetaLink
|
|
405
|
+
this.bind<TStaticAssetsComponentOptions>({
|
|
406
|
+
key: StaticAssetComponentBindingKeys.STATIC_ASSET_COMPONENT_OPTIONS,
|
|
407
|
+
}).toValue({
|
|
408
|
+
uploads: {
|
|
409
|
+
controller: {
|
|
410
|
+
name: 'UploadController',
|
|
411
|
+
basePath: '/uploads',
|
|
412
|
+
isStrict: true,
|
|
413
|
+
},
|
|
414
|
+
storage: StaticAssetStorageTypes.MINIO,
|
|
415
|
+
helper: new MinioHelper({ /* ... */ }),
|
|
416
|
+
useMetaLink: true,
|
|
417
|
+
metaLink: {
|
|
418
|
+
model: FileMetaLinkModel,
|
|
419
|
+
repository: this.getSync(FileMetaLinkRepository),
|
|
420
|
+
},
|
|
421
|
+
extra: {
|
|
422
|
+
parseMultipartBody: { storage: 'memory' },
|
|
423
|
+
},
|
|
424
|
+
},
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
this.component(StaticAssetComponent);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
### API Response with MetaLink
|
|
433
|
+
|
|
434
|
+
When `useMetaLink: true`, upload responses include the database record:
|
|
435
|
+
|
|
436
|
+
```json
|
|
437
|
+
[
|
|
438
|
+
{
|
|
439
|
+
"bucketName": "user-uploads",
|
|
440
|
+
"objectName": "document.pdf",
|
|
441
|
+
"link": "/uploads/buckets/user-uploads/objects/document.pdf",
|
|
442
|
+
"metaLink": {
|
|
443
|
+
"id": "550e8400-e29b-41d4-a716-446655440000",
|
|
444
|
+
"bucketName": "user-uploads",
|
|
445
|
+
"objectName": "document.pdf",
|
|
446
|
+
"link": "/uploads/buckets/user-uploads/objects/document.pdf",
|
|
447
|
+
"mimetype": "application/pdf",
|
|
448
|
+
"size": 1048576,
|
|
449
|
+
"etag": "abc123def456",
|
|
450
|
+
"metadata": {
|
|
451
|
+
"originalName": "My Document.pdf",
|
|
452
|
+
"uploadedBy": "user123"
|
|
453
|
+
},
|
|
454
|
+
"storageType": "minio",
|
|
455
|
+
"isSynced": true,
|
|
456
|
+
"createdAt": "2025-12-15T03:00:00.000Z",
|
|
457
|
+
"modifiedAt": "2025-12-15T03:00:00.000Z"
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
]
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
**Note:** The `isSynced` field is automatically set to `true` when files are uploaded, indicating the MetaLink is synchronized with the actual file in storage.
|
|
464
|
+
|
|
465
|
+
### Error Handling
|
|
466
|
+
|
|
467
|
+
If MetaLink creation fails, the upload still succeeds:
|
|
468
|
+
|
|
469
|
+
```json
|
|
470
|
+
[
|
|
471
|
+
{
|
|
472
|
+
"bucketName": "user-uploads",
|
|
473
|
+
"objectName": "document.pdf",
|
|
474
|
+
"link": "/uploads/buckets/user-uploads/objects/document.pdf",
|
|
475
|
+
"metaLink": null,
|
|
476
|
+
"metaLinkError": "Database connection error"
|
|
477
|
+
}
|
|
478
|
+
]
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
### Querying MetaLinks
|
|
482
|
+
|
|
483
|
+
```typescript
|
|
484
|
+
// Get all files in a bucket
|
|
485
|
+
const files = await fileMetaLinkRepository.find({
|
|
486
|
+
where: { bucketName: 'user-uploads' },
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
// Get files by mimetype
|
|
490
|
+
const pdfs = await fileMetaLinkRepository.find({
|
|
491
|
+
where: { mimetype: 'application/pdf' },
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
// Get files by storage type
|
|
495
|
+
const minioFiles = await fileMetaLinkRepository.find({
|
|
496
|
+
where: { storageType: 'minio' },
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
// Get synced files only
|
|
500
|
+
const syncedFiles = await fileMetaLinkRepository.find({
|
|
501
|
+
where: { isSynced: true },
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
// Get unsynced files (for manual sync operations)
|
|
505
|
+
const unsyncedFiles = await fileMetaLinkRepository.find({
|
|
506
|
+
where: { isSynced: false },
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
// Count synced files
|
|
510
|
+
const syncedCount = await fileMetaLinkRepository.count({
|
|
511
|
+
where: { isSynced: true },
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
// Get recent uploads
|
|
515
|
+
const recent = await fileMetaLinkRepository.find({
|
|
516
|
+
orderBy: { createdAt: 'desc' },
|
|
517
|
+
limit: 10,
|
|
518
|
+
});
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
### Automatic Cleanup
|
|
522
|
+
|
|
523
|
+
When you delete a file, MetaLink records are automatically deleted:
|
|
524
|
+
|
|
525
|
+
```http
|
|
526
|
+
DELETE /uploads/buckets/user-uploads/objects/document.pdf
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
- Deletes file from storage
|
|
530
|
+
- Deletes MetaLink record from database
|
|
531
|
+
- Returns `{ "success": true }`
|
|
532
|
+
|
|
533
|
+
---
|
|
534
|
+
|
|
535
|
+
## Storage Helpers
|
|
536
|
+
|
|
537
|
+
### DiskHelper (Local Filesystem)
|
|
538
|
+
|
|
539
|
+
Stores files on the local filesystem using a bucket-based directory structure.
|
|
540
|
+
|
|
541
|
+
#### Constructor
|
|
542
|
+
|
|
543
|
+
```typescript
|
|
544
|
+
new DiskHelper({
|
|
545
|
+
basePath: string; // Base directory for storage
|
|
546
|
+
scope?: string; // Logger scope
|
|
547
|
+
identifier?: string; // Helper identifier
|
|
548
|
+
})
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
#### Example
|
|
552
|
+
|
|
553
|
+
```typescript
|
|
554
|
+
const diskHelper = new DiskHelper({
|
|
555
|
+
basePath: './app_data/storage',
|
|
556
|
+
});
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
**Directory Structure:**
|
|
560
|
+
```
|
|
561
|
+
app_data/storage/
|
|
562
|
+
├── bucket-1/
|
|
563
|
+
│ ├── file1.pdf
|
|
564
|
+
│ └── file2.jpg
|
|
565
|
+
├── bucket-2/
|
|
566
|
+
│ └── document.docx
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
#### Features
|
|
570
|
+
|
|
571
|
+
- Automatic directory creation
|
|
572
|
+
- Built-in path validation
|
|
573
|
+
- Metadata stored in file stats
|
|
574
|
+
- Stream-based file operations
|
|
575
|
+
|
|
576
|
+
---
|
|
577
|
+
|
|
578
|
+
### MinioHelper (S3-Compatible Storage)
|
|
579
|
+
|
|
580
|
+
Connects to MinIO or any S3-compatible object storage.
|
|
581
|
+
|
|
582
|
+
#### Constructor
|
|
583
|
+
|
|
584
|
+
```typescript
|
|
585
|
+
new MinioHelper({
|
|
586
|
+
endPoint: string; // MinIO server hostname
|
|
587
|
+
port: number; // API port (default: 9000)
|
|
588
|
+
useSSL: boolean; // Use HTTPS
|
|
589
|
+
accessKey: string; // Access key
|
|
590
|
+
secretKey: string; // Secret key
|
|
591
|
+
})
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
#### Example
|
|
595
|
+
|
|
596
|
+
```typescript
|
|
597
|
+
const minioHelper = new MinioHelper({
|
|
598
|
+
endPoint: 'minio.example.com',
|
|
599
|
+
port: 9000,
|
|
600
|
+
useSSL: true,
|
|
601
|
+
accessKey: process.env.MINIO_ACCESS_KEY,
|
|
602
|
+
secretKey: process.env.MINIO_SECRET_KEY,
|
|
603
|
+
});
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
---
|
|
607
|
+
|
|
608
|
+
## IStorageHelper Interface
|
|
609
|
+
|
|
610
|
+
All storage helpers implement this unified interface:
|
|
611
|
+
|
|
612
|
+
```typescript
|
|
613
|
+
interface IStorageHelper {
|
|
614
|
+
// Name validation
|
|
615
|
+
isValidName(name: string): boolean;
|
|
616
|
+
|
|
617
|
+
// Bucket operations
|
|
618
|
+
isBucketExists(opts: { name: string }): Promise<boolean>;
|
|
619
|
+
getBuckets(): Promise<IBucketInfo[]>;
|
|
620
|
+
getBucket(opts: { name: string }): Promise<IBucketInfo | null>;
|
|
621
|
+
createBucket(opts: { name: string }): Promise<IBucketInfo | null>;
|
|
622
|
+
removeBucket(opts: { name: string }): Promise<boolean>;
|
|
623
|
+
|
|
624
|
+
// File operations
|
|
625
|
+
upload(opts: {
|
|
626
|
+
bucket: string;
|
|
627
|
+
files: IUploadFile[];
|
|
628
|
+
normalizeNameFn?: (opts: { originalName: string }) => string;
|
|
629
|
+
normalizeLinkFn?: (opts: { bucketName: string; normalizeName: string }) => string;
|
|
630
|
+
}): Promise<IUploadResult[]>;
|
|
631
|
+
|
|
632
|
+
getFile(opts: { bucket: string; name: string; options?: any }): Promise<Readable>;
|
|
633
|
+
getStat(opts: { bucket: string; name: string }): Promise<IFileStat>;
|
|
634
|
+
removeObject(opts: { bucket: string; name: string }): Promise<void>;
|
|
635
|
+
removeObjects(opts: { bucket: string; names: string[] }): Promise<void>;
|
|
636
|
+
listObjects(opts: IListObjectsOptions): Promise<IObjectInfo[]>;
|
|
637
|
+
|
|
638
|
+
// Utility
|
|
639
|
+
getFileType(opts: { mimeType: string }): string;
|
|
640
|
+
}
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
---
|
|
644
|
+
|
|
645
|
+
## API Endpoints
|
|
646
|
+
|
|
647
|
+
The component dynamically generates REST endpoints for each configured storage backend.
|
|
648
|
+
|
|
649
|
+
### Common Endpoints
|
|
650
|
+
|
|
651
|
+
All storage backends expose the same API structure:
|
|
652
|
+
|
|
653
|
+
#### **Get All Buckets**
|
|
654
|
+
|
|
655
|
+
```http
|
|
656
|
+
GET /{basePath}/buckets
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
**Response:**
|
|
660
|
+
```json
|
|
661
|
+
[
|
|
662
|
+
{ "name": "my-bucket", "creationDate": "2025-01-01T00:00:00.000Z" }
|
|
663
|
+
]
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
---
|
|
667
|
+
|
|
668
|
+
#### **Get Bucket by Name**
|
|
669
|
+
|
|
670
|
+
```http
|
|
671
|
+
GET /{basePath}/buckets/:bucketName
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
**Parameters:**
|
|
675
|
+
- `bucketName` (path): Bucket name
|
|
676
|
+
|
|
677
|
+
**Validation:**
|
|
678
|
+
- ✅ Bucket name validated with `isValidName()`
|
|
679
|
+
- ❌ Returns 400 if invalid
|
|
680
|
+
|
|
681
|
+
**Response:**
|
|
682
|
+
```json
|
|
683
|
+
{ "name": "my-bucket", "creationDate": "2025-01-01T00:00:00.000Z" }
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
---
|
|
687
|
+
|
|
688
|
+
#### **Create Bucket**
|
|
689
|
+
|
|
690
|
+
```http
|
|
691
|
+
POST /{basePath}/buckets/:bucketName
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
**Parameters:**
|
|
695
|
+
- `bucketName` (path): Name of the new bucket
|
|
696
|
+
|
|
697
|
+
**Response:**
|
|
698
|
+
```json
|
|
699
|
+
{ "name": "my-bucket", "creationDate": "2025-12-13T00:00:00.000Z" }
|
|
700
|
+
```
|
|
701
|
+
|
|
702
|
+
---
|
|
703
|
+
|
|
704
|
+
#### **Delete Bucket**
|
|
705
|
+
|
|
706
|
+
```http
|
|
707
|
+
DELETE /{basePath}/buckets/:bucketName
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
**Parameters:**
|
|
711
|
+
- `bucketName` (path): Bucket to delete
|
|
712
|
+
|
|
713
|
+
**Response:**
|
|
714
|
+
```json
|
|
715
|
+
{ "success": true }
|
|
716
|
+
```
|
|
717
|
+
|
|
718
|
+
---
|
|
719
|
+
|
|
720
|
+
#### **Upload Files**
|
|
721
|
+
|
|
722
|
+
```http
|
|
723
|
+
POST /{basePath}/buckets/:bucketName/upload
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
**Request Body:**
|
|
727
|
+
- `multipart/form-data` with file fields
|
|
728
|
+
- Each file can optionally include `folderPath` for organization
|
|
729
|
+
|
|
730
|
+
**Response (without MetaLink):**
|
|
731
|
+
```json
|
|
732
|
+
[
|
|
733
|
+
{
|
|
734
|
+
"bucketName": "my-bucket",
|
|
735
|
+
"objectName": "file.pdf",
|
|
736
|
+
"link": "/assets/buckets/my-bucket/objects/file.pdf"
|
|
737
|
+
}
|
|
738
|
+
]
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
**Response (with MetaLink enabled):**
|
|
742
|
+
```json
|
|
743
|
+
[
|
|
744
|
+
{
|
|
745
|
+
"bucketName": "my-bucket",
|
|
746
|
+
"objectName": "file.pdf",
|
|
747
|
+
"link": "/assets/buckets/my-bucket/objects/file.pdf",
|
|
748
|
+
"metaLink": {
|
|
749
|
+
"id": "uuid",
|
|
750
|
+
"bucketName": "my-bucket",
|
|
751
|
+
"objectName": "file.pdf",
|
|
752
|
+
"link": "/assets/buckets/my-bucket/objects/file.pdf",
|
|
753
|
+
"mimetype": "application/pdf",
|
|
754
|
+
"size": 1024,
|
|
755
|
+
"etag": "abc123",
|
|
756
|
+
"metadata": {},
|
|
757
|
+
"storageType": "minio",
|
|
758
|
+
"isSynced": true,
|
|
759
|
+
"createdAt": "2025-12-15T03:00:00.000Z",
|
|
760
|
+
"modifiedAt": "2025-12-15T03:00:00.000Z"
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
]
|
|
764
|
+
```
|
|
765
|
+
|
|
766
|
+
**Example:**
|
|
767
|
+
```typescript
|
|
768
|
+
const formData = new FormData();
|
|
769
|
+
formData.append('file', fileBlob, 'document.pdf');
|
|
770
|
+
|
|
771
|
+
const response = await fetch('/assets/buckets/uploads/upload', {
|
|
772
|
+
method: 'POST',
|
|
773
|
+
body: formData,
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
const result = await response.json();
|
|
777
|
+
console.log(result[0].metaLink); // Database record (if MetaLink enabled)
|
|
778
|
+
```
|
|
779
|
+
|
|
780
|
+
---
|
|
781
|
+
|
|
782
|
+
#### **Get Object (Stream)**
|
|
783
|
+
|
|
784
|
+
```http
|
|
785
|
+
GET /{basePath}/buckets/:bucketName/objects/:objectName
|
|
786
|
+
```
|
|
787
|
+
|
|
788
|
+
**Parameters:**
|
|
789
|
+
- `bucketName` (path): Bucket name
|
|
790
|
+
- `objectName` (path): Object name (URL-encoded)
|
|
791
|
+
|
|
792
|
+
**Validation:**
|
|
793
|
+
- ✅ Both bucket and object names validated
|
|
794
|
+
- ❌ Returns 400 if either is invalid
|
|
795
|
+
|
|
796
|
+
**Response:**
|
|
797
|
+
- Streams file content with appropriate headers
|
|
798
|
+
- Content-Type: From metadata or `application/octet-stream`
|
|
799
|
+
- Content-Length: File size in bytes
|
|
800
|
+
- X-Content-Type-Options: `nosniff`
|
|
801
|
+
|
|
802
|
+
---
|
|
803
|
+
|
|
804
|
+
#### **Download Object**
|
|
805
|
+
|
|
806
|
+
```http
|
|
807
|
+
GET /{basePath}/buckets/:bucketName/objects/:objectName/download
|
|
808
|
+
```
|
|
809
|
+
|
|
810
|
+
**Parameters:**
|
|
811
|
+
- `bucketName` (path): Bucket name
|
|
812
|
+
- `objectName` (path): Object name (URL-encoded)
|
|
813
|
+
|
|
814
|
+
**Response:**
|
|
815
|
+
- Streams file with download headers
|
|
816
|
+
- Content-Disposition: `attachment; filename="..."`
|
|
817
|
+
- Triggers browser download dialog
|
|
818
|
+
|
|
819
|
+
**Example:**
|
|
820
|
+
```typescript
|
|
821
|
+
const downloadUrl = `/assets/buckets/uploads/objects/${encodeURIComponent('document.pdf')}/download`;
|
|
822
|
+
window.open(downloadUrl, '_blank');
|
|
823
|
+
```
|
|
824
|
+
|
|
825
|
+
---
|
|
826
|
+
|
|
827
|
+
#### **Delete Object**
|
|
828
|
+
|
|
829
|
+
```http
|
|
830
|
+
DELETE /{basePath}/buckets/:bucketName/objects/:objectName
|
|
831
|
+
```
|
|
832
|
+
|
|
833
|
+
**Parameters:**
|
|
834
|
+
- `bucketName` (path): Bucket name
|
|
835
|
+
- `objectName` (path): Object to delete
|
|
836
|
+
|
|
837
|
+
**Response:**
|
|
838
|
+
```json
|
|
839
|
+
{ "success": true }
|
|
840
|
+
```
|
|
841
|
+
|
|
842
|
+
---
|
|
843
|
+
|
|
844
|
+
#### **List Objects**
|
|
845
|
+
|
|
846
|
+
```http
|
|
847
|
+
GET /{basePath}/buckets/:bucketName/objects
|
|
848
|
+
```
|
|
849
|
+
|
|
850
|
+
**Query Parameters:**
|
|
851
|
+
- `prefix` (optional): Filter by prefix
|
|
852
|
+
- `recursive` (optional, boolean): Recursive listing
|
|
853
|
+
- `maxKeys` (optional, number): Maximum objects to return
|
|
854
|
+
|
|
855
|
+
**Response:**
|
|
856
|
+
```json
|
|
857
|
+
[
|
|
858
|
+
{
|
|
859
|
+
"name": "file1.pdf",
|
|
860
|
+
"size": 1024,
|
|
861
|
+
"lastModified": "2025-12-13T00:00:00.000Z",
|
|
862
|
+
"etag": "abc123"
|
|
863
|
+
}
|
|
864
|
+
]
|
|
865
|
+
```
|
|
866
|
+
|
|
867
|
+
---
|
|
868
|
+
|
|
869
|
+
#### **Delete Object**
|
|
870
|
+
|
|
871
|
+
```http
|
|
872
|
+
DELETE /{basePath}/buckets/:bucketName/objects/:objectName
|
|
873
|
+
```
|
|
874
|
+
|
|
875
|
+
**Parameters:**
|
|
876
|
+
- `bucketName` (path): Bucket name
|
|
877
|
+
- `objectName` (path): Object to delete (URL-encoded)
|
|
878
|
+
|
|
879
|
+
**Validation:**
|
|
880
|
+
- ✅ Both bucket and object names validated
|
|
881
|
+
- ❌ Returns 400 if either is invalid
|
|
882
|
+
|
|
883
|
+
**Behavior:**
|
|
884
|
+
- Deletes file from storage
|
|
885
|
+
- If MetaLink enabled, also deletes database record
|
|
886
|
+
- MetaLink deletion errors are logged but don't fail the request
|
|
887
|
+
|
|
888
|
+
**Response:**
|
|
889
|
+
```json
|
|
890
|
+
{
|
|
891
|
+
"success": true
|
|
892
|
+
}
|
|
893
|
+
```
|
|
894
|
+
|
|
895
|
+
**Example:**
|
|
896
|
+
```typescript
|
|
897
|
+
const bucketName = 'user-uploads';
|
|
898
|
+
const objectName = 'document.pdf';
|
|
899
|
+
|
|
900
|
+
await fetch(`/assets/buckets/${bucketName}/objects/${encodeURIComponent(objectName)}`, {
|
|
901
|
+
method: 'DELETE',
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
// File deleted from storage
|
|
905
|
+
// MetaLink record deleted from database (if enabled)
|
|
906
|
+
```
|
|
907
|
+
|
|
908
|
+
---
|
|
909
|
+
|
|
910
|
+
#### **Sync MetaLink** (MetaLink only)
|
|
911
|
+
|
|
912
|
+
```http
|
|
913
|
+
PUT /{basePath}/buckets/:bucketName/objects/:objectName/meta-links
|
|
914
|
+
```
|
|
915
|
+
|
|
916
|
+
**Availability:** Only available when `useMetaLink: true`
|
|
917
|
+
|
|
918
|
+
**Parameters:**
|
|
919
|
+
- `bucketName` (path): Bucket name
|
|
920
|
+
- `objectName` (path): Object name (URL-encoded)
|
|
921
|
+
|
|
922
|
+
**Validation:**
|
|
923
|
+
- ✅ Both bucket and object names validated
|
|
924
|
+
- ❌ Returns 400 if either is invalid
|
|
925
|
+
|
|
926
|
+
**Behavior:**
|
|
927
|
+
- Fetches current file metadata from storage
|
|
928
|
+
- If MetaLink exists: Updates with latest metadata
|
|
929
|
+
- If MetaLink doesn't exist: Creates new MetaLink record
|
|
930
|
+
- Sets `isSynced: true` to mark as synchronized
|
|
931
|
+
|
|
932
|
+
**Use Cases:**
|
|
933
|
+
- Manually sync files that exist in storage but not in database
|
|
934
|
+
- Update MetaLink metadata after file changes
|
|
935
|
+
- Rebuild MetaLink records after database restore
|
|
936
|
+
- Bulk synchronization operations
|
|
937
|
+
|
|
938
|
+
**Response (MetaLink created):**
|
|
939
|
+
```json
|
|
940
|
+
{
|
|
941
|
+
"id": "uuid",
|
|
942
|
+
"bucketName": "user-uploads",
|
|
943
|
+
"objectName": "document.pdf",
|
|
944
|
+
"link": "/assets/buckets/user-uploads/objects/document.pdf",
|
|
945
|
+
"mimetype": "application/pdf",
|
|
946
|
+
"size": 1048576,
|
|
947
|
+
"etag": "abc123",
|
|
948
|
+
"metadata": {},
|
|
949
|
+
"storageType": "minio",
|
|
950
|
+
"isSynced": true,
|
|
951
|
+
"createdAt": "2025-12-15T03:00:00.000Z",
|
|
952
|
+
"modifiedAt": "2025-12-15T03:00:00.000Z"
|
|
953
|
+
}
|
|
954
|
+
```
|
|
955
|
+
|
|
956
|
+
**Response (MetaLink updated):**
|
|
957
|
+
```json
|
|
958
|
+
{
|
|
959
|
+
"id": "existing-uuid",
|
|
960
|
+
"bucketName": "user-uploads",
|
|
961
|
+
"objectName": "document.pdf",
|
|
962
|
+
"link": "/assets/buckets/user-uploads/objects/document.pdf",
|
|
963
|
+
"mimetype": "application/pdf",
|
|
964
|
+
"size": 1048576,
|
|
965
|
+
"etag": "abc123updated",
|
|
966
|
+
"metadata": {},
|
|
967
|
+
"storageType": "minio",
|
|
968
|
+
"isSynced": true,
|
|
969
|
+
"createdAt": "2025-12-15T02:00:00.000Z",
|
|
970
|
+
"modifiedAt": "2025-12-15T03:00:00.000Z"
|
|
971
|
+
}
|
|
972
|
+
```
|
|
973
|
+
|
|
974
|
+
**Example:**
|
|
975
|
+
```typescript
|
|
976
|
+
// Sync a single file
|
|
977
|
+
const bucketName = 'user-uploads';
|
|
978
|
+
const objectName = 'document.pdf';
|
|
979
|
+
|
|
980
|
+
const response = await fetch(
|
|
981
|
+
`/assets/buckets/${bucketName}/objects/${encodeURIComponent(objectName)}/meta-links`,
|
|
982
|
+
{ method: 'PUT' }
|
|
983
|
+
);
|
|
984
|
+
|
|
985
|
+
const metaLink = await response.json();
|
|
986
|
+
console.log('Synced:', metaLink.isSynced); // true
|
|
987
|
+
|
|
988
|
+
// Bulk sync example: sync all files in storage
|
|
989
|
+
const objects = await fetch(`/assets/buckets/${bucketName}/objects`).then(r => r.json());
|
|
990
|
+
|
|
991
|
+
for (const obj of objects) {
|
|
992
|
+
await fetch(
|
|
993
|
+
`/assets/buckets/${bucketName}/objects/${encodeURIComponent(obj.name)}/meta-links`,
|
|
994
|
+
{ method: 'PUT' }
|
|
995
|
+
);
|
|
996
|
+
}
|
|
997
|
+
```
|
|
998
|
+
|
|
999
|
+
---
|
|
1000
|
+
|
|
1001
|
+
## Security Features
|
|
1002
|
+
|
|
1003
|
+
### Built-in Name Validation
|
|
1004
|
+
|
|
1005
|
+
All storage helpers implement comprehensive name validation:
|
|
1006
|
+
|
|
1007
|
+
```typescript
|
|
1008
|
+
isValidName(name: string): boolean {
|
|
1009
|
+
// ❌ Prevents path traversal
|
|
1010
|
+
if (name.includes('..') || name.includes('/') || name.includes('\\'))
|
|
1011
|
+
return false;
|
|
1012
|
+
|
|
1013
|
+
// ❌ Prevents hidden files
|
|
1014
|
+
if (name.startsWith('.')) return false;
|
|
1015
|
+
|
|
1016
|
+
// ❌ Prevents shell injection
|
|
1017
|
+
const dangerousChars = /[;|&$`<>(){}[\]!#]/;
|
|
1018
|
+
if (dangerousChars.test(name)) return false;
|
|
1019
|
+
|
|
1020
|
+
// ❌ Prevents header injection
|
|
1021
|
+
if (name.includes('\n') || name.includes('\r') || name.includes('\0'))
|
|
1022
|
+
return false;
|
|
1023
|
+
|
|
1024
|
+
// ❌ Prevents DoS (long names)
|
|
1025
|
+
if (name.length > 255) return false;
|
|
1026
|
+
|
|
1027
|
+
// ❌ Prevents empty names
|
|
1028
|
+
if (name.trim().length === 0) return false;
|
|
1029
|
+
|
|
1030
|
+
return true;
|
|
1031
|
+
}
|
|
1032
|
+
```
|
|
1033
|
+
|
|
1034
|
+
**Blocked Patterns:**
|
|
1035
|
+
```
|
|
1036
|
+
../etc/passwd ❌ Path traversal
|
|
1037
|
+
.hidden ❌ Hidden file
|
|
1038
|
+
file;rm -rf / ❌ Shell injection
|
|
1039
|
+
file\ninjected ❌ Header injection
|
|
1040
|
+
very_long_name... ❌ > 255 characters
|
|
1041
|
+
```
|
|
1042
|
+
|
|
1043
|
+
### HTTP Security Headers
|
|
1044
|
+
|
|
1045
|
+
All responses include security headers:
|
|
1046
|
+
|
|
1047
|
+
```http
|
|
1048
|
+
X-Content-Type-Options: nosniff
|
|
1049
|
+
Content-Disposition: attachment; filename="..."
|
|
1050
|
+
```
|
|
1051
|
+
|
|
1052
|
+
---
|
|
1053
|
+
|
|
1054
|
+
## Usage Examples
|
|
1055
|
+
|
|
1056
|
+
### Example 1: Multiple Storage Backends
|
|
1057
|
+
|
|
1058
|
+
```typescript
|
|
1059
|
+
this.bind<TStaticAssetsComponentOptions>({
|
|
1060
|
+
key: StaticAssetComponentBindingKeys.STATIC_ASSET_COMPONENT_OPTIONS,
|
|
1061
|
+
}).toValue({
|
|
1062
|
+
// User uploads → MinIO
|
|
1063
|
+
uploads: {
|
|
1064
|
+
controller: { name: 'UploadController', basePath: '/uploads' },
|
|
1065
|
+
storage: StaticAssetStorageTypes.MINIO,
|
|
1066
|
+
helper: new MinioHelper({ /* ... */ }),
|
|
1067
|
+
extra: { parseMultipartBody: { storage: 'memory' } },
|
|
1068
|
+
},
|
|
1069
|
+
|
|
1070
|
+
// Temporary files → Local disk
|
|
1071
|
+
temp: {
|
|
1072
|
+
controller: { name: 'TempController', basePath: '/temp' },
|
|
1073
|
+
storage: StaticAssetStorageTypes.DISK,
|
|
1074
|
+
helper: new DiskHelper({ basePath: './temp' }),
|
|
1075
|
+
extra: { parseMultipartBody: { storage: 'disk' } },
|
|
1076
|
+
},
|
|
1077
|
+
|
|
1078
|
+
// Public assets → Local disk
|
|
1079
|
+
public: {
|
|
1080
|
+
controller: { name: 'PublicController', basePath: '/public' },
|
|
1081
|
+
storage: StaticAssetStorageTypes.DISK,
|
|
1082
|
+
helper: new DiskHelper({ basePath: './public' }),
|
|
1083
|
+
extra: { parseMultipartBody: { storage: 'memory' } },
|
|
1084
|
+
},
|
|
1085
|
+
});
|
|
1086
|
+
```
|
|
1087
|
+
|
|
1088
|
+
**Result:** 3 independent storage systems with different endpoints:
|
|
1089
|
+
- `/uploads/buckets/...`
|
|
1090
|
+
- `/temp/buckets/...`
|
|
1091
|
+
- `/public/buckets/...`
|
|
1092
|
+
|
|
1093
|
+
### Example 2: Frontend Integration
|
|
1094
|
+
|
|
1095
|
+
```typescript
|
|
1096
|
+
// Upload file to MinIO
|
|
1097
|
+
async function uploadFile(file: File) {
|
|
1098
|
+
const formData = new FormData();
|
|
1099
|
+
formData.append('file', file);
|
|
1100
|
+
|
|
1101
|
+
const response = await fetch('/assets/buckets/user-uploads/upload', {
|
|
1102
|
+
method: 'POST',
|
|
1103
|
+
body: formData,
|
|
1104
|
+
});
|
|
1105
|
+
|
|
1106
|
+
const result = await response.json();
|
|
1107
|
+
return result[0].link;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
// Download file
|
|
1111
|
+
function downloadFile(bucketName: string, objectName: string) {
|
|
1112
|
+
const url = `/assets/buckets/${bucketName}/objects/${encodeURIComponent(objectName)}/download`;
|
|
1113
|
+
window.open(url, '_blank');
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
// List files in bucket
|
|
1117
|
+
async function listFiles(bucketName: string, prefix?: string) {
|
|
1118
|
+
const url = new URL(`/assets/buckets/${bucketName}/objects`, window.location.origin);
|
|
1119
|
+
if (prefix) url.searchParams.append('prefix', prefix);
|
|
1120
|
+
|
|
1121
|
+
const response = await fetch(url);
|
|
1122
|
+
return await response.json();
|
|
1123
|
+
}
|
|
1124
|
+
```
|
|
1125
|
+
|
|
1126
|
+
### Example 3: Custom Filename Normalization
|
|
1127
|
+
|
|
1128
|
+
```typescript
|
|
1129
|
+
{
|
|
1130
|
+
uploads: {
|
|
1131
|
+
controller: { name: 'UploadController', basePath: '/uploads' },
|
|
1132
|
+
storage: StaticAssetStorageTypes.MINIO,
|
|
1133
|
+
helper: new MinioHelper({ /* ... */ }),
|
|
1134
|
+
extra: {
|
|
1135
|
+
parseMultipartBody: { storage: 'memory' },
|
|
1136
|
+
normalizeNameFn: ({ originalName }) => {
|
|
1137
|
+
// Add timestamp prefix
|
|
1138
|
+
return `${Date.now()}_${originalName.toLowerCase().replace(/\s/g, '_')}`;
|
|
1139
|
+
},
|
|
1140
|
+
normalizeLinkFn: ({ bucketName, normalizeName }) => {
|
|
1141
|
+
// Custom link format
|
|
1142
|
+
return `/api/files/${bucketName}/${encodeURIComponent(normalizeName)}`;
|
|
1143
|
+
},
|
|
1144
|
+
},
|
|
1145
|
+
},
|
|
1146
|
+
}
|
|
1147
|
+
```
|
|
1148
|
+
|
|
1149
|
+
---
|
|
1150
|
+
|
|
1151
|
+
## Custom Storage Implementation
|
|
1152
|
+
|
|
1153
|
+
You can implement your own storage backend by extending `BaseStorageHelper`:
|
|
1154
|
+
|
|
1155
|
+
```typescript
|
|
1156
|
+
import { BaseStorageHelper, IUploadFile, IUploadResult } from '@venizia/ignis-helpers';
|
|
1157
|
+
|
|
1158
|
+
class S3Helper extends BaseStorageHelper {
|
|
1159
|
+
constructor(config: S3Config) {
|
|
1160
|
+
super({ scope: 'S3Helper', identifier: 'S3Helper' });
|
|
1161
|
+
// Initialize S3 client
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
async isBucketExists(opts: { name: string }): Promise<boolean> {
|
|
1165
|
+
// Implementation
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
async upload(opts: {
|
|
1169
|
+
bucket: string;
|
|
1170
|
+
files: IUploadFile[];
|
|
1171
|
+
}): Promise<IUploadResult[]> {
|
|
1172
|
+
// Implementation
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// Implement other IStorageHelper methods...
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
// Usage
|
|
1179
|
+
this.bind<TStaticAssetsComponentOptions>({
|
|
1180
|
+
key: StaticAssetComponentBindingKeys.STATIC_ASSET_COMPONENT_OPTIONS,
|
|
1181
|
+
}).toValue({
|
|
1182
|
+
s3Storage: {
|
|
1183
|
+
controller: { name: 'S3Controller', basePath: '/s3-assets' },
|
|
1184
|
+
storage: 'custom-s3',
|
|
1185
|
+
helper: new S3Helper({ /* ... */ }),
|
|
1186
|
+
extra: {},
|
|
1187
|
+
},
|
|
1188
|
+
});
|
|
1189
|
+
```
|
|
1190
|
+
|
|
1191
|
+
---
|
|
1192
|
+
|
|
1193
|
+
## Troubleshooting
|
|
1194
|
+
|
|
1195
|
+
### Issue: "Invalid bucket/object name" errors
|
|
1196
|
+
|
|
1197
|
+
**Cause:** Name fails `isValidName()` validation
|
|
1198
|
+
|
|
1199
|
+
**Solution:** Ensure names:
|
|
1200
|
+
- Don't contain `..`, `/`, `\`
|
|
1201
|
+
- Don't start with `.`
|
|
1202
|
+
- Don't contain shell special characters
|
|
1203
|
+
- Are <= 255 characters
|
|
1204
|
+
- Are not empty or whitespace-only
|
|
1205
|
+
|
|
1206
|
+
### Issue: Controller not registering
|
|
1207
|
+
|
|
1208
|
+
**Cause:** Configuration key might be invalid or missing required fields
|
|
1209
|
+
|
|
1210
|
+
**Solution:** Ensure each storage configuration has all required fields:
|
|
1211
|
+
```typescript
|
|
1212
|
+
{
|
|
1213
|
+
[uniqueKey]: {
|
|
1214
|
+
controller: { name, basePath, isStrict },
|
|
1215
|
+
storage: 'disk' | 'minio',
|
|
1216
|
+
helper: IStorageHelper,
|
|
1217
|
+
extra: {}
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
```
|
|
1221
|
+
|
|
1222
|
+
### Issue: Files not uploading
|
|
1223
|
+
|
|
1224
|
+
**DiskHelper:**
|
|
1225
|
+
- Ensure `basePath` directory exists or can be created
|
|
1226
|
+
- Check filesystem permissions
|
|
1227
|
+
|
|
1228
|
+
**MinioHelper:**
|
|
1229
|
+
- Verify MinIO server is running
|
|
1230
|
+
- Check credentials (accessKey, secretKey)
|
|
1231
|
+
- Verify network connectivity (endPoint, port)
|
|
1232
|
+
|
|
1233
|
+
### Issue: Large file uploads failing
|
|
1234
|
+
|
|
1235
|
+
**Solution:** Switch to disk-based multipart parsing:
|
|
1236
|
+
|
|
1237
|
+
```typescript
|
|
1238
|
+
extra: {
|
|
1239
|
+
parseMultipartBody: {
|
|
1240
|
+
storage: 'disk', // Use disk instead of memory
|
|
1241
|
+
uploadDir: './uploads', // Temporary upload directory
|
|
1242
|
+
},
|
|
1243
|
+
}
|
|
1244
|
+
```
|
|
1245
|
+
|
|
1246
|
+
---
|
|
1247
|
+
|
|
1248
|
+
## Docker Setup for Development
|
|
1249
|
+
|
|
1250
|
+
### Docker Compose for MinIO
|
|
1251
|
+
|
|
1252
|
+
```yaml
|
|
1253
|
+
version: '3.8'
|
|
1254
|
+
services:
|
|
1255
|
+
minio:
|
|
1256
|
+
image: minio/minio:latest
|
|
1257
|
+
container_name: minio
|
|
1258
|
+
ports:
|
|
1259
|
+
- "9000:9000" # API port
|
|
1260
|
+
- "9001:9001" # Console port
|
|
1261
|
+
environment:
|
|
1262
|
+
MINIO_ROOT_USER: minioadmin
|
|
1263
|
+
MINIO_ROOT_PASSWORD: minioadmin
|
|
1264
|
+
command: server /data --console-address ":9001"
|
|
1265
|
+
volumes:
|
|
1266
|
+
- minio_data:/data
|
|
1267
|
+
|
|
1268
|
+
volumes:
|
|
1269
|
+
minio_data:
|
|
1270
|
+
```
|
|
1271
|
+
|
|
1272
|
+
**Start MinIO:**
|
|
1273
|
+
```bash
|
|
1274
|
+
docker-compose up -d
|
|
1275
|
+
```
|
|
1276
|
+
|
|
1277
|
+
**Access MinIO Console:**
|
|
1278
|
+
```
|
|
1279
|
+
http://localhost:9001
|
|
1280
|
+
```
|
|
1281
|
+
|
|
1282
|
+
---
|
|
1283
|
+
|
|
1284
|
+
## Related Documentation
|
|
1285
|
+
|
|
1286
|
+
- [Storage Helpers](../helpers/storage.md) - DiskHelper, MinioHelper, BaseStorageHelper
|
|
1287
|
+
- [Request Utilities](../utilities/request.md) - `parseMultipartBody`, `createContentDispositionHeader`
|
|
1288
|
+
- [Components Overview](./index.md)
|
|
1289
|
+
- [Controllers](../base/controllers.md)
|