@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
|
@@ -29,7 +29,7 @@ import { RequestScopedApiService, HybridCache } from '@flusys/nestjs-shared/clas
|
|
|
29
29
|
import { UtilsService } from '@flusys/nestjs-shared/modules';
|
|
30
30
|
import { applyCompanyFilter } from '@flusys/nestjs-shared/utils';
|
|
31
31
|
import { Inject, Injectable, Scope } from '@nestjs/common';
|
|
32
|
-
import { StorageConfigService } from '
|
|
32
|
+
import { StorageConfigService } from './storage-config.service';
|
|
33
33
|
import { Folder, FolderWithCompany } from '../entities';
|
|
34
34
|
import { StorageDataSourceProvider } from './storage-datasource.provider';
|
|
35
35
|
export class FolderService extends RequestScopedApiService {
|
package/fesm/services/index.js
CHANGED
|
@@ -27,120 +27,71 @@ function _ts_param(paramIndex, decorator) {
|
|
|
27
27
|
}
|
|
28
28
|
import { Inject, Injectable } from '@nestjs/common';
|
|
29
29
|
import { StorageModuleOptions } from '../interfaces';
|
|
30
|
-
import { STORAGE_MODULE_OPTIONS, DEFAULT_MAX_FILE_SIZE, DEFAULT_ALLOWED_FILE_TYPES } from '
|
|
30
|
+
import { STORAGE_MODULE_OPTIONS, DEFAULT_MAX_FILE_SIZE, DEFAULT_ALLOWED_FILE_TYPES } from '../config/storage.constants';
|
|
31
|
+
const BYTES_PER_MB = 1024 * 1024;
|
|
32
|
+
const DEFAULT_LOCAL_PATH = './uploads';
|
|
33
|
+
const DEFAULT_PORT = '3000';
|
|
31
34
|
export class StorageConfigService {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
*/ isCompanyFeatureEnabled() {
|
|
35
|
+
// ─── IModuleConfigService Implementation ────────────────────────────────────
|
|
36
|
+
isCompanyFeatureEnabled() {
|
|
35
37
|
return this.options.bootstrapAppConfig?.enableCompanyFeature ?? false;
|
|
36
38
|
}
|
|
37
|
-
|
|
38
|
-
* Get database mode
|
|
39
|
-
*/ getDatabaseMode() {
|
|
39
|
+
getDatabaseMode() {
|
|
40
40
|
return this.options.bootstrapAppConfig?.databaseMode ?? 'single';
|
|
41
41
|
}
|
|
42
|
-
|
|
43
|
-
* Check if running in multi-tenant mode
|
|
44
|
-
*/ isMultiTenant() {
|
|
42
|
+
isMultiTenant() {
|
|
45
43
|
return this.getDatabaseMode() === 'multi-tenant';
|
|
46
44
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
*/ getMaxFileSize() {
|
|
45
|
+
// ─── Config Getters ─────────────────────────────────────────────────────────
|
|
46
|
+
getMaxFileSize() {
|
|
50
47
|
return this.options.config?.maxFileSize ?? DEFAULT_MAX_FILE_SIZE;
|
|
51
48
|
}
|
|
52
|
-
|
|
53
|
-
* Get allowed file types (MIME types or patterns)
|
|
54
|
-
*/ getAllowedFileTypes() {
|
|
49
|
+
getAllowedFileTypes() {
|
|
55
50
|
return this.options.config?.allowedFileTypes ?? DEFAULT_ALLOWED_FILE_TYPES;
|
|
56
51
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
52
|
+
getOptions() {
|
|
53
|
+
return this.options;
|
|
54
|
+
}
|
|
55
|
+
getDefaultLocalStoragePath() {
|
|
56
|
+
return this.options.config?.localStoragePath ?? DEFAULT_LOCAL_PATH;
|
|
57
|
+
}
|
|
58
|
+
getAppUrl() {
|
|
59
|
+
return this.options.config?.appUrl ?? process.env.APP_URL ?? `http://localhost:${process.env.PORT ?? DEFAULT_PORT}`;
|
|
60
|
+
}
|
|
61
|
+
// ─── Validation Methods ─────────────────────────────────────────────────────
|
|
62
|
+
isFileTypeAllowed(mimeType) {
|
|
60
63
|
const allowedTypes = this.getAllowedFileTypes();
|
|
61
|
-
// If wildcard, allow all
|
|
62
64
|
if (allowedTypes.includes('*/*')) {
|
|
63
65
|
return true;
|
|
64
66
|
}
|
|
65
|
-
|
|
66
|
-
return allowedTypes.some((allowedType)=>{
|
|
67
|
-
if (allowedType.endsWith('/*')) {
|
|
68
|
-
const prefix = allowedType.slice(0, -2);
|
|
69
|
-
return mimeType.startsWith(prefix);
|
|
70
|
-
}
|
|
71
|
-
return allowedType === mimeType;
|
|
72
|
-
});
|
|
67
|
+
return allowedTypes.some((type)=>type.endsWith('/*') ? mimeType.startsWith(type.slice(0, -2)) : type === mimeType);
|
|
73
68
|
}
|
|
74
|
-
|
|
75
|
-
* Validate file size
|
|
76
|
-
*/ validateFileSize(fileSize) {
|
|
69
|
+
validateFileSize(fileSize) {
|
|
77
70
|
const maxSize = this.getMaxFileSize();
|
|
78
71
|
if (fileSize > maxSize) {
|
|
79
|
-
const maxSizeMB = (maxSize / (1024 * 1024)).toFixed(2);
|
|
80
|
-
const fileSizeMB = (fileSize / (1024 * 1024)).toFixed(2);
|
|
81
72
|
return {
|
|
82
73
|
valid: false,
|
|
83
|
-
message: `File size (${
|
|
74
|
+
message: `File size (${this.toMB(fileSize)}MB) exceeds maximum ${this.toMB(maxSize)}MB`
|
|
84
75
|
};
|
|
85
76
|
}
|
|
86
77
|
return {
|
|
87
78
|
valid: true
|
|
88
79
|
};
|
|
89
80
|
}
|
|
90
|
-
|
|
91
|
-
* Validate file type
|
|
92
|
-
*/ validateFileType(mimeType) {
|
|
81
|
+
validateFileType(mimeType) {
|
|
93
82
|
if (!this.isFileTypeAllowed(mimeType)) {
|
|
94
|
-
const allowedTypes = this.getAllowedFileTypes();
|
|
95
83
|
return {
|
|
96
84
|
valid: false,
|
|
97
|
-
message: `File type "${mimeType}"
|
|
85
|
+
message: `File type "${mimeType}" not allowed. Allowed: ${this.getAllowedFileTypes().join(', ')}`
|
|
98
86
|
};
|
|
99
87
|
}
|
|
100
88
|
return {
|
|
101
89
|
valid: true
|
|
102
90
|
};
|
|
103
91
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
return this.options.config?.defaultDatabaseConfig;
|
|
108
|
-
}
|
|
109
|
-
/**
|
|
110
|
-
* Get all options
|
|
111
|
-
*/ getOptions() {
|
|
112
|
-
return this.options;
|
|
113
|
-
}
|
|
114
|
-
/**
|
|
115
|
-
* Get default local storage base path
|
|
116
|
-
* Used for serving local files without database lookup
|
|
117
|
-
* Falls back to './uploads' if not configured
|
|
118
|
-
*/ getDefaultLocalStoragePath() {
|
|
119
|
-
// Check if localStoragePath is configured in module options
|
|
120
|
-
const configuredPath = this.options.config?.localStoragePath;
|
|
121
|
-
if (configuredPath) {
|
|
122
|
-
return configuredPath;
|
|
123
|
-
}
|
|
124
|
-
// Default to ./uploads in the project root
|
|
125
|
-
return './uploads';
|
|
126
|
-
}
|
|
127
|
-
/**
|
|
128
|
-
* Get application base URL for generating file URLs
|
|
129
|
-
* - First tries module config
|
|
130
|
-
* - Falls back to APP_URL env var
|
|
131
|
-
* - Finally constructs from PORT env var
|
|
132
|
-
*/ getAppUrl() {
|
|
133
|
-
// Try from module config first
|
|
134
|
-
if (this.options.config?.appUrl) {
|
|
135
|
-
return this.options.config.appUrl;
|
|
136
|
-
}
|
|
137
|
-
// Fallback: read directly from environment
|
|
138
|
-
if (process.env.APP_URL) {
|
|
139
|
-
return process.env.APP_URL;
|
|
140
|
-
}
|
|
141
|
-
// Last resort: construct from PORT
|
|
142
|
-
const port = process.env.PORT || '3000';
|
|
143
|
-
return `http://localhost:${port}`;
|
|
92
|
+
// ─── Private Helpers ────────────────────────────────────────────────────────
|
|
93
|
+
toMB(bytes) {
|
|
94
|
+
return (bytes / BYTES_PER_MB).toFixed(2);
|
|
144
95
|
}
|
|
145
96
|
constructor(options){
|
|
146
97
|
_define_property(this, "options", void 0);
|
|
@@ -29,8 +29,7 @@ import { MultiTenantDataSourceService } from '@flusys/nestjs-shared/modules';
|
|
|
29
29
|
import { Inject, Injectable, Logger, Optional, Scope } from '@nestjs/common';
|
|
30
30
|
import { REQUEST } from '@nestjs/core';
|
|
31
31
|
import { Request } from 'express';
|
|
32
|
-
import {
|
|
33
|
-
import { STORAGE_MODULE_OPTIONS } from '../config/storage.constants';
|
|
32
|
+
import { StorageConfigService } from './storage-config.service';
|
|
34
33
|
export class StorageDataSourceProvider extends MultiTenantDataSourceService {
|
|
35
34
|
// Factory Methods
|
|
36
35
|
/** Build parent options from StorageModuleOptions */ static buildParentOptions(options) {
|
|
@@ -42,14 +41,11 @@ export class StorageDataSourceProvider extends MultiTenantDataSourceService {
|
|
|
42
41
|
};
|
|
43
42
|
}
|
|
44
43
|
// Feature Flags
|
|
45
|
-
/** Get global enable company feature flag */ getEnableCompanyFeature() {
|
|
46
|
-
return this.storageOptions.bootstrapAppConfig?.enableCompanyFeature ?? false;
|
|
47
|
-
}
|
|
48
44
|
/**
|
|
49
45
|
* Get enable company feature for specific tenant
|
|
50
46
|
* Falls back to global setting if not specified per-tenant
|
|
51
47
|
*/ getEnableCompanyFeatureForTenant(tenant) {
|
|
52
|
-
return tenant?.enableCompanyFeature ?? this.
|
|
48
|
+
return tenant?.enableCompanyFeature ?? this.configService.isCompanyFeatureEnabled();
|
|
53
49
|
}
|
|
54
50
|
/**
|
|
55
51
|
* Get enable company feature for current request context
|
|
@@ -62,22 +58,16 @@ export class StorageDataSourceProvider extends MultiTenantDataSourceService {
|
|
|
62
58
|
* For TypeORM repositories, we always use the base entities (FileManager, etc.)
|
|
63
59
|
* but for migrations, we need the correct entity based on the feature flag.
|
|
64
60
|
*/ async getStorageEntities(enableCompanyFeature) {
|
|
65
|
-
const enable = enableCompanyFeature ?? this.
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
];
|
|
76
|
-
}
|
|
77
|
-
return [
|
|
78
|
-
FileManager,
|
|
79
|
-
Folder,
|
|
80
|
-
StorageConfig
|
|
61
|
+
const enable = enableCompanyFeature ?? this.configService.isCompanyFeatureEnabled();
|
|
62
|
+
const entities = await import('../entities');
|
|
63
|
+
return enable ? [
|
|
64
|
+
entities.FileManagerWithCompany,
|
|
65
|
+
entities.FolderWithCompany,
|
|
66
|
+
entities.StorageConfigWithCompany
|
|
67
|
+
] : [
|
|
68
|
+
entities.FileManager,
|
|
69
|
+
entities.Folder,
|
|
70
|
+
entities.StorageConfig
|
|
81
71
|
];
|
|
82
72
|
}
|
|
83
73
|
// Overrides
|
|
@@ -138,8 +128,8 @@ export class StorageDataSourceProvider extends MultiTenantDataSourceService {
|
|
|
138
128
|
StorageDataSourceProvider.connectionLocks.delete(tenant.id);
|
|
139
129
|
}
|
|
140
130
|
}
|
|
141
|
-
constructor(
|
|
142
|
-
super(StorageDataSourceProvider.buildParentOptions(
|
|
131
|
+
constructor(configService, request){
|
|
132
|
+
super(StorageDataSourceProvider.buildParentOptions(configService.getOptions()), request), _define_property(this, "configService", void 0), _define_property(this, "logger", void 0), this.configService = configService, this.logger = new Logger(StorageDataSourceProvider.name);
|
|
143
133
|
}
|
|
144
134
|
}
|
|
145
135
|
// Override parent's static properties to have Storage-specific cache
|
|
@@ -153,12 +143,12 @@ StorageDataSourceProvider = _ts_decorate([
|
|
|
153
143
|
Injectable({
|
|
154
144
|
scope: Scope.REQUEST
|
|
155
145
|
}),
|
|
156
|
-
_ts_param(0, Inject(
|
|
146
|
+
_ts_param(0, Inject(StorageConfigService)),
|
|
157
147
|
_ts_param(1, Optional()),
|
|
158
148
|
_ts_param(1, Inject(REQUEST)),
|
|
159
149
|
_ts_metadata("design:type", Function),
|
|
160
150
|
_ts_metadata("design:paramtypes", [
|
|
161
|
-
typeof
|
|
151
|
+
typeof StorageConfigService === "undefined" ? Object : StorageConfigService,
|
|
162
152
|
typeof Request === "undefined" ? Object : Request
|
|
163
153
|
])
|
|
164
154
|
], StorageDataSourceProvider);
|
|
@@ -29,7 +29,7 @@ import { RequestScopedApiService, HybridCache } from '@flusys/nestjs-shared/clas
|
|
|
29
29
|
import { UtilsService } from '@flusys/nestjs-shared/modules';
|
|
30
30
|
import { applyCompanyFilter, buildCompanyWhereCondition } from '@flusys/nestjs-shared/utils';
|
|
31
31
|
import { Inject, Injectable, Scope } from '@nestjs/common';
|
|
32
|
-
import { StorageConfigService } from '
|
|
32
|
+
import { StorageConfigService } from './storage-config.service';
|
|
33
33
|
import { StorageConfig, StorageConfigWithCompany } from '../entities';
|
|
34
34
|
import { StorageDataSourceProvider } from './storage-datasource.provider';
|
|
35
35
|
export class StorageProviderConfigService extends RequestScopedApiService {
|
|
@@ -27,7 +27,7 @@ function _ts_param(paramIndex, decorator) {
|
|
|
27
27
|
}
|
|
28
28
|
import { ErrorHandler, validateCompanyOwnership } from '@flusys/nestjs-shared/utils';
|
|
29
29
|
import { BadRequestException, Inject, Injectable, Logger, NotFoundException, Scope } from '@nestjs/common';
|
|
30
|
-
import { StorageConfigService } from '
|
|
30
|
+
import { StorageConfigService } from './storage-config.service';
|
|
31
31
|
import { FileLocationEnum } from '../enums/file-location.enum';
|
|
32
32
|
import { StorageFactoryService } from '../providers/storage-factory.service';
|
|
33
33
|
import { StorageProviderConfigService } from './storage-provider-config.service';
|
|
@@ -62,6 +62,25 @@ export class UploadService {
|
|
|
62
62
|
file.originalname = FileValidator.sanitizeFilename(file.originalname);
|
|
63
63
|
}
|
|
64
64
|
/**
|
|
65
|
+
* Create provider from storage config entity
|
|
66
|
+
*/ async createProviderFromConfig(config) {
|
|
67
|
+
return this.storageFactory.createProvider({
|
|
68
|
+
provider: config.storage,
|
|
69
|
+
config: config.config
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Create fallback local provider
|
|
74
|
+
*/ async createFallbackLocalProvider() {
|
|
75
|
+
this.logger.warn('No storage config found, using fallback local provider');
|
|
76
|
+
return this.storageFactory.createProvider({
|
|
77
|
+
provider: FileLocationEnum.LOCAL,
|
|
78
|
+
config: {
|
|
79
|
+
basePath: this.storageConfigService.getDefaultLocalStoragePath()
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
65
84
|
* Get storage provider and config info based on storage config ID
|
|
66
85
|
* Validates company ownership when company feature is enabled
|
|
67
86
|
*/ async getStorageProviderWithConfig(storageConfigId, user) {
|
|
@@ -84,13 +103,7 @@ export class UploadService {
|
|
|
84
103
|
}
|
|
85
104
|
storageConfig = defaultConfig;
|
|
86
105
|
}
|
|
87
|
-
|
|
88
|
-
const providerConfig = {
|
|
89
|
-
provider: storageConfig.storage,
|
|
90
|
-
config: storageConfig.config
|
|
91
|
-
};
|
|
92
|
-
// Get or create provider instance
|
|
93
|
-
const provider = await this.storageFactory.createProvider(providerConfig);
|
|
106
|
+
const provider = await this.createProviderFromConfig(storageConfig);
|
|
94
107
|
return {
|
|
95
108
|
provider,
|
|
96
109
|
location: storageConfig.storage,
|
|
@@ -98,74 +111,33 @@ export class UploadService {
|
|
|
98
111
|
};
|
|
99
112
|
}
|
|
100
113
|
/**
|
|
101
|
-
* Get storage provider based on storage config ID (convenience method)
|
|
102
|
-
*/ async getStorageProvider(storageConfigId, user) {
|
|
103
|
-
const { provider } = await this.getStorageProviderWithConfig(storageConfigId, user);
|
|
104
|
-
return provider;
|
|
105
|
-
}
|
|
106
|
-
/**
|
|
107
114
|
* Get storage provider for delete operations with fallback
|
|
108
115
|
* If the original storage config doesn't exist, falls back based on locationHint
|
|
109
|
-
* This ensures files can still be deleted even if storage config was removed
|
|
110
|
-
* @param storageConfigId - The storage config ID to look up
|
|
111
|
-
* @param user - User context for company filtering
|
|
112
|
-
* @param locationHint - The file's original location type (used for fallback)
|
|
113
116
|
*/ async getStorageProviderForDelete(storageConfigId, user, locationHint) {
|
|
114
|
-
//
|
|
117
|
+
// Try to find by storageConfigId
|
|
115
118
|
if (storageConfigId) {
|
|
116
119
|
const config = await this.storageProviderConfigService.findByIdDirect(storageConfigId);
|
|
117
120
|
if (config) {
|
|
118
|
-
|
|
119
|
-
provider: config.storage,
|
|
120
|
-
config: config.config
|
|
121
|
-
};
|
|
122
|
-
return await this.storageFactory.createProvider(providerConfig);
|
|
121
|
+
return this.createProviderFromConfig(config);
|
|
123
122
|
}
|
|
124
|
-
|
|
125
|
-
this.logger.warn(`Storage config ${storageConfigId} not found, trying fallback for delete`);
|
|
123
|
+
this.logger.warn(`Storage config ${storageConfigId} not found, trying fallback`);
|
|
126
124
|
}
|
|
127
|
-
// Fallback: Use locationHint to find a matching config
|
|
125
|
+
// Fallback: Use locationHint to find a matching config
|
|
128
126
|
if (locationHint) {
|
|
129
|
-
// Try to find a config matching the file's original location type
|
|
130
127
|
const matchingConfigs = await this.storageProviderConfigService.getConfigByType(locationHint, user);
|
|
131
128
|
if (matchingConfigs.length > 0) {
|
|
132
|
-
|
|
133
|
-
provider: matchingConfigs[0].storage,
|
|
134
|
-
config: matchingConfigs[0].config
|
|
135
|
-
};
|
|
136
|
-
this.logger.debug(`Using matching ${locationHint} config for delete fallback`);
|
|
137
|
-
return await this.storageFactory.createProvider(providerConfig);
|
|
129
|
+
return this.createProviderFromConfig(matchingConfigs[0]);
|
|
138
130
|
}
|
|
139
|
-
// No matching config found, create a basic provider based on locationHint
|
|
140
131
|
if (locationHint === FileLocationEnum.LOCAL) {
|
|
141
|
-
this.
|
|
142
|
-
const localProviderConfig = {
|
|
143
|
-
provider: FileLocationEnum.LOCAL,
|
|
144
|
-
config: {
|
|
145
|
-
basePath: this.storageConfigService.getDefaultLocalStoragePath()
|
|
146
|
-
}
|
|
147
|
-
};
|
|
148
|
-
return await this.storageFactory.createProvider(localProviderConfig);
|
|
132
|
+
return this.createFallbackLocalProvider();
|
|
149
133
|
}
|
|
150
134
|
}
|
|
151
|
-
// Last resort: Try default config
|
|
135
|
+
// Last resort: Try default config
|
|
152
136
|
const defaultConfig = await this.storageProviderConfigService.getDefaultConfig(user);
|
|
153
137
|
if (defaultConfig) {
|
|
154
|
-
|
|
155
|
-
provider: defaultConfig.storage,
|
|
156
|
-
config: defaultConfig.config
|
|
157
|
-
};
|
|
158
|
-
return await this.storageFactory.createProvider(providerConfig);
|
|
138
|
+
return this.createProviderFromConfig(defaultConfig);
|
|
159
139
|
}
|
|
160
|
-
|
|
161
|
-
this.logger.warn('No storage config found, using fallback local provider for delete');
|
|
162
|
-
const localProviderConfig = {
|
|
163
|
-
provider: FileLocationEnum.LOCAL,
|
|
164
|
-
config: {
|
|
165
|
-
basePath: this.storageConfigService.getDefaultLocalStoragePath()
|
|
166
|
-
}
|
|
167
|
-
};
|
|
168
|
-
return await this.storageFactory.createProvider(localProviderConfig);
|
|
140
|
+
return this.createFallbackLocalProvider();
|
|
169
141
|
}
|
|
170
142
|
async uploadSingleFile(file, options, user) {
|
|
171
143
|
try {
|
|
@@ -263,7 +235,7 @@ export class UploadService {
|
|
|
263
235
|
* For Local/SFTP: returns direct path
|
|
264
236
|
*/ async makeFileUrl(key, storageConfigId, expiresIn = 3600, user) {
|
|
265
237
|
try {
|
|
266
|
-
const provider = await this.
|
|
238
|
+
const { provider } = await this.getStorageProviderWithConfig(storageConfigId, user);
|
|
267
239
|
// Check if provider supports presigned URLs
|
|
268
240
|
if (provider.generatePresignedUrl) {
|
|
269
241
|
return await provider.generatePresignedUrl(key, expiresIn);
|
|
@@ -293,10 +293,33 @@ import { Logger } from '@nestjs/common';
|
|
|
293
293
|
'image/svg+xml',
|
|
294
294
|
'application/xhtml+xml'
|
|
295
295
|
];
|
|
296
|
+
/**
|
|
297
|
+
* ZIP-based format prefixes that are valid when detected as application/zip.
|
|
298
|
+
*/ const ZIP_VARIANT_PREFIXES = [
|
|
299
|
+
'application/vnd.openxmlformats-officedocument',
|
|
300
|
+
'application/x-zip',
|
|
301
|
+
'application/x-compressed'
|
|
302
|
+
];
|
|
296
303
|
/**
|
|
297
304
|
* Utility class for validating file content using magic bytes.
|
|
298
305
|
* Prevents file type spoofing by checking actual file content.
|
|
299
306
|
*/ export class FileValidator {
|
|
307
|
+
// Result Helpers
|
|
308
|
+
static failureResult(message, detectedType, declaredType) {
|
|
309
|
+
return {
|
|
310
|
+
valid: false,
|
|
311
|
+
detectedType,
|
|
312
|
+
declaredType,
|
|
313
|
+
message
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
static successResult(detectedType, declaredType) {
|
|
317
|
+
return {
|
|
318
|
+
valid: true,
|
|
319
|
+
detectedType,
|
|
320
|
+
declaredType
|
|
321
|
+
};
|
|
322
|
+
}
|
|
300
323
|
/**
|
|
301
324
|
* Detect file type from buffer using magic bytes.
|
|
302
325
|
* @param buffer - File buffer to analyze
|
|
@@ -342,15 +365,8 @@ import { Logger } from '@nestjs/common';
|
|
|
342
365
|
const detectedCategory = detected.split('/')[0];
|
|
343
366
|
const declaredCategory = declared.split('/')[0];
|
|
344
367
|
// For ZIP-based formats, allow any ZIP-detected file if declared is a ZIP variant
|
|
345
|
-
if (detected === 'application/zip') {
|
|
346
|
-
|
|
347
|
-
'application/vnd.openxmlformats-officedocument',
|
|
348
|
-
'application/x-zip',
|
|
349
|
-
'application/x-compressed'
|
|
350
|
-
];
|
|
351
|
-
if (zipVariants.some((v)=>declared.startsWith(v))) {
|
|
352
|
-
return true;
|
|
353
|
-
}
|
|
368
|
+
if (detected === 'application/zip' && ZIP_VARIANT_PREFIXES.some((v)=>declared.startsWith(v))) {
|
|
369
|
+
return true;
|
|
354
370
|
}
|
|
355
371
|
return detectedCategory === declaredCategory;
|
|
356
372
|
}
|
|
@@ -378,74 +394,46 @@ import { Logger } from '@nestjs/common';
|
|
|
378
394
|
'*/*'
|
|
379
395
|
]) {
|
|
380
396
|
try {
|
|
381
|
-
// Detect actual file type from magic bytes
|
|
382
397
|
const detectedType = this.detectFileType(buffer);
|
|
383
|
-
//
|
|
398
|
+
// No magic bytes detected - handle text-based types
|
|
384
399
|
if (!detectedType) {
|
|
385
|
-
|
|
386
|
-
if (this.isDangerousTextType(declaredMimeType)) {
|
|
387
|
-
// Only allow dangerous types if explicitly in allowedTypes (not via wildcard)
|
|
388
|
-
const explicitlyAllowed = allowedTypes.some((t)=>t === declaredMimeType && t !== '*/*' && !t.endsWith('/*'));
|
|
389
|
-
if (!explicitlyAllowed) {
|
|
390
|
-
this.logger.warn(`Blocked dangerous file type: ${declaredMimeType} - requires explicit allowlisting`);
|
|
391
|
-
return {
|
|
392
|
-
valid: false,
|
|
393
|
-
detectedType: declaredMimeType,
|
|
394
|
-
declaredType: declaredMimeType,
|
|
395
|
-
message: `File type "${declaredMimeType}" is potentially dangerous and not explicitly allowed`
|
|
396
|
-
};
|
|
397
|
-
}
|
|
398
|
-
this.logger.warn(`Allowing explicitly permitted dangerous file type: ${declaredMimeType}`);
|
|
399
|
-
}
|
|
400
|
-
if (this.isTextBasedType(declaredMimeType)) {
|
|
401
|
-
// Safe text-based files don't have magic bytes, trust the declared type
|
|
402
|
-
const isAllowed = this.isTypeAllowed(declaredMimeType, allowedTypes);
|
|
403
|
-
return {
|
|
404
|
-
valid: isAllowed,
|
|
405
|
-
detectedType: declaredMimeType,
|
|
406
|
-
declaredType: declaredMimeType,
|
|
407
|
-
message: isAllowed ? undefined : `File type "${declaredMimeType}" is not allowed`
|
|
408
|
-
};
|
|
409
|
-
}
|
|
410
|
-
// For binary files without recognized signatures, be cautious
|
|
411
|
-
this.logger.warn(`Unable to detect file type for declared type: ${declaredMimeType}`);
|
|
412
|
-
return {
|
|
413
|
-
valid: false,
|
|
414
|
-
declaredType: declaredMimeType,
|
|
415
|
-
message: 'Unable to verify file type. File may be corrupted or unsupported.'
|
|
416
|
-
};
|
|
400
|
+
return this.validateUndetectedType(declaredMimeType, allowedTypes);
|
|
417
401
|
}
|
|
418
|
-
//
|
|
402
|
+
// Verify detected type matches declared type
|
|
419
403
|
if (!this.mimeTypesMatch(detectedType, declaredMimeType)) {
|
|
420
404
|
this.logger.warn(`MIME type mismatch: declared=${declaredMimeType}, detected=${detectedType}`);
|
|
421
|
-
return {
|
|
422
|
-
valid: false,
|
|
423
|
-
detectedType,
|
|
424
|
-
declaredType: declaredMimeType,
|
|
425
|
-
message: `File content does not match declared type. Detected: ${detectedType}, Declared: ${declaredMimeType}`
|
|
426
|
-
};
|
|
405
|
+
return this.failureResult(`File content does not match declared type. Detected: ${detectedType}, Declared: ${declaredMimeType}`, detectedType, declaredMimeType);
|
|
427
406
|
}
|
|
428
|
-
//
|
|
407
|
+
// Verify type is in allowed list
|
|
429
408
|
if (!this.isTypeAllowed(detectedType, allowedTypes)) {
|
|
430
|
-
return {
|
|
431
|
-
valid: false,
|
|
432
|
-
detectedType,
|
|
433
|
-
declaredType: declaredMimeType,
|
|
434
|
-
message: `File type "${detectedType}" is not allowed`
|
|
435
|
-
};
|
|
409
|
+
return this.failureResult(`File type "${detectedType}" is not allowed`, detectedType, declaredMimeType);
|
|
436
410
|
}
|
|
437
|
-
return
|
|
438
|
-
valid: true,
|
|
439
|
-
detectedType,
|
|
440
|
-
declaredType: declaredMimeType
|
|
441
|
-
};
|
|
411
|
+
return this.successResult(detectedType, declaredMimeType);
|
|
442
412
|
} catch (error) {
|
|
443
413
|
this.logger.error('File validation error:', error);
|
|
444
|
-
return
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
414
|
+
return this.failureResult('File validation failed');
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Handle validation for files without detectable magic bytes.
|
|
419
|
+
*/ static validateUndetectedType(declaredMimeType, allowedTypes) {
|
|
420
|
+
// Check for dangerous text types first (HTML, JS, SVG)
|
|
421
|
+
if (this.isDangerousTextType(declaredMimeType)) {
|
|
422
|
+
const explicitlyAllowed = allowedTypes.some((t)=>t === declaredMimeType && t !== '*/*' && !t.endsWith('/*'));
|
|
423
|
+
if (!explicitlyAllowed) {
|
|
424
|
+
this.logger.warn(`Blocked dangerous file type: ${declaredMimeType} - requires explicit allowlisting`);
|
|
425
|
+
return this.failureResult(`File type "${declaredMimeType}" is potentially dangerous and not explicitly allowed`, declaredMimeType, declaredMimeType);
|
|
426
|
+
}
|
|
427
|
+
this.logger.warn(`Allowing explicitly permitted dangerous file type: ${declaredMimeType}`);
|
|
428
|
+
}
|
|
429
|
+
// Safe text-based files don't have magic bytes, trust declared type
|
|
430
|
+
if (this.isTextBasedType(declaredMimeType)) {
|
|
431
|
+
const isAllowed = this.isTypeAllowed(declaredMimeType, allowedTypes);
|
|
432
|
+
return isAllowed ? this.successResult(declaredMimeType, declaredMimeType) : this.failureResult(`File type "${declaredMimeType}" is not allowed`, declaredMimeType, declaredMimeType);
|
|
448
433
|
}
|
|
434
|
+
// Binary files without recognized signatures - be cautious
|
|
435
|
+
this.logger.warn(`Unable to detect file type for declared type: ${declaredMimeType}`);
|
|
436
|
+
return this.failureResult('Unable to verify file type. File may be corrupted or unsupported.', undefined, declaredMimeType);
|
|
449
437
|
}
|
|
450
438
|
/**
|
|
451
439
|
* Sanitize filename to prevent path traversal and special character issues.
|
|
@@ -99,12 +99,9 @@ const sharp = sharpModule.default || sharpModule;
|
|
|
99
99
|
break;
|
|
100
100
|
}
|
|
101
101
|
try {
|
|
102
|
-
|
|
103
|
-
const { data, info } = await image.toBuffer({
|
|
104
|
-
resolveWithObject: true
|
|
105
|
-
});
|
|
102
|
+
const compressedBuffer = await image.toBuffer();
|
|
106
103
|
return {
|
|
107
|
-
buffer:
|
|
104
|
+
buffer: compressedBuffer,
|
|
108
105
|
format: `image/${targetFormat}`
|
|
109
106
|
};
|
|
110
107
|
} catch {
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { IIdentity } from '@flusys/nestjs-shared/interfaces';
|
|
2
|
-
import { FileLocationEnum } from '../enums';
|
|
3
2
|
export interface IStorageConfig extends IIdentity {
|
|
4
3
|
name: string;
|
|
5
|
-
storage:
|
|
4
|
+
storage: string;
|
|
6
5
|
config: Record<string, any>;
|
|
7
6
|
isActive: boolean;
|
|
8
7
|
isDefault: boolean;
|
|
@@ -7,17 +7,12 @@ export interface IStorageModuleConfig extends IDataSourceServiceOptions {
|
|
|
7
7
|
localStoragePath?: string;
|
|
8
8
|
appUrl?: string;
|
|
9
9
|
}
|
|
10
|
-
export interface IStorageModuleConfigFull {
|
|
11
|
-
bootstrapAppConfig?: IBootstrapAppConfig;
|
|
12
|
-
config?: IStorageModuleConfig;
|
|
13
|
-
}
|
|
14
10
|
export interface StorageModuleOptions extends IDynamicModuleConfig {
|
|
15
11
|
bootstrapAppConfig?: IBootstrapAppConfig;
|
|
16
12
|
config?: IStorageModuleConfig;
|
|
17
13
|
}
|
|
18
14
|
export interface StorageOptionsFactory extends IModuleOptionsFactory<IStorageModuleConfig> {
|
|
19
15
|
createStorageOptions(): Promise<IStorageModuleConfig> | IStorageModuleConfig;
|
|
20
|
-
createOptions(): Promise<IStorageModuleConfig> | IStorageModuleConfig;
|
|
21
16
|
}
|
|
22
17
|
export interface StorageModuleAsyncOptions extends Pick<ModuleMetadata, 'imports'>, IDynamicModuleConfig {
|
|
23
18
|
bootstrapAppConfig: IBootstrapAppConfig;
|
|
@@ -3,7 +3,15 @@ import { Request, Response, NextFunction } from 'express';
|
|
|
3
3
|
import { UploadService } from '../services/upload.service';
|
|
4
4
|
export declare class FileServeMiddleware implements NestMiddleware {
|
|
5
5
|
private uploadService;
|
|
6
|
-
private logger;
|
|
6
|
+
private readonly logger;
|
|
7
|
+
private readonly uploadDir;
|
|
7
8
|
constructor(uploadService: UploadService);
|
|
8
9
|
use(req: Request, res: Response, next: NextFunction): Promise<void>;
|
|
10
|
+
private extractFilePath;
|
|
11
|
+
private resolveFilePath;
|
|
12
|
+
private tryFallbackPaths;
|
|
13
|
+
private getMimeType;
|
|
14
|
+
private setResponseHeaders;
|
|
15
|
+
private streamFile;
|
|
16
|
+
private sendErrorResponse;
|
|
9
17
|
}
|
|
@@ -4,7 +4,6 @@ export declare class StorageModule implements NestModule {
|
|
|
4
4
|
configure(consumer: MiddlewareConsumer): void;
|
|
5
5
|
static forRoot(options: StorageModuleOptions): DynamicModule;
|
|
6
6
|
static forRootAsync(options: StorageModuleAsyncOptions): DynamicModule;
|
|
7
|
-
private static
|
|
8
|
-
private static getProviders;
|
|
7
|
+
private static buildProviders;
|
|
9
8
|
private static createAsyncProviders;
|
|
10
9
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flusys/nestjs-storage",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Modular storage package with optional AWS S3, Azure Blob, and SFTP providers",
|
|
5
5
|
"main": "cjs/index.js",
|
|
6
6
|
"module": "fesm/index.js",
|
|
@@ -128,7 +128,7 @@
|
|
|
128
128
|
}
|
|
129
129
|
},
|
|
130
130
|
"dependencies": {
|
|
131
|
-
"@flusys/nestjs-core": "1.0.
|
|
132
|
-
"@flusys/nestjs-shared": "1.0.
|
|
131
|
+
"@flusys/nestjs-core": "1.0.1",
|
|
132
|
+
"@flusys/nestjs-shared": "1.0.1"
|
|
133
133
|
}
|
|
134
134
|
}
|