@flusys/nestjs-storage 1.0.0-beta → 1.0.0-rc

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 (60) hide show
  1. package/README.md +131 -13
  2. package/cjs/config/storage-config.service.js +5 -0
  3. package/cjs/config/storage.constants.js +0 -8
  4. package/cjs/controllers/file-manager.controller.js +44 -1
  5. package/cjs/controllers/folder.controller.js +44 -1
  6. package/cjs/controllers/storage-config.controller.js +44 -1
  7. package/cjs/controllers/upload.controller.js +6 -12
  8. package/cjs/dtos/file-manager.dto.js +8 -5
  9. package/cjs/dtos/storage-config.dto.js +41 -1
  10. package/cjs/dtos/upload.dto.js +7 -0
  11. package/cjs/entities/storage-config-base.entity.js +31 -2
  12. package/cjs/interfaces/index.js +0 -1
  13. package/cjs/middlewares/file-serve.middleware.js +6 -0
  14. package/cjs/providers/local-provider.js +52 -2
  15. package/cjs/providers/storage-factory.service.js +2 -2
  16. package/cjs/services/file-manager.service.js +37 -24
  17. package/cjs/services/folder.service.js +5 -8
  18. package/cjs/services/storage-provider-config.service.js +18 -35
  19. package/cjs/services/upload.service.js +39 -27
  20. package/cjs/utils/file-validator.util.js +470 -0
  21. package/cjs/utils/image-compressor.util.js +1 -3
  22. package/config/storage-config.service.d.ts +5 -2
  23. package/config/storage.constants.d.ts +0 -2
  24. package/controllers/upload.controller.d.ts +2 -6
  25. package/dtos/file-manager.dto.d.ts +2 -4
  26. package/dtos/folder.dto.d.ts +2 -4
  27. package/dtos/storage-config.dto.d.ts +9 -6
  28. package/entities/storage-config-base.entity.d.ts +2 -0
  29. package/fesm/config/storage-config.service.js +5 -0
  30. package/fesm/config/storage.constants.js +0 -2
  31. package/fesm/controllers/file-manager.controller.js +45 -2
  32. package/fesm/controllers/folder.controller.js +45 -2
  33. package/fesm/controllers/storage-config.controller.js +45 -2
  34. package/fesm/controllers/upload.controller.js +7 -13
  35. package/fesm/dtos/file-manager.dto.js +8 -5
  36. package/fesm/dtos/storage-config.dto.js +45 -11
  37. package/fesm/dtos/upload.dto.js +8 -1
  38. package/fesm/entities/index.js +1 -5
  39. package/fesm/entities/storage-config-base.entity.js +33 -7
  40. package/fesm/interfaces/index.js +0 -1
  41. package/fesm/interfaces/storage-config.interface.js +1 -3
  42. package/fesm/middlewares/file-serve.middleware.js +7 -1
  43. package/fesm/providers/local-provider.js +52 -2
  44. package/fesm/providers/storage-factory.service.js +2 -2
  45. package/fesm/services/file-manager.service.js +38 -25
  46. package/fesm/services/folder.service.js +5 -8
  47. package/fesm/services/storage-provider-config.service.js +18 -35
  48. package/fesm/services/upload.service.js +40 -28
  49. package/fesm/utils/file-validator.util.js +463 -0
  50. package/fesm/utils/image-compressor.util.js +1 -3
  51. package/interfaces/file-manager.interface.d.ts +7 -4
  52. package/interfaces/index.d.ts +0 -1
  53. package/interfaces/storage-config.interface.d.ts +2 -20
  54. package/package.json +6 -6
  55. package/providers/local-provider.d.ts +2 -0
  56. package/services/file-manager.service.d.ts +2 -2
  57. package/utils/file-validator.util.d.ts +16 -0
  58. package/cjs/interfaces/file-upload-response.interface.js +0 -4
  59. package/fesm/interfaces/file-upload-response.interface.js +0 -1
  60. package/interfaces/file-upload-response.interface.d.ts +0 -6
