@flusys/nestjs-storage 1.1.0-beta → 1.1.0
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 +153 -8
- package/cjs/config/index.js +0 -1
- package/cjs/config/storage.constants.js +0 -17
- package/cjs/controllers/file-manager.controller.js +44 -1
- package/cjs/controllers/folder.controller.js +44 -1
- package/cjs/controllers/storage-config.controller.js +44 -1
- package/cjs/controllers/upload.controller.js +18 -29
- package/cjs/docs/storage-swagger.config.js +24 -136
- package/cjs/dtos/file-manager.dto.js +71 -35
- package/cjs/dtos/folder.dto.js +15 -9
- package/cjs/dtos/storage-config.dto.js +5 -86
- package/cjs/dtos/upload.dto.js +24 -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/interfaces/index.js +0 -1
- package/cjs/middlewares/file-serve.middleware.js +113 -100
- package/cjs/modules/storage.module.js +82 -136
- package/cjs/providers/azure-provider.optional.js +10 -38
- package/cjs/providers/local-provider.js +38 -31
- 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 +238 -323
- package/cjs/services/folder.service.js +8 -11
- package/cjs/services/index.js +1 -0
- package/cjs/{config → services}/storage-config.service.js +32 -76
- package/cjs/services/storage-datasource.provider.js +16 -26
- package/cjs/services/storage-provider-config.service.js +15 -37
- package/cjs/services/upload.service.js +72 -88
- package/cjs/utils/file-validator.util.js +458 -0
- package/cjs/utils/image-compressor.util.js +3 -8
- package/config/index.d.ts +0 -1
- package/config/storage.constants.d.ts +0 -8
- package/controllers/upload.controller.d.ts +3 -6
- package/dtos/file-manager.dto.d.ts +13 -7
- package/dtos/folder.dto.d.ts +5 -5
- package/dtos/storage-config.dto.d.ts +9 -16
- 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 -8
- package/fesm/controllers/file-manager.controller.js +45 -2
- package/fesm/controllers/folder.controller.js +45 -2
- package/fesm/controllers/storage-config.controller.js +45 -2
- package/fesm/controllers/upload.controller.js +19 -30
- package/fesm/docs/storage-swagger.config.js +27 -142
- package/fesm/dtos/file-manager.dto.js +72 -36
- package/fesm/dtos/folder.dto.js +16 -10
- package/fesm/dtos/storage-config.dto.js +9 -96
- package/fesm/dtos/upload.dto.js +25 -19
- 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 +5 -13
- package/fesm/entities/storage-config-with-company.entity.js +3 -4
- package/fesm/entities/storage-config.entity.js +74 -4
- package/fesm/interfaces/index.js +0 -1
- package/fesm/interfaces/storage-config.interface.js +1 -3
- package/fesm/middlewares/file-serve.middleware.js +114 -101
- package/fesm/modules/storage.module.js +83 -136
- package/fesm/providers/azure-provider.optional.js +14 -45
- package/fesm/providers/local-provider.js +38 -31
- 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 -322
- package/fesm/services/folder.service.js +6 -9
- package/fesm/services/index.js +1 -0
- package/fesm/{config → services}/storage-config.service.js +32 -76
- package/fesm/services/storage-datasource.provider.js +16 -26
- package/fesm/services/storage-provider-config.service.js +13 -35
- package/fesm/services/upload.service.js +71 -87
- package/fesm/utils/file-validator.util.js +451 -0
- package/fesm/utils/image-compressor.util.js +3 -8
- package/interfaces/file-manager.interface.d.ts +7 -4
- package/interfaces/index.d.ts +0 -1
- package/interfaces/storage-config.interface.d.ts +1 -22
- 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 +6 -6
- package/providers/azure-provider.optional.d.ts +8 -6
- package/providers/local-provider.d.ts +2 -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 +23 -16
- package/services/folder.service.d.ts +4 -4
- package/services/index.d.ts +1 -0
- package/services/storage-config.service.d.ts +24 -0
- 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 +19 -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/cjs/interfaces/file-upload-response.interface.js +0 -4
- package/config/storage-config.service.d.ts +0 -22
- 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
- package/fesm/interfaces/file-upload-response.interface.js +0 -1
- package/interfaces/file-upload-response.interface.d.ts +0 -6
|
@@ -27,8 +27,9 @@ function _ts_param(paramIndex, decorator) {
|
|
|
27
27
|
}
|
|
28
28
|
import { RequestScopedApiService, HybridCache } from '@flusys/nestjs-shared/classes';
|
|
29
29
|
import { UtilsService } from '@flusys/nestjs-shared/modules';
|
|
30
|
+
import { applyCompanyFilter } from '@flusys/nestjs-shared/utils';
|
|
30
31
|
import { Inject, Injectable, Scope } from '@nestjs/common';
|
|
31
|
-
import { StorageConfigService } from '
|
|
32
|
+
import { StorageConfigService } from './storage-config.service';
|
|
32
33
|
import { Folder, FolderWithCompany } from '../entities';
|
|
33
34
|
import { StorageDataSourceProvider } from './storage-datasource.provider';
|
|
34
35
|
export class FolderService extends RequestScopedApiService {
|
|
@@ -38,16 +39,13 @@ export class FolderService extends RequestScopedApiService {
|
|
|
38
39
|
getDataSourceProvider() {
|
|
39
40
|
return this.dataSourceProvider;
|
|
40
41
|
}
|
|
41
|
-
// Entity Conversion
|
|
42
42
|
async convertSingleDtoToEntity(dto, user) {
|
|
43
43
|
const entity = await super.convertSingleDtoToEntity(dto, user);
|
|
44
|
-
// Set companyId from user context if company feature enabled
|
|
45
44
|
if (this.storageConfig.isCompanyFeatureEnabled()) {
|
|
46
45
|
entity.companyId = user?.companyId ?? null;
|
|
47
46
|
}
|
|
48
47
|
return entity;
|
|
49
48
|
}
|
|
50
|
-
// Query Customization
|
|
51
49
|
async getSelectQuery(query, _user, select) {
|
|
52
50
|
if (!select?.length) {
|
|
53
51
|
select = [
|
|
@@ -69,11 +67,10 @@ export class FolderService extends RequestScopedApiService {
|
|
|
69
67
|
}
|
|
70
68
|
async getExtraManipulateQuery(query, filterDto, user) {
|
|
71
69
|
const result = await super.getExtraManipulateQuery(query, filterDto, user);
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
70
|
+
applyCompanyFilter(query, {
|
|
71
|
+
isCompanyFeatureEnabled: this.storageConfig.isCompanyFeatureEnabled(),
|
|
72
|
+
entityAlias: 'folder'
|
|
73
|
+
}, user);
|
|
77
74
|
return result;
|
|
78
75
|
}
|
|
79
76
|
constructor(cacheManager, utilsService, storageConfig, dataSourceProvider){
|
package/fesm/services/index.js
CHANGED
|
@@ -27,115 +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
|
-
|
|
44
|
-
|
|
42
|
+
isMultiTenant() {
|
|
43
|
+
return this.getDatabaseMode() === 'multi-tenant';
|
|
44
|
+
}
|
|
45
|
+
// ─── Config Getters ─────────────────────────────────────────────────────────
|
|
46
|
+
getMaxFileSize() {
|
|
45
47
|
return this.options.config?.maxFileSize ?? DEFAULT_MAX_FILE_SIZE;
|
|
46
48
|
}
|
|
47
|
-
|
|
48
|
-
* Get allowed file types (MIME types or patterns)
|
|
49
|
-
*/ getAllowedFileTypes() {
|
|
49
|
+
getAllowedFileTypes() {
|
|
50
50
|
return this.options.config?.allowedFileTypes ?? DEFAULT_ALLOWED_FILE_TYPES;
|
|
51
51
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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) {
|
|
55
63
|
const allowedTypes = this.getAllowedFileTypes();
|
|
56
|
-
// If wildcard, allow all
|
|
57
64
|
if (allowedTypes.includes('*/*')) {
|
|
58
65
|
return true;
|
|
59
66
|
}
|
|
60
|
-
|
|
61
|
-
return allowedTypes.some((allowedType)=>{
|
|
62
|
-
if (allowedType.endsWith('/*')) {
|
|
63
|
-
const prefix = allowedType.slice(0, -2);
|
|
64
|
-
return mimeType.startsWith(prefix);
|
|
65
|
-
}
|
|
66
|
-
return allowedType === mimeType;
|
|
67
|
-
});
|
|
67
|
+
return allowedTypes.some((type)=>type.endsWith('/*') ? mimeType.startsWith(type.slice(0, -2)) : type === mimeType);
|
|
68
68
|
}
|
|
69
|
-
|
|
70
|
-
* Validate file size
|
|
71
|
-
*/ validateFileSize(fileSize) {
|
|
69
|
+
validateFileSize(fileSize) {
|
|
72
70
|
const maxSize = this.getMaxFileSize();
|
|
73
71
|
if (fileSize > maxSize) {
|
|
74
|
-
const maxSizeMB = (maxSize / (1024 * 1024)).toFixed(2);
|
|
75
|
-
const fileSizeMB = (fileSize / (1024 * 1024)).toFixed(2);
|
|
76
72
|
return {
|
|
77
73
|
valid: false,
|
|
78
|
-
message: `File size (${
|
|
74
|
+
message: `File size (${this.toMB(fileSize)}MB) exceeds maximum ${this.toMB(maxSize)}MB`
|
|
79
75
|
};
|
|
80
76
|
}
|
|
81
77
|
return {
|
|
82
78
|
valid: true
|
|
83
79
|
};
|
|
84
80
|
}
|
|
85
|
-
|
|
86
|
-
* Validate file type
|
|
87
|
-
*/ validateFileType(mimeType) {
|
|
81
|
+
validateFileType(mimeType) {
|
|
88
82
|
if (!this.isFileTypeAllowed(mimeType)) {
|
|
89
|
-
const allowedTypes = this.getAllowedFileTypes();
|
|
90
83
|
return {
|
|
91
84
|
valid: false,
|
|
92
|
-
message: `File type "${mimeType}"
|
|
85
|
+
message: `File type "${mimeType}" not allowed. Allowed: ${this.getAllowedFileTypes().join(', ')}`
|
|
93
86
|
};
|
|
94
87
|
}
|
|
95
88
|
return {
|
|
96
89
|
valid: true
|
|
97
90
|
};
|
|
98
91
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
return this.options.config?.defaultDatabaseConfig;
|
|
103
|
-
}
|
|
104
|
-
/**
|
|
105
|
-
* Get all options
|
|
106
|
-
*/ getOptions() {
|
|
107
|
-
return this.options;
|
|
108
|
-
}
|
|
109
|
-
/**
|
|
110
|
-
* Get default local storage base path
|
|
111
|
-
* Used for serving local files without database lookup
|
|
112
|
-
* Falls back to './uploads' if not configured
|
|
113
|
-
*/ getDefaultLocalStoragePath() {
|
|
114
|
-
// Check if localStoragePath is configured in module options
|
|
115
|
-
const configuredPath = this.options.config?.localStoragePath;
|
|
116
|
-
if (configuredPath) {
|
|
117
|
-
return configuredPath;
|
|
118
|
-
}
|
|
119
|
-
// Default to ./uploads in the project root
|
|
120
|
-
return './uploads';
|
|
121
|
-
}
|
|
122
|
-
/**
|
|
123
|
-
* Get application base URL for generating file URLs
|
|
124
|
-
* - First tries module config
|
|
125
|
-
* - Falls back to APP_URL env var
|
|
126
|
-
* - Finally constructs from PORT env var
|
|
127
|
-
*/ getAppUrl() {
|
|
128
|
-
// Try from module config first
|
|
129
|
-
if (this.options.config?.appUrl) {
|
|
130
|
-
return this.options.config.appUrl;
|
|
131
|
-
}
|
|
132
|
-
// Fallback: read directly from environment
|
|
133
|
-
if (process.env.APP_URL) {
|
|
134
|
-
return process.env.APP_URL;
|
|
135
|
-
}
|
|
136
|
-
// Last resort: construct from PORT
|
|
137
|
-
const port = process.env.PORT || '3000';
|
|
138
|
-
return `http://localhost:${port}`;
|
|
92
|
+
// ─── Private Helpers ────────────────────────────────────────────────────────
|
|
93
|
+
toMB(bytes) {
|
|
94
|
+
return (bytes / BYTES_PER_MB).toFixed(2);
|
|
139
95
|
}
|
|
140
96
|
constructor(options){
|
|
141
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);
|
|
@@ -27,8 +27,9 @@ function _ts_param(paramIndex, decorator) {
|
|
|
27
27
|
}
|
|
28
28
|
import { RequestScopedApiService, HybridCache } from '@flusys/nestjs-shared/classes';
|
|
29
29
|
import { UtilsService } from '@flusys/nestjs-shared/modules';
|
|
30
|
+
import { applyCompanyFilter, buildCompanyWhereCondition } from '@flusys/nestjs-shared/utils';
|
|
30
31
|
import { Inject, Injectable, Scope } from '@nestjs/common';
|
|
31
|
-
import { StorageConfigService } from '
|
|
32
|
+
import { StorageConfigService } from './storage-config.service';
|
|
32
33
|
import { StorageConfig, StorageConfigWithCompany } from '../entities';
|
|
33
34
|
import { StorageDataSourceProvider } from './storage-datasource.provider';
|
|
34
35
|
export class StorageProviderConfigService extends RequestScopedApiService {
|
|
@@ -38,16 +39,13 @@ export class StorageProviderConfigService extends RequestScopedApiService {
|
|
|
38
39
|
getDataSourceProvider() {
|
|
39
40
|
return this.dataSourceProvider;
|
|
40
41
|
}
|
|
41
|
-
// Entity Conversion
|
|
42
42
|
async convertSingleDtoToEntity(dto, user) {
|
|
43
43
|
const entity = await super.convertSingleDtoToEntity(dto, user);
|
|
44
|
-
// Set companyId from user context if company feature enabled
|
|
45
44
|
if (this.storageConfig.isCompanyFeatureEnabled()) {
|
|
46
45
|
entity.companyId = user?.companyId ?? null;
|
|
47
46
|
}
|
|
48
47
|
return entity;
|
|
49
48
|
}
|
|
50
|
-
// Query Customization
|
|
51
49
|
async getSelectQuery(query, _user, select) {
|
|
52
50
|
if (!select?.length) {
|
|
53
51
|
select = [
|
|
@@ -72,19 +70,14 @@ export class StorageProviderConfigService extends RequestScopedApiService {
|
|
|
72
70
|
}
|
|
73
71
|
async getExtraManipulateQuery(query, filterDto, user) {
|
|
74
72
|
const result = await super.getExtraManipulateQuery(query, filterDto, user);
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
73
|
+
applyCompanyFilter(query, {
|
|
74
|
+
isCompanyFeatureEnabled: this.storageConfig.isCompanyFeatureEnabled(),
|
|
75
|
+
entityAlias: 'storageConfig'
|
|
76
|
+
}, user);
|
|
80
77
|
query.orderBy(`${this.entityName}.createdAt`, 'DESC');
|
|
81
78
|
return result;
|
|
82
79
|
}
|
|
83
|
-
|
|
84
|
-
* Find storage config by ID without throwing (returns null if not found)
|
|
85
|
-
* Uses direct repository query - bypasses company filtering
|
|
86
|
-
* Use for internal operations like file deletion where config ID is already known
|
|
87
|
-
*/ async findByIdDirect(id) {
|
|
80
|
+
async findByIdDirect(id) {
|
|
88
81
|
await this.ensureRepositoryInitialized();
|
|
89
82
|
return await this.repository.findOne({
|
|
90
83
|
where: {
|
|
@@ -92,19 +85,11 @@ export class StorageProviderConfigService extends RequestScopedApiService {
|
|
|
92
85
|
}
|
|
93
86
|
});
|
|
94
87
|
}
|
|
95
|
-
|
|
96
|
-
* Get default storage configuration (scoped to user's company if enabled)
|
|
97
|
-
* Priority: isDefault=true > any active config (oldest first)
|
|
98
|
-
*/ async getDefaultConfig(user) {
|
|
88
|
+
async getDefaultConfig(user) {
|
|
99
89
|
await this.ensureRepositoryInitialized();
|
|
100
|
-
const baseWhere = {
|
|
90
|
+
const baseWhere = buildCompanyWhereCondition({
|
|
101
91
|
isActive: true
|
|
102
|
-
};
|
|
103
|
-
// Filter by company only if company feature is enabled and user is provided
|
|
104
|
-
if (this.storageConfig.isCompanyFeatureEnabled() && user?.companyId) {
|
|
105
|
-
baseWhere.companyId = user.companyId;
|
|
106
|
-
}
|
|
107
|
-
// First try to find config marked as default
|
|
92
|
+
}, this.storageConfig.isCompanyFeatureEnabled(), user);
|
|
108
93
|
const defaultConfig = await this.repository.findOne({
|
|
109
94
|
where: {
|
|
110
95
|
...baseWhere,
|
|
@@ -117,7 +102,6 @@ export class StorageProviderConfigService extends RequestScopedApiService {
|
|
|
117
102
|
if (defaultConfig) {
|
|
118
103
|
return defaultConfig;
|
|
119
104
|
}
|
|
120
|
-
// Fall back to any available active config (oldest first for consistency)
|
|
121
105
|
return await this.repository.findOne({
|
|
122
106
|
where: baseWhere,
|
|
123
107
|
order: {
|
|
@@ -125,18 +109,12 @@ export class StorageProviderConfigService extends RequestScopedApiService {
|
|
|
125
109
|
}
|
|
126
110
|
});
|
|
127
111
|
}
|
|
128
|
-
|
|
129
|
-
* Get storage configuration by type (scoped to user's company if enabled)
|
|
130
|
-
*/ async getConfigByType(storage, user) {
|
|
112
|
+
async getConfigByType(storage, user) {
|
|
131
113
|
await this.ensureRepositoryInitialized();
|
|
132
|
-
const where = {
|
|
114
|
+
const where = buildCompanyWhereCondition({
|
|
133
115
|
storage,
|
|
134
116
|
isActive: true
|
|
135
|
-
};
|
|
136
|
-
// Filter by company only if company feature is enabled and user is provided
|
|
137
|
-
if (this.storageConfig.isCompanyFeatureEnabled() && user?.companyId) {
|
|
138
|
-
where.companyId = user.companyId;
|
|
139
|
-
}
|
|
117
|
+
}, this.storageConfig.isCompanyFeatureEnabled(), user);
|
|
140
118
|
return await this.repository.find({
|
|
141
119
|
where
|
|
142
120
|
});
|
|
@@ -25,25 +25,60 @@ function _ts_param(paramIndex, decorator) {
|
|
|
25
25
|
decorator(target, key, paramIndex);
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
28
|
+
import { ErrorHandler, validateCompanyOwnership } from '@flusys/nestjs-shared/utils';
|
|
29
|
+
import { BadRequestException, Inject, Injectable, Logger, NotFoundException, Scope } from '@nestjs/common';
|
|
30
|
+
import { StorageConfigService } from './storage-config.service';
|
|
30
31
|
import { FileLocationEnum } from '../enums/file-location.enum';
|
|
31
32
|
import { StorageFactoryService } from '../providers/storage-factory.service';
|
|
32
33
|
import { StorageProviderConfigService } from './storage-provider-config.service';
|
|
34
|
+
import { FileValidator } from '../utils/file-validator.util';
|
|
33
35
|
export class UploadService {
|
|
34
36
|
/**
|
|
35
|
-
* Validate file before upload
|
|
37
|
+
* Validate file before upload - includes size, type, and content validation.
|
|
38
|
+
* Uses magic bytes to verify file content matches declared MIME type.
|
|
36
39
|
*/ validateFile(file) {
|
|
37
40
|
// Validate file size
|
|
38
41
|
const sizeValidation = this.storageConfigService.validateFileSize(file.size);
|
|
39
42
|
if (!sizeValidation.valid) {
|
|
40
43
|
throw new BadRequestException(sizeValidation.message);
|
|
41
44
|
}
|
|
42
|
-
// Validate file type
|
|
45
|
+
// Validate declared file type (MIME)
|
|
43
46
|
const typeValidation = this.storageConfigService.validateFileType(file.mimetype);
|
|
44
47
|
if (!typeValidation.valid) {
|
|
45
48
|
throw new BadRequestException(typeValidation.message);
|
|
46
49
|
}
|
|
50
|
+
// Validate file content matches declared type (magic bytes check)
|
|
51
|
+
// This prevents MIME type spoofing attacks
|
|
52
|
+
const allowedTypes = this.storageConfigService.getAllowedFileTypes();
|
|
53
|
+
const contentValidation = FileValidator.validateFileContent(file.buffer, file.mimetype, allowedTypes);
|
|
54
|
+
if (!contentValidation.valid) {
|
|
55
|
+
this.logger.warn(`File content validation failed: ${contentValidation.message}`, {
|
|
56
|
+
declaredType: file.mimetype,
|
|
57
|
+
detectedType: contentValidation.detectedType
|
|
58
|
+
});
|
|
59
|
+
throw new BadRequestException(contentValidation.message || 'File content validation failed');
|
|
60
|
+
}
|
|
61
|
+
// Sanitize filename to prevent path traversal attacks
|
|
62
|
+
file.originalname = FileValidator.sanitizeFilename(file.originalname);
|
|
63
|
+
}
|
|
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
|
+
});
|
|
47
82
|
}
|
|
48
83
|
/**
|
|
49
84
|
* Get storage provider and config info based on storage config ID
|
|
@@ -57,13 +92,8 @@ export class UploadService {
|
|
|
57
92
|
if (!config) {
|
|
58
93
|
throw new NotFoundException('Storage configuration not found');
|
|
59
94
|
}
|
|
60
|
-
// Validate company ownership
|
|
61
|
-
|
|
62
|
-
const configWithCompany = config;
|
|
63
|
-
if (configWithCompany.companyId && configWithCompany.companyId !== user.companyId) {
|
|
64
|
-
throw new BadRequestException('Storage configuration belongs to another company');
|
|
65
|
-
}
|
|
66
|
-
}
|
|
95
|
+
// Validate company ownership using shared utility
|
|
96
|
+
validateCompanyOwnership(config, user, this.storageConfigService.isCompanyFeatureEnabled(), 'Storage configuration');
|
|
67
97
|
storageConfig = config;
|
|
68
98
|
} else {
|
|
69
99
|
// Use default config (scoped to user's company/branch)
|
|
@@ -73,13 +103,7 @@ export class UploadService {
|
|
|
73
103
|
}
|
|
74
104
|
storageConfig = defaultConfig;
|
|
75
105
|
}
|
|
76
|
-
|
|
77
|
-
const providerConfig = {
|
|
78
|
-
provider: storageConfig.storage,
|
|
79
|
-
config: storageConfig.config
|
|
80
|
-
};
|
|
81
|
-
// Get or create provider instance
|
|
82
|
-
const provider = await this.storageFactory.createProvider(providerConfig);
|
|
106
|
+
const provider = await this.createProviderFromConfig(storageConfig);
|
|
83
107
|
return {
|
|
84
108
|
provider,
|
|
85
109
|
location: storageConfig.storage,
|
|
@@ -87,74 +111,33 @@ export class UploadService {
|
|
|
87
111
|
};
|
|
88
112
|
}
|
|
89
113
|
/**
|
|
90
|
-
* Get storage provider based on storage config ID (convenience method)
|
|
91
|
-
*/ async getStorageProvider(storageConfigId, user) {
|
|
92
|
-
const { provider } = await this.getStorageProviderWithConfig(storageConfigId, user);
|
|
93
|
-
return provider;
|
|
94
|
-
}
|
|
95
|
-
/**
|
|
96
114
|
* Get storage provider for delete operations with fallback
|
|
97
115
|
* If the original storage config doesn't exist, falls back based on locationHint
|
|
98
|
-
* This ensures files can still be deleted even if storage config was removed
|
|
99
|
-
* @param storageConfigId - The storage config ID to look up
|
|
100
|
-
* @param user - User context for company filtering
|
|
101
|
-
* @param locationHint - The file's original location type (used for fallback)
|
|
102
116
|
*/ async getStorageProviderForDelete(storageConfigId, user, locationHint) {
|
|
103
|
-
//
|
|
117
|
+
// Try to find by storageConfigId
|
|
104
118
|
if (storageConfigId) {
|
|
105
119
|
const config = await this.storageProviderConfigService.findByIdDirect(storageConfigId);
|
|
106
120
|
if (config) {
|
|
107
|
-
|
|
108
|
-
provider: config.storage,
|
|
109
|
-
config: config.config
|
|
110
|
-
};
|
|
111
|
-
return await this.storageFactory.createProvider(providerConfig);
|
|
121
|
+
return this.createProviderFromConfig(config);
|
|
112
122
|
}
|
|
113
|
-
|
|
114
|
-
this.logger.warn(`Storage config ${storageConfigId} not found, trying fallback for delete`);
|
|
123
|
+
this.logger.warn(`Storage config ${storageConfigId} not found, trying fallback`);
|
|
115
124
|
}
|
|
116
|
-
// Fallback: Use locationHint to find a matching config
|
|
125
|
+
// Fallback: Use locationHint to find a matching config
|
|
117
126
|
if (locationHint) {
|
|
118
|
-
// Try to find a config matching the file's original location type
|
|
119
127
|
const matchingConfigs = await this.storageProviderConfigService.getConfigByType(locationHint, user);
|
|
120
128
|
if (matchingConfigs.length > 0) {
|
|
121
|
-
|
|
122
|
-
provider: matchingConfigs[0].storage,
|
|
123
|
-
config: matchingConfigs[0].config
|
|
124
|
-
};
|
|
125
|
-
this.logger.debug(`Using matching ${locationHint} config for delete fallback`);
|
|
126
|
-
return await this.storageFactory.createProvider(providerConfig);
|
|
129
|
+
return this.createProviderFromConfig(matchingConfigs[0]);
|
|
127
130
|
}
|
|
128
|
-
// No matching config found, create a basic provider based on locationHint
|
|
129
131
|
if (locationHint === FileLocationEnum.LOCAL) {
|
|
130
|
-
this.
|
|
131
|
-
const localProviderConfig = {
|
|
132
|
-
provider: FileLocationEnum.LOCAL,
|
|
133
|
-
config: {
|
|
134
|
-
basePath: this.storageConfigService.getDefaultLocalStoragePath()
|
|
135
|
-
}
|
|
136
|
-
};
|
|
137
|
-
return await this.storageFactory.createProvider(localProviderConfig);
|
|
132
|
+
return this.createFallbackLocalProvider();
|
|
138
133
|
}
|
|
139
134
|
}
|
|
140
|
-
// Last resort: Try default config
|
|
135
|
+
// Last resort: Try default config
|
|
141
136
|
const defaultConfig = await this.storageProviderConfigService.getDefaultConfig(user);
|
|
142
137
|
if (defaultConfig) {
|
|
143
|
-
|
|
144
|
-
provider: defaultConfig.storage,
|
|
145
|
-
config: defaultConfig.config
|
|
146
|
-
};
|
|
147
|
-
return await this.storageFactory.createProvider(providerConfig);
|
|
138
|
+
return this.createProviderFromConfig(defaultConfig);
|
|
148
139
|
}
|
|
149
|
-
|
|
150
|
-
this.logger.warn('No storage config found, using fallback local provider for delete');
|
|
151
|
-
const localProviderConfig = {
|
|
152
|
-
provider: FileLocationEnum.LOCAL,
|
|
153
|
-
config: {
|
|
154
|
-
basePath: this.storageConfigService.getDefaultLocalStoragePath()
|
|
155
|
-
}
|
|
156
|
-
};
|
|
157
|
-
return await this.storageFactory.createProvider(localProviderConfig);
|
|
140
|
+
return this.createFallbackLocalProvider();
|
|
158
141
|
}
|
|
159
142
|
async uploadSingleFile(file, options, user) {
|
|
160
143
|
try {
|
|
@@ -168,9 +151,9 @@ export class UploadService {
|
|
|
168
151
|
location,
|
|
169
152
|
storageConfigId: configId
|
|
170
153
|
};
|
|
171
|
-
} catch (
|
|
172
|
-
this.logger
|
|
173
|
-
|
|
154
|
+
} catch (error) {
|
|
155
|
+
ErrorHandler.logError(this.logger, error, 'uploadSingleFile');
|
|
156
|
+
ErrorHandler.rethrowError(error);
|
|
174
157
|
}
|
|
175
158
|
}
|
|
176
159
|
async uploadMultipleFiles(files, options, user) {
|
|
@@ -187,31 +170,31 @@ export class UploadService {
|
|
|
187
170
|
location,
|
|
188
171
|
storageConfigId: configId
|
|
189
172
|
}));
|
|
190
|
-
} catch (
|
|
191
|
-
this.logger
|
|
192
|
-
|
|
173
|
+
} catch (error) {
|
|
174
|
+
ErrorHandler.logError(this.logger, error, 'uploadMultipleFiles');
|
|
175
|
+
ErrorHandler.rethrowError(error);
|
|
193
176
|
}
|
|
194
177
|
}
|
|
195
178
|
async deleteSingleFile(key, storageConfigId, user, locationHint) {
|
|
196
179
|
try {
|
|
197
|
-
if (!key) throw new
|
|
180
|
+
if (!key) throw new BadRequestException('No file path provided');
|
|
198
181
|
const provider = await this.getStorageProviderForDelete(storageConfigId, user, locationHint);
|
|
199
182
|
await provider.deleteFile(key);
|
|
200
183
|
return true;
|
|
201
|
-
} catch (
|
|
202
|
-
this.logger
|
|
203
|
-
|
|
184
|
+
} catch (error) {
|
|
185
|
+
ErrorHandler.logError(this.logger, error, 'deleteSingleFile');
|
|
186
|
+
ErrorHandler.rethrowError(error);
|
|
204
187
|
}
|
|
205
188
|
}
|
|
206
189
|
async deleteMultipleFile(keys, storageConfigId, user, locationHint) {
|
|
207
190
|
try {
|
|
208
|
-
if (!keys || !keys.length) throw new
|
|
191
|
+
if (!keys || !keys.length) throw new BadRequestException('No file paths provided');
|
|
209
192
|
const provider = await this.getStorageProviderForDelete(storageConfigId, user, locationHint);
|
|
210
193
|
await provider.deleteMultipleFiles(keys);
|
|
211
194
|
return true;
|
|
212
|
-
} catch (
|
|
213
|
-
this.logger
|
|
214
|
-
|
|
195
|
+
} catch (error) {
|
|
196
|
+
ErrorHandler.logError(this.logger, error, 'deleteMultipleFiles');
|
|
197
|
+
ErrorHandler.rethrowError(error);
|
|
215
198
|
}
|
|
216
199
|
}
|
|
217
200
|
bytesToKb(bytes) {
|
|
@@ -241,7 +224,8 @@ export class UploadService {
|
|
|
241
224
|
}
|
|
242
225
|
return null;
|
|
243
226
|
} catch (error) {
|
|
244
|
-
|
|
227
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
228
|
+
this.logger.warn(`Failed to get local storage basePath: ${errorMessage}`);
|
|
245
229
|
return null;
|
|
246
230
|
}
|
|
247
231
|
}
|
|
@@ -251,16 +235,16 @@ export class UploadService {
|
|
|
251
235
|
* For Local/SFTP: returns direct path
|
|
252
236
|
*/ async makeFileUrl(key, storageConfigId, expiresIn = 3600, user) {
|
|
253
237
|
try {
|
|
254
|
-
const provider = await this.
|
|
238
|
+
const { provider } = await this.getStorageProviderWithConfig(storageConfigId, user);
|
|
255
239
|
// Check if provider supports presigned URLs
|
|
256
240
|
if (provider.generatePresignedUrl) {
|
|
257
241
|
return await provider.generatePresignedUrl(key, expiresIn);
|
|
258
242
|
}
|
|
259
243
|
// For SFTP or other providers without presigned URLs
|
|
260
244
|
return key;
|
|
261
|
-
} catch (
|
|
262
|
-
this.logger
|
|
263
|
-
|
|
245
|
+
} catch (error) {
|
|
246
|
+
ErrorHandler.logError(this.logger, error, 'makeFileUrl');
|
|
247
|
+
ErrorHandler.rethrowError(error);
|
|
264
248
|
}
|
|
265
249
|
}
|
|
266
250
|
// NOTE: @Inject() required for bundled code - type metadata may be lost during esbuild
|