@flusys/nestjs-storage 0.1.0-beta.2 → 1.0.0-beta

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.
Files changed (57) hide show
  1. package/README.md +3 -7
  2. package/cjs/config/storage-config.service.js +31 -0
  3. package/cjs/controllers/file-manager.controller.js +8 -6
  4. package/cjs/controllers/folder.controller.js +3 -4
  5. package/cjs/controllers/storage-config.controller.js +3 -4
  6. package/cjs/controllers/upload.controller.js +22 -169
  7. package/cjs/dtos/file-manager.dto.js +36 -2
  8. package/cjs/dtos/upload.dto.js +16 -0
  9. package/cjs/middlewares/file-serve.middleware.js +204 -0
  10. package/cjs/middlewares/index.js +18 -0
  11. package/cjs/modules/storage.module.js +58 -14
  12. package/cjs/providers/azure-provider.optional.js +1 -2
  13. package/cjs/providers/local-provider.js +43 -11
  14. package/cjs/providers/storage-factory.service.js +49 -5
  15. package/cjs/services/file-manager.service.js +134 -9
  16. package/cjs/services/folder.service.js +17 -48
  17. package/cjs/services/storage-datasource.provider.js +10 -16
  18. package/cjs/services/storage-provider-config.service.js +26 -32
  19. package/cjs/services/upload.service.js +135 -24
  20. package/cjs/utils/image-compressor.util.js +43 -5
  21. package/config/storage-config.service.d.ts +2 -0
  22. package/controllers/file-manager.controller.d.ts +1 -1
  23. package/controllers/upload.controller.d.ts +5 -4
  24. package/dtos/file-manager.dto.d.ts +4 -0
  25. package/dtos/upload.dto.d.ts +2 -0
  26. package/fesm/config/storage-config.service.js +31 -0
  27. package/fesm/controllers/file-manager.controller.js +8 -6
  28. package/fesm/controllers/folder.controller.js +5 -6
  29. package/fesm/controllers/storage-config.controller.js +5 -6
  30. package/fesm/controllers/upload.controller.js +25 -131
  31. package/fesm/dtos/file-manager.dto.js +36 -2
  32. package/fesm/dtos/upload.dto.js +16 -0
  33. package/fesm/middlewares/file-serve.middleware.js +153 -0
  34. package/fesm/middlewares/index.js +1 -0
  35. package/fesm/modules/storage.module.js +60 -16
  36. package/fesm/providers/azure-provider.optional.js +1 -2
  37. package/fesm/providers/local-provider.js +43 -11
  38. package/fesm/providers/storage-factory.service.js +50 -6
  39. package/fesm/services/file-manager.service.js +134 -9
  40. package/fesm/services/folder.service.js +18 -49
  41. package/fesm/services/storage-datasource.provider.js +10 -16
  42. package/fesm/services/storage-provider-config.service.js +26 -32
  43. package/fesm/services/upload.service.js +135 -24
  44. package/fesm/utils/image-compressor.util.js +3 -1
  45. package/interfaces/file-manager.interface.d.ts +2 -0
  46. package/interfaces/storage-module-options.interface.d.ts +2 -0
  47. package/interfaces/storage-provider.interface.d.ts +2 -0
  48. package/middlewares/file-serve.middleware.d.ts +9 -0
  49. package/middlewares/index.d.ts +1 -0
  50. package/modules/storage.module.d.ts +3 -2
  51. package/package.json +26 -11
  52. package/providers/local-provider.d.ts +2 -1
  53. package/providers/storage-factory.service.d.ts +4 -0
  54. package/services/file-manager.service.d.ts +7 -1
  55. package/services/folder.service.d.ts +1 -2
  56. package/services/storage-provider-config.service.d.ts +2 -2
  57. package/services/upload.service.d.ts +6 -2
@@ -26,28 +26,28 @@ function _ts_param(paramIndex, decorator) {
26
26
  };
27
27
  }
28
28
  import { JwtAuthGuard } from '@flusys/nestjs-shared/guards';
29
- import { ApiResponseDto, CurrentUser, Public } from '@flusys/nestjs-shared/decorators';
29
+ import { ApiResponseDto, CurrentUser } from '@flusys/nestjs-shared/decorators';
30
30
  import { ILoggedUserInfo } from '@flusys/nestjs-shared/interfaces';
