@flusys/nestjs-storage 7.0.2 → 8.0.0-beta.1

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 (202) hide show
  1. package/config/index.d.ts +3 -3
  2. package/controllers/file-manager.controller.d.ts +4 -4
  3. package/controllers/folder.controller.d.ts +2 -2
  4. package/controllers/index.d.ts +4 -4
  5. package/controllers/storage-config.controller.d.ts +2 -2
  6. package/controllers/upload.controller.d.ts +3 -3
  7. package/docs/index.d.ts +1 -1
  8. package/docs/storage-swagger.config.d.ts +2 -2
  9. package/dtos/index.d.ts +4 -4
  10. package/entities/file-manager-with-company.entity.d.ts +2 -2
  11. package/entities/file-manager.entity.d.ts +1 -1
  12. package/entities/folder-with-company.entity.d.ts +1 -1
  13. package/entities/index.d.ts +15 -15
  14. package/entities/storage-config-with-company.entity.d.ts +1 -1
  15. package/enums/index.d.ts +1 -1
  16. package/fesm/493.js +2768 -0
  17. package/fesm/537.js +1157 -0
  18. package/fesm/562.js +786 -0
  19. package/fesm/{providers/azure-provider.optional.js → chunks/190.js} +33 -30
  20. package/fesm/chunks/252.js +12 -0
  21. package/fesm/chunks/327.js +12 -0
  22. package/fesm/chunks/385.js +12 -0
  23. package/fesm/{providers/s3-provider.optional.js → chunks/591.js} +37 -34
  24. package/fesm/chunks/746.js +12 -0
  25. package/fesm/chunks/798.js +12 -0
  26. package/fesm/{providers/sftp-provider.optional.js → chunks/918.js} +35 -31
  27. package/fesm/config/index.js +119 -3
  28. package/fesm/controllers/index.js +16 -4
  29. package/fesm/docs/index.js +206 -1
  30. package/fesm/dtos/index.js +879 -4
  31. package/fesm/entities/index.js +463 -18
  32. package/fesm/enums/index.js +36 -1
  33. package/fesm/index.js +398 -12
  34. package/fesm/interfaces/index.js +19 -5
  35. package/fesm/modules/index.js +16 -1
  36. package/fesm/providers/index.js +364 -3
  37. package/fesm/runtime.js +252 -0
  38. package/fesm/services/index.js +16 -6
  39. package/fesm/utils/index.js +149 -1
  40. package/index.d.ts +11 -11
  41. package/interfaces/file-manager.interface.d.ts +1 -1
  42. package/interfaces/folder.interface.d.ts +1 -1
  43. package/interfaces/index.d.ts +5 -5
  44. package/interfaces/storage-config.interface.d.ts +1 -1
  45. package/interfaces/storage-module-options.interface.d.ts +3 -3
  46. package/interfaces/storage-provider.interface.d.ts +1 -1
  47. package/middlewares/file-serve.middleware.d.ts +4 -3
  48. package/middlewares/index.d.ts +1 -1
  49. package/modules/index.d.ts +1 -1
  50. package/modules/storage.module.d.ts +2 -2
  51. package/package.json +30 -41
  52. package/providers/azure-provider.optional.d.ts +2 -2
  53. package/providers/index.d.ts +3 -3
  54. package/providers/local-provider.d.ts +2 -2
  55. package/providers/s3-provider.optional.d.ts +2 -2
  56. package/providers/sftp-provider.optional.d.ts +2 -2
  57. package/providers/storage-factory.service.d.ts +3 -3
  58. package/providers/storage-provider.registry.d.ts +2 -2
  59. package/services/file-manager.service.d.ts +9 -8
  60. package/services/folder.service.d.ts +6 -6
  61. package/services/index.d.ts +6 -6
  62. package/services/storage-config.service.d.ts +3 -3
  63. package/services/storage-datasource.provider.d.ts +3 -3
  64. package/services/storage-provider-config.service.d.ts +7 -7
  65. package/services/upload.service.d.ts +6 -6
  66. package/utils/image-compressor.util.d.ts +1 -1
  67. package/utils/index.d.ts +1 -1
  68. package/cjs/config/event-actions.js +0 -34
  69. package/cjs/config/index.js +0 -20
  70. package/cjs/config/message-keys.js +0 -46
  71. package/cjs/config/storage.constants.js +0 -28
  72. package/cjs/controllers/file-manager.controller.js +0 -152
  73. package/cjs/controllers/file-manager.controller.spec.js +0 -135
  74. package/cjs/controllers/folder.controller.js +0 -102
  75. package/cjs/controllers/folder.controller.spec.js +0 -86
  76. package/cjs/controllers/index.js +0 -21
  77. package/cjs/controllers/storage-config.controller.js +0 -102
  78. package/cjs/controllers/storage-config.controller.spec.js +0 -85
  79. package/cjs/controllers/upload.controller.js +0 -234
  80. package/cjs/controllers/upload.controller.spec.js +0 -132
  81. package/cjs/docs/index.js +0 -11
  82. package/cjs/docs/storage-swagger.config.js +0 -188
  83. package/cjs/docs/storage-swagger.config.spec.js +0 -72
  84. package/cjs/dtos/file-manager.dto.js +0 -327
  85. package/cjs/dtos/folder.dto.js +0 -105
  86. package/cjs/dtos/index.js +0 -21
  87. package/cjs/dtos/storage-config.dto.js +0 -170
  88. package/cjs/dtos/upload.dto.js +0 -272
  89. package/cjs/entities/file-manager-with-company.entity.js +0 -61
  90. package/cjs/entities/file-manager.entity.js +0 -122
  91. package/cjs/entities/folder-with-company.entity.js +0 -55
  92. package/cjs/entities/folder.entity.js +0 -64
  93. package/cjs/entities/index.js +0 -63
  94. package/cjs/entities/index.spec.js +0 -18
  95. package/cjs/entities/storage-config-with-company.entity.js +0 -51
  96. package/cjs/entities/storage-config.entity.js +0 -96
  97. package/cjs/enums/file-location.enum.js +0 -17
  98. package/cjs/enums/index.js +0 -18
  99. package/cjs/index.js +0 -29
  100. package/cjs/interfaces/file-manager.interface.js +0 -4
  101. package/cjs/interfaces/folder.interface.js +0 -4
  102. package/cjs/interfaces/index.js +0 -22
  103. package/cjs/interfaces/storage-config.interface.js +0 -4
  104. package/cjs/interfaces/storage-module-options.interface.js +0 -4
  105. package/cjs/interfaces/storage-provider.interface.js +0 -4
  106. package/cjs/middlewares/file-serve.middleware.js +0 -241
  107. package/cjs/middlewares/file-serve.middleware.spec.js +0 -211
  108. package/cjs/middlewares/index.js +0 -18
  109. package/cjs/modules/index.js +0 -18
  110. package/cjs/modules/storage.module.js +0 -165
  111. package/cjs/modules/storage.module.spec.js +0 -184
  112. package/cjs/providers/azure-provider.optional.js +0 -155
  113. package/cjs/providers/azure-provider.optional.spec.js +0 -228
  114. package/cjs/providers/index.js +0 -26
  115. package/cjs/providers/local-provider.js +0 -230
  116. package/cjs/providers/local-provider.spec.js +0 -239
  117. package/cjs/providers/s3-provider.optional.js +0 -175
  118. package/cjs/providers/s3-provider.optional.spec.js +0 -243
  119. package/cjs/providers/sftp-provider.optional.js +0 -203
  120. package/cjs/providers/sftp-provider.optional.spec.js +0 -217
  121. package/cjs/providers/storage-factory.service.js +0 -175
  122. package/cjs/providers/storage-factory.service.spec.js +0 -253
  123. package/cjs/providers/storage-provider.registry.js +0 -41
  124. package/cjs/providers/storage-provider.registry.spec.js +0 -67
  125. package/cjs/services/file-manager.service.js +0 -387
  126. package/cjs/services/file-manager.service.spec.js +0 -580
  127. package/cjs/services/folder.service.js +0 -101
  128. package/cjs/services/folder.service.spec.js +0 -167
  129. package/cjs/services/index.js +0 -23
  130. package/cjs/services/storage-config.service.js +0 -129
  131. package/cjs/services/storage-config.service.spec.js +0 -257
  132. package/cjs/services/storage-datasource.provider.js +0 -137
  133. package/cjs/services/storage-datasource.provider.spec.js +0 -217
  134. package/cjs/services/storage-provider-config.service.js +0 -171
  135. package/cjs/services/storage-provider-config.service.spec.js +0 -265
  136. package/cjs/services/upload.service.js +0 -376
  137. package/cjs/services/upload.service.spec.js +0 -426
  138. package/cjs/utils/file-validator.util.js +0 -440
  139. package/cjs/utils/file-validator.util.spec.js +0 -259
  140. package/cjs/utils/image-compressor.util.js +0 -162
  141. package/cjs/utils/image-compressor.util.spec.js +0 -166
  142. package/cjs/utils/index.js +0 -18
  143. package/fesm/config/event-actions.js +0 -13
  144. package/fesm/config/message-keys.js +0 -22
  145. package/fesm/config/storage.constants.js +0 -7
  146. package/fesm/controllers/file-manager.controller.js +0 -142
  147. package/fesm/controllers/file-manager.controller.spec.js +0 -131
  148. package/fesm/controllers/folder.controller.js +0 -92
  149. package/fesm/controllers/folder.controller.spec.js +0 -82
  150. package/fesm/controllers/storage-config.controller.js +0 -92
  151. package/fesm/controllers/storage-config.controller.spec.js +0 -81
  152. package/fesm/controllers/upload.controller.js +0 -224
  153. package/fesm/controllers/upload.controller.spec.js +0 -128
  154. package/fesm/docs/storage-swagger.config.js +0 -181
  155. package/fesm/docs/storage-swagger.config.spec.js +0 -68
  156. package/fesm/dtos/file-manager.dto.js +0 -300
  157. package/fesm/dtos/folder.dto.js +0 -84
  158. package/fesm/dtos/storage-config.dto.js +0 -149
  159. package/fesm/dtos/upload.dto.js +0 -245
  160. package/fesm/entities/file-manager-with-company.entity.js +0 -51
  161. package/fesm/entities/file-manager.entity.js +0 -112
  162. package/fesm/entities/folder-with-company.entity.js +0 -45
  163. package/fesm/entities/folder.entity.js +0 -54
  164. package/fesm/entities/index.spec.js +0 -14
  165. package/fesm/entities/storage-config-with-company.entity.js +0 -41
  166. package/fesm/entities/storage-config.entity.js +0 -86
  167. package/fesm/enums/file-location.enum.js +0 -7
  168. package/fesm/interfaces/file-manager.interface.js +0 -1
  169. package/fesm/interfaces/folder.interface.js +0 -1
  170. package/fesm/interfaces/storage-config.interface.js +0 -1
  171. package/fesm/interfaces/storage-module-options.interface.js +0 -3
  172. package/fesm/interfaces/storage-provider.interface.js +0 -4
  173. package/fesm/middlewares/file-serve.middleware.js +0 -190
  174. package/fesm/middlewares/file-serve.middleware.spec.js +0 -207
  175. package/fesm/middlewares/index.js +0 -1
  176. package/fesm/modules/storage.module.js +0 -156
  177. package/fesm/modules/storage.module.spec.js +0 -180
  178. package/fesm/providers/azure-provider.optional.spec.js +0 -224
  179. package/fesm/providers/local-provider.js +0 -182
  180. package/fesm/providers/local-provider.spec.js +0 -194
  181. package/fesm/providers/s3-provider.optional.spec.js +0 -239
  182. package/fesm/providers/sftp-provider.optional.spec.js +0 -213
  183. package/fesm/providers/storage-factory.service.js +0 -124
  184. package/fesm/providers/storage-factory.service.spec.js +0 -249
  185. package/fesm/providers/storage-provider.registry.js +0 -34
  186. package/fesm/providers/storage-provider.registry.spec.js +0 -63
  187. package/fesm/services/file-manager.service.js +0 -336
  188. package/fesm/services/file-manager.service.spec.js +0 -576
  189. package/fesm/services/folder.service.js +0 -91
  190. package/fesm/services/folder.service.spec.js +0 -163
  191. package/fesm/services/storage-config.service.js +0 -119
  192. package/fesm/services/storage-config.service.spec.js +0 -253
  193. package/fesm/services/storage-datasource.provider.js +0 -86
  194. package/fesm/services/storage-datasource.provider.spec.js +0 -172
  195. package/fesm/services/storage-provider-config.service.js +0 -161
  196. package/fesm/services/storage-provider-config.service.spec.js +0 -261
  197. package/fesm/services/upload.service.js +0 -366
  198. package/fesm/services/upload.service.spec.js +0 -422
  199. package/fesm/utils/file-validator.util.js +0 -433
  200. package/fesm/utils/file-validator.util.spec.js +0 -255
  201. package/fesm/utils/image-compressor.util.js +0 -114
  202. package/fesm/utils/image-compressor.util.spec.js +0 -162
