@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.
- package/README.md +3 -7
- package/cjs/config/storage-config.service.js +31 -0
- package/cjs/controllers/file-manager.controller.js +8 -6
- package/cjs/controllers/folder.controller.js +3 -4
- package/cjs/controllers/storage-config.controller.js +3 -4
- package/cjs/controllers/upload.controller.js +22 -169
- package/cjs/dtos/file-manager.dto.js +36 -2
- package/cjs/dtos/upload.dto.js +16 -0
- package/cjs/middlewares/file-serve.middleware.js +204 -0
- package/cjs/middlewares/index.js +18 -0
- package/cjs/modules/storage.module.js +58 -14
- package/cjs/providers/azure-provider.optional.js +1 -2
- package/cjs/providers/local-provider.js +43 -11
- package/cjs/providers/storage-factory.service.js +49 -5
- package/cjs/services/file-manager.service.js +134 -9
- package/cjs/services/folder.service.js +17 -48
- package/cjs/services/storage-datasource.provider.js +10 -16
- package/cjs/services/storage-provider-config.service.js +26 -32
- package/cjs/services/upload.service.js +135 -24
- package/cjs/utils/image-compressor.util.js +43 -5
- package/config/storage-config.service.d.ts +2 -0
- package/controllers/file-manager.controller.d.ts +1 -1
- package/controllers/upload.controller.d.ts +5 -4
- package/dtos/file-manager.dto.d.ts +4 -0
- package/dtos/upload.dto.d.ts +2 -0
- package/fesm/config/storage-config.service.js +31 -0
- package/fesm/controllers/file-manager.controller.js +8 -6
- package/fesm/controllers/folder.controller.js +5 -6
- package/fesm/controllers/storage-config.controller.js +5 -6
- package/fesm/controllers/upload.controller.js +25 -131
- package/fesm/dtos/file-manager.dto.js +36 -2
- package/fesm/dtos/upload.dto.js +16 -0
- package/fesm/middlewares/file-serve.middleware.js +153 -0
- package/fesm/middlewares/index.js +1 -0
- package/fesm/modules/storage.module.js +60 -16
- package/fesm/providers/azure-provider.optional.js +1 -2
- package/fesm/providers/local-provider.js +43 -11
- package/fesm/providers/storage-factory.service.js +50 -6
- package/fesm/services/file-manager.service.js +134 -9
- package/fesm/services/folder.service.js +18 -49
- package/fesm/services/storage-datasource.provider.js +10 -16
- package/fesm/services/storage-provider-config.service.js +26 -32
- package/fesm/services/upload.service.js +135 -24
- package/fesm/utils/image-compressor.util.js +3 -1
- package/interfaces/file-manager.interface.d.ts +2 -0
- package/interfaces/storage-module-options.interface.d.ts +2 -0
- package/interfaces/storage-provider.interface.d.ts +2 -0
- package/middlewares/file-serve.middleware.d.ts +9 -0
- package/middlewares/index.d.ts +1 -0
- package/modules/storage.module.d.ts +3 -2
- package/package.json +26 -11
- package/providers/local-provider.d.ts +2 -1
- package/providers/storage-factory.service.d.ts +4 -0
- package/services/file-manager.service.d.ts +7 -1
- package/services/folder.service.d.ts +1 -2
- package/services/storage-provider-config.service.d.ts +2 -2
- 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
|
|
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,
|
|
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,
|
|
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
|
|
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(
|
|
48
|
-
name:
|
|
49
|
-
key:
|
|
50
|
-
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
|
-
|
|
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, "
|
|
88
|
+
_define_property(this, "storageConfigService", void 0);
|
|
89
|
+
_define_property(this, "storageFactoryService", void 0);
|
|
158
90
|
this.uploadService = uploadService;
|
|
159
|
-
this.
|
|
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: '
|
|
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);
|
package/fesm/dtos/upload.dto.js
CHANGED
|
@@ -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 {
|
|
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
|
-
//
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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 (
|
|
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,
|