31
31
  import { DeleteMultipleFileDto, DeleteSingleFileDto, FileUploadResponsePayloadDto, UploadOptionsDto } from '../dtos';
32
- import { Body, Controller, Get, Inject, Logger, NotFoundException, Param, Post, Query, Res, UploadedFile, UploadedFiles, UseGuards, UseInterceptors } from '@nestjs/common';
32
+ import { Body, Controller, Inject, Post, Query, UploadedFile, UploadedFiles, UseGuards, UseInterceptors } from '@nestjs/common';
33
33
  import { FileInterceptor, FilesInterceptor } from '@nestjs/platform-express';
34
- import { ApiBearerAuth, ApiBody, ApiConsumes, ApiOperation, ApiParam, ApiProduces, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger';
35
- import { Response } from 'express';
36
- import { createReadStream, existsSync, statSync } from 'fs';
37
- import * as mime from 'mime-types';
38
- import { join } from 'path';
34
+ import { ApiBearerAuth, ApiBody, ApiConsumes, ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger';
39
35
  import { UploadService } from '../services/upload.service';
36
+ import { StorageConfigService } from '../config';
37
+ import { StorageFactoryService } from '../providers/storage-factory.service';
40
38
  export class UploadController {
41
39
  async uploadSingleFile(file, options, user) {
42
- const url = await this.uploadService.uploadSingleFile(file, options, user);
40
+ const result = await this.uploadService.uploadSingleFile(file, options, user);
43
41
  return {
44
42
  success: true,
45
43
  message: 'File uploaded successfully',
46
44
  data: {
47
- size: this.uploadService.bytesToKb(url.size),
48
- name: url.name,
49
- key: url.key,
50
- contentType: url.contentType
45
+ size: this.uploadService.bytesToKb(result.size),
46
+ name: result.name,
47
+ key: result.key,
48
+ contentType: result.contentType,
49
+ location: result.location || 'local',
50
+ storageConfigId: result.storageConfigId || ''
51
51
  }
52
52
  };
53
53
  }
@@ -57,7 +57,9 @@ export class UploadController {
57
57
  size: this.uploadService.bytesToKb(file.size),
58
58
  name: file.name,
59
59
  key: file.key,
60
- contentType: file.contentType
60
+ contentType: file.contentType,
61
+ location: file.location || 'local',
62
+ storageConfigId: file.storageConfigId || ''
61
63
  }));
62
64
  return {
63
65
  success: true,
@@ -81,82 +83,13 @@ export class UploadController {
81
83
  data
82
84
  };
83
85
  }
84
- async seeUploadedFile(filePath, res) {
85
- try {
86
- const uploadDir = join(process.cwd(), 'uploads');
87
- const normalizedPath = Array.isArray(filePath) ? filePath.join('/') : filePath;
88
- const fullPath = join(uploadDir, normalizedPath);
89
- this.logger.debug(`Attempting to serve file from: ${fullPath}`);
90
- // Validate path is inside uploads directory
91
- if (!fullPath.startsWith(uploadDir)) {
92
- throw new NotFoundException('Invalid file path');
93
- }
94
- // Check if file exists
95
- if (!existsSync(fullPath)) {
96
- throw new NotFoundException(`File not found: ${normalizedPath}`);
97
- }
98
- const stats = statSync(fullPath);
99
- if (!stats.isFile()) {
100
- throw new NotFoundException(`Not a file: ${normalizedPath}`);
101
- }
102
- // Enhanced MIME type detection
103
- let mimeType = mime.lookup(fullPath) || 'application/octet-stream';
104
- // Special handling for SVG files
105
- if (fullPath.toLowerCase().endsWith('.svg')) {
106
- mimeType = 'image/svg+xml';
107
- }
108
- // Determine if file should be displayed inline or downloaded
109
- // Inline: PDFs, images, videos, audio, text files
110
- // Download: Office docs, archives, executables, etc.
111
- const viewableTypes = [
112
- 'image/',
113
- 'video/',
114
- 'audio/',
115
- 'text/',
116
- 'application/pdf',
117
- 'application/json',
118
- 'application/xml'
119
- ];
120
- const isViewable = viewableTypes.some((type)=>mimeType.startsWith(type));
121
- const contentDisposition = isViewable ? 'inline' : `attachment; filename="${encodeURIComponent(normalizedPath.split('/').pop() || 'download')}"`;
122
- // Set headers optimized for different file types
123
- res.set({
124
- 'Content-Type': mimeType,
125
- 'Content-Length': stats.size,
126
- 'Content-Disposition': contentDisposition,
127
- 'Cache-Control': 'public, max-age=3600',
128
- 'Accept-Ranges': 'bytes',
129
- 'Cross-Origin-Resource-Policy': 'cross-origin',
130
- 'Access-Control-Allow-Origin': '*',
131
- 'X-Content-Type-Options': 'nosniff'
132
- });
133
- // Stream the file
134
- const stream = createReadStream(fullPath);
135
- stream.pipe(res);
136
- return new Promise((resolve, reject)=>{
137
- stream.on('end', ()=>{
138
- res.end();
139
- resolve();
140
- });
141
- stream.on('error', (err)=>{
142
- this.logger.error('File stream error', err);
143
- reject(new NotFoundException(`Failed to serve file: ${normalizedPath}`));
144
- });
145
- // Handle client disconnection
146
- res.on('close', ()=>{
147
- stream.destroy();
148
- });
149
- });
150
- } catch (error) {
151
- this.logger.error('File retrieval error:', error);
152
- throw new NotFoundException(`File not found: ${filePath}`);
153
- }
154
- }
155
- constructor(uploadService){
86
+ constructor(uploadService, storageConfigService, storageFactoryService){
156
87
  _define_property(this, "uploadService", void 0);
157
- _define_property(this, "logger", void 0);
88
+ _define_property(this, "storageConfigService", void 0);
89
+ _define_property(this, "storageFactoryService", void 0);
158
90
  this.uploadService = uploadService;
159
- this.logger = new Logger(UploadController.name);
91
+ this.storageConfigService = storageConfigService;
92
+ this.storageFactoryService = storageFactoryService;
160
93
  }
161
94
  }
162
95
  _ts_decorate([
@@ -278,57 +211,18 @@ _ts_decorate([
278
211
  ]),
279
212
  _ts_metadata("design:returntype", Promise)
280
213
  ], UploadController.prototype, "deleteMultipleFile", null);
281
- _ts_decorate([
282
- Public(),
283
- Get('file/*filePath'),
284
- ApiOperation({
285
- summary: 'Serve uploaded file by relative path (Public endpoint)',
286
- description: `
287
- This endpoint returns a stored file from the server.
288
-
289
- - The \`filePath\` is a **wildcard path parameter** that supports nested folders.
290
- - Example:
291
- \`GET /uploads/file/profile/user123/avatar.png\`
292
- → Will serve the file from \`./profile/user123/avatar.png\`
293
-
294
- **Use case:**
295
- Access uploaded files publicly through a structured path.
296
-
297
- **Note:** This endpoint is public and does not require authentication.
298
- `
299
- }),
300
- ApiParam({
301
- name: 'filePath',
302
- required: true,
303
- description: 'Relative path to the uploaded file (supports nested directories, e.g. "profile/user123/avatar.png")',
304
- example: 'profile/user123/avatar.png'
305
- }),
306
- ApiProduces('image/jpeg'),
307
- ApiResponse({
308
- status: 200,
309
- description: 'Uploaded file successfully returned',
310
- schema: {
311
- type: 'string',
312
- format: 'binary'
313
- }
314
- }),
315
- _ts_param(0, Param('filePath')),
316
- _ts_param(1, Res()),
317
- _ts_metadata("design:type", Function),
318
- _ts_metadata("design:paramtypes", [
319
- Object,
320
- typeof Response === "undefined" ? Object : Response
321
- ]),
322
- _ts_metadata("design:returntype", Promise)
323
- ], UploadController.prototype, "seeUploadedFile", null);
324
214
  UploadController = _ts_decorate([
325
215
  ApiTags('Upload'),
326
216
  ApiBearerAuth(),
327
217
  Controller('storage/upload'),
328
218
  UseGuards(JwtAuthGuard),
329
219
  _ts_param(0, Inject(UploadService)),
220
+ _ts_param(1, Inject(StorageConfigService)),
221
+ _ts_param(2, Inject(StorageFactoryService)),
330
222
  _ts_metadata("design:type", Function),
331
223
  _ts_metadata("design:paramtypes", [
332
- typeof UploadService === "undefined" ? Object : UploadService
224
+ typeof UploadService === "undefined" ? Object : UploadService,
225
+ typeof StorageConfigService === "undefined" ? Object : StorageConfigService,
226
+ typeof StorageFactoryService === "undefined" ? Object : StorageFactoryService
333
227
  ])
334
228
  ], UploadController);
@@ -122,7 +122,17 @@ _ts_decorate([
122
122
  _ts_metadata("design:type", String)
123
123
  ], UpdateFileManagerDto.prototype, "id", void 0);
124
124
  export class FileManagerResponseDto extends UpdateFileManagerDto {
125
+ constructor(...args){
126
+ super(...args), _define_property(this, "providerName", void 0);
127
+ }
125
128
  }
129
+ _ts_decorate([
130
+ ApiPropertyOptional({
131
+ example: 'My S3 Storage',
132
+ description: 'Name of the storage configuration/provider'
133
+ }),
134
+ _ts_metadata("design:type", String)
135
+ ], FileManagerResponseDto.prototype, "providerName", void 0);
126
136
  export class GetFilesRequestDto {
127
137
  constructor(){
128
138
  _define_property(this, "id", void 0);
@@ -140,6 +150,9 @@ export class FilesResponseDto {
140
150
  _define_property(this, "name", void 0);
141
151
  _define_property(this, "contentType", void 0);
142
152
  _define_property(this, "url", void 0);
153
+ _define_property(this, "location", void 0);
154
+ _define_property(this, "storageConfigId", void 0);
155
+ _define_property(this, "providerName", void 0);
143
156
  }
144
157
  }
145
158
  _ts_decorate([
@@ -156,13 +169,34 @@ _ts_decorate([
156
169
  ], FilesResponseDto.prototype, "name", void 0);
157
170
  _ts_decorate([
158
171
  ApiProperty({
159
- example: 'file123.jpg'
172
+ example: 'image/jpeg'
160
173
  }),
161
174
  _ts_metadata("design:type", String)
162
175
  ], FilesResponseDto.prototype, "contentType", void 0);
163
176
  _ts_decorate([
164
177
  ApiProperty({
165
- example: 'file123.jpg'
178
+ example: 'https://example.com/file123.jpg'
166
179
  }),
167
180
  _ts_metadata("design:type", String)
168
181
  ], FilesResponseDto.prototype, "url", void 0);
182
+ _ts_decorate([
183
+ ApiPropertyOptional({
184
+ example: 'local',
185
+ description: 'Storage provider type (local, aws, azure, sftp)'
186
+ }),
187
+ _ts_metadata("design:type", String)
188
+ ], FilesResponseDto.prototype, "location", void 0);
189
+ _ts_decorate([
190
+ ApiPropertyOptional({
191
+ example: '123e4567-e89b-12d3-a456-426614174000',
192
+ description: 'Storage configuration ID'
193
+ }),
194
+ _ts_metadata("design:type", String)
195
+ ], FilesResponseDto.prototype, "storageConfigId", void 0);
196
+ _ts_decorate([
197
+ ApiPropertyOptional({
198
+ example: 'My S3 Storage',
199
+ description: 'Name of the storage configuration/provider'
200
+ }),
201
+ _ts_metadata("design:type", String)
202
+ ], FilesResponseDto.prototype, "providerName", void 0);
@@ -184,6 +184,8 @@ export class FileUploadResponsePayloadDto {
184
184
  _define_property(this, "contentType", void 0);
185
185
  _define_property(this, "size", void 0);
186
186
  _define_property(this, "key", void 0);
187
+ _define_property(this, "location", void 0);
188
+ _define_property(this, "storageConfigId", void 0);
187
189
  }
188
190
  }
189
191
  _ts_decorate([
@@ -210,3 +212,17 @@ _ts_decorate([
210
212
  }),
211
213
  _ts_metadata("design:type", String)
212
214
  ], FileUploadResponsePayloadDto.prototype, "key", void 0);
215
+ _ts_decorate([
216
+ ApiProperty({
217
+ example: 'local',
218
+ description: 'Storage provider type (local, aws, azure, sftp)'
219
+ }),
220
+ _ts_metadata("design:type", String)
221
+ ], FileUploadResponsePayloadDto.prototype, "location", void 0);
222
+ _ts_decorate([
223
+ ApiProperty({
224
+ example: '123e4567-e89b-12d3-a456-426614174000',
225
+ description: 'Storage configuration ID used for this upload'
226
+ }),
227
+ _ts_metadata("design:type", String)
228
+ ], FileUploadResponsePayloadDto.prototype, "storageConfigId", void 0);
@@ -0,0 +1,153 @@
1
+ function _define_property(obj, key, value) {
2
+ if (key in obj) {
3
+ Object.defineProperty(obj, key, {
4
+ value: value,
5
+ enumerable: true,
6
+ configurable: true,
7
+ writable: true
8
+ });
9
+ } else {
10
+ obj[key] = value;
11
+ }
12
+ return obj;
13
+ }
14
+ function _ts_decorate(decorators, target, key, desc) {
15
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
16
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
17
+ else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
18
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
19
+ }
20
+ function _ts_metadata(k, v) {
21
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
22
+ }
23
+ import { Injectable, Logger, NotFoundException } from '@nestjs/common';
24
+ import { createReadStream, existsSync, statSync } from 'fs';
25
+ import { join, resolve } from 'path';
26
+ import * as mime from 'mime-types';
27
+ import { UploadService } from '../services/upload.service';
28
+ export class FileServeMiddleware {
29
+ async use(req, res, next) {
30
+ try {
31
+ const uploadDir = process.cwd();
32
+ // Extract path after /storage/upload/file/
33
+ const urlPath = req.path || req.url;
34
+ const match = urlPath.match(/\/storage\/upload\/file\/(.+)/);
35
+ const normalizedPath = match ? match[1] : '';
36
+ if (!normalizedPath) {
37
+ throw new NotFoundException('File path is required');
38
+ }
39
+ this.logger.debug(`Attempting to serve file, normalizedPath: ${normalizedPath}`);
40
+ // Get the local storage basePath for fallback lookups
41
+ const basePath = await this.uploadService.getLocalStorageBasePath();
42
+ const normalizedBasePath = basePath ? basePath.replace(/^\.\//, '').replace(/\/$/, '') : null;
43
+ // Strategy 1: Try path relative to CWD (new format: key includes basePath)
44
+ let fullPath = join(uploadDir, normalizedPath);
45
+ let resolvedPath = resolve(fullPath);
46
+ this.logger.debug(`Strategy 1 - CWD relative: ${fullPath}`);
47
+ // Validate path is inside the project directory (prevent path traversal)
48
+ if (!resolvedPath.startsWith(uploadDir)) {
49
+ throw new NotFoundException('Invalid file path');
50
+ }
51
+ // Check if file exists
52
+ if (!existsSync(fullPath)) {
53
+ // Strategy 2: If key already starts with basePath, try using basePath directly
54
+ if (basePath && normalizedBasePath) {
55
+ const pathStartsWithBase = normalizedPath.startsWith(normalizedBasePath + '/') || normalizedPath.startsWith(normalizedBasePath);
56
+ if (pathStartsWithBase) {
57
+ const remainingPath = normalizedPath.substring(normalizedBasePath.length).replace(/^\//, '');
58
+ const fallbackPath = join(basePath, remainingPath);
59
+ this.logger.debug(`Strategy 2 - basePath + remaining: ${fallbackPath}`);
60
+ if (existsSync(fallbackPath)) {
61
+ fullPath = fallbackPath;
62
+ resolvedPath = resolve(fullPath);
63
+ }
64
+ } else {
65
+ // Old format: key doesn't include basePath, prepend it
66
+ const fallbackPath = join(basePath, normalizedPath);
67
+ this.logger.debug(`Strategy 3 - basePath + full key (old format): ${fallbackPath}`);
68
+ if (existsSync(fallbackPath)) {
69
+ fullPath = fallbackPath;
70
+ resolvedPath = resolve(fullPath);
71
+ }
72
+ }
73
+ }
74
+ // Final check if file was found
75
+ if (!existsSync(fullPath)) {
76
+ throw new NotFoundException(`File not found: ${normalizedPath}`);
77
+ }
78
+ }
79
+ const stats = statSync(fullPath);
80
+ if (!stats.isFile()) {
81
+ throw new NotFoundException(`Not a file: ${normalizedPath}`);
82
+ }
83
+ // Enhanced MIME type detection
84
+ let mimeType = mime.lookup(fullPath) || 'application/octet-stream';
85
+ // Special handling for SVG files
86
+ if (fullPath.toLowerCase().endsWith('.svg')) {
87
+ mimeType = 'image/svg+xml';
88
+ }
89
+ // Determine if file should be displayed inline or downloaded
90
+ const viewableTypes = [
91
+ 'image/',
92
+ 'video/',
93
+ 'audio/',
94
+ 'text/',
95
+ 'application/pdf',
96
+ 'application/json',
97
+ 'application/xml'
98
+ ];
99
+ const isViewable = viewableTypes.some((type)=>mimeType.startsWith(type));
100
+ const contentDisposition = isViewable ? 'inline' : `attachment; filename="${encodeURIComponent(normalizedPath.split('/').pop() || 'download')}"`;
101
+ // Set headers
102
+ res.set({
103
+ 'Content-Type': mimeType,
104
+ 'Content-Length': stats.size,
105
+ 'Content-Disposition': contentDisposition,
106
+ 'Cache-Control': 'public, max-age=3600',
107
+ 'Accept-Ranges': 'bytes',
108
+ 'Cross-Origin-Resource-Policy': 'cross-origin',
109
+ 'Access-Control-Allow-Origin': '*',
110
+ 'X-Content-Type-Options': 'nosniff'
111
+ });
112
+ // Stream the file
113
+ const stream = createReadStream(fullPath);
114
+ stream.pipe(res);
115
+ stream.on('error', (err)=>{
116
+ this.logger.error('File stream error', err);
117
+ if (!res.headersSent) {
118
+ res.status(404).json({
119
+ message: `Failed to serve file: ${normalizedPath}`,
120
+ error: 'Not Found',
121
+ statusCode: 404
122
+ });
123
+ }
124
+ });
125
+ // Handle client disconnection
126
+ res.on('close', ()=>{
127
+ stream.destroy();
128
+ });
129
+ } catch (error) {
130
+ this.logger.error('File retrieval error:', error);
131
+ if (!res.headersSent) {
132
+ res.status(404).json({
133
+ message: error instanceof NotFoundException ? error.message : `File not found`,
134
+ error: 'Not Found',
135
+ statusCode: 404
136
+ });
137
+ }
138
+ }
139
+ }
140
+ constructor(uploadService){
141
+ _define_property(this, "uploadService", void 0);
142
+ _define_property(this, "logger", void 0);
143
+ this.uploadService = uploadService;
144
+ this.logger = new Logger(FileServeMiddleware.name);
145
+ }
146
+ }
147
+ FileServeMiddleware = _ts_decorate([
148
+ Injectable(),
149
+ _ts_metadata("design:type", Function),
150
+ _ts_metadata("design:paramtypes", [
151
+ typeof UploadService === "undefined" ? Object : UploadService
152
+ ])
153
+ ], FileServeMiddleware);
@@ -0,0 +1 @@
1
+ export * from './file-serve.middleware';
@@ -5,15 +5,54 @@ 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 { Module } from '@nestjs/common';
8
+ import { Module, Logger, RequestMethod } from '@nestjs/common';
9
+ import { FileServeMiddleware } from '../middlewares';
9
10
  import { StorageConfigService } from '../config';
10
11
  import { STORAGE_MODULE_OPTIONS } from '../config/storage.constants';
11
12
  import { FileManagerController, FolderController, StorageConfigController, UploadController } from '../controllers';
12
- import { StorageFactoryService } from '../providers';
13
+ import { FileLocationEnum } from '../enums/file-location.enum';
14
+ import { StorageFactoryService, StorageProviderRegistry } from '../providers';
15
+ import { LocalProvider } from '../providers/local-provider';
13
16
  import { FileManagerService, FolderService, StorageDataSourceProvider, UploadService } from '../services';
14
17
  import { StorageProviderConfigService } from '../services/storage-provider-config.service';
18
+ // Auto-register built-in storage providers
19
+ const logger = new Logger('StorageModule');
20
+ // Register LocalProvider (always available - uses Node.js built-in fs)
21
+ StorageProviderRegistry.register(FileLocationEnum.LOCAL, LocalProvider);
22
+ logger.log('Registered LocalProvider');
23
+ // Try to register optional providers (only if dependencies are installed)
24
+ try {
25
+ const { S3Provider } = require('../providers/s3-provider.optional');
26
+ StorageProviderRegistry.register(FileLocationEnum.AWS, S3Provider);
27
+ logger.log('Registered S3Provider');
28
+ } catch {
29
+ logger.debug('S3Provider not available (install @aws-sdk/client-s3 to enable)');
30
+ }
31
+ try {
32
+ const { AzureProvider } = require('../providers/azure-provider.optional');
33
+ StorageProviderRegistry.register(FileLocationEnum.AZURE, AzureProvider);
34
+ logger.log('Registered AzureProvider');
35
+ } catch {
36
+ logger.debug('AzureProvider not available (install @azure/storage-blob to enable)');
37
+ }
38
+ try {
39
+ const { SftpProvider } = require('../providers/sftp-provider.optional');
40
+ StorageProviderRegistry.register(FileLocationEnum.SFTP, SftpProvider);
41
+ logger.log('Registered SftpProvider');
42
+ } catch {
43
+ logger.debug('SftpProvider not available (install ssh2-sftp-client to enable)');
44
+ }
15
45
  export class StorageModule {
16
46
  /**
47
+ * Configure middleware for file serving
48
+ * This bypasses path-to-regexp issues with wildcard routes
49
+ */ configure(consumer) {
50
+ consumer.apply(FileServeMiddleware).forRoutes({
51
+ path: 'storage/upload/file/*',
52
+ method: RequestMethod.GET
53
+ });
54
+ }
55
+ /**
17
56
  * Register StorageModule synchronously
18
57
  */ static forRoot(options) {
19
58
  const controllers = this.getControllers(options);
@@ -52,9 +91,10 @@ export class StorageModule {
52
91
  UtilsModule
53
92
  ],
54
93
  controllers: options.includeController !== false ? controllers : [],
94
+ // Pass false to exclude STORAGE_MODULE_OPTIONS - it's already in asyncProviders
55
95
  providers: [
56
96
  ...asyncProviders,
57
- ...this.getProviders(options)
97
+ ...this.getProviders(options, false)
58
98
  ],
59
99
  exports: [
60
100
  StorageConfigService,
@@ -67,10 +107,8 @@ export class StorageModule {
67
107
  ]
68
108
  };
69
109
  }
70
- // ==================== Private Helper Methods ====================
71
- /**
72
- * Get controllers (all controllers always loaded)
73
- */ static getControllers(options) {
110
+ // Private Helper Methods
111
+ /** Get controllers (all controllers always loaded) */ static getControllers(options) {
74
112
  return [
75
113
  FileManagerController,
76
114
  FolderController,
@@ -83,22 +121,28 @@ export class StorageModule {
83
121
  * This ensures dynamic entity loading based on runtime configuration
84
122
  */ /**
85
123
  * Get providers (all providers always loaded)
86
- */ static getProviders(options) {
87
- const enableCompanyFeature = options.bootstrapAppConfig?.enableCompanyFeature ?? false;
88
- const optionsProvider = {
89
- provide: STORAGE_MODULE_OPTIONS,
90
- useValue: options
91
- };
92
- return [
93
- optionsProvider,
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) {
127
+ const providers = [
94
128
  StorageConfigService,
95
129
  StorageDataSourceProvider,
96
130
  FileManagerService,
97
131
  FolderService,
98
132
  StorageProviderConfigService,
99
133
  UploadService,
100
- StorageFactoryService
134
+ StorageFactoryService,
135
+ FileServeMiddleware
101
136
  ];
137
+ // Only include options provider for sync registration
138
+ // For async registration, createAsyncProviders handles it
139
+ if (includeOptionsProvider) {
140
+ providers.unshift({
141
+ provide: STORAGE_MODULE_OPTIONS,
142
+ useValue: options
143
+ });
144
+ }
145
+ return providers;
102
146
  }
103
147
  /**
104
148
  * Create async providers for forRootAsync
@@ -53,7 +53,7 @@ import { v4 as uuidv4 } from 'uuid';
53
53
  // Ensure container exists
54
54
  await this.containerClient.createIfNotExists();
55
55
  this.logger.log(`Azure Provider initialized: account=${config.accountName}, container=${config.containerName}`);
56
- } catch (error) {
56
+ } catch (_error) {
57
57
  this.logger.error('Failed to initialize AzureProvider. Make sure @azure/storage-blob is installed.');
58
58
  throw new Error('Azure Storage SDK not found. Install it with: npm install @azure/storage-blob');
59
59
  }
@@ -81,7 +81,6 @@ import { v4 as uuidv4 } from 'uuid';
81
81
  blobContentType: contentType
82
82
  }
83
83
  });
84
- const url = blockBlobClient.url;
85
84
  return {
86
85
  name: fileName,
87
86
  key: blobName,