@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
|
@@ -20,13 +20,29 @@ function _ts_decorate(decorators, target, key, desc) {
|
|
|
20
20
|
function _ts_metadata(k, v) {
|
|
21
21
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
22
22
|
}
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
export class Folder extends
|
|
23
|
+
import { Identity } from '@flusys/nestjs-shared/entities';
|
|
24
|
+
import { Column, Entity, OneToMany } from 'typeorm';
|
|
25
|
+
export class Folder extends Identity {
|
|
26
26
|
constructor(...args){
|
|
27
|
-
super(...args), _define_property(this, "fileManager", void 0);
|
|
27
|
+
super(...args), _define_property(this, "name", void 0), _define_property(this, "slug", void 0), _define_property(this, "fileManager", void 0);
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
|
+
_ts_decorate([
|
|
31
|
+
Column({
|
|
32
|
+
type: 'varchar',
|
|
33
|
+
length: 255,
|
|
34
|
+
nullable: false
|
|
35
|
+
}),
|
|
36
|
+
_ts_metadata("design:type", String)
|
|
37
|
+
], Folder.prototype, "name", void 0);
|
|
38
|
+
_ts_decorate([
|
|
39
|
+
Column({
|
|
40
|
+
type: 'varchar',
|
|
41
|
+
length: 255,
|
|
42
|
+
nullable: false
|
|
43
|
+
}),
|
|
44
|
+
_ts_metadata("design:type", String)
|
|
45
|
+
], Folder.prototype, "slug", void 0);
|
|
30
46
|
_ts_decorate([
|
|
31
47
|
OneToMany('FileManager', (fileManager)=>fileManager.folder),
|
|
32
48
|
_ts_metadata("design:type", Array)
|
package/fesm/entities/index.js
CHANGED
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
// Base entities
|
|
2
|
-
export * from './file-manager-base.entity';
|
|
3
|
-
export * from './folder-base.entity';
|
|
4
|
-
export * from './storage-config-base.entity';
|
|
5
1
|
// Entities without company feature
|
|
6
2
|
export * from './file-manager.entity';
|
|
7
3
|
export * from './folder.entity';
|
|
@@ -28,14 +24,10 @@ export const StorageCompanyEntities = [
|
|
|
28
24
|
FolderWithCompany,
|
|
29
25
|
StorageConfigWithCompany
|
|
30
26
|
];
|
|
31
|
-
|
|
32
|
-
export const StorageAllEntities = [
|
|
33
|
-
...StorageCoreEntities,
|
|
34
|
-
...StorageCompanyEntities
|
|
35
|
-
];
|
|
36
|
-
/**
|
|
37
|
-
* Get Storage entities based on configuration
|
|
38
|
-
* @param enableCompanyFeature - Whether company feature is enabled
|
|
39
|
-
*/ export function getStorageEntitiesByConfig(enableCompanyFeature) {
|
|
27
|
+
export function getStorageEntitiesByConfig(enableCompanyFeature) {
|
|
40
28
|
return enableCompanyFeature ? StorageCompanyEntities : StorageCoreEntities;
|
|
41
29
|
}
|
|
30
|
+
// Base type aliases for backwards compatibility with services
|
|
31
|
+
export { FileManager as FileManagerBase } from './file-manager.entity';
|
|
32
|
+
export { Folder as FolderBase } from './folder.entity';
|
|
33
|
+
export { StorageConfig as StorageConfigBase } from './storage-config.entity';
|
|
@@ -21,11 +21,10 @@ function _ts_metadata(k, v) {
|
|
|
21
21
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
22
22
|
}
|
|
23
23
|
import { Column, Entity } from 'typeorm';
|
|
24
|
-
import {
|
|
25
|
-
export class StorageConfigWithCompany extends
|
|
24
|
+
import { StorageConfig } from './storage-config.entity';
|
|
25
|
+
export class StorageConfigWithCompany extends StorageConfig {
|
|
26
26
|
constructor(...args){
|
|
27
|
-
super(...args),
|
|
28
|
-
_define_property(this, "companyId", void 0);
|
|
27
|
+
super(...args), _define_property(this, "companyId", void 0);
|
|
29
28
|
}
|
|
30
29
|
}
|
|
31
30
|
_ts_decorate([
|
|
@@ -1,15 +1,85 @@
|
|
|
1
|
+
function _define_property(obj, key, value) {
|
|
2
|
+
if (key in obj) {
|
|
3
|
+
Object.defineProperty(obj, key, {
|
|
4
|
+
value: value,
|
|
5
|
+
enumerable: true,
|
|
6
|
+
configurable: true,
|
|
7
|
+
writable: true
|
|
8
|
+
});
|
|
9
|
+
} else {
|
|
10
|
+
obj[key] = value;
|
|
11
|
+
}
|
|
12
|
+
return obj;
|
|
13
|
+
}
|
|
1
14
|
function _ts_decorate(decorators, target, key, desc) {
|
|
2
15
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
16
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
17
|
else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
18
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
19
|
}
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
20
|
+
function _ts_metadata(k, v) {
|
|
21
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
22
|
+
}
|
|
23
|
+
import { Identity } from '@flusys/nestjs-shared/entities';
|
|
24
|
+
import { Column, Entity, Index } from 'typeorm';
|
|
25
|
+
export class StorageConfig extends Identity {
|
|
26
|
+
constructor(...args){
|
|
27
|
+
super(...args), _define_property(this, "name", void 0), _define_property(this, "storage", void 0), _define_property(this, "config", void 0), _define_property(this, "isActive", void 0), _define_property(this, "isDefault", void 0);
|
|
28
|
+
}
|
|
10
29
|
}
|
|
30
|
+
_ts_decorate([
|
|
31
|
+
Column({
|
|
32
|
+
type: 'varchar',
|
|
33
|
+
length: 255,
|
|
34
|
+
nullable: false
|
|
35
|
+
}),
|
|
36
|
+
_ts_metadata("design:type", String)
|
|
37
|
+
], StorageConfig.prototype, "name", void 0);
|
|
38
|
+
_ts_decorate([
|
|
39
|
+
Column({
|
|
40
|
+
type: 'varchar',
|
|
41
|
+
length: 50,
|
|
42
|
+
nullable: false
|
|
43
|
+
}),
|
|
44
|
+
_ts_metadata("design:type", String)
|
|
45
|
+
], StorageConfig.prototype, "storage", void 0);
|
|
46
|
+
_ts_decorate([
|
|
47
|
+
Column({
|
|
48
|
+
type: 'json',
|
|
49
|
+
nullable: false
|
|
50
|
+
}),
|
|
51
|
+
_ts_metadata("design:type", typeof Record === "undefined" ? Object : Record)
|
|
52
|
+
], StorageConfig.prototype, "config", void 0);
|
|
53
|
+
_ts_decorate([
|
|
54
|
+
Column({
|
|
55
|
+
type: 'boolean',
|
|
56
|
+
default: true,
|
|
57
|
+
name: 'is_active'
|
|
58
|
+
}),
|
|
59
|
+
_ts_metadata("design:type", Boolean)
|
|
60
|
+
], StorageConfig.prototype, "isActive", void 0);
|
|
61
|
+
_ts_decorate([
|
|
62
|
+
Column({
|
|
63
|
+
type: 'boolean',
|
|
64
|
+
default: false,
|
|
65
|
+
name: 'is_default'
|
|
66
|
+
}),
|
|
67
|
+
_ts_metadata("design:type", Boolean)
|
|
68
|
+
], StorageConfig.prototype, "isDefault", void 0);
|
|
11
69
|
StorageConfig = _ts_decorate([
|
|
12
70
|
Entity({
|
|
13
71
|
name: 'storage_config'
|
|
14
|
-
})
|
|
72
|
+
}),
|
|
73
|
+
Index([
|
|
74
|
+
'name'
|
|
75
|
+
]),
|
|
76
|
+
Index([
|
|
77
|
+
'storage'
|
|
78
|
+
]),
|
|
79
|
+
Index([
|
|
80
|
+
'isActive'
|
|
81
|
+
]),
|
|
82
|
+
Index([
|
|
83
|
+
'isDefault'
|
|
84
|
+
])
|
|
15
85
|
], StorageConfig);
|
package/fesm/interfaces/index.js
CHANGED
|
@@ -20,132 +20,145 @@ function _ts_decorate(decorators, target, key, desc) {
|
|
|
20
20
|
function _ts_metadata(k, v) {
|
|
21
21
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
22
22
|
}
|
|
23
|
-
|
|
23
|
+
function _ts_param(paramIndex, decorator) {
|
|
24
|
+
return function(target, key) {
|
|
25
|
+
decorator(target, key, paramIndex);
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
import { Injectable, Logger, NotFoundException, Inject } from '@nestjs/common';
|
|
24
29
|
import { createReadStream, existsSync, statSync } from 'fs';
|
|
25
30
|
import { join, resolve } from 'path';
|
|
26
31
|
import * as mime from 'mime-types';
|
|
27
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
|
+
];
|
|
28
42
|
export class FileServeMiddleware {
|
|
29
43
|
async use(req, res, next) {
|
|
30
44
|
try {
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
const urlPath = req.path || req.url;
|
|
34
|
-
const match = urlPath.match(/\/storage\/upload\/file\/(.+)/);
|
|
35
|
-
const normalizedPath = match ? match[1] : '';
|
|
36
|
-
if (!normalizedPath) {
|
|
37
|
-
throw new NotFoundException('File path is required');
|
|
38
|
-
}
|
|
39
|
-
this.logger.debug(`Attempting to serve file, normalizedPath: ${normalizedPath}`);
|
|
40
|
-
// Get the local storage basePath for fallback lookups
|
|
41
|
-
const basePath = await this.uploadService.getLocalStorageBasePath();
|
|
42
|
-
const normalizedBasePath = basePath ? basePath.replace(/^\.\//, '').replace(/\/$/, '') : null;
|
|
43
|
-
// Strategy 1: Try path relative to CWD (new format: key includes basePath)
|
|
44
|
-
let fullPath = join(uploadDir, normalizedPath);
|
|
45
|
-
let resolvedPath = resolve(fullPath);
|
|
46
|
-
this.logger.debug(`Strategy 1 - CWD relative: ${fullPath}`);
|
|
47
|
-
// Validate path is inside the project directory (prevent path traversal)
|
|
48
|
-
if (!resolvedPath.startsWith(uploadDir)) {
|
|
49
|
-
throw new NotFoundException('Invalid file path');
|
|
50
|
-
}
|
|
51
|
-
// Check if file exists
|
|
52
|
-
if (!existsSync(fullPath)) {
|
|
53
|
-
// Strategy 2: If key already starts with basePath, try using basePath directly
|
|
54
|
-
if (basePath && normalizedBasePath) {
|
|
55
|
-
const pathStartsWithBase = normalizedPath.startsWith(normalizedBasePath + '/') || normalizedPath.startsWith(normalizedBasePath);
|
|
56
|
-
if (pathStartsWithBase) {
|
|
57
|
-
const remainingPath = normalizedPath.substring(normalizedBasePath.length).replace(/^\//, '');
|
|
58
|
-
const fallbackPath = join(basePath, remainingPath);
|
|
59
|
-
this.logger.debug(`Strategy 2 - basePath + remaining: ${fallbackPath}`);
|
|
60
|
-
if (existsSync(fallbackPath)) {
|
|
61
|
-
fullPath = fallbackPath;
|
|
62
|
-
resolvedPath = resolve(fullPath);
|
|
63
|
-
}
|
|
64
|
-
} else {
|
|
65
|
-
// Old format: key doesn't include basePath, prepend it
|
|
66
|
-
const fallbackPath = join(basePath, normalizedPath);
|
|
67
|
-
this.logger.debug(`Strategy 3 - basePath + full key (old format): ${fallbackPath}`);
|
|
68
|
-
if (existsSync(fallbackPath)) {
|
|
69
|
-
fullPath = fallbackPath;
|
|
70
|
-
resolvedPath = resolve(fullPath);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
// Final check if file was found
|
|
75
|
-
if (!existsSync(fullPath)) {
|
|
76
|
-
throw new NotFoundException(`File not found: ${normalizedPath}`);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
45
|
+
const normalizedPath = this.extractFilePath(req);
|
|
46
|
+
const fullPath = await this.resolveFilePath(normalizedPath);
|
|
79
47
|
const stats = statSync(fullPath);
|
|
80
48
|
if (!stats.isFile()) {
|
|
81
49
|
throw new NotFoundException(`Not a file: ${normalizedPath}`);
|
|
82
50
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
if (fullPath.toLowerCase().endsWith('.svg')) {
|
|
87
|
-
mimeType = 'image/svg+xml';
|
|
88
|
-
}
|
|
89
|
-
// Determine if file should be displayed inline or downloaded
|
|
90
|
-
const viewableTypes = [
|
|
91
|
-
'image/',
|
|
92
|
-
'video/',
|
|
93
|
-
'audio/',
|
|
94
|
-
'text/',
|
|
95
|
-
'application/pdf',
|
|
96
|
-
'application/json',
|
|
97
|
-
'application/xml'
|
|
98
|
-
];
|
|
99
|
-
const isViewable = viewableTypes.some((type)=>mimeType.startsWith(type));
|
|
100
|
-
const contentDisposition = isViewable ? 'inline' : `attachment; filename="${encodeURIComponent(normalizedPath.split('/').pop() || 'download')}"`;
|
|
101
|
-
// Set headers
|
|
102
|
-
res.set({
|
|
103
|
-
'Content-Type': mimeType,
|
|
104
|
-
'Content-Length': stats.size,
|
|
105
|
-
'Content-Disposition': contentDisposition,
|
|
106
|
-
'Cache-Control': 'public, max-age=3600',
|
|
107
|
-
'Accept-Ranges': 'bytes',
|
|
108
|
-
'Cross-Origin-Resource-Policy': 'cross-origin',
|
|
109
|
-
'Access-Control-Allow-Origin': '*',
|
|
110
|
-
'X-Content-Type-Options': 'nosniff'
|
|
111
|
-
});
|
|
112
|
-
// Stream the file
|
|
113
|
-
const stream = createReadStream(fullPath);
|
|
114
|
-
stream.pipe(res);
|
|
115
|
-
stream.on('error', (err)=>{
|
|
116
|
-
this.logger.error('File stream error', err);
|
|
117
|
-
if (!res.headersSent) {
|
|
118
|
-
res.status(404).json({
|
|
119
|
-
message: `Failed to serve file: ${normalizedPath}`,
|
|
120
|
-
error: 'Not Found',
|
|
121
|
-
statusCode: 404
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
});
|
|
125
|
-
// Handle client disconnection
|
|
126
|
-
res.on('close', ()=>{
|
|
127
|
-
stream.destroy();
|
|
128
|
-
});
|
|
51
|
+
const mimeType = this.getMimeType(fullPath);
|
|
52
|
+
this.setResponseHeaders(res, stats.size, mimeType, normalizedPath);
|
|
53
|
+
this.streamFile(res, fullPath, normalizedPath);
|
|
129
54
|
} catch (error) {
|
|
130
|
-
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);
|
|
131
133
|
if (!res.headersSent) {
|
|
132
|
-
|
|
133
|
-
message: error instanceof NotFoundException ? error.message : `File not found`,
|
|
134
|
-
error: 'Not Found',
|
|
135
|
-
statusCode: 404
|
|
136
|
-
});
|
|
134
|
+
this.sendErrorResponse(res, new NotFoundException(`Failed to serve file: ${normalizedPath}`));
|
|
137
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
|
+
});
|
|
138
148
|
}
|
|
139
149
|
}
|
|
140
150
|
constructor(uploadService){
|
|
141
151
|
_define_property(this, "uploadService", void 0);
|
|
142
152
|
_define_property(this, "logger", void 0);
|
|
153
|
+
_define_property(this, "uploadDir", void 0);
|
|
143
154
|
this.uploadService = uploadService;
|
|
144
155
|
this.logger = new Logger(FileServeMiddleware.name);
|
|
156
|
+
this.uploadDir = process.cwd();
|
|
145
157
|
}
|
|
146
158
|
}
|
|
147
159
|
FileServeMiddleware = _ts_decorate([
|
|
148
160
|
Injectable(),
|
|
161
|
+
_ts_param(0, Inject(UploadService)),
|
|
149
162
|
_ts_metadata("design:type", Function),
|
|
150
163
|
_ts_metadata("design:paramtypes", [
|
|
151
164
|
typeof UploadService === "undefined" ? Object : UploadService
|