@@ -1,182 +0,0 @@
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
- /**
15
- * LOCAL File System Provider
16
- *
17
- * This provider uses Node.js built-in 'fs' module - NO external dependencies required!
18
- * Perfect for same-server storage scenarios.
19
- *
20
- * Usage:
21
- * import { LocalProvider } from '@flusys/nestjs-storage/providers/local-provider';
22
- * import { StorageProviderRegistry, FileLocationEnum } from '@flusys/nestjs-storage';
23
- *
24
- * StorageProviderRegistry.register(FileLocationEnum.LOCAL, LocalProvider);
25
- */ import { BadRequestException } from '@nestjs/common';
26
- import { SYSTEM_MESSAGES } from '@flusys/nestjs-shared/constants';
27
- import { ImageCompressor } from '../utils/image-compressor.util';
28
- import * as fs from 'fs/promises';
29
- import * as path from 'path';
30
- import { v4 as uuidv4 } from 'uuid';
31
- /**
32
- * Local File System Storage Provider
33
- * Uses Node.js built-in fs module - no external dependencies
34
- */ export class LocalProvider {
35
- /**
36
- * SECURITY: Validates that a target path does not escape the base directory
37
- * Prevents path traversal attacks using ../ sequences
38
- * @throws Error if path traversal is detected
39
- */ validatePathWithinBase(targetPath) {
40
- const normalizedBasePath = path.resolve(this.basePath);
41
- const normalizedTargetPath = path.resolve(targetPath);
42
- if (!normalizedTargetPath.startsWith(normalizedBasePath + path.sep) && normalizedTargetPath !== normalizedBasePath) {
43
- throw new BadRequestException({
44
- message: 'Invalid path: Path traversal attempt detected',
45
- messageKey: SYSTEM_MESSAGES.PATH_TRAVERSAL_DETECTED
46
- });
47
- }
48
- }
49
- /**
50
- * SECURITY: Validates file key format to prevent malicious input
51
- * @throws Error if key contains suspicious patterns
52
- */ validateKeyFormat(key) {
53
- if (!key || typeof key !== 'string' || key.trim().length === 0) {
54
- throw new BadRequestException({
55
- message: 'Invalid file key: empty or invalid',
56
- messageKey: SYSTEM_MESSAGES.INVALID_FILE_KEY
57
- });
58
- }
59
- if (key.includes('\0')) {
60
- throw new BadRequestException({
61
- message: 'Invalid file key: contains null bytes',
62
- messageKey: SYSTEM_MESSAGES.INVALID_FILE_KEY
63
- });
64
- }
65
- }
66
- /**
67
- * Initialize Local File System provider with configuration
68
- * @param config.basePath - Base path for file storage (default: './uploads')
69
- * @param config.baseUrl - Optional base URL for generating file URLs
70
- */ async initialize(config) {
71
- // Store original relative path for key generation
72
- this.relativeBasePath = config?.basePath || './uploads';
73
- // Resolve to absolute path for file operations
74
- this.basePath = path.resolve(this.relativeBasePath);
75
- this.baseUrl = config?.baseUrl || '';
76
- // Ensure base directory exists
77
- await fs.mkdir(this.basePath, {
78
- recursive: true
79
- });
80
- }
81
- async uploadFile(file, options) {
82
- let processedBuffer = file.buffer;
83
- let contentType = file.mimetype;
84
- const fileName = `${uuidv4()}-${file.originalname}`;
85
- // Compress image if needed
86
- if (options.compress && file.mimetype.startsWith('image/')) {
87
- const result = await ImageCompressor.compress(file.buffer, file.mimetype, {
88
- maxWidth: options.maxWidth,
89
- maxHeight: options.maxHeight,
90
- quality: options.quality,
91
- format: options.format
92
- });
93
- processedBuffer = result.buffer;
94
- contentType = result.format;
95
- }
96
- // Build file path
97
- const folderPath = options.folderPath ? path.join(this.basePath, options.folderPath) : this.basePath;
98
- // SECURITY: Validate path does not escape base directory
99
- this.validatePathWithinBase(folderPath);
100
- const filePath = path.join(folderPath, fileName);
101
- // SECURITY: Double-check final file path
102
- this.validatePathWithinBase(filePath);
103
- // Ensure directory exists
104
- await fs.mkdir(folderPath, {
105
- recursive: true
106
- });
107
- // Write file to disk
108
- await fs.writeFile(filePath, processedBuffer);
109
- // Generate key that includes the base path (relative to cwd)
110
- // This makes the key self-contained for serving files
111
- const relativeKey = path.join(this.relativeBasePath, options.folderPath || '', fileName).replace(/\\/g, '/'); // Normalize path separators
112
- return {
113
- name: fileName,
114
- key: relativeKey,
115
- size: processedBuffer.length,
116
- contentType
117
- };
118
- }
119
- async uploadMultipleFiles(files, options) {
120
- return Promise.all(files.map((file)=>this.uploadFile(file, options)));
121
- }
122
- async deleteFile(key) {
123
- try {
124
- // SECURITY: Validate key format first
125
- this.validateKeyFormat(key);
126
- // Key now includes the basePath, resolve from cwd
127
- let filePath = path.resolve(key);
128
- // SECURITY: Validate resolved path is within base directory
129
- this.validatePathWithinBase(filePath);
130
- // Check if file exists at the resolved path
131
- try {
132
- await fs.access(filePath);
133
- } catch {
134
- // Fallback: try with basePath prefix (for old keys without basePath)
135
- const fallbackPath = path.join(this.basePath, key);
136
- // SECURITY: Validate fallback path as well
137
- this.validatePathWithinBase(fallbackPath);
138
- try {
139
- await fs.access(fallbackPath);
140
- filePath = fallbackPath;
141
- } catch {
142
- // File doesn't exist at either location - silent return
143
- return;
144
- }
145
- }
146
- await fs.unlink(filePath);
147
- } catch (error) {
148
- // Re-throw security errors
149
- if (error instanceof Error && error.message.includes('Invalid')) {
150
- throw error;
151
- }
152
- // Silent failure for other errors
153
- }
154
- }
155
- async deleteMultipleFiles(keys) {
156
- await Promise.all(keys.map((key)=>this.deleteFile(key)));
157
- }
158
- async generatePresignedUrl(key, _expiresInSeconds) {
159
- // Return public URL or relative path
160
- // Note: Local storage doesn't support true presigned URLs
161
- // If baseUrl is provided, return full URL with file serving endpoint, otherwise return relative path
162
- if (this.baseUrl) {
163
- // Construct URL with the file serving endpoint
164
- const baseUrlWithoutTrailingSlash = this.baseUrl.replace(/\/$/, '');
165
- return `${baseUrlWithoutTrailingSlash}/storage/upload/file/${key}`;
166
- }
167
- return `/storage/upload/file/${key}`;
168
- }
169
- async healthCheck() {
170
- try {
171
- await fs.access(this.basePath);
172
- return true;
173
- } catch {
174
- return false;
175
- }
176
- }
177
- constructor(){
178
- _define_property(this, "basePath", '');
179
- _define_property(this, "baseUrl", '');
180
- _define_property(this, "relativeBasePath", ''); // Path relative to cwd for key generation
181
- }
182
- }
@@ -1,194 +0,0 @@
1
- const mockMkdir = jest.fn();
2
- const mockWriteFile = jest.fn();
3
- const mockAccess = jest.fn();
4
- const mockUnlink = jest.fn();
5
- jest.mock('fs/promises', ()=>({
6
- mkdir: (...args)=>mockMkdir(...args),
7
- writeFile: (...args)=>mockWriteFile(...args),
8
- access: (...args)=>mockAccess(...args),
9
- unlink: (...args)=>mockUnlink(...args)
10
- }));
11
- jest.mock('../utils/image-compressor.util', ()=>({
12
- ImageCompressor: {
13
- compress: jest.fn()
14
- }
15
- }));
16
- import * as path from 'path';
17
- import { LocalProvider } from './local-provider';
18
- import { ImageCompressor } from '../utils/image-compressor.util';
19
- function buildMulterFile(overrides = {}) {
20
- return {
21
- fieldname: 'file',
22
- originalname: 'photo.jpg',
23
- encoding: '7bit',
24
- mimetype: 'image/jpeg',
25
- buffer: Buffer.from('file-bytes'),
26
- size: 10,
27
- stream: undefined,
28
- destination: '',
29
- filename: '',
30
- path: '',
31
- ...overrides
32
- };
33
- }
34
- describe('LocalProvider', ()=>{
35
- let provider;
36
- beforeEach(async ()=>{
37
- jest.clearAllMocks();
38
- mockMkdir.mockResolvedValue(undefined);
39
- mockWriteFile.mockResolvedValue(undefined);
40
- mockAccess.mockResolvedValue(undefined);
41
- mockUnlink.mockResolvedValue(undefined);
42
- provider = new LocalProvider();
43
- await provider.initialize({
44
- basePath: './uploads-test',
45
- baseUrl: 'http://localhost:2002'
46
- });
47
- });
48
- describe('initialize', ()=>{
49
- it('resolves the base path and ensures the directory exists', async ()=>{
50
- expect(mockMkdir).toHaveBeenCalledWith(path.resolve('./uploads-test'), expect.objectContaining({
51
- recursive: true
52
- }));
53
- });
54
- it('defaults basePath to ./uploads when not provided', async ()=>{
55
- mockMkdir.mockClear();
56
- const defaultProvider = new LocalProvider();
57
- await defaultProvider.initialize({});
58
- expect(mockMkdir).toHaveBeenCalledWith(path.resolve('./uploads'), expect.objectContaining({
59
- recursive: true
60
- }));
61
- });
62
- });
63
- describe('uploadFile', ()=>{
64
- it('writes the file to disk and returns metadata with a self-contained key', async ()=>{
65
- const file = buildMulterFile();
66
- const result = await provider.uploadFile(file, {});
67
- expect(mockWriteFile).toHaveBeenCalled();
68
- expect(result.name).toMatch(/photo\.jpg$/);
69
- expect(result.key).toContain('uploads-test');
70
- expect(result.size).toBe(file.buffer.length);
71
- expect(result.contentType).toBe('image/jpeg');
72
- });
73
- it('nests the file under folderPath when provided', async ()=>{
74
- const file = buildMulterFile();
75
- const result = await provider.uploadFile(file, {
76
- folderPath: 'avatars'
77
- });
78
- expect(result.key).toContain('avatars');
79
- expect(mockMkdir).toHaveBeenCalledWith(expect.stringContaining('avatars'), expect.objectContaining({
80
- recursive: true
81
- }));
82
- });
83
- it('compresses image files when options.compress is true', async ()=>{
84
- ImageCompressor.compress.mockResolvedValue({
85
- buffer: Buffer.from('compressed'),
86
- format: 'image/webp'
87
- });
88
- const file = buildMulterFile();
89
- const result = await provider.uploadFile(file, {
90
- compress: true,
91
- maxWidth: 800,
92
- maxHeight: 600,
93
- quality: 70
94
- });
95
- expect(ImageCompressor.compress).toHaveBeenCalledWith(file.buffer, 'image/jpeg', expect.objectContaining({
96
- maxWidth: 800,
97
- maxHeight: 600,
98
- quality: 70
99
- }));
100
- expect(result.contentType).toBe('image/webp');
101
- expect(result.size).toBe(Buffer.from('compressed').length);
102
- });
103
- it('does not compress non-image mimetypes even when compress=true', async ()=>{
104
- const file = buildMulterFile({
105
- mimetype: 'application/pdf',
106
- originalname: 'doc.pdf'
107
- });
108
- await provider.uploadFile(file, {
109
- compress: true
110
- });
111
- expect(ImageCompressor.compress).not.toHaveBeenCalled();
112
- });
113
- it('rejects a folderPath that attempts to escape the base directory via path traversal', async ()=>{
114
- const file = buildMulterFile();
115
- await expect(provider.uploadFile(file, {
116
- folderPath: '../../../etc'
117
- })).rejects.toThrow();
118
- });
119
- });
120
- describe('uploadMultipleFiles', ()=>{
121
- it('uploads all files and returns their metadata', async ()=>{
122
- const files = [
123
- buildMulterFile({
124
- originalname: 'a.jpg'
125
- }),
126
- buildMulterFile({
127
- originalname: 'b.jpg'
128
- })
129
- ];
130
- const results = await provider.uploadMultipleFiles(files, {});
131
- expect(results).toHaveLength(2);
132
- expect(mockWriteFile).toHaveBeenCalledTimes(2);
133
- });
134
- });
135
- describe('deleteFile', ()=>{
136
- it('deletes a file that exists at the resolved key path', async ()=>{
137
- const key = path.join('uploads-test', 'some-uuid-photo.jpg');
138
- await provider.deleteFile(key);
139
- expect(mockUnlink).toHaveBeenCalled();
140
- });
141
- it('falls back to basePath-prefixed lookup when the direct key path does not exist', async ()=>{
142
- mockAccess.mockRejectedValueOnce(new Error('ENOENT')).mockResolvedValueOnce(undefined);
143
- await provider.deleteFile('uploads-test/some-uuid-photo.jpg');
144
- expect(mockUnlink).toHaveBeenCalled();
145
- });
146
- it('silently returns (no throw, no unlink) when the file does not exist at either location', async ()=>{
147
- mockAccess.mockRejectedValue(new Error('ENOENT'));
148
- await expect(provider.deleteFile('uploads-test/missing-file.jpg')).resolves.toBeUndefined();
149
- expect(mockUnlink).not.toHaveBeenCalled();
150
- });
151
- it('rejects an empty key', async ()=>{
152
- await expect(provider.deleteFile('')).rejects.toThrow();
153
- });
154
- it('rejects a key containing a null byte', async ()=>{
155
- await expect(provider.deleteFile('evil\0.txt')).rejects.toThrow();
156
- });
157
- it('re-throws path traversal security errors rather than silently swallowing them', async ()=>{
158
- await expect(provider.deleteFile('../../../etc/passwd')).rejects.toThrow(/Invalid/);
159
- });
160
- });
161
- describe('deleteMultipleFiles', ()=>{
162
- it('deletes every provided key', async ()=>{
163
- await provider.deleteMultipleFiles([
164
- 'uploads-test/a.jpg',
165
- 'uploads-test/b.jpg'
166
- ]);
167
- expect(mockUnlink).toHaveBeenCalledTimes(2);
168
- });
169
- });
170
- describe('generatePresignedUrl', ()=>{
171
- it('builds a full URL using the configured baseUrl', async ()=>{
172
- const url = await provider.generatePresignedUrl('uploads-test/photo.jpg');
173
- expect(url).toBe('http://localhost:2002/storage/upload/file/uploads-test/photo.jpg');
174
- });
175
- it('returns a relative path when no baseUrl was configured', async ()=>{
176
- const noUrlProvider = new LocalProvider();
177
- await noUrlProvider.initialize({
178
- basePath: './uploads-test2'
179
- });
180
- const url = await noUrlProvider.generatePresignedUrl('uploads-test2/photo.jpg');
181
- expect(url).toBe('/storage/upload/file/uploads-test2/photo.jpg');
182
- });
183
- });
184
- describe('healthCheck', ()=>{
185
- it('returns true when the base path is accessible', async ()=>{
186
- mockAccess.mockResolvedValue(undefined);
187
- await expect(provider.healthCheck()).resolves.toBe(true);
188
- });
189
- it('returns false when the base path is not accessible', async ()=>{
190
- mockAccess.mockRejectedValue(new Error('ENOENT'));
191
- await expect(provider.healthCheck()).resolves.toBe(false);
192
- });
193
- });
194
- });
@@ -1,239 +0,0 @@
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
- const mockS3ClientSend = jest.fn();
15
- const mockS3ClientCtor = jest.fn().mockImplementation(()=>({
16
- send: mockS3ClientSend
17
- }));
18
- let MockDeleteObjectCommand = class MockDeleteObjectCommand {
19
- constructor(input){
20
- _define_property(this, "input", void 0);
21
- this.input = input;
22
- }
23
- };
24
- let MockDeleteObjectsCommand = class MockDeleteObjectsCommand {
25
- constructor(input){
26
- _define_property(this, "input", void 0);
27
- this.input = input;
28
- }
29
- };
30
- let MockGetObjectCommand = class MockGetObjectCommand {
31
- constructor(input){
32
- _define_property(this, "input", void 0);
33
- this.input = input;
34
- }
35
- };
36
- let MockHeadBucketCommand = class MockHeadBucketCommand {
37
- constructor(input){
38
- _define_property(this, "input", void 0);
39
- this.input = input;
40
- }
41
- };
42
- jest.mock('@aws-sdk/client-s3', ()=>({
43
- S3Client: mockS3ClientCtor,
44
- DeleteObjectCommand: MockDeleteObjectCommand,
45
- DeleteObjectsCommand: MockDeleteObjectsCommand,
46
- GetObjectCommand: MockGetObjectCommand,
47
- HeadBucketCommand: MockHeadBucketCommand
48
- }));
49
- const mockUploadDone = jest.fn();
50
- const mockUploadCtor = jest.fn().mockImplementation(()=>({
51
- done: mockUploadDone
52
- }));
53
- jest.mock('@aws-sdk/lib-storage', ()=>({
54
- Upload: mockUploadCtor
55
- }));
56
- const mockGetSignedUrl = jest.fn();
57
- jest.mock('@aws-sdk/s3-request-presigner', ()=>({
58
- getSignedUrl: (...args)=>mockGetSignedUrl(...args)
59
- }));
60
- jest.mock('../utils/image-compressor.util', ()=>({
61
- ImageCompressor: {
62
- compress: jest.fn()
63
- }
64
- }));
65
- import { S3Provider } from './s3-provider.optional';
66
- import { ImageCompressor } from '../utils/image-compressor.util';
67
- function buildMulterFile(overrides = {}) {
68
- return {
69
- fieldname: 'file',
70
- originalname: 'photo.jpg',
71
- encoding: '7bit',
72
- mimetype: 'image/jpeg',
73
- buffer: Buffer.from('file-bytes'),
74
- size: 10,
75
- stream: undefined,
76
- destination: '',
77
- filename: '',
78
- path: '',
79
- ...overrides
80
- };
81
- }
82
- describe('S3Provider', ()=>{
83
- let provider;
84
- beforeEach(async ()=>{
85
- jest.clearAllMocks();
86
- mockUploadDone.mockResolvedValue(undefined);
87
- provider = new S3Provider();
88
- await provider.initialize({
89
- region: 'us-east-1',
90
- bucket: 'my-bucket'
91
- });
92
- });
93
- describe('initialize', ()=>{
94
- it('creates an S3Client with region/endpoint/credentials', async ()=>{
95
- jest.clearAllMocks();
96
- const p = new S3Provider();
97
- await p.initialize({
98
- region: 'eu-west-1',
99
- bucket: 'bucket-2',
100
- accessKeyId: 'AKIA...',
101
- secretAccessKey: 'secret',
102
- endpoint: 'https://s3.custom.example.com'
103
- });
104
- expect(mockS3ClientCtor).toHaveBeenCalledWith(expect.objectContaining({
105
- region: 'eu-west-1',
106
- endpoint: 'https://s3.custom.example.com',
107
- credentials: {
108
- accessKeyId: 'AKIA...',
109
- secretAccessKey: 'secret'
110
- }
111
- }));
112
- });
113
- it('omits credentials when access key / secret are not provided', async ()=>{
114
- jest.clearAllMocks();
115
- const p = new S3Provider();
116
- await p.initialize({
117
- region: 'us-east-1',
118
- bucket: 'bucket-3'
119
- });
120
- expect(mockS3ClientCtor).toHaveBeenCalledWith(expect.objectContaining({
121
- credentials: undefined
122
- }));
123
- });
124
- it('throws InternalServerErrorException when the AWS SDK cannot be initialized', async ()=>{
125
- mockS3ClientCtor.mockImplementationOnce(()=>{
126
- throw new Error('module not found');
127
- });
128
- const brokenProvider = new S3Provider();
129
- await expect(brokenProvider.initialize({
130
- region: 'us-east-1',
131
- bucket: 'x'
132
- })).rejects.toThrow('AWS SDK not found');
133
- });
134
- });
135
- describe('uploadFile', ()=>{
136
- it('uploads via the S3 Upload helper and returns file metadata', async ()=>{
137
- const file = buildMulterFile();
138
- const result = await provider.uploadFile(file, {});
139
- expect(mockUploadCtor).toHaveBeenCalledWith(expect.objectContaining({
140
- params: expect.objectContaining({
141
- Bucket: 'my-bucket',
142
- ContentType: 'image/jpeg'
143
- })
144
- }));
145
- expect(mockUploadDone).toHaveBeenCalled();
146
- expect(result.name).toMatch(/photo\.jpg$/);
147
- expect(result.key).toBe(result.name);
148
- });
149
- it('prefixes the key with folderPath when provided', async ()=>{
150
- const file = buildMulterFile();
151
- const result = await provider.uploadFile(file, {
152
- folderPath: 'avatars'
153
- });
154
- expect(result.key).toMatch(/^avatars\//);
155
- });
156
- it('compresses image files before upload when options.compress is true', async ()=>{
157
- ImageCompressor.compress.mockResolvedValue({
158
- buffer: Buffer.from('compressed'),
159
- format: 'image/webp'
160
- });
161
- const file = buildMulterFile();
162
- const result = await provider.uploadFile(file, {
163
- compress: true
164
- });
165
- expect(ImageCompressor.compress).toHaveBeenCalled();
166
- expect(result.contentType).toBe('image/webp');
167
- });
168
- });
169
- describe('uploadMultipleFiles', ()=>{
170
- it('uploads each file and returns metadata for all', async ()=>{
171
- const files = [
172
- buildMulterFile({
173
- originalname: 'a.jpg'
174
- }),
175
- buildMulterFile({
176
- originalname: 'b.jpg'
177
- })
178
- ];
179
- const results = await provider.uploadMultipleFiles(files, {});
180
- expect(results).toHaveLength(2);
181
- expect(mockUploadCtor).toHaveBeenCalledTimes(2);
182
- });
183
- });
184
- describe('deleteFile', ()=>{
185
- it('sends a DeleteObjectCommand with bucket and key', async ()=>{
186
- await provider.deleteFile('some/key.jpg');
187
- expect(mockS3ClientSend).toHaveBeenCalledWith(expect.any(MockDeleteObjectCommand));
188
- const sentCommand = mockS3ClientSend.mock.calls[0][0];
189
- expect(sentCommand.input).toEqual({
190
- Bucket: 'my-bucket',
191
- Key: 'some/key.jpg'
192
- });
193
- });
194
- });
195
- describe('deleteMultipleFiles', ()=>{
196
- it('sends a DeleteObjectsCommand containing all keys', async ()=>{
197
- await provider.deleteMultipleFiles([
198
- 'a.jpg',
199
- 'b.jpg'
200
- ]);
201
- const sentCommand = mockS3ClientSend.mock.calls[0][0];
202
- expect(sentCommand.input.Delete.Objects).toEqual([
203
- {
204
- Key: 'a.jpg'
205
- },
206
- {
207
- Key: 'b.jpg'
208
- }
209
- ]);
210
- });
211
- });
212
- describe('generatePresignedUrl', ()=>{
213
- it('delegates to getSignedUrl with the configured expiry', async ()=>{
214
- mockGetSignedUrl.mockResolvedValue('https://signed.example.com/some/key.jpg');
215
- const url = await provider.generatePresignedUrl('some/key.jpg', 900);
216
- expect(mockGetSignedUrl).toHaveBeenCalledWith(expect.anything(), expect.any(MockGetObjectCommand), {
217
- expiresIn: 900
218
- });
219
- expect(url).toBe('https://signed.example.com/some/key.jpg');
220
- });
221
- it('defaults expiry to 3600 seconds when not provided', async ()=>{
222
- mockGetSignedUrl.mockResolvedValue('https://signed.example.com/x');
223
- await provider.generatePresignedUrl('x');
224
- expect(mockGetSignedUrl).toHaveBeenCalledWith(expect.anything(), expect.anything(), {
225
- expiresIn: 3600
226
- });
227
- });
228
- });
229
- describe('healthCheck', ()=>{
230
- it('returns true when HeadBucketCommand succeeds', async ()=>{
231
- mockS3ClientSend.mockResolvedValue({});
232
- await expect(provider.healthCheck()).resolves.toBe(true);
233
- });
234
- it('returns false when HeadBucketCommand rejects (bucket missing/unauthorized)', async ()=>{
235
- mockS3ClientSend.mockRejectedValue(new Error('Forbidden'));
236
- await expect(provider.healthCheck()).resolves.toBe(false);
237
- });
238
- });
239
- });