@flusys/nestjs-storage 1.0.0-rc → 1.0.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.
- package/README.md +44 -1
- package/cjs/config/index.js +0 -1
- package/cjs/config/storage.constants.js +0 -9
- package/cjs/controllers/upload.controller.js +12 -17
- package/cjs/docs/storage-swagger.config.js +24 -136
- package/cjs/dtos/file-manager.dto.js +65 -32
- package/cjs/dtos/folder.dto.js +15 -9
- package/cjs/dtos/storage-config.dto.js +5 -86
- package/cjs/dtos/upload.dto.js +17 -17
- package/cjs/entities/file-manager-with-company.entity.js +3 -4
- package/cjs/entities/file-manager.entity.js +71 -3
- package/cjs/entities/folder-with-company.entity.js +3 -4
- package/cjs/entities/folder.entity.js +19 -3
- package/cjs/entities/index.js +9 -10
- package/cjs/entities/storage-config-with-company.entity.js +3 -4
- package/cjs/entities/storage-config.entity.js +73 -3
- package/cjs/middlewares/file-serve.middleware.js +107 -100
- package/cjs/modules/storage.module.js +82 -136
- package/cjs/providers/azure-provider.optional.js +10 -38
- package/cjs/providers/local-provider.js +0 -43
- package/cjs/providers/s3-provider.optional.js +19 -40
- package/cjs/providers/storage-factory.service.js +54 -99
- package/cjs/providers/storage-provider.registry.js +8 -18
- package/cjs/services/file-manager.service.js +239 -337
- package/cjs/services/folder.service.js +3 -3
- package/cjs/services/index.js +1 -0
- package/cjs/{config → services}/storage-config.service.js +30 -79
- package/cjs/services/storage-datasource.provider.js +16 -26
- package/cjs/services/storage-provider-config.service.js +3 -3
- package/cjs/services/upload.service.js +33 -61
- package/cjs/utils/file-validator.util.js +54 -66
- package/cjs/utils/image-compressor.util.js +2 -5
- package/config/index.d.ts +0 -1
- package/config/storage.constants.d.ts +0 -6
- package/controllers/upload.controller.d.ts +1 -0
- package/dtos/file-manager.dto.d.ts +11 -3
- package/dtos/folder.dto.d.ts +3 -1
- package/dtos/storage-config.dto.d.ts +7 -11
- package/entities/file-manager-with-company.entity.d.ts +2 -2
- package/entities/file-manager.entity.d.ts +11 -2
- package/entities/folder-with-company.entity.d.ts +2 -2
- package/entities/folder.entity.d.ts +4 -2
- package/entities/index.d.ts +3 -4
- package/entities/storage-config-with-company.entity.d.ts +2 -2
- package/entities/storage-config.entity.d.ts +7 -2
- package/fesm/config/index.js +0 -1
- package/fesm/config/storage.constants.js +0 -6
- package/fesm/controllers/upload.controller.js +12 -17
- package/fesm/docs/storage-swagger.config.js +27 -142
- package/fesm/dtos/file-manager.dto.js +66 -33
- package/fesm/dtos/folder.dto.js +16 -10
- package/fesm/dtos/storage-config.dto.js +7 -88
- package/fesm/dtos/upload.dto.js +17 -18
- package/fesm/entities/file-manager-with-company.entity.js +3 -4
- package/fesm/entities/file-manager.entity.js +72 -4
- package/fesm/entities/folder-with-company.entity.js +3 -4
- package/fesm/entities/folder.entity.js +20 -4
- package/fesm/entities/index.js +4 -8
- package/fesm/entities/storage-config-with-company.entity.js +3 -4
- package/fesm/entities/storage-config.entity.js +74 -4
- package/fesm/middlewares/file-serve.middleware.js +107 -100
- package/fesm/modules/storage.module.js +83 -136
- package/fesm/providers/azure-provider.optional.js +14 -45
- package/fesm/providers/local-provider.js +0 -43
- package/fesm/providers/s3-provider.optional.js +23 -47
- package/fesm/providers/storage-factory.service.js +52 -97
- package/fesm/providers/storage-provider.registry.js +10 -20
- package/fesm/services/file-manager.service.js +237 -335
- package/fesm/services/folder.service.js +1 -1
- package/fesm/services/index.js +1 -0
- package/fesm/{config → services}/storage-config.service.js +30 -79
- package/fesm/services/storage-datasource.provider.js +16 -26
- package/fesm/services/storage-provider-config.service.js +1 -1
- package/fesm/services/upload.service.js +31 -59
- package/fesm/utils/file-validator.util.js +54 -66
- package/fesm/utils/image-compressor.util.js +2 -5
- package/interfaces/storage-config.interface.d.ts +1 -2
- package/interfaces/storage-module-options.interface.d.ts +0 -5
- package/middlewares/file-serve.middleware.d.ts +9 -1
- package/modules/storage.module.d.ts +1 -2
- package/package.json +3 -3
- package/providers/azure-provider.optional.d.ts +8 -6
- package/providers/local-provider.d.ts +0 -7
- package/providers/s3-provider.optional.d.ts +9 -7
- package/providers/storage-factory.service.d.ts +8 -9
- package/providers/storage-provider.registry.d.ts +4 -4
- package/services/file-manager.service.d.ts +21 -14
- package/services/folder.service.d.ts +4 -4
- package/services/index.d.ts +1 -0
- package/{config → services}/storage-config.service.d.ts +9 -10
- package/services/storage-datasource.provider.d.ts +3 -4
- package/services/storage-provider-config.service.d.ts +5 -6
- package/services/upload.service.d.ts +5 -5
- package/utils/file-validator.util.d.ts +3 -0
- package/cjs/entities/file-manager-base.entity.js +0 -115
- package/cjs/entities/folder-base.entity.js +0 -55
- package/cjs/entities/storage-config-base.entity.js +0 -93
- package/entities/file-manager-base.entity.d.ts +0 -13
- package/entities/folder-base.entity.d.ts +0 -5
- package/entities/storage-config-base.entity.d.ts +0 -9
- package/fesm/entities/file-manager-base.entity.js +0 -108
- package/fesm/entities/folder-base.entity.js +0 -48
- package/fesm/entities/storage-config-base.entity.js +0 -83
|
@@ -30,123 +30,130 @@ import { createReadStream, existsSync, statSync } from 'fs';
|
|
|
30
30
|
import { join, resolve } from 'path';
|
|
31
31
|
import * as mime from 'mime-types';
|
|
32
32
|
import { UploadService } from '../services/upload.service';
|
|
33
|
+
/** MIME type prefixes that can be displayed inline in browser */ const VIEWABLE_TYPE_PREFIXES = [
|
|
34
|
+
'image/',
|
|
35
|
+
'video/',
|
|
36
|
+
'audio/',
|
|
37
|
+
'text/',
|
|
38
|
+
'application/pdf',
|
|
39
|
+
'application/json',
|
|
40
|
+
'application/xml'
|
|
41
|
+
];
|
|
33
42
|
export class FileServeMiddleware {
|
|
34
43
|
async use(req, res, next) {
|
|
35
44
|
try {
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
const urlPath = req.path || req.url;
|
|
39
|
-
const match = urlPath.match(/\/storage\/upload\/file\/(.+)/);
|
|
40
|
-
const normalizedPath = match ? match[1] : '';
|
|
41
|
-
if (!normalizedPath) {
|
|
42
|
-
throw new NotFoundException('File path is required');
|
|
43
|
-
}
|
|
44
|
-
this.logger.debug(`Attempting to serve file, normalizedPath: ${normalizedPath}`);
|
|
45
|
-
// Get the local storage basePath for fallback lookups
|
|
46
|
-
const basePath = await this.uploadService.getLocalStorageBasePath();
|
|
47
|
-
const normalizedBasePath = basePath ? basePath.replace(/^\.\//, '').replace(/\/$/, '') : null;
|
|
48
|
-
// Strategy 1: Try path relative to CWD (new format: key includes basePath)
|
|
49
|
-
let fullPath = join(uploadDir, normalizedPath);
|
|
50
|
-
let resolvedPath = resolve(fullPath);
|
|
51
|
-
this.logger.debug(`Strategy 1 - CWD relative: ${fullPath}`);
|
|
52
|
-
// Validate path is inside the project directory (prevent path traversal)
|
|
53
|
-
if (!resolvedPath.startsWith(uploadDir)) {
|
|
54
|
-
throw new NotFoundException('Invalid file path');
|
|
55
|
-
}
|
|
56
|
-
// Check if file exists
|
|
57
|
-
if (!existsSync(fullPath)) {
|
|
58
|
-
// Strategy 2: If key already starts with basePath, try using basePath directly
|
|
59
|
-
if (basePath && normalizedBasePath) {
|
|
60
|
-
const pathStartsWithBase = normalizedPath.startsWith(normalizedBasePath + '/') || normalizedPath.startsWith(normalizedBasePath);
|
|
61
|
-
if (pathStartsWithBase) {
|
|
62
|
-
const remainingPath = normalizedPath.substring(normalizedBasePath.length).replace(/^\//, '');
|
|
63
|
-
const fallbackPath = join(basePath, remainingPath);
|
|
64
|
-
this.logger.debug(`Strategy 2 - basePath + remaining: ${fallbackPath}`);
|
|
65
|
-
if (existsSync(fallbackPath)) {
|
|
66
|
-
fullPath = fallbackPath;
|
|
67
|
-
resolvedPath = resolve(fullPath);
|
|
68
|
-
}
|
|
69
|
-
} else {
|
|
70
|
-
// Old format: key doesn't include basePath, prepend it
|
|
71
|
-
const fallbackPath = join(basePath, normalizedPath);
|
|
72
|
-
this.logger.debug(`Strategy 3 - basePath + full key (old format): ${fallbackPath}`);
|
|
73
|
-
if (existsSync(fallbackPath)) {
|
|
74
|
-
fullPath = fallbackPath;
|
|
75
|
-
resolvedPath = resolve(fullPath);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
// Final check if file was found
|
|
80
|
-
if (!existsSync(fullPath)) {
|
|
81
|
-
throw new NotFoundException(`File not found: ${normalizedPath}`);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
45
|
+
const normalizedPath = this.extractFilePath(req);
|
|
46
|
+
const fullPath = await this.resolveFilePath(normalizedPath);
|
|
84
47
|
const stats = statSync(fullPath);
|
|
85
48
|
if (!stats.isFile()) {
|
|
86
49
|
throw new NotFoundException(`Not a file: ${normalizedPath}`);
|
|
87
50
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if (fullPath.toLowerCase().endsWith('.svg')) {
|
|
92
|
-
mimeType = 'image/svg+xml';
|
|
93
|
-
}
|
|
94
|
-
// Determine if file should be displayed inline or downloaded
|
|
95
|
-
const viewableTypes = [
|
|
96
|
-
'image/',
|
|
97
|
-
'video/',
|
|
98
|
-
'audio/',
|
|
99
|
-
'text/',
|
|
100
|
-
'application/pdf',
|
|
101
|
-
'application/json',
|
|
102
|
-
'application/xml'
|
|
103
|
-
];
|
|
104
|
-
const isViewable = viewableTypes.some((type)=>mimeType.startsWith(type));
|
|
105
|
-
const contentDisposition = isViewable ? 'inline' : `attachment; filename="${encodeURIComponent(normalizedPath.split('/').pop() || 'download')}"`;
|
|
106
|
-
// Set headers
|
|
107
|
-
res.set({
|
|
108
|
-
'Content-Type': mimeType,
|
|
109
|
-
'Content-Length': stats.size,
|
|
110
|
-
'Content-Disposition': contentDisposition,
|
|
111
|
-
'Cache-Control': 'public, max-age=3600',
|
|
112
|
-
'Accept-Ranges': 'bytes',
|
|
113
|
-
'Cross-Origin-Resource-Policy': 'cross-origin',
|
|
114
|
-
'Access-Control-Allow-Origin': '*',
|
|
115
|
-
'X-Content-Type-Options': 'nosniff'
|
|
116
|
-
});
|
|
117
|
-
// Stream the file
|
|
118
|
-
const stream = createReadStream(fullPath);
|
|
119
|
-
stream.pipe(res);
|
|
120
|
-
stream.on('error', (err)=>{
|
|
121
|
-
this.logger.error('File stream error', err);
|
|
122
|
-
if (!res.headersSent) {
|
|
123
|
-
res.status(404).json({
|
|
124
|
-
message: `Failed to serve file: ${normalizedPath}`,
|
|
125
|
-
error: 'Not Found',
|
|
126
|
-
statusCode: 404
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
});
|
|
130
|
-
// Handle client disconnection
|
|
131
|
-
res.on('close', ()=>{
|
|
132
|
-
stream.destroy();
|
|
133
|
-
});
|
|
51
|
+
const mimeType = this.getMimeType(fullPath);
|
|
52
|
+
this.setResponseHeaders(res, stats.size, mimeType, normalizedPath);
|
|
53
|
+
this.streamFile(res, fullPath, normalizedPath);
|
|
134
54
|
} catch (error) {
|
|
135
|
-
this.
|
|
55
|
+
this.sendErrorResponse(res, error);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** Extract and validate file path from request URL */ extractFilePath(req) {
|
|
59
|
+
const urlPath = req.path || req.url;
|
|
60
|
+
const match = urlPath.match(/\/storage\/upload\/file\/(.+)/);
|
|
61
|
+
const normalizedPath = match ? match[1] : '';
|
|
62
|
+
if (!normalizedPath) {
|
|
63
|
+
throw new NotFoundException('File path is required');
|
|
64
|
+
}
|
|
65
|
+
this.logger.debug(`Attempting to serve file, normalizedPath: ${normalizedPath}`);
|
|
66
|
+
return normalizedPath;
|
|
67
|
+
}
|
|
68
|
+
/** Resolve file path using multiple fallback strategies */ async resolveFilePath(normalizedPath) {
|
|
69
|
+
// Strategy 1: Path relative to CWD (new format: key includes basePath)
|
|
70
|
+
let fullPath = join(this.uploadDir, normalizedPath);
|
|
71
|
+
const resolvedPath = resolve(fullPath);
|
|
72
|
+
this.logger.debug(`Strategy 1 - CWD relative: ${fullPath}`);
|
|
73
|
+
// Prevent path traversal attacks
|
|
74
|
+
if (!resolvedPath.startsWith(this.uploadDir)) {
|
|
75
|
+
throw new NotFoundException('Invalid file path');
|
|
76
|
+
}
|
|
77
|
+
if (existsSync(fullPath)) {
|
|
78
|
+
return fullPath;
|
|
79
|
+
}
|
|
80
|
+
// Try fallback strategies with basePath
|
|
81
|
+
const basePath = await this.uploadService.getLocalStorageBasePath();
|
|
82
|
+
if (basePath) {
|
|
83
|
+
const fallbackPath = this.tryFallbackPaths(normalizedPath, basePath);
|
|
84
|
+
if (fallbackPath) {
|
|
85
|
+
return fallbackPath;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
throw new NotFoundException(`File not found: ${normalizedPath}`);
|
|
89
|
+
}
|
|
90
|
+
/** Try basePath-based fallback strategies */ tryFallbackPaths(normalizedPath, basePath) {
|
|
91
|
+
const normalizedBasePath = basePath.replace(/^\.\//, '').replace(/\/$/, '');
|
|
92
|
+
const pathStartsWithBase = normalizedPath.startsWith(normalizedBasePath + '/') || normalizedPath.startsWith(normalizedBasePath);
|
|
93
|
+
if (pathStartsWithBase) {
|
|
94
|
+
// Strategy 2: basePath + remaining path
|
|
95
|
+
const remainingPath = normalizedPath.substring(normalizedBasePath.length).replace(/^\//, '');
|
|
96
|
+
const fallbackPath = join(basePath, remainingPath);
|
|
97
|
+
this.logger.debug(`Strategy 2 - basePath + remaining: ${fallbackPath}`);
|
|
98
|
+
if (existsSync(fallbackPath)) return fallbackPath;
|
|
99
|
+
} else {
|
|
100
|
+
// Strategy 3: Old format - prepend basePath
|
|
101
|
+
const fallbackPath = join(basePath, normalizedPath);
|
|
102
|
+
this.logger.debug(`Strategy 3 - basePath + full key (old format): ${fallbackPath}`);
|
|
103
|
+
if (existsSync(fallbackPath)) return fallbackPath;
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
/** Get MIME type with SVG special handling */ getMimeType(fullPath) {
|
|
108
|
+
if (fullPath.toLowerCase().endsWith('.svg')) {
|
|
109
|
+
return 'image/svg+xml';
|
|
110
|
+
}
|
|
111
|
+
return mime.lookup(fullPath) || 'application/octet-stream';
|
|
112
|
+
}
|
|
113
|
+
/** Set response headers for file serving */ setResponseHeaders(res, size, mimeType, normalizedPath) {
|
|
114
|
+
const isViewable = VIEWABLE_TYPE_PREFIXES.some((type)=>mimeType.startsWith(type));
|
|
115
|
+
const filename = normalizedPath.split('/').pop() || 'download';
|
|
116
|
+
const contentDisposition = isViewable ? 'inline' : `attachment; filename="${encodeURIComponent(filename)}"`;
|
|
117
|
+
res.set({
|
|
118
|
+
'Content-Type': mimeType,
|
|
119
|
+
'Content-Length': size,
|
|
120
|
+
'Content-Disposition': contentDisposition,
|
|
121
|
+
'Cache-Control': 'public, max-age=3600',
|
|
122
|
+
'Accept-Ranges': 'bytes',
|
|
123
|
+
'Cross-Origin-Resource-Policy': 'cross-origin',
|
|
124
|
+
'Access-Control-Allow-Origin': '*',
|
|
125
|
+
'X-Content-Type-Options': 'nosniff'
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
/** Stream file to response with error handling */ streamFile(res, fullPath, normalizedPath) {
|
|
129
|
+
const stream = createReadStream(fullPath);
|
|
130
|
+
stream.pipe(res);
|
|
131
|
+
stream.on('error', (err)=>{
|
|
132
|
+
this.logger.error('File stream error', err);
|
|
136
133
|
if (!res.headersSent) {
|
|
137
|
-
|
|
138
|
-
message: error instanceof NotFoundException ? error.message : `File not found`,
|
|
139
|
-
error: 'Not Found',
|
|
140
|
-
statusCode: 404
|
|
141
|
-
});
|
|
134
|
+
this.sendErrorResponse(res, new NotFoundException(`Failed to serve file: ${normalizedPath}`));
|
|
142
135
|
}
|
|
136
|
+
});
|
|
137
|
+
res.on('close', ()=>stream.destroy());
|
|
138
|
+
}
|
|
139
|
+
/** Send consistent error response */ sendErrorResponse(res, error) {
|
|
140
|
+
this.logger.error('File retrieval error:', error);
|
|
141
|
+
if (!res.headersSent) {
|
|
142
|
+
const message = error instanceof NotFoundException ? error.message : 'File not found';
|
|
143
|
+
res.status(404).json({
|
|
144
|
+
message,
|
|
145
|
+
error: 'Not Found',
|
|
146
|
+
statusCode: 404
|
|
147
|
+
});
|
|
143
148
|
}
|
|
144
149
|
}
|
|
145
150
|
constructor(uploadService){
|
|
146
151
|
_define_property(this, "uploadService", void 0);
|
|
147
152
|
_define_property(this, "logger", void 0);
|
|
153
|
+
_define_property(this, "uploadDir", void 0);
|
|
148
154
|
this.uploadService = uploadService;
|
|
149
155
|
this.logger = new Logger(FileServeMiddleware.name);
|
|
156
|
+
this.uploadDir = process.cwd();
|
|
150
157
|
}
|
|
151
158
|
}
|
|
152
159
|
FileServeMiddleware = _ts_decorate([
|
|
@@ -5,9 +5,9 @@ function _ts_decorate(decorators, target, key, desc) {
|
|
|
5
5
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
6
|
}
|
|
7
7
|
import { CacheModule, UtilsModule } from '@flusys/nestjs-shared/modules';
|
|
8
|
-
import {
|
|
8
|
+
import { Logger, Module, RequestMethod } from '@nestjs/common';
|
|
9
9
|
import { FileServeMiddleware } from '../middlewares';
|
|
10
|
-
import { StorageConfigService } from '../
|
|
10
|
+
import { StorageConfigService } from '../services';
|
|
11
11
|
import { STORAGE_MODULE_OPTIONS } from '../config/storage.constants';
|
|
12
12
|
import { FileManagerController, FolderController, StorageConfigController, UploadController } from '../controllers';
|
|
13
13
|
import { FileLocationEnum } from '../enums/file-location.enum';
|
|
@@ -15,48 +15,62 @@ import { StorageFactoryService, StorageProviderRegistry } from '../providers';
|
|
|
15
15
|
import { LocalProvider } from '../providers/local-provider';
|
|
16
16
|
import { FileManagerService, FolderService, StorageDataSourceProvider, UploadService } from '../services';
|
|
17
17
|
import { StorageProviderConfigService } from '../services/storage-provider-config.service';
|
|
18
|
-
//
|
|
18
|
+
// ─── Module Constants ─────────────────────────────────────────────────────────
|
|
19
19
|
const logger = new Logger('StorageModule');
|
|
20
|
-
|
|
20
|
+
const CONTROLLERS = [
|
|
21
|
+
FileManagerController,
|
|
22
|
+
FolderController,
|
|
23
|
+
StorageConfigController,
|
|
24
|
+
UploadController
|
|
25
|
+
];
|
|
26
|
+
const EXPORTED_SERVICES = [
|
|
27
|
+
StorageConfigService,
|
|
28
|
+
StorageDataSourceProvider,
|
|
29
|
+
FileManagerService,
|
|
30
|
+
FolderService,
|
|
31
|
+
StorageProviderConfigService,
|
|
32
|
+
UploadService,
|
|
33
|
+
StorageFactoryService
|
|
34
|
+
];
|
|
35
|
+
// ─── Provider Registration ────────────────────────────────────────────────────
|
|
21
36
|
StorageProviderRegistry.register(FileLocationEnum.LOCAL, LocalProvider);
|
|
22
37
|
logger.log('Registered LocalProvider');
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
38
|
+
const OPTIONAL_PROVIDERS = [
|
|
39
|
+
{
|
|
40
|
+
location: FileLocationEnum.AWS,
|
|
41
|
+
path: '../providers/s3-provider.optional',
|
|
42
|
+
name: 'S3Provider',
|
|
43
|
+
dep: '@aws-sdk/client-s3'
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
location: FileLocationEnum.AZURE,
|
|
47
|
+
path: '../providers/azure-provider.optional',
|
|
48
|
+
name: 'AzureProvider',
|
|
49
|
+
dep: '@azure/storage-blob'
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
location: FileLocationEnum.SFTP,
|
|
53
|
+
path: '../providers/sftp-provider.optional',
|
|
54
|
+
name: 'SftpProvider',
|
|
55
|
+
dep: 'ssh2-sftp-client'
|
|
56
|
+
}
|
|
57
|
+
];
|
|
58
|
+
for (const { location, path, name, dep } of OPTIONAL_PROVIDERS){
|
|
59
|
+
try {
|
|
60
|
+
StorageProviderRegistry.register(location, require(path)[name]);
|
|
61
|
+
logger.log(`Registered ${name}`);
|
|
62
|
+
} catch {
|
|
63
|
+
logger.debug(`${name} not available (install ${dep} to enable)`);
|
|
64
|
+
}
|
|
44
65
|
}
|
|
45
66
|
export class StorageModule {
|
|
46
|
-
|
|
47
|
-
* Configure middleware for file serving
|
|
48
|
-
* This bypasses path-to-regexp issues with wildcard routes
|
|
49
|
-
*/ configure(consumer) {
|
|
67
|
+
configure(consumer) {
|
|
50
68
|
consumer.apply(FileServeMiddleware).forRoutes({
|
|
51
69
|
path: 'storage/upload/file/*',
|
|
52
70
|
method: RequestMethod.GET
|
|
53
71
|
});
|
|
54
72
|
}
|
|
55
|
-
|
|
56
|
-
* Register StorageModule synchronously
|
|
57
|
-
*/ static forRoot(options) {
|
|
58
|
-
const controllers = this.getControllers(options);
|
|
59
|
-
const providers = this.getProviders(options);
|
|
73
|
+
static forRoot(options) {
|
|
60
74
|
return {
|
|
61
75
|
module: StorageModule,
|
|
62
76
|
global: options.global ?? false,
|
|
@@ -64,24 +78,12 @@ export class StorageModule {
|
|
|
64
78
|
CacheModule,
|
|
65
79
|
UtilsModule
|
|
66
80
|
],
|
|
67
|
-
controllers: options.includeController !== false ?
|
|
68
|
-
providers,
|
|
69
|
-
exports:
|
|
70
|
-
StorageConfigService,
|
|
71
|
-
StorageDataSourceProvider,
|
|
72
|
-
FileManagerService,
|
|
73
|
-
FolderService,
|
|
74
|
-
StorageProviderConfigService,
|
|
75
|
-
UploadService,
|
|
76
|
-
StorageFactoryService
|
|
77
|
-
]
|
|
81
|
+
controllers: options.includeController !== false ? CONTROLLERS : [],
|
|
82
|
+
providers: this.buildProviders(options),
|
|
83
|
+
exports: EXPORTED_SERVICES
|
|
78
84
|
};
|
|
79
85
|
}
|
|
80
|
-
|
|
81
|
-
* Register StorageModule asynchronously
|
|
82
|
-
*/ static forRootAsync(options) {
|
|
83
|
-
const controllers = this.getControllers(options);
|
|
84
|
-
const asyncProviders = this.createAsyncProviders(options);
|
|
86
|
+
static forRootAsync(options) {
|
|
85
87
|
return {
|
|
86
88
|
module: StorageModule,
|
|
87
89
|
global: options.global ?? false,
|
|
@@ -90,53 +92,21 @@ export class StorageModule {
|
|
|
90
92
|
CacheModule,
|
|
91
93
|
UtilsModule
|
|
92
94
|
],
|
|
93
|
-
controllers: options.includeController !== false ?
|
|
94
|
-
// Pass false to exclude STORAGE_MODULE_OPTIONS - it's already in asyncProviders
|
|
95
|
+
controllers: options.includeController !== false ? CONTROLLERS : [],
|
|
95
96
|
providers: [
|
|
96
|
-
...
|
|
97
|
-
...this.
|
|
97
|
+
...this.createAsyncProviders(options),
|
|
98
|
+
...this.buildProviders()
|
|
98
99
|
],
|
|
99
|
-
exports:
|
|
100
|
-
StorageConfigService,
|
|
101
|
-
StorageDataSourceProvider,
|
|
102
|
-
FileManagerService,
|
|
103
|
-
FolderService,
|
|
104
|
-
StorageProviderConfigService,
|
|
105
|
-
UploadService,
|
|
106
|
-
StorageFactoryService
|
|
107
|
-
]
|
|
100
|
+
exports: EXPORTED_SERVICES
|
|
108
101
|
};
|
|
109
102
|
}
|
|
110
|
-
// Private
|
|
111
|
-
|
|
112
|
-
return [
|
|
113
|
-
FileManagerController,
|
|
114
|
-
FolderController,
|
|
115
|
-
StorageConfigController,
|
|
116
|
-
UploadController
|
|
117
|
-
];
|
|
118
|
-
}
|
|
119
|
-
/**
|
|
120
|
-
* NOTE: Repository providers removed - services now use StorageDataSourceProvider directly
|
|
121
|
-
* This ensures dynamic entity loading based on runtime configuration
|
|
122
|
-
*/ /**
|
|
123
|
-
* Get providers (all providers always loaded)
|
|
124
|
-
* @param options Module options
|
|
125
|
-
* @param includeOptionsProvider Whether to include the STORAGE_MODULE_OPTIONS provider (false for async registration)
|
|
126
|
-
*/ static getProviders(options, includeOptionsProvider = true) {
|
|
103
|
+
// ─── Private Helpers ──────────────────────────────────────────────────────────
|
|
104
|
+
static buildProviders(options) {
|
|
127
105
|
const providers = [
|
|
128
|
-
|
|
129
|
-
StorageDataSourceProvider,
|
|
130
|
-
FileManagerService,
|
|
131
|
-
FolderService,
|
|
132
|
-
StorageProviderConfigService,
|
|
133
|
-
UploadService,
|
|
134
|
-
StorageFactoryService,
|
|
106
|
+
...EXPORTED_SERVICES,
|
|
135
107
|
FileServeMiddleware
|
|
136
108
|
];
|
|
137
|
-
|
|
138
|
-
// For async registration, createAsyncProviders handles it
|
|
139
|
-
if (includeOptionsProvider) {
|
|
109
|
+
if (options) {
|
|
140
110
|
providers.unshift({
|
|
141
111
|
provide: STORAGE_MODULE_OPTIONS,
|
|
142
112
|
useValue: options
|
|
@@ -144,63 +114,40 @@ export class StorageModule {
|
|
|
144
114
|
}
|
|
145
115
|
return providers;
|
|
146
116
|
}
|
|
147
|
-
|
|
148
|
-
* Create async providers for forRootAsync
|
|
149
|
-
*/ static createAsyncProviders(options) {
|
|
117
|
+
static createAsyncProviders(options) {
|
|
150
118
|
if (options.useFactory) {
|
|
151
119
|
return [
|
|
152
120
|
{
|
|
153
121
|
provide: STORAGE_MODULE_OPTIONS,
|
|
154
|
-
useFactory: async (...args)=>{
|
|
155
|
-
const config = await options.useFactory(...args);
|
|
156
|
-
return {
|
|
122
|
+
useFactory: async (...args)=>({
|
|
157
123
|
...options,
|
|
158
|
-
config
|
|
159
|
-
}
|
|
160
|
-
},
|
|
124
|
+
config: await options.useFactory(...args)
|
|
125
|
+
}),
|
|
161
126
|
inject: options.inject || []
|
|
162
127
|
}
|
|
163
128
|
];
|
|
164
129
|
}
|
|
130
|
+
const factoryClass = options.useClass || options.useExisting;
|
|
131
|
+
if (!factoryClass) return [];
|
|
132
|
+
const providers = [
|
|
133
|
+
{
|
|
134
|
+
provide: STORAGE_MODULE_OPTIONS,
|
|
135
|
+
useFactory: async (factory)=>({
|
|
136
|
+
...options,
|
|
137
|
+
config: await factory.createStorageOptions()
|
|
138
|
+
}),
|
|
139
|
+
inject: [
|
|
140
|
+
factoryClass
|
|
141
|
+
]
|
|
142
|
+
}
|
|
143
|
+
];
|
|
165
144
|
if (options.useClass) {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const config = await optionsFactory.createStorageOptions();
|
|
171
|
-
return {
|
|
172
|
-
...options,
|
|
173
|
-
config
|
|
174
|
-
};
|
|
175
|
-
},
|
|
176
|
-
inject: [
|
|
177
|
-
options.useClass
|
|
178
|
-
]
|
|
179
|
-
},
|
|
180
|
-
{
|
|
181
|
-
provide: options.useClass,
|
|
182
|
-
useClass: options.useClass
|
|
183
|
-
}
|
|
184
|
-
];
|
|
185
|
-
}
|
|
186
|
-
if (options.useExisting) {
|
|
187
|
-
return [
|
|
188
|
-
{
|
|
189
|
-
provide: STORAGE_MODULE_OPTIONS,
|
|
190
|
-
useFactory: async (optionsFactory)=>{
|
|
191
|
-
const config = await optionsFactory.createStorageOptions();
|
|
192
|
-
return {
|
|
193
|
-
...options,
|
|
194
|
-
config
|
|
195
|
-
};
|
|
196
|
-
},
|
|
197
|
-
inject: [
|
|
198
|
-
options.useExisting
|
|
199
|
-
]
|
|
200
|
-
}
|
|
201
|
-
];
|
|
145
|
+
providers.push({
|
|
146
|
+
provide: options.useClass,
|
|
147
|
+
useClass: options.useClass
|
|
148
|
+
});
|
|
202
149
|
}
|
|
203
|
-
return
|
|
150
|
+
return providers;
|
|
204
151
|
}
|
|
205
152
|
}
|
|
206
153
|
StorageModule = _ts_decorate([
|
|
@@ -1,18 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* OPTIONAL Azure Blob Storage Provider
|
|
3
|
-
*
|
|
4
|
-
* This provider requires @azure/storage-blob package to be installed.
|
|
5
|
-
* Only import this if you need Azure Blob storage.
|
|
6
|
-
*
|
|
7
|
-
* Installation:
|
|
8
|
-
* npm install @azure/storage-blob
|
|
9
|
-
*
|
|
10
|
-
* Usage:
|
|
11
|
-
* import { AzureProvider } from '@flusys/nestjs-storage/providers/azure-provider.optional';
|
|
12
|
-
* import { StorageProviderRegistry, FileLocationEnum } from '@flusys/nestjs-storage';
|
|
13
|
-
*
|
|
14
|
-
* StorageProviderRegistry.register(FileLocationEnum.AZURE, AzureProvider);
|
|
15
|
-
*/ function _define_property(obj, key, value) {
|
|
1
|
+
function _define_property(obj, key, value) {
|
|
16
2
|
if (key in obj) {
|
|
17
3
|
Object.defineProperty(obj, key, {
|
|
18
4
|
value: value,
|
|
@@ -25,44 +11,30 @@
|
|
|
25
11
|
}
|
|
26
12
|
return obj;
|
|
27
13
|
}
|
|
28
|
-
import { ImageCompressor } from '../utils/image-compressor.util';
|
|
29
|
-
import { Logger } from '@nestjs/common';
|
|
30
|
-
import { v4 as uuidv4 } from 'uuid';
|
|
31
14
|
/**
|
|
32
|
-
* Azure Blob Storage Provider
|
|
33
|
-
* Requires @azure/storage-blob
|
|
34
|
-
*/
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
15
|
+
* Optional Azure Blob Storage Provider
|
|
16
|
+
* Requires: npm install @azure/storage-blob
|
|
17
|
+
*/ import { Logger } from '@nestjs/common';
|
|
18
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
19
|
+
import { ImageCompressor } from '../utils/image-compressor.util';
|
|
20
|
+
export class AzureProvider {
|
|
21
|
+
async initialize(config) {
|
|
38
22
|
try {
|
|
39
|
-
|
|
40
|
-
const { BlobServiceClient } = await import('@azure/storage-blob');
|
|
23
|
+
const { BlobServiceClient, StorageSharedKeyCredential } = await import('@azure/storage-blob');
|
|
41
24
|
this.accountName = config.accountName;
|
|
42
25
|
this.containerName = config.containerName;
|
|
43
|
-
|
|
44
|
-
if (config.connectionString) {
|
|
45
|
-
this.blobServiceClient = BlobServiceClient.fromConnectionString(config.connectionString);
|
|
46
|
-
} else {
|
|
47
|
-
const { StorageSharedKeyCredential } = await import('@azure/storage-blob');
|
|
48
|
-
const sharedKeyCredential = new StorageSharedKeyCredential(config.accountName, config.accountKey);
|
|
49
|
-
this.blobServiceClient = new BlobServiceClient(`https://${config.accountName}.blob.core.windows.net`, sharedKeyCredential);
|
|
50
|
-
}
|
|
51
|
-
// Get container client
|
|
26
|
+
this.blobServiceClient = config.connectionString ? BlobServiceClient.fromConnectionString(config.connectionString) : new BlobServiceClient(`https://${config.accountName}.blob.core.windows.net`, new StorageSharedKeyCredential(config.accountName, config.accountKey));
|
|
52
27
|
this.containerClient = this.blobServiceClient.getContainerClient(config.containerName);
|
|
53
|
-
// Ensure container exists
|
|
54
28
|
await this.containerClient.createIfNotExists();
|
|
55
29
|
this.logger.log(`Azure Provider initialized: account=${config.accountName}, container=${config.containerName}`);
|
|
56
|
-
} catch
|
|
57
|
-
|
|
58
|
-
throw new Error('Azure Storage SDK not found. Install it with: npm install @azure/storage-blob');
|
|
30
|
+
} catch {
|
|
31
|
+
throw new Error('Azure Storage SDK not found. Install: npm install @azure/storage-blob');
|
|
59
32
|
}
|
|
60
33
|
}
|
|
61
34
|
async uploadFile(file, options) {
|
|
62
35
|
let processedBuffer = file.buffer;
|
|
63
36
|
let contentType = file.mimetype;
|
|
64
37
|
const fileName = `${uuidv4()}-${file.originalname}`;
|
|
65
|
-
// Compress image if needed
|
|
66
38
|
if (options.compress && file.mimetype.startsWith('image/')) {
|
|
67
39
|
const result = await ImageCompressor.compress(file.buffer, file.mimetype, {
|
|
68
40
|
maxWidth: options.maxWidth,
|
|
@@ -74,9 +46,7 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
74
46
|
contentType = result.format;
|
|
75
47
|
}
|
|
76
48
|
const blobName = options.folderPath ? `${options.folderPath}/${fileName}` : fileName;
|
|
77
|
-
|
|
78
|
-
// Upload to Azure
|
|
79
|
-
await blockBlobClient.upload(processedBuffer, processedBuffer.length, {
|
|
49
|
+
await this.containerClient.getBlockBlobClient(blobName).upload(processedBuffer, processedBuffer.length, {
|
|
80
50
|
blobHTTPHeaders: {
|
|
81
51
|
blobContentType: contentType
|
|
82
52
|
}
|
|
@@ -92,8 +62,7 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
92
62
|
return Promise.all(files.map((file)=>this.uploadFile(file, options)));
|
|
93
63
|
}
|
|
94
64
|
async deleteFile(key) {
|
|
95
|
-
|
|
96
|
-
await blockBlobClient.deleteIfExists();
|
|
65
|
+
await this.containerClient.getBlockBlobClient(key).deleteIfExists();
|
|
97
66
|
this.logger.log(`Deleted file from Azure: ${key}`);
|
|
98
67
|
}
|
|
99
68
|
async deleteMultipleFiles(keys) {
|
|
@@ -171,49 +171,6 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
171
171
|
return false;
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
|
-
/**
|
|
175
|
-
* Get the absolute path for a file key
|
|
176
|
-
* Key now includes basePath, so resolve from cwd
|
|
177
|
-
*/ getAbsolutePath(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;
|
|
184
|
-
}
|
|
185
|
-
/**
|
|
186
|
-
* Check if a file exists
|
|
187
|
-
*/ async fileExists(key) {
|
|
188
|
-
try {
|
|
189
|
-
// SECURITY: Validate key format
|
|
190
|
-
this.validateKeyFormat(key);
|
|
191
|
-
// Key now includes basePath, resolve from cwd
|
|
192
|
-
const filePath = path.resolve(key);
|
|
193
|
-
// SECURITY: Validate path is within base directory
|
|
194
|
-
this.validatePathWithinBase(filePath);
|
|
195
|
-
await fs.access(filePath);
|
|
196
|
-
return true;
|
|
197
|
-
} catch {
|
|
198
|
-
return false;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
/**
|
|
202
|
-
* Get file stats
|
|
203
|
-
*/ async getFileStats(key) {
|
|
204
|
-
// SECURITY: Validate key format
|
|
205
|
-
this.validateKeyFormat(key);
|
|
206
|
-
// Key now includes basePath, resolve from cwd
|
|
207
|
-
const filePath = path.resolve(key);
|
|
208
|
-
// SECURITY: Validate path is within base directory
|
|
209
|
-
this.validatePathWithinBase(filePath);
|
|
210
|
-
const stats = await fs.stat(filePath);
|
|
211
|
-
return {
|
|
212
|
-
size: stats.size,
|
|
213
|
-
createdAt: stats.birthtime,
|
|
214
|
-
modifiedAt: stats.mtime
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
174
|
constructor(){
|
|
218
175
|
_define_property(this, "logger", new Logger(LocalProvider.name));
|
|
219
176
|
_define_property(this, "basePath", '');
|