@@ -25,13 +25,56 @@ function _ts_param(paramIndex, decorator) {
25
25
  decorator(target, key, paramIndex);
26
26
  };
27
27
  }
28
- import { createApiController } from '@flusys/nestjs-shared/classes';
28
+ import { createApiController, FOLDER_PERMISSIONS } from '@flusys/nestjs-shared/classes';
29
29
  import { CreateFolderDto, FolderResponseDto, UpdateFolderDto } from '../dtos';
30
30
  import { Controller, Inject } from '@nestjs/common';
31
31
  import { ApiTags } from '@nestjs/swagger';
32
32
  import { FolderService } from '../services/folder.service';
33
33
  export class FolderController extends createApiController(CreateFolderDto, UpdateFolderDto, FolderResponseDto, {
34
- security: 'jwt'
34
+ security: {
35
+ insert: {
36
+ level: 'permission',
37
+ permissions: [
38
+ FOLDER_PERMISSIONS.CREATE
39
+ ]
40
+ },
41
+ insertMany: {
42
+ level: 'permission',
43
+ permissions: [
44
+ FOLDER_PERMISSIONS.CREATE
45
+ ]
46
+ },
47
+ getById: {
48
+ level: 'permission',
49
+ permissions: [
50
+ FOLDER_PERMISSIONS.READ
51
+ ]
52
+ },
53
+ getAll: {
54
+ level: 'permission',
55
+ permissions: [
56
+ FOLDER_PERMISSIONS.READ
57
+ ]
58
+ },
59
+ update: {
60
+ level: 'permission',
61
+ permissions: [
62
+ FOLDER_PERMISSIONS.UPDATE
63
+ ]
64
+ },
65
+ updateMany: {
66
+ level: 'permission',
67
+ permissions: [
68
+ FOLDER_PERMISSIONS.UPDATE
69
+ ]
70
+ },
71
+ delete: {
72
+ level: 'permission',
73
+ permissions: [
74
+ FOLDER_PERMISSIONS.DELETE
75
+ ]
76
+ }
77
+ }
35
78
  }) {
36
79
  constructor(folderService){
37
80
  super(folderService), _define_property(this, "folderService", void 0), this.folderService = folderService;
@@ -25,13 +25,56 @@ function _ts_param(paramIndex, decorator) {
25
25
  decorator(target, key, paramIndex);
26
26
  };
27
27
  }
28
- import { createApiController } from '@flusys/nestjs-shared/classes';
28
+ import { createApiController, STORAGE_CONFIG_PERMISSIONS } from '@flusys/nestjs-shared/classes';
29
29
  import { CreateStorageConfigDto, StorageConfigResponseDto, UpdateStorageConfigDto } from '../dtos';
30
30
  import { Controller, Inject } from '@nestjs/common';
31
31
  import { ApiTags } from '@nestjs/swagger';
32
32
  import { StorageProviderConfigService } from '../services/storage-provider-config.service';
33
33
  export class StorageConfigController extends createApiController(CreateStorageConfigDto, UpdateStorageConfigDto, StorageConfigResponseDto, {
34
- security: 'jwt'
34
+ security: {
35
+ insert: {
36
+ level: 'permission',
37
+ permissions: [
38
+ STORAGE_CONFIG_PERMISSIONS.CREATE
39
+ ]
40
+ },
41
+ insertMany: {
42
+ level: 'permission',
43
+ permissions: [
44
+ STORAGE_CONFIG_PERMISSIONS.CREATE
45
+ ]
46
+ },
47
+ getById: {
48
+ level: 'permission',
49
+ permissions: [
50
+ STORAGE_CONFIG_PERMISSIONS.READ
51
+ ]
52
+ },
53
+ getAll: {
54
+ level: 'permission',
55
+ permissions: [
56
+ STORAGE_CONFIG_PERMISSIONS.READ
57
+ ]
58
+ },
59
+ update: {
60
+ level: 'permission',
61
+ permissions: [
62
+ STORAGE_CONFIG_PERMISSIONS.UPDATE
63
+ ]
64
+ },
65
+ updateMany: {
66
+ level: 'permission',
67
+ permissions: [
68
+ STORAGE_CONFIG_PERMISSIONS.UPDATE
69
+ ]
70
+ },
71
+ delete: {
72
+ level: 'permission',
73
+ permissions: [
74
+ STORAGE_CONFIG_PERMISSIONS.DELETE
75
+ ]
76
+ }
77
+ }
35
78
  }) {
36
79
  constructor(storageConfigService){
37
80
  super(storageConfigService), _define_property(this, "storageConfigService", void 0), this.storageConfigService = storageConfigService;
@@ -26,15 +26,13 @@ function _ts_param(paramIndex, decorator) {
26
26
  };
27
27
  }
28
28
  import { JwtAuthGuard } from '@flusys/nestjs-shared/guards';
29
- import { ApiResponseDto, CurrentUser } from '@flusys/nestjs-shared/decorators';
29
+ import { ApiResponseDto, CurrentUser, RequirePermission } from '@flusys/nestjs-shared/decorators';
30
30
  import { ILoggedUserInfo } from '@flusys/nestjs-shared/interfaces';
31
31
  import { DeleteMultipleFileDto, DeleteSingleFileDto, FileUploadResponsePayloadDto, UploadOptionsDto } from '../dtos';
32
32
  import { Body, Controller, Inject, Post, Query, UploadedFile, UploadedFiles, UseGuards, UseInterceptors } from '@nestjs/common';
33
33
  import { FileInterceptor, FilesInterceptor } from '@nestjs/platform-express';
34
34
  import { ApiBearerAuth, ApiBody, ApiConsumes, ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger';
35
35
  import { UploadService } from '../services/upload.service';
36
- import { StorageConfigService } from '../config';
37
- import { StorageFactoryService } from '../providers/storage-factory.service';
38
36
  export class UploadController {
39
37
  async uploadSingleFile(file, options, user) {
40
38
  const result = await this.uploadService.uploadSingleFile(file, options, user);
@@ -83,17 +81,14 @@ export class UploadController {
83
81
  data
84
82
  };
85
83
  }
86
- constructor(uploadService, storageConfigService, storageFactoryService){
84
+ constructor(uploadService){
87
85
  _define_property(this, "uploadService", void 0);
88
- _define_property(this, "storageConfigService", void 0);
89
- _define_property(this, "storageFactoryService", void 0);
90
86
  this.uploadService = uploadService;
91
- this.storageConfigService = storageConfigService;
92
- this.storageFactoryService = storageFactoryService;
93
87
  }
94
88
  }
95
89
  _ts_decorate([
96
90
  Post('single-file'),
91
+ RequirePermission('file.upload'),
97
92
  ApiResponseDto(FileUploadResponsePayloadDto, false),
98
93
  ApiOperation({
99
94
  summary: 'Upload a single file'
@@ -135,6 +130,7 @@ _ts_decorate([
135
130
  ], UploadController.prototype, "uploadSingleFile", null);
136
131
  _ts_decorate([
137
132
  Post('multiple-file'),
133
+ RequirePermission('file.upload'),
138
134
  ApiOperation({
139
135
  summary: 'Upload multiple files (up to 50)'
140
136
  }),
@@ -179,6 +175,7 @@ _ts_decorate([
179
175
  ], UploadController.prototype, "uploadMultipleFiles", null);
180
176
  _ts_decorate([
181
177
  Post('delete-single-file'),
178
+ RequirePermission('file.delete'),
182
179
  ApiOperation({
183
180
  summary: 'Delete a single file'
184
181
  }),
@@ -196,6 +193,7 @@ _ts_decorate([
196
193
  ], UploadController.prototype, "deleteSingleFile", null);
197
194
  _ts_decorate([
198
195
  Post('delete-multiple-file'),
196
+ RequirePermission('file.delete'),
199
197
  ApiOperation({
200
198
  summary: 'Delete multiple files'
201
199
  }),
@@ -217,12 +215,8 @@ UploadController = _ts_decorate([
217
215
  Controller('storage/upload'),
218
216
  UseGuards(JwtAuthGuard),
219
217
  _ts_param(0, Inject(UploadService)),
220
- _ts_param(1, Inject(StorageConfigService)),
221
- _ts_param(2, Inject(StorageFactoryService)),
222
218
  _ts_metadata("design:type", Function),
223
219
  _ts_metadata("design:paramtypes", [
224
- typeof UploadService === "undefined" ? Object : UploadService,
225
- typeof StorageConfigService === "undefined" ? Object : StorageConfigService,
226
- typeof StorageFactoryService === "undefined" ? Object : StorageFactoryService
220
+ typeof UploadService === "undefined" ? Object : UploadService
227
221
  ])
228
222
  ], UploadController);
@@ -37,8 +37,8 @@ export class CreateFileManagerDto {
37
37
  }
38
38
  _ts_decorate([
39
39
  ApiProperty({
40
- description: 'Name of the FileManager item',
41
- example: 'Summer Vacation 2025'
40
+ description: 'Original file name',
41
+ example: 'vacation-photo.jpg'
42
42
  }),
43
43
  IsNotEmpty(),
44
44
  IsString(),
@@ -46,8 +46,8 @@ _ts_decorate([
46
46
  ], CreateFileManagerDto.prototype, "name", void 0);
47
47
  _ts_decorate([
48
48
  ApiProperty({
49
- description: 'Name of the FileManager item',
50
- example: 'Summer Vacation 2025'
49
+ description: 'Storage key or path of the file',
50
+ example: 'uploads/abc123-vacation-photo.jpg'
51
51
  }),
52
52
  IsNotEmpty(),
53
53
  IsString(),
@@ -115,10 +115,11 @@ export class UpdateFileManagerDto extends PartialType(CreateFileManagerDto) {
115
115
  }
116
116
  _ts_decorate([
117
117
  ApiProperty({
118
- description: 'Unique identifier of the folder',
118
+ description: 'Unique identifier of the file',
119
119
  example: 'f2e9c8d0-7a2a-11eb-9439-0242ac130002'
120
120
  }),
121
121
  IsNotEmpty(),
122
+ IsString(),
122
123
  _ts_metadata("design:type", String)
123
124
  ], UpdateFileManagerDto.prototype, "id", void 0);
124
125
  export class FileManagerResponseDto extends UpdateFileManagerDto {
@@ -142,6 +143,8 @@ _ts_decorate([
142
143
  ApiProperty({
143
144
  example: 'f2e9c8d0-7a2a-11eb-9439-0242ac130002'
144
145
  }),
146
+ IsNotEmpty(),
147
+ IsString(),
145
148
  _ts_metadata("design:type", String)
146
149
  ], GetFilesRequestDto.prototype, "id", void 0);
147
150
  export class FilesResponseDto {
@@ -23,14 +23,14 @@ function _ts_metadata(k, v) {
23
23
  import { IdentityResponseDto } from '@flusys/nestjs-shared/dtos';
24
24
  import { FileLocationEnum } from '../enums';
25
25
  import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
26
- import { IsEnum, IsNotEmpty, IsObject, IsOptional, IsString, MaxLength } from 'class-validator';
27
- /**
28
- * DTO for creating storage configuration
29
- */ export class CreateStorageConfigDto {
26
+ import { IsBoolean, IsEnum, IsNotEmpty, IsObject, IsOptional, IsString, MaxLength } from 'class-validator';
27
+ export class CreateStorageConfigDto {
30
28
  constructor(){
31
29
  _define_property(this, "name", void 0);
32
30
  _define_property(this, "storage", void 0);
33
31
  _define_property(this, "config", void 0);
32
+ _define_property(this, "isActive", void 0);
33
+ _define_property(this, "isDefault", void 0);
34
34
  }
35
35
  }
36
36
  _ts_decorate([
@@ -78,14 +78,32 @@ _ts_decorate([
78
78
  IsNotEmpty(),
79
79
  _ts_metadata("design:type", typeof Record === "undefined" ? Object : Record)
80
80
  ], CreateStorageConfigDto.prototype, "config", void 0);
81
- /**
82
- * DTO for updating storage configuration
83
- */ export class UpdateStorageConfigDto {
81
+ _ts_decorate([
82
+ ApiPropertyOptional({
83
+ example: true,
84
+ description: 'Whether storage configuration is active'
85
+ }),
86
+ IsBoolean(),
87
+ IsOptional(),
88
+ _ts_metadata("design:type", Boolean)
89
+ ], CreateStorageConfigDto.prototype, "isActive", void 0);
90
+ _ts_decorate([
91
+ ApiPropertyOptional({
92
+ example: false,
93
+ description: 'Set as default storage configuration'
94
+ }),
95
+ IsBoolean(),
96
+ IsOptional(),
97
+ _ts_metadata("design:type", Boolean)
98
+ ], CreateStorageConfigDto.prototype, "isDefault", void 0);
99
+ export class UpdateStorageConfigDto {
84
100
  constructor(){
85
101
  _define_property(this, "id", void 0);
86
102
  _define_property(this, "name", void 0);
87
103
  _define_property(this, "storage", void 0);
88
104
  _define_property(this, "config", void 0);
105
+ _define_property(this, "isActive", void 0);
106
+ _define_property(this, "isDefault", void 0);
89
107
  }
90
108
  }
91
109
  _ts_decorate([
@@ -140,10 +158,26 @@ _ts_decorate([
140
158
  IsOptional(),
141
159
  _ts_metadata("design:type", typeof Record === "undefined" ? Object : Record)
142
160
  ], UpdateStorageConfigDto.prototype, "config", void 0);
143
- /**
144
- * Response DTO for storage configuration
145
- */ export class StorageConfigResponseDto extends IdentityResponseDto {
161
+ _ts_decorate([
162
+ ApiPropertyOptional({
163
+ example: true,
164
+ description: 'Whether storage configuration is active'
165
+ }),
166
+ IsBoolean(),
167
+ IsOptional(),
168
+ _ts_metadata("design:type", Boolean)
169
+ ], UpdateStorageConfigDto.prototype, "isActive", void 0);
170
+ _ts_decorate([
171
+ ApiPropertyOptional({
172
+ example: false,
173
+ description: 'Set as default storage configuration'
174
+ }),
175
+ IsBoolean(),
176
+ IsOptional(),
177
+ _ts_metadata("design:type", Boolean)
178
+ ], UpdateStorageConfigDto.prototype, "isDefault", void 0);
179
+ export class StorageConfigResponseDto extends IdentityResponseDto {
146
180
  constructor(...args){
147
- super(...args), _define_property(this, "name", void 0), _define_property(this, "storage", void 0), _define_property(this, "config", void 0);
181
+ super(...args), _define_property(this, "name", void 0), _define_property(this, "storage", void 0), _define_property(this, "config", void 0), _define_property(this, "isActive", void 0), _define_property(this, "isDefault", void 0);
148
182
  }
149
183
  }
@@ -21,7 +21,7 @@ function _ts_metadata(k, v) {
21
21
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
22
22
  }
23
23
  import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
24
- import { IsBoolean, IsEnum, IsInt, IsOptional, IsUUID, Matches, Max, Min } from 'class-validator';
24
+ import { IsArray, IsBoolean, IsEnum, IsInt, IsNotEmpty, IsOptional, IsString, IsUUID, Matches, Max, Min } from 'class-validator';
25
25
  import { Type } from 'class-transformer';
26
26
  // Image format enum
27
27
  export var ImageFormat = /*#__PURE__*/ function(ImageFormat) {
@@ -42,6 +42,8 @@ _ts_decorate([
42
42
  description: 'Key of the file to delete',
43
43
  example: '/uploads/some path'
44
44
  }),
45
+ IsNotEmpty(),
46
+ IsString(),
45
47
  _ts_metadata("design:type", String)
46
48
  ], DeleteSingleFileDto.prototype, "key", void 0);
47
49
  _ts_decorate([
@@ -70,6 +72,11 @@ _ts_decorate([
70
72
  String
71
73
  ]
72
74
  }),
75
+ IsNotEmpty(),
76
+ IsArray(),
77
+ IsString({
78
+ each: true
79
+ }),
73
80
  _ts_metadata("design:type", Array)
74
81
  ], DeleteMultipleFileDto.prototype, "keys", void 0);
75
82
  _ts_decorate([
@@ -28,14 +28,10 @@ export const StorageCompanyEntities = [
28
28
  FolderWithCompany,
29
29
  StorageConfigWithCompany
30
30
  ];
31
- // All entities
32
31
  export const StorageAllEntities = [
33
32
  ...StorageCoreEntities,
34
33
  ...StorageCompanyEntities
35
34
  ];
36
- /**
37
- * Get Storage entities based on configuration
38
- * @param enableCompanyFeature - Whether company feature is enabled
39
- */ export function getStorageEntitiesByConfig(enableCompanyFeature) {
35
+ export function getStorageEntitiesByConfig(enableCompanyFeature) {
40
36
  return enableCompanyFeature ? StorageCompanyEntities : StorageCoreEntities;
41
37
  }
@@ -22,14 +22,10 @@ function _ts_metadata(k, v) {
22
22
  }
23
23
  import { Identity } from '@flusys/nestjs-shared/entities';
24
24
  import { FileLocationEnum } from '../enums';
25
- import { Column } from 'typeorm';
26
- /**
27
- * Base StorageConfig Entity
28
- * Contains common fields shared by both StorageConfig and StorageConfigWithCompany
29
- */ export class StorageConfigBase extends Identity {
25
+ import { Column, Index } from 'typeorm';
26
+ export class StorageConfigBase extends Identity {
30
27
  constructor(...args){
31
- super(...args), _define_property(this, "name", void 0), _define_property(this, "storage", void 0), // add config for storage
32
- _define_property(this, "config", void 0);
28
+ super(...args), _define_property(this, "name", void 0), _define_property(this, "storage", void 0), _define_property(this, "config", void 0), _define_property(this, "isActive", void 0), _define_property(this, "isDefault", void 0);
33
29
  }
34
30
  }
35
31
  _ts_decorate([
@@ -55,3 +51,33 @@ _ts_decorate([
55
51
  }),
56
52
  _ts_metadata("design:type", typeof Record === "undefined" ? Object : Record)
57
53
  ], StorageConfigBase.prototype, "config", void 0);
54
+ _ts_decorate([
55
+ Column({
56
+ type: 'boolean',
57
+ default: true,
58
+ name: 'is_active'
59
+ }),
60
+ _ts_metadata("design:type", Boolean)
61
+ ], StorageConfigBase.prototype, "isActive", void 0);
62
+ _ts_decorate([
63
+ Column({
64
+ type: 'boolean',
65
+ default: false,
66
+ name: 'is_default'
67
+ }),
68
+ _ts_metadata("design:type", Boolean)
69
+ ], StorageConfigBase.prototype, "isDefault", void 0);
70
+ StorageConfigBase = _ts_decorate([
71
+ Index([
72
+ 'name'
73
+ ]),
74
+ Index([
75
+ 'storage'
76
+ ]),
77
+ Index([
78
+ 'isActive'
79
+ ]),
80
+ Index([
81
+ 'isDefault'
82
+ ])
83
+ ], StorageConfigBase);
@@ -1,5 +1,4 @@
1
1
  export * from './file-manager.interface';
2
- export * from './file-upload-response.interface';
3
2
  export * from './folder.interface';
4
3
  export * from './storage-config.interface';
5
4
  export * from './storage-module-options.interface';
@@ -1,3 +1 @@
1
- /**
2
- * SFTP Configuration
3
- */ export { };
1
+ export { };
@@ -20,7 +20,12 @@ function _ts_decorate(decorators, target, key, desc) {
20
20
  function _ts_metadata(k, v) {
21
21
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
22
22
  }
23
- import { Injectable, Logger, NotFoundException } from '@nestjs/common';
23
+ function _ts_param(paramIndex, decorator) {
24
+ return function(target, key) {
25
+ decorator(target, key, paramIndex);
26
+ };
27
+ }
28
+ import { Injectable, Logger, NotFoundException, Inject } from '@nestjs/common';
24
29
  import { createReadStream, existsSync, statSync } from 'fs';
25
30
  import { join, resolve } from 'path';
26
31
  import * as mime from 'mime-types';
@@ -146,6 +151,7 @@ export class FileServeMiddleware {
146
151
  }
147
152
  FileServeMiddleware = _ts_decorate([
148
153
  Injectable(),
154
+ _ts_param(0, Inject(UploadService)),
149
155
  _ts_metadata("design:type", Function),
150
156
  _ts_metadata("design:paramtypes", [
151
157
  typeof UploadService === "undefined" ? Object : UploadService
@@ -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 (_error) {
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
  }
@@ -138,14 +175,23 @@ import { v4 as uuidv4 } from 'uuid';
138
175
  * Get the absolute path for a file key
139
176
  * Key now includes basePath, so resolve from cwd
140
177
  */ getAbsolutePath(key) {
141
- return path.resolve(key);
178
+ // SECURITY: Validate key format
179
+ this.validateKeyFormat(key);
180
+ const filePath = path.resolve(key);
181
+ // SECURITY: Validate path is within base directory
182
+ this.validatePathWithinBase(filePath);
183
+ return filePath;
142
184
  }
143
185
  /**
144
186
  * Check if a file exists
145
187
  */ async fileExists(key) {
146
188
  try {
189
+ // SECURITY: Validate key format
190
+ this.validateKeyFormat(key);
147
191
  // Key now includes basePath, resolve from cwd
148
192
  const filePath = path.resolve(key);
193
+ // SECURITY: Validate path is within base directory
194
+ this.validatePathWithinBase(filePath);
149
195
  await fs.access(filePath);
150
196
  return true;
151
197
  } catch {
@@ -155,8 +201,12 @@ import { v4 as uuidv4 } from 'uuid';
155
201
  /**
156
202
  * Get file stats
157
203
  */ async getFileStats(key) {
204
+ // SECURITY: Validate key format
205
+ this.validateKeyFormat(key);
158
206
  // Key now includes basePath, resolve from cwd
159
207
  const filePath = path.resolve(key);
208
+ // SECURITY: Validate path is within base directory
209
+ this.validatePathWithinBase(filePath);
160
210
  const stats = await fs.stat(filePath);
161
211
  return {
162
212
  size: stats.size,
@@ -79,8 +79,8 @@ export class StorageFactoryService {
79
79
  } catch (error) {
80
80
  this.logger.error(`Failed to create provider ${config.provider}:`, error);
81
81
  // Preserve original error message for better debugging
82
- const originalMessage = error?.message || 'Unknown error';
83
- throw new Error(`Failed to initialize storage provider '${config.provider}': ${originalMessage}`);
82
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
83
+ throw new Error(`Failed to initialize storage provider '${config.provider}': ${errorMessage}`);
84
84
  }
85
85
  }
86
86
  /**