@flusys/nestjs-storage 1.1.0-beta → 2.0.0-rc.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/README.md +148 -6
- package/cjs/config/index.js +0 -1
- package/cjs/config/storage.constants.js +0 -17
- package/cjs/controllers/file-manager.controller.js +44 -1
- package/cjs/controllers/folder.controller.js +44 -1
- package/cjs/controllers/storage-config.controller.js +44 -1
- package/cjs/controllers/upload.controller.js +18 -29
- package/cjs/docs/storage-swagger.config.js +24 -136
- package/cjs/dtos/file-manager.dto.js +70 -34
- package/cjs/dtos/folder.dto.js +15 -9
- package/cjs/dtos/storage-config.dto.js +4 -85
- package/cjs/dtos/upload.dto.js +24 -17
- package/cjs/entities/file-manager-with-company.entity.js +3 -4
- package/cjs/entities/file-manager.entity.js +71 -3
- package/cjs/entities/folder-with-company.entity.js +3 -4
- package/cjs/entities/folder.entity.js +19 -3
- package/cjs/entities/index.js +9 -10
- package/cjs/entities/storage-config-with-company.entity.js +3 -4
- package/cjs/entities/storage-config.entity.js +74 -3
- package/cjs/interfaces/index.js +0 -1
- package/cjs/middlewares/file-serve.middleware.js +113 -100
- package/cjs/modules/storage.module.js +82 -136
- package/cjs/providers/azure-provider.optional.js +10 -38
- package/cjs/providers/local-provider.js +38 -31
- package/cjs/providers/s3-provider.optional.js +19 -40
- package/cjs/providers/storage-factory.service.js +54 -99
- package/cjs/providers/storage-provider.registry.js +8 -18
- package/cjs/services/file-manager.service.js +238 -323
- package/cjs/services/folder.service.js +8 -11
- package/cjs/services/index.js +1 -0
- package/cjs/{config → services}/storage-config.service.js +32 -76
- package/cjs/services/storage-datasource.provider.js +16 -26
- package/cjs/services/storage-provider-config.service.js +15 -37
- package/cjs/services/upload.service.js +72 -88
- package/cjs/utils/file-validator.util.js +458 -0
- package/cjs/utils/image-compressor.util.js +3 -8
- package/config/index.d.ts +0 -1
- package/config/storage.constants.d.ts +0 -8
- package/controllers/upload.controller.d.ts +3 -6
- package/dtos/file-manager.dto.d.ts +12 -5
- package/dtos/folder.dto.d.ts +5 -5
- package/dtos/storage-config.dto.d.ts +7 -13
- package/entities/file-manager-with-company.entity.d.ts +2 -2
- package/entities/file-manager.entity.d.ts +12 -2
- package/entities/folder-with-company.entity.d.ts +2 -2
- package/entities/folder.entity.d.ts +4 -2
- package/entities/index.d.ts +3 -4
- package/entities/storage-config-with-company.entity.d.ts +2 -2
- package/entities/storage-config.entity.d.ts +8 -2
- package/fesm/config/index.js +0 -1
- package/fesm/config/storage.constants.js +0 -8
- package/fesm/controllers/file-manager.controller.js +45 -2
- package/fesm/controllers/folder.controller.js +45 -2
- package/fesm/controllers/storage-config.controller.js +45 -2
- package/fesm/controllers/upload.controller.js +19 -30
- package/fesm/docs/storage-swagger.config.js +27 -142
- package/fesm/dtos/file-manager.dto.js +71 -35
- package/fesm/dtos/folder.dto.js +16 -10
- package/fesm/dtos/storage-config.dto.js +8 -95
- package/fesm/dtos/upload.dto.js +25 -19
- package/fesm/entities/file-manager-with-company.entity.js +3 -4
- package/fesm/entities/file-manager.entity.js +72 -4
- package/fesm/entities/folder-with-company.entity.js +3 -4
- package/fesm/entities/folder.entity.js +20 -4
- package/fesm/entities/index.js +5 -13
- package/fesm/entities/storage-config-with-company.entity.js +3 -4
- package/fesm/entities/storage-config.entity.js +75 -4
- package/fesm/interfaces/index.js +0 -1
- package/fesm/interfaces/storage-config.interface.js +1 -3
- package/fesm/middlewares/file-serve.middleware.js +114 -101
- package/fesm/modules/storage.module.js +83 -136
- package/fesm/providers/azure-provider.optional.js +11 -42
- package/fesm/providers/local-provider.js +38 -31
- package/fesm/providers/s3-provider.optional.js +20 -44
- package/fesm/providers/storage-factory.service.js +52 -97
- package/fesm/providers/storage-provider.registry.js +10 -20
- package/fesm/services/file-manager.service.js +237 -322
- package/fesm/services/folder.service.js +6 -9
- package/fesm/services/index.js +1 -0
- package/fesm/{config → services}/storage-config.service.js +32 -76
- package/fesm/services/storage-datasource.provider.js +16 -26
- package/fesm/services/storage-provider-config.service.js +13 -35
- package/fesm/services/upload.service.js +71 -87
- package/fesm/utils/file-validator.util.js +451 -0
- package/fesm/utils/image-compressor.util.js +3 -8
- package/interfaces/file-manager.interface.d.ts +7 -4
- package/interfaces/index.d.ts +0 -1
- package/interfaces/storage-config.interface.d.ts +0 -20
- package/interfaces/storage-module-options.interface.d.ts +0 -5
- package/middlewares/file-serve.middleware.d.ts +9 -1
- package/modules/storage.module.d.ts +1 -2
- package/package.json +6 -6
- package/providers/azure-provider.optional.d.ts +8 -6
- package/providers/local-provider.d.ts +2 -7
- package/providers/s3-provider.optional.d.ts +9 -7
- package/providers/storage-factory.service.d.ts +8 -9
- package/providers/storage-provider.registry.d.ts +4 -4
- package/services/file-manager.service.d.ts +23 -16
- package/services/folder.service.d.ts +4 -4
- package/services/index.d.ts +1 -0
- package/services/storage-config.service.d.ts +24 -0
- package/services/storage-datasource.provider.d.ts +3 -4
- package/services/storage-provider-config.service.d.ts +4 -4
- package/services/upload.service.d.ts +3 -2
- package/utils/file-validator.util.d.ts +19 -0
- package/cjs/entities/file-manager-base.entity.js +0 -115
- package/cjs/entities/folder-base.entity.js +0 -55
- package/cjs/entities/storage-config-base.entity.js +0 -93
- package/cjs/interfaces/file-upload-response.interface.js +0 -4
- package/config/storage-config.service.d.ts +0 -22
- package/entities/file-manager-base.entity.d.ts +0 -13
- package/entities/folder-base.entity.d.ts +0 -5
- package/entities/storage-config-base.entity.d.ts +0 -9
- package/fesm/entities/file-manager-base.entity.js +0 -108
- package/fesm/entities/folder-base.entity.js +0 -48
- package/fesm/entities/storage-config-base.entity.js +0 -83
- package/fesm/interfaces/file-upload-response.interface.js +0 -1
- package/interfaces/file-upload-response.interface.d.ts +0 -6
|
@@ -5,9 +5,9 @@ function _ts_decorate(decorators, target, key, desc) {
|
|
|
5
5
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
6
|
}
|
|
7
7
|
import { CacheModule, UtilsModule } from '@flusys/nestjs-shared/modules';
|
|
8
|
-
import {
|
|
8
|
+
import { Logger, Module, RequestMethod } from '@nestjs/common';
|
|
9
9
|
import { FileServeMiddleware } from '../middlewares';
|
|
10
|
-
import { StorageConfigService } from '../
|
|
10
|
+
import { StorageConfigService } from '../services';
|
|
11
11
|
import { STORAGE_MODULE_OPTIONS } from '../config/storage.constants';
|
|
12
12
|
import { FileManagerController, FolderController, StorageConfigController, UploadController } from '../controllers';
|
|
13
13
|
import { FileLocationEnum } from '../enums/file-location.enum';
|
|
@@ -15,48 +15,62 @@ import { StorageFactoryService, StorageProviderRegistry } from '../providers';
|
|
|
15
15
|
import { LocalProvider } from '../providers/local-provider';
|
|
16
16
|
import { FileManagerService, FolderService, StorageDataSourceProvider, UploadService } from '../services';
|
|
17
17
|
import { StorageProviderConfigService } from '../services/storage-provider-config.service';
|
|
18
|
-
//
|
|
18
|
+
// ─── Module Constants ─────────────────────────────────────────────────────────
|
|
19
19
|
const logger = new Logger('StorageModule');
|
|
20
|
-
|
|
20
|
+
const CONTROLLERS = [
|
|
21
|
+
FileManagerController,
|
|
22
|
+
FolderController,
|
|
23
|
+
StorageConfigController,
|
|
24
|
+
UploadController
|
|
25
|
+
];
|
|
26
|
+
const EXPORTED_SERVICES = [
|
|
27
|
+
StorageConfigService,
|
|
28
|
+
StorageDataSourceProvider,
|
|
29
|
+
FileManagerService,
|
|
30
|
+
FolderService,
|
|
31
|
+
StorageProviderConfigService,
|
|
32
|
+
UploadService,
|
|
33
|
+
StorageFactoryService
|
|
34
|
+
];
|
|
35
|
+
// ─── Provider Registration ────────────────────────────────────────────────────
|
|
21
36
|
StorageProviderRegistry.register(FileLocationEnum.LOCAL, LocalProvider);
|
|
22
37
|
logger.log('Registered LocalProvider');
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
38
|
+
const OPTIONAL_PROVIDERS = [
|
|
39
|
+
{
|
|
40
|
+
location: FileLocationEnum.AWS,
|
|
41
|
+
path: '../providers/s3-provider.optional',
|
|
42
|
+
name: 'S3Provider',
|
|
43
|
+
dep: '@aws-sdk/client-s3'
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
location: FileLocationEnum.AZURE,
|
|
47
|
+
path: '../providers/azure-provider.optional',
|
|
48
|
+
name: 'AzureProvider',
|
|
49
|
+
dep: '@azure/storage-blob'
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
location: FileLocationEnum.SFTP,
|
|
53
|
+
path: '../providers/sftp-provider.optional',
|
|
54
|
+
name: 'SftpProvider',
|
|
55
|
+
dep: 'ssh2-sftp-client'
|
|
56
|
+
}
|
|
57
|
+
];
|
|
58
|
+
for (const { location, path, name, dep } of OPTIONAL_PROVIDERS){
|
|
59
|
+
try {
|
|
60
|
+
StorageProviderRegistry.register(location, require(path)[name]);
|
|
61
|
+
logger.log(`Registered ${name}`);
|
|
62
|
+
} catch {
|
|
63
|
+
logger.debug(`${name} not available (install ${dep} to enable)`);
|
|
64
|
+
}
|
|
44
65
|
}
|
|
45
66
|
export class StorageModule {
|
|
46
|
-
|
|
47
|
-
* Configure middleware for file serving
|
|
48
|
-
* This bypasses path-to-regexp issues with wildcard routes
|
|
49
|
-
*/ configure(consumer) {
|
|
67
|
+
configure(consumer) {
|
|
50
68
|
consumer.apply(FileServeMiddleware).forRoutes({
|
|
51
69
|
path: 'storage/upload/file/*',
|
|
52
70
|
method: RequestMethod.GET
|
|
53
71
|
});
|
|
54
72
|
}
|
|
55
|
-
|
|
56
|
-
* Register StorageModule synchronously
|
|
57
|
-
*/ static forRoot(options) {
|
|
58
|
-
const controllers = this.getControllers(options);
|
|
59
|
-
const providers = this.getProviders(options);
|
|
73
|
+
static forRoot(options) {
|
|
60
74
|
return {
|
|
61
75
|
module: StorageModule,
|
|
62
76
|
global: options.global ?? false,
|
|
@@ -64,24 +78,12 @@ export class StorageModule {
|
|
|
64
78
|
CacheModule,
|
|
65
79
|
UtilsModule
|
|
66
80
|
],
|
|
67
|
-
controllers: options.includeController !== false ?
|
|
68
|
-
providers,
|
|
69
|
-
exports:
|
|
70
|
-
StorageConfigService,
|
|
71
|
-
StorageDataSourceProvider,
|
|
72
|
-
FileManagerService,
|
|
73
|
-
FolderService,
|
|
74
|
-
StorageProviderConfigService,
|
|
75
|
-
UploadService,
|
|
76
|
-
StorageFactoryService
|
|
77
|
-
]
|
|
81
|
+
controllers: options.includeController !== false ? CONTROLLERS : [],
|
|
82
|
+
providers: this.buildProviders(options),
|
|
83
|
+
exports: EXPORTED_SERVICES
|
|
78
84
|
};
|
|
79
85
|
}
|
|
80
|
-
|
|
81
|
-
* Register StorageModule asynchronously
|
|
82
|
-
*/ static forRootAsync(options) {
|
|
83
|
-
const controllers = this.getControllers(options);
|
|
84
|
-
const asyncProviders = this.createAsyncProviders(options);
|
|
86
|
+
static forRootAsync(options) {
|
|
85
87
|
return {
|
|
86
88
|
module: StorageModule,
|
|
87
89
|
global: options.global ?? false,
|
|
@@ -90,53 +92,21 @@ export class StorageModule {
|
|
|
90
92
|
CacheModule,
|
|
91
93
|
UtilsModule
|
|
92
94
|
],
|
|
93
|
-
controllers: options.includeController !== false ?
|
|
94
|
-
// Pass false to exclude STORAGE_MODULE_OPTIONS - it's already in asyncProviders
|
|
95
|
+
controllers: options.includeController !== false ? CONTROLLERS : [],
|
|
95
96
|
providers: [
|
|
96
|
-
...
|
|
97
|
-
...this.
|
|
97
|
+
...this.createAsyncProviders(options),
|
|
98
|
+
...this.buildProviders()
|
|
98
99
|
],
|
|
99
|
-
exports:
|
|
100
|
-
StorageConfigService,
|
|
101
|
-
StorageDataSourceProvider,
|
|
102
|
-
FileManagerService,
|
|
103
|
-
FolderService,
|
|
104
|
-
StorageProviderConfigService,
|
|
105
|
-
UploadService,
|
|
106
|
-
StorageFactoryService
|
|
107
|
-
]
|
|
100
|
+
exports: EXPORTED_SERVICES
|
|
108
101
|
};
|
|
109
102
|
}
|
|
110
|
-
// Private
|
|
111
|
-
|
|
112
|
-
return [
|
|
113
|
-
FileManagerController,
|
|
114
|
-
FolderController,
|
|
115
|
-
StorageConfigController,
|
|
116
|
-
UploadController
|
|
117
|
-
];
|
|
118
|
-
}
|
|
119
|
-
/**
|
|
120
|
-
* NOTE: Repository providers removed - services now use StorageDataSourceProvider directly
|
|
121
|
-
* This ensures dynamic entity loading based on runtime configuration
|
|
122
|
-
*/ /**
|
|
123
|
-
* Get providers (all providers always loaded)
|
|
124
|
-
* @param options Module options
|
|
125
|
-
* @param includeOptionsProvider Whether to include the STORAGE_MODULE_OPTIONS provider (false for async registration)
|
|
126
|
-
*/ static getProviders(options, includeOptionsProvider = true) {
|
|
103
|
+
// ─── Private Helpers ──────────────────────────────────────────────────────────
|
|
104
|
+
static buildProviders(options) {
|
|
127
105
|
const providers = [
|
|
128
|
-
|
|
129
|
-
StorageDataSourceProvider,
|
|
130
|
-
FileManagerService,
|
|
131
|
-
FolderService,
|
|
132
|
-
StorageProviderConfigService,
|
|
133
|
-
UploadService,
|
|
134
|
-
StorageFactoryService,
|
|
106
|
+
...EXPORTED_SERVICES,
|
|
135
107
|
FileServeMiddleware
|
|
136
108
|
];
|
|
137
|
-
|
|
138
|
-
// For async registration, createAsyncProviders handles it
|
|
139
|
-
if (includeOptionsProvider) {
|
|
109
|
+
if (options) {
|
|
140
110
|
providers.unshift({
|
|
141
111
|
provide: STORAGE_MODULE_OPTIONS,
|
|
142
112
|
useValue: options
|
|
@@ -144,63 +114,40 @@ export class StorageModule {
|
|
|
144
114
|
}
|
|
145
115
|
return providers;
|
|
146
116
|
}
|
|
147
|
-
|
|
148
|
-
* Create async providers for forRootAsync
|
|
149
|
-
*/ static createAsyncProviders(options) {
|
|
117
|
+
static createAsyncProviders(options) {
|
|
150
118
|
if (options.useFactory) {
|
|
151
119
|
return [
|
|
152
120
|
{
|
|
153
121
|
provide: STORAGE_MODULE_OPTIONS,
|
|
154
|
-
useFactory: async (...args)=>{
|
|
155
|
-
const config = await options.useFactory(...args);
|
|
156
|
-
return {
|
|
122
|
+
useFactory: async (...args)=>({
|
|
157
123
|
...options,
|
|
158
|
-
config
|
|
159
|
-
}
|
|
160
|
-
},
|
|
124
|
+
config: await options.useFactory(...args)
|
|
125
|
+
}),
|
|
161
126
|
inject: options.inject || []
|
|
162
127
|
}
|
|
163
128
|
];
|
|
164
129
|
}
|
|
130
|
+
const factoryClass = options.useClass || options.useExisting;
|
|
131
|
+
if (!factoryClass) return [];
|
|
132
|
+
const providers = [
|
|
133
|
+
{
|
|
134
|
+
provide: STORAGE_MODULE_OPTIONS,
|
|
135
|
+
useFactory: async (factory)=>({
|
|
136
|
+
...options,
|
|
137
|
+
config: await factory.createStorageOptions()
|
|
138
|
+
}),
|
|
139
|
+
inject: [
|
|
140
|
+
factoryClass
|
|
141
|
+
]
|
|
142
|
+
}
|
|
143
|
+
];
|
|
165
144
|
if (options.useClass) {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const config = await optionsFactory.createStorageOptions();
|
|
171
|
-
return {
|
|
172
|
-
...options,
|
|
173
|
-
config
|
|
174
|
-
};
|
|
175
|
-
},
|
|
176
|
-
inject: [
|
|
177
|
-
options.useClass
|
|
178
|
-
]
|
|
179
|
-
},
|
|
180
|
-
{
|
|
181
|
-
provide: options.useClass,
|
|
182
|
-
useClass: options.useClass
|
|
183
|
-
}
|
|
184
|
-
];
|
|
185
|
-
}
|
|
186
|
-
if (options.useExisting) {
|
|
187
|
-
return [
|
|
188
|
-
{
|
|
189
|
-
provide: STORAGE_MODULE_OPTIONS,
|
|
190
|
-
useFactory: async (optionsFactory)=>{
|
|
191
|
-
const config = await optionsFactory.createStorageOptions();
|
|
192
|
-
return {
|
|
193
|
-
...options,
|
|
194
|
-
config
|
|
195
|
-
};
|
|
196
|
-
},
|
|
197
|
-
inject: [
|
|
198
|
-
options.useExisting
|
|
199
|
-
]
|
|
200
|
-
}
|
|
201
|
-
];
|
|
145
|
+
providers.push({
|
|
146
|
+
provide: options.useClass,
|
|
147
|
+
useClass: options.useClass
|
|
148
|
+
});
|
|
202
149
|
}
|
|
203
|
-
return
|
|
150
|
+
return providers;
|
|
204
151
|
}
|
|
205
152
|
}
|
|
206
153
|
StorageModule = _ts_decorate([
|
|
@@ -1,17 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* This provider requires @azure/storage-blob package to be installed.
|
|
5
|
-
* Only import this if you need Azure Blob storage.
|
|
6
|
-
*
|
|
7
|
-
* Installation:
|
|
8
|
-
* npm install @azure/storage-blob
|
|
9
|
-
*
|
|
10
|
-
* Usage:
|
|
11
|
-
* import { AzureProvider } from '@flusys/nestjs-storage/providers/azure-provider.optional';
|
|
12
|
-
* import { StorageProviderRegistry, FileLocationEnum } from '@flusys/nestjs-storage';
|
|
13
|
-
*
|
|
14
|
-
* StorageProviderRegistry.register(FileLocationEnum.AZURE, AzureProvider);
|
|
2
|
+
* Optional Azure Blob Storage Provider
|
|
3
|
+
* Requires: npm install @azure/storage-blob
|
|
15
4
|
*/ function _define_property(obj, key, value) {
|
|
16
5
|
if (key in obj) {
|
|
17
6
|
Object.defineProperty(obj, key, {
|
|
@@ -25,44 +14,27 @@
|
|
|
25
14
|
}
|
|
26
15
|
return obj;
|
|
27
16
|
}
|
|
28
|
-
import { ImageCompressor } from '../utils/image-compressor.util';
|
|
29
17
|
import { Logger } from '@nestjs/common';
|
|
30
18
|
import { v4 as uuidv4 } from 'uuid';
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
*/ export class AzureProvider {
|
|
35
|
-
/**
|
|
36
|
-
* Initialize Azure provider with configuration
|
|
37
|
-
*/ async initialize(config) {
|
|
19
|
+
import { ImageCompressor } from '../utils/image-compressor.util';
|
|
20
|
+
export class AzureProvider {
|
|
21
|
+
async initialize(config) {
|
|
38
22
|
try {
|
|
39
|
-
|
|
40
|
-
const { BlobServiceClient } = await import('@azure/storage-blob');
|
|
23
|
+
const { BlobServiceClient, StorageSharedKeyCredential } = await import('@azure/storage-blob');
|
|
41
24
|
this.accountName = config.accountName;
|
|
42
25
|
this.containerName = config.containerName;
|
|
43
|
-
|
|
44
|
-
if (config.connectionString) {
|
|
45
|
-
this.blobServiceClient = BlobServiceClient.fromConnectionString(config.connectionString);
|
|
46
|
-
} else {
|
|
47
|
-
const { StorageSharedKeyCredential } = await import('@azure/storage-blob');
|
|
48
|
-
const sharedKeyCredential = new StorageSharedKeyCredential(config.accountName, config.accountKey);
|
|
49
|
-
this.blobServiceClient = new BlobServiceClient(`https://${config.accountName}.blob.core.windows.net`, sharedKeyCredential);
|
|
50
|
-
}
|
|
51
|
-
// Get container client
|
|
26
|
+
this.blobServiceClient = config.connectionString ? BlobServiceClient.fromConnectionString(config.connectionString) : new BlobServiceClient(`https://${config.accountName}.blob.core.windows.net`, new StorageSharedKeyCredential(config.accountName, config.accountKey));
|
|
52
27
|
this.containerClient = this.blobServiceClient.getContainerClient(config.containerName);
|
|
53
|
-
// Ensure container exists
|
|
54
28
|
await this.containerClient.createIfNotExists();
|
|
55
29
|
this.logger.log(`Azure Provider initialized: account=${config.accountName}, container=${config.containerName}`);
|
|
56
|
-
} catch
|
|
57
|
-
|
|
58
|
-
throw new Error('Azure Storage SDK not found. Install it with: npm install @azure/storage-blob');
|
|
30
|
+
} catch {
|
|
31
|
+
throw new Error('Azure Storage SDK not found. Install: npm install @azure/storage-blob');
|
|
59
32
|
}
|
|
60
33
|
}
|
|
61
34
|
async uploadFile(file, options) {
|
|
62
35
|
let processedBuffer = file.buffer;
|
|
63
36
|
let contentType = file.mimetype;
|
|
64
37
|
const fileName = `${uuidv4()}-${file.originalname}`;
|
|
65
|
-
// Compress image if needed
|
|
66
38
|
if (options.compress && file.mimetype.startsWith('image/')) {
|
|
67
39
|
const result = await ImageCompressor.compress(file.buffer, file.mimetype, {
|
|
68
40
|
maxWidth: options.maxWidth,
|
|
@@ -74,9 +46,7 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
74
46
|
contentType = result.format;
|
|
75
47
|
}
|
|
76
48
|
const blobName = options.folderPath ? `${options.folderPath}/${fileName}` : fileName;
|
|
77
|
-
|
|
78
|
-
// Upload to Azure
|
|
79
|
-
await blockBlobClient.upload(processedBuffer, processedBuffer.length, {
|
|
49
|
+
await this.containerClient.getBlockBlobClient(blobName).upload(processedBuffer, processedBuffer.length, {
|
|
80
50
|
blobHTTPHeaders: {
|
|
81
51
|
blobContentType: contentType
|
|
82
52
|
}
|
|
@@ -92,8 +62,7 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
92
62
|
return Promise.all(files.map((file)=>this.uploadFile(file, options)));
|
|
93
63
|
}
|
|
94
64
|
async deleteFile(key) {
|
|
95
|
-
|
|
96
|
-
await blockBlobClient.deleteIfExists();
|
|
65
|
+
await this.containerClient.getBlockBlobClient(key).deleteIfExists();
|
|
97
66
|
this.logger.log(`Deleted file from Azure: ${key}`);
|
|
98
67
|
}
|
|
99
68
|
async deleteMultipleFiles(keys) {
|
|
@@ -32,6 +32,29 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
32
32
|
* Uses Node.js built-in fs module - no external dependencies
|
|
33
33
|
*/ export class LocalProvider {
|
|
34
34
|
/**
|
|
35
|
+
* SECURITY: Validates that a target path does not escape the base directory
|
|
36
|
+
* Prevents path traversal attacks using ../ sequences
|
|
37
|
+
* @throws Error if path traversal is detected
|
|
38
|
+
*/ validatePathWithinBase(targetPath) {
|
|
39
|
+
const normalizedBasePath = path.resolve(this.basePath);
|
|
40
|
+
const normalizedTargetPath = path.resolve(targetPath);
|
|
41
|
+
if (!normalizedTargetPath.startsWith(normalizedBasePath + path.sep) && normalizedTargetPath !== normalizedBasePath) {
|
|
42
|
+
this.logger.warn(`Path traversal attempt detected: ${targetPath}`);
|
|
43
|
+
throw new Error('Invalid path: Path traversal attempt detected');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* SECURITY: Validates file key format to prevent malicious input
|
|
48
|
+
* @throws Error if key contains suspicious patterns
|
|
49
|
+
*/ validateKeyFormat(key) {
|
|
50
|
+
if (!key || typeof key !== 'string' || key.trim().length === 0) {
|
|
51
|
+
throw new Error('Invalid file key: empty or invalid');
|
|
52
|
+
}
|
|
53
|
+
if (key.includes('\0')) {
|
|
54
|
+
throw new Error('Invalid file key: contains null bytes');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
35
58
|
* Initialize Local File System provider with configuration
|
|
36
59
|
* @param config.basePath - Base path for file storage (default: './uploads')
|
|
37
60
|
* @param config.baseUrl - Optional base URL for generating file URLs
|
|
@@ -64,7 +87,11 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
64
87
|
}
|
|
65
88
|
// Build file path
|
|
66
89
|
const folderPath = options.folderPath ? path.join(this.basePath, options.folderPath) : this.basePath;
|
|
90
|
+
// SECURITY: Validate path does not escape base directory
|
|
91
|
+
this.validatePathWithinBase(folderPath);
|
|
67
92
|
const filePath = path.join(folderPath, fileName);
|
|
93
|
+
// SECURITY: Double-check final file path
|
|
94
|
+
this.validatePathWithinBase(filePath);
|
|
68
95
|
// Ensure directory exists
|
|
69
96
|
await fs.mkdir(folderPath, {
|
|
70
97
|
recursive: true
|
|
@@ -87,14 +114,20 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
87
114
|
}
|
|
88
115
|
async deleteFile(key) {
|
|
89
116
|
try {
|
|
117
|
+
// SECURITY: Validate key format first
|
|
118
|
+
this.validateKeyFormat(key);
|
|
90
119
|
// Key now includes the basePath, resolve from cwd
|
|
91
120
|
let filePath = path.resolve(key);
|
|
121
|
+
// SECURITY: Validate resolved path is within base directory
|
|
122
|
+
this.validatePathWithinBase(filePath);
|
|
92
123
|
// Check if file exists at the resolved path
|
|
93
124
|
try {
|
|
94
125
|
await fs.access(filePath);
|
|
95
126
|
} catch {
|
|
96
127
|
// Fallback: try with basePath prefix (for old keys without basePath)
|
|
97
128
|
const fallbackPath = path.join(this.basePath, key);
|
|
129
|
+
// SECURITY: Validate fallback path as well
|
|
130
|
+
this.validatePathWithinBase(fallbackPath);
|
|
98
131
|
try {
|
|
99
132
|
await fs.access(fallbackPath);
|
|
100
133
|
filePath = fallbackPath;
|
|
@@ -107,7 +140,11 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
107
140
|
}
|
|
108
141
|
await fs.unlink(filePath);
|
|
109
142
|
this.logger.log(`Deleted file from local storage: ${key}`);
|
|
110
|
-
} catch (
|
|
143
|
+
} catch (error) {
|
|
144
|
+
// Re-throw security errors
|
|
145
|
+
if (error instanceof Error && error.message.includes('Invalid')) {
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
111
148
|
this.logger.warn(`Failed to delete file from local storage: ${key}`);
|
|
112
149
|
}
|
|
113
150
|
}
|
|
@@ -134,36 +171,6 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
134
171
|
return false;
|
|
135
172
|
}
|
|
136
173
|
}
|
|
137
|
-
/**
|
|
138
|
-
* Get the absolute path for a file key
|
|
139
|
-
* Key now includes basePath, so resolve from cwd
|
|
140
|
-
*/ getAbsolutePath(key) {
|
|
141
|
-
return path.resolve(key);
|
|
142
|
-
}
|
|
143
|
-
/**
|
|
144
|
-
* Check if a file exists
|
|
145
|
-
*/ async fileExists(key) {
|
|
146
|
-
try {
|
|
147
|
-
// Key now includes basePath, resolve from cwd
|
|
148
|
-
const filePath = path.resolve(key);
|
|
149
|
-
await fs.access(filePath);
|
|
150
|
-
return true;
|
|
151
|
-
} catch {
|
|
152
|
-
return false;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
/**
|
|
156
|
-
* Get file stats
|
|
157
|
-
*/ async getFileStats(key) {
|
|
158
|
-
// Key now includes basePath, resolve from cwd
|
|
159
|
-
const filePath = path.resolve(key);
|
|
160
|
-
const stats = await fs.stat(filePath);
|
|
161
|
-
return {
|
|
162
|
-
size: stats.size,
|
|
163
|
-
createdAt: stats.birthtime,
|
|
164
|
-
modifiedAt: stats.mtime
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
174
|
constructor(){
|
|
168
175
|
_define_property(this, "logger", new Logger(LocalProvider.name));
|
|
169
176
|
_define_property(this, "basePath", '');
|
|
@@ -1,17 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* This provider requires @aws-sdk packages to be installed.
|
|
5
|
-
* Only import this if you need AWS S3 storage.
|
|
6
|
-
*
|
|
7
|
-
* Installation:
|
|
8
|
-
* npm install @aws-sdk/client-s3 @aws-sdk/lib-storage @aws-sdk/s3-request-presigner
|
|
9
|
-
*
|
|
10
|
-
* Usage:
|
|
11
|
-
* import { S3Provider } from '@flusys/nestjs-storage/providers/s3-provider.optional';
|
|
12
|
-
* import { StorageProviderRegistry, FileLocationEnum } from '@flusys/nestjs-storage';
|
|
13
|
-
*
|
|
14
|
-
* StorageProviderRegistry.register(FileLocationEnum.AWS, S3Provider);
|
|
2
|
+
* Optional AWS S3 Storage Provider
|
|
3
|
+
* Requires: npm install @aws-sdk/client-s3 @aws-sdk/lib-storage @aws-sdk/s3-request-presigner
|
|
15
4
|
*/ function _define_property(obj, key, value) {
|
|
16
5
|
if (key in obj) {
|
|
17
6
|
Object.defineProperty(obj, key, {
|
|
@@ -25,33 +14,26 @@
|
|
|
25
14
|
}
|
|
26
15
|
return obj;
|
|
27
16
|
}
|
|
28
|
-
import { ImageCompressor } from '../utils/image-compressor.util';
|
|
29
17
|
import { Logger } from '@nestjs/common';
|
|
30
18
|
import { v4 as uuidv4 } from 'uuid';
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
*/ export class S3Provider {
|
|
35
|
-
/**
|
|
36
|
-
* Initialize S3 provider with configuration
|
|
37
|
-
*/ async initialize(config) {
|
|
19
|
+
import { ImageCompressor } from '../utils/image-compressor.util';
|
|
20
|
+
export class S3Provider {
|
|
21
|
+
async initialize(config) {
|
|
38
22
|
try {
|
|
39
|
-
// Dynamic import - only loads AWS SDK if this provider is used
|
|
40
23
|
const { S3Client } = await import('@aws-sdk/client-s3');
|
|
41
24
|
this.region = config.region;
|
|
42
25
|
this.bucketName = config.bucket;
|
|
43
26
|
this.s3 = new S3Client({
|
|
44
27
|
region: config.region,
|
|
28
|
+
endpoint: config.endpoint,
|
|
45
29
|
credentials: config.accessKeyId && config.secretAccessKey ? {
|
|
46
30
|
accessKeyId: config.accessKeyId,
|
|
47
31
|
secretAccessKey: config.secretAccessKey
|
|
48
|
-
} : undefined
|
|
49
|
-
endpoint: config.endpoint
|
|
32
|
+
} : undefined
|
|
50
33
|
});
|
|
51
34
|
this.logger.log(`S3 Provider initialized: region=${config.region}, bucket=${config.bucket}`);
|
|
52
|
-
} catch
|
|
53
|
-
|
|
54
|
-
throw new Error('AWS SDK not found. Install it with: npm install @aws-sdk/client-s3 @aws-sdk/lib-storage @aws-sdk/s3-request-presigner');
|
|
35
|
+
} catch {
|
|
36
|
+
throw new Error('AWS SDK not found. Install: npm install @aws-sdk/client-s3 @aws-sdk/lib-storage @aws-sdk/s3-request-presigner');
|
|
55
37
|
}
|
|
56
38
|
}
|
|
57
39
|
async uploadFile(file, options) {
|
|
@@ -59,7 +41,6 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
59
41
|
let processedBuffer = file.buffer;
|
|
60
42
|
let contentType = file.mimetype;
|
|
61
43
|
const fileName = `${uuidv4()}-${file.originalname}`;
|
|
62
|
-
// Compress image if needed
|
|
63
44
|
if (options.compress && file.mimetype.startsWith('image/')) {
|
|
64
45
|
const result = await ImageCompressor.compress(file.buffer, file.mimetype, {
|
|
65
46
|
maxWidth: options.maxWidth,
|
|
@@ -71,7 +52,7 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
71
52
|
contentType = result.format;
|
|
72
53
|
}
|
|
73
54
|
const key = options.folderPath ? `${options.folderPath}/${fileName}` : fileName;
|
|
74
|
-
|
|
55
|
+
await new Upload({
|
|
75
56
|
client: this.s3,
|
|
76
57
|
params: {
|
|
77
58
|
Bucket: this.bucketName,
|
|
@@ -79,8 +60,7 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
79
60
|
Body: processedBuffer,
|
|
80
61
|
ContentType: contentType
|
|
81
62
|
}
|
|
82
|
-
});
|
|
83
|
-
await upload.done();
|
|
63
|
+
}).done();
|
|
84
64
|
return {
|
|
85
65
|
name: fileName,
|
|
86
66
|
key,
|
|
@@ -93,44 +73,40 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
93
73
|
}
|
|
94
74
|
async deleteFile(key) {
|
|
95
75
|
const { DeleteObjectCommand } = await import('@aws-sdk/client-s3');
|
|
96
|
-
|
|
76
|
+
await this.s3.send(new DeleteObjectCommand({
|
|
97
77
|
Bucket: this.bucketName,
|
|
98
78
|
Key: key
|
|
99
|
-
});
|
|
100
|
-
await this.s3.send(command);
|
|
79
|
+
}));
|
|
101
80
|
this.logger.log(`Deleted file from S3: ${key}`);
|
|
102
81
|
}
|
|
103
82
|
async deleteMultipleFiles(keys) {
|
|
104
83
|
const { DeleteObjectsCommand } = await import('@aws-sdk/client-s3');
|
|
105
|
-
|
|
84
|
+
await this.s3.send(new DeleteObjectsCommand({
|
|
106
85
|
Bucket: this.bucketName,
|
|
107
86
|
Delete: {
|
|
108
87
|
Objects: keys.map((key)=>({
|
|
109
88
|
Key: key
|
|
110
89
|
}))
|
|
111
90
|
}
|
|
112
|
-
});
|
|
113
|
-
await this.s3.send(command);
|
|
91
|
+
}));
|
|
114
92
|
this.logger.log(`Deleted ${keys.length} files from S3`);
|
|
115
93
|
}
|
|
116
94
|
async generatePresignedUrl(key, expiresInSeconds = 3600) {
|
|
117
95
|
const { GetObjectCommand } = await import('@aws-sdk/client-s3');
|
|
118
96
|
const { getSignedUrl } = await import('@aws-sdk/s3-request-presigner');
|
|
119
|
-
|
|
97
|
+
return getSignedUrl(this.s3, new GetObjectCommand({
|
|
120
98
|
Bucket: this.bucketName,
|
|
121
99
|
Key: key
|
|
122
|
-
})
|
|
123
|
-
return getSignedUrl(this.s3, command, {
|
|
100
|
+
}), {
|
|
124
101
|
expiresIn: expiresInSeconds
|
|
125
102
|
});
|
|
126
103
|
}
|
|
127
104
|
async healthCheck() {
|
|
128
105
|
try {
|
|
129
106
|
const { HeadBucketCommand } = await import('@aws-sdk/client-s3');
|
|
130
|
-
|
|
107
|
+
await this.s3.send(new HeadBucketCommand({
|
|
131
108
|
Bucket: this.bucketName
|
|
132
|
-
});
|
|
133
|
-
await this.s3.send(command);
|
|
109
|
+
}));
|
|
134
110
|
return true;
|
|
135
111
|
} catch {
|
|
136
112
|
return false;
|
|
@@ -138,7 +114,7 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
138
114
|
}
|
|
139
115
|
constructor(){
|
|
140
116
|
_define_property(this, "logger", new Logger(S3Provider.name));
|
|
141
|
-
_define_property(this, "s3", void 0);
|
|
117
|
+
_define_property(this, "s3", void 0);
|
|
142
118
|
_define_property(this, "bucketName", '');
|
|
143
119
|
_define_property(this, "region", '');
|
|
144
120
|
}
|