@ackplus/nest-file-storage 1.1.2 → 1.1.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ackplus/nest-file-storage",
3
- "version": "1.1.2",
3
+ "version": "1.1.6",
4
4
  "type": "commonjs",
5
5
  "main": "./src/index.js",
6
6
  "types": "./src/index.d.ts",
package/src/index.js ADDED
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ tslib_1.__exportStar(require("./lib/nest-file-storage.module"), exports);
5
+ tslib_1.__exportStar(require("./lib/file-storage.service"), exports);
6
+ tslib_1.__exportStar(require("./lib/interceptor/file-storage.interceptor"), exports);
7
+ tslib_1.__exportStar(require("./lib/types"), exports);
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FILE_STORAGE_OPTIONS = void 0;
4
+ exports.FILE_STORAGE_OPTIONS = 'FileStorageOptions';
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FileStorageService = void 0;
4
+ const storage_factory_1 = require("./storage.factory");
5
+ class FileStorageService {
6
+ static setOptions(options) {
7
+ FileStorageService.options = options;
8
+ }
9
+ static getOptions() {
10
+ return FileStorageService.options;
11
+ }
12
+ static async getStorage(storageType) {
13
+ const options = this.getOptions();
14
+ // Check if it's a class factory approach
15
+ if ('storageFactory' in options) {
16
+ const classOptions = options;
17
+ const StorageClass = await classOptions.storageFactory();
18
+ return new StorageClass(classOptions.options);
19
+ }
20
+ // Configuration-based approach
21
+ const configOptions = options;
22
+ if (!storageType) {
23
+ storageType = configOptions.storage;
24
+ }
25
+ const config = configOptions[`Config`];
26
+ return await storage_factory_1.StorageFactory.createStorage(storageType, config);
27
+ }
28
+ }
29
+ exports.FileStorageService = FileStorageService;
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ tslib_1.__exportStar(require("./nest-file-storage.module"), exports);
5
+ tslib_1.__exportStar(require("./types"), exports);
6
+ tslib_1.__exportStar(require("./storage.factory"), exports);
7
+ tslib_1.__exportStar(require("./interceptor/file-storage.interceptor"), exports);
8
+ tslib_1.__exportStar(require("./file-storage.service"), exports);
@@ -0,0 +1,138 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FileStorageInterceptor = FileStorageInterceptor;
4
+ const tslib_1 = require("tslib");
5
+ const multer_1 = tslib_1.__importDefault(require("multer"));
6
+ const file_storage_service_1 = require("../file-storage.service");
7
+ const storage_factory_1 = require("../storage.factory");
8
+ const types_1 = require("../types");
9
+ // Helper function to map file object
10
+ function mapFileObject(file) {
11
+ return {
12
+ fieldName: file.fieldname,
13
+ originalName: file.originalname,
14
+ fileName: file.filename,
15
+ mimetype: file.mimetype,
16
+ size: file.size,
17
+ key: file.key,
18
+ path: file.path,
19
+ url: file.url,
20
+ encoding: file.encoding,
21
+ fullPath: file.fullPath
22
+ };
23
+ }
24
+ // Helper function to apply file mapping with callback
25
+ function applyFileKeyMapping(request, fileConfig, interceptorOptions) {
26
+ // Default callback returns the file key
27
+ const mapCallback = interceptorOptions?.mapToRequestBody || ((file) => {
28
+ // For arrays, return array of keys
29
+ if (Array.isArray(file)) {
30
+ return file.map(f => f.key);
31
+ }
32
+ // For single file, return the key
33
+ return file.key;
34
+ });
35
+ if (fileConfig.type === 'single') {
36
+ const file = request.file;
37
+ if (file) {
38
+ const fieldName = fileConfig.fieldName || 'file';
39
+ const mappedFile = mapFileObject(file);
40
+ request.body[fieldName] = mapCallback(mappedFile, fieldName, request);
41
+ }
42
+ }
43
+ else if (fileConfig.type === 'array') {
44
+ const files = request.files;
45
+ if (files && files.length > 0) {
46
+ const fieldName = fileConfig.fieldName || 'files';
47
+ const mappedFiles = files.map(file => mapFileObject(file));
48
+ request.body[fieldName] = mapCallback(mappedFiles, fieldName, request);
49
+ }
50
+ }
51
+ else if (fileConfig.type === 'fields') {
52
+ const files = request.files;
53
+ if (files) {
54
+ Object.keys(files).forEach(fieldName => {
55
+ const mappedFiles = files[fieldName].map(file => mapFileObject(file));
56
+ request.body[fieldName] = mapCallback(mappedFiles, fieldName, request);
57
+ });
58
+ }
59
+ }
60
+ }
61
+ /**
62
+ * Function-based interceptor that accepts storage options dynamically.
63
+ */
64
+ function FileStorageInterceptor(fileConfig, interceptorOptions) {
65
+ if (typeof fileConfig === 'string') {
66
+ fileConfig = {
67
+ type: 'single',
68
+ fieldName: fileConfig,
69
+ };
70
+ }
71
+ return {
72
+ async intercept(context, next) {
73
+ const options = file_storage_service_1.FileStorageService.getOptions();
74
+ const request = context.switchToHttp().getRequest();
75
+ const response = context.switchToHttp().getResponse();
76
+ // Determine storage type - handle both config approaches
77
+ let storageType;
78
+ let storageConfig;
79
+ if ('storage' in options) {
80
+ // Configuration-based approach
81
+ const configOptions = options;
82
+ storageType = interceptorOptions?.storageType ?? configOptions.storage;
83
+ storageConfig = configOptions[`${storageType}Config`];
84
+ }
85
+ else {
86
+ // Class factory approach - default to LOCAL
87
+ storageType = interceptorOptions?.storageType ?? types_1.FileStorageEnum.LOCAL;
88
+ storageConfig = {};
89
+ }
90
+ const storageOptions = {
91
+ ...storageConfig,
92
+ ...(interceptorOptions?.storageOptions || {}),
93
+ fileName: interceptorOptions?.fileName || storageConfig?.fileName,
94
+ fileDist: (file, req) => {
95
+ if (interceptorOptions?.fileDist) {
96
+ return interceptorOptions.fileDist(file, req);
97
+ }
98
+ return storageConfig?.fileDist?.(file, req);
99
+ },
100
+ prefix: interceptorOptions?.prefix || storageConfig?.prefix,
101
+ };
102
+ // Create storage instance dynamically
103
+ const storage = await storage_factory_1.StorageFactory.createStorage(storageType, storageOptions);
104
+ const multerInstance = (0, multer_1.default)({ storage });
105
+ // Multer setup based on fileConfig
106
+ let multerMiddleware;
107
+ switch (fileConfig.type) {
108
+ case 'single':
109
+ if (!fileConfig.fieldName) {
110
+ throw new Error('fieldName is required for single file upload.');
111
+ }
112
+ multerMiddleware = multerInstance.single(fileConfig.fieldName);
113
+ break;
114
+ case 'array':
115
+ if (!fileConfig.fieldName) {
116
+ throw new Error('fieldName is required for multiple file upload.');
117
+ }
118
+ multerMiddleware = multerInstance.array(fileConfig.fieldName, fileConfig.maxCount);
119
+ break;
120
+ case 'fields':
121
+ if (!fileConfig.fields || !Array.isArray(fileConfig.fields)) {
122
+ throw new Error('fields array is required for multiple fields file upload.');
123
+ }
124
+ multerMiddleware = multerInstance.fields(fileConfig.fields);
125
+ break;
126
+ default:
127
+ throw new Error('Invalid file upload type. Use "single", "array", or "fields".');
128
+ }
129
+ // Execute Multer middleware
130
+ await new Promise((resolve, reject) => {
131
+ multerMiddleware(request, response, (err) => (err ? reject(err) : resolve(true)));
132
+ });
133
+ // Apply file key mapping after multer processing
134
+ applyFileKeyMapping(request, fileConfig, interceptorOptions);
135
+ return next.handle();
136
+ }
137
+ };
138
+ }
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ var NestFileStorageModule_1;
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.NestFileStorageModule = void 0;
5
+ const tslib_1 = require("tslib");
6
+ const common_1 = require("@nestjs/common");
7
+ const constants_1 = require("./constants");
8
+ const file_storage_service_1 = require("./file-storage.service");
9
+ let NestFileStorageModule = NestFileStorageModule_1 = class NestFileStorageModule {
10
+ static forRoot(options) {
11
+ return {
12
+ module: NestFileStorageModule_1,
13
+ providers: [
14
+ {
15
+ provide: constants_1.FILE_STORAGE_OPTIONS,
16
+ useFactory: async () => {
17
+ file_storage_service_1.FileStorageService.setOptions(options); // ✅ Store globally
18
+ return options;
19
+ },
20
+ inject: [],
21
+ },
22
+ file_storage_service_1.FileStorageService,
23
+ ],
24
+ exports: [],
25
+ };
26
+ }
27
+ static forRootAsync(options) {
28
+ const asyncProviders = this.createAsyncProviders(options);
29
+ return {
30
+ module: NestFileStorageModule_1,
31
+ imports: options.imports || [],
32
+ providers: [...asyncProviders, file_storage_service_1.FileStorageService],
33
+ exports: [],
34
+ };
35
+ }
36
+ static createAsyncProviders(options) {
37
+ if (options.useExisting || options.useFactory) {
38
+ return [this.createAsyncOptionsProvider(options)];
39
+ }
40
+ return [
41
+ this.createAsyncOptionsProvider(options),
42
+ {
43
+ provide: options.useClass,
44
+ useClass: options.useClass,
45
+ },
46
+ ];
47
+ }
48
+ static createAsyncOptionsProvider(options) {
49
+ if (options.useFactory) {
50
+ return {
51
+ provide: constants_1.FILE_STORAGE_OPTIONS,
52
+ useFactory: async (...args) => {
53
+ const fileStorageOptions = await options.useFactory(...args);
54
+ file_storage_service_1.FileStorageService.setOptions(fileStorageOptions);
55
+ return fileStorageOptions;
56
+ },
57
+ inject: options.inject || [],
58
+ };
59
+ }
60
+ return {
61
+ provide: constants_1.FILE_STORAGE_OPTIONS,
62
+ useFactory: async (optionsFactory) => {
63
+ const fileStorageOptions = await optionsFactory.createFileStorageOptions();
64
+ file_storage_service_1.FileStorageService.setOptions(fileStorageOptions);
65
+ return fileStorageOptions;
66
+ },
67
+ inject: [options.useExisting || options.useClass],
68
+ };
69
+ }
70
+ };
71
+ exports.NestFileStorageModule = NestFileStorageModule;
72
+ exports.NestFileStorageModule = NestFileStorageModule = NestFileStorageModule_1 = tslib_1.__decorate([
73
+ (0, common_1.Module)({})
74
+ ], NestFileStorageModule);
@@ -0,0 +1,153 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AzureStorage = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const storage_blob_1 = require("@azure/storage-blob");
6
+ const concat_stream_1 = tslib_1.__importDefault(require("concat-stream"));
7
+ const moment_1 = tslib_1.__importDefault(require("moment"));
8
+ const path_1 = tslib_1.__importStar(require("path"));
9
+ const uuid_1 = require("uuid");
10
+ class AzureStorage {
11
+ constructor(options) {
12
+ this.options = options;
13
+ this.fileNameFunction = options.fileName || ((file, _req) => {
14
+ return `${(0, uuid_1.v4)()}-${file.originalname}`;
15
+ });
16
+ this.fileDistFunction = options.fileDist || ((_file, _req) => {
17
+ return path_1.default.join('uploads', (0, moment_1.default)().format('YYYY'), (0, moment_1.default)().format('MM'), (0, moment_1.default)().format('DD'));
18
+ });
19
+ const sharedKeyCredential = new storage_blob_1.StorageSharedKeyCredential(options.account, options.accountKey);
20
+ this.blobServiceClient = new storage_blob_1.BlobServiceClient(`https://${options.account}.blob.core.windows.net`, sharedKeyCredential);
21
+ }
22
+ async _handleFile(req, file, cb) {
23
+ try {
24
+ const dist = await this.fileDistFunction(file, req);
25
+ const key = await this.fileNameFunction(file, req);
26
+ const filePath = (0, path_1.join)(dist, key);
27
+ file.stream.pipe((0, concat_stream_1.default)({ encoding: 'buffer' }, async (buffer) => {
28
+ const uploadedFile = await this.putFile(buffer, filePath);
29
+ const fileInfo = {
30
+ ...uploadedFile,
31
+ fieldName: file.fieldname,
32
+ originalName: file.originalname,
33
+ mimetype: file.mimetype,
34
+ };
35
+ let transformData = fileInfo;
36
+ if (this.options?.transformUploadedFileObject) {
37
+ transformData = await this.options.transformUploadedFileObject(fileInfo);
38
+ }
39
+ cb(null, transformData);
40
+ }));
41
+ }
42
+ catch (error) {
43
+ cb(error);
44
+ }
45
+ file.stream.on('error', (err) => cb(err));
46
+ }
47
+ async _removeFile(_req, file, cb) {
48
+ try {
49
+ const blobClient = this.blobServiceClient
50
+ .getContainerClient(this.options.container)
51
+ .getBlobClient(file.key);
52
+ await blobClient.delete();
53
+ cb(null);
54
+ }
55
+ catch (error) {
56
+ cb(error);
57
+ }
58
+ }
59
+ getUrl(key) {
60
+ return `https://${this.options.account}.blob.core.windows.net/${this.options.container}/${key}`;
61
+ }
62
+ getSignedUrl(key, signatureValues = {}) {
63
+ if (!key)
64
+ return '';
65
+ const cloudFrontDomain = process.env['AZURE_CDN_DOMAIN_NAME'];
66
+ if (cloudFrontDomain) {
67
+ return `${cloudFrontDomain}/${key}`;
68
+ }
69
+ const containerClient = this.blobServiceClient.getContainerClient(this.options.container);
70
+ const blobClient = containerClient.getBlobClient(key);
71
+ const sharedKeyCredential = new storage_blob_1.StorageSharedKeyCredential(this.options.account, this.options.accountKey);
72
+ // Set the SAS token options
73
+ const expiresOn = new Date();
74
+ expiresOn.setHours(expiresOn.getHours() + 1); // Set expiration time to 1 hour from now
75
+ const sasToken = (0, storage_blob_1.generateBlobSASQueryParameters)({
76
+ containerName: this.options.container,
77
+ blobName: key,
78
+ permissions: storage_blob_1.BlobSASPermissions.parse('r'), // Read permissions
79
+ protocol: storage_blob_1.SASProtocol.Https, // HTTPS only
80
+ startsOn: new Date(), // Start time (now)
81
+ expiresOn, // Expiration time
82
+ ...signatureValues,
83
+ }, sharedKeyCredential).toString();
84
+ return `${blobClient.url}?${sasToken}`;
85
+ }
86
+ async getFile(key) {
87
+ const containerClient = this.blobServiceClient.getContainerClient(this.options.container);
88
+ const blobClient = containerClient.getBlobClient(key);
89
+ const downloadResponse = await blobClient.download();
90
+ const stream = downloadResponse.readableStreamBody;
91
+ return new Promise((resolve, reject) => {
92
+ const chunks = [];
93
+ stream?.on('data', (chunk) => chunks.push(chunk));
94
+ stream?.on('end', () => resolve(Buffer.concat(chunks)));
95
+ stream?.on('error', reject);
96
+ });
97
+ }
98
+ async putFile(buffer, key) {
99
+ try {
100
+ const containerClient = this.blobServiceClient.getContainerClient(this.options.container);
101
+ const blockBlobClient = containerClient.getBlockBlobClient(key);
102
+ await blockBlobClient.uploadData(buffer);
103
+ const fileInfo = {
104
+ originalName: (0, path_1.basename)(key),
105
+ fileName: (0, path_1.basename)(key),
106
+ size: buffer.length,
107
+ buffer,
108
+ key,
109
+ fullPath: key,
110
+ url: this.getUrl(key),
111
+ };
112
+ return fileInfo;
113
+ }
114
+ catch (error) {
115
+ console.error(`Error uploading file "${key}":`, error);
116
+ throw error;
117
+ }
118
+ }
119
+ async deleteFile(key) {
120
+ try {
121
+ const blobClient = this.blobServiceClient
122
+ .getContainerClient(this.options.container)
123
+ .getBlobClient(key);
124
+ await blobClient.delete();
125
+ }
126
+ catch (error) {
127
+ console.error(`Error deleting blob "${key}":`, error);
128
+ }
129
+ }
130
+ async copyFile(oldKey, newKey) {
131
+ try {
132
+ const containerClient = this.blobServiceClient.getContainerClient(this.options.container);
133
+ const sourceBlobClient = containerClient.getBlobClient(oldKey);
134
+ const destinationBlobClient = containerClient.getBlobClient(newKey);
135
+ const copyPoller = await destinationBlobClient.beginCopyFromURL(sourceBlobClient.url);
136
+ await copyPoller.pollUntilDone();
137
+ const properties = await destinationBlobClient.getProperties();
138
+ return {
139
+ originalName: (0, path_1.basename)(newKey),
140
+ size: properties.contentLength || 0,
141
+ fileName: (0, path_1.basename)(newKey),
142
+ key: newKey,
143
+ fullPath: newKey,
144
+ url: this.getUrl(newKey),
145
+ };
146
+ }
147
+ catch (error) {
148
+ console.error('Error copying file in Azure Blob Storage:', error);
149
+ throw error;
150
+ }
151
+ }
152
+ }
153
+ exports.AzureStorage = AzureStorage;
@@ -0,0 +1,183 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LocalStorage = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const concat_stream_1 = tslib_1.__importDefault(require("concat-stream"));
6
+ const fs = tslib_1.__importStar(require("fs"));
7
+ const moment_1 = tslib_1.__importDefault(require("moment"));
8
+ const path_1 = require("path");
9
+ const uuid_1 = require("uuid");
10
+ class LocalStorage {
11
+ /**
12
+ * Convert OS-specific file path to URL-friendly key
13
+ * This ensures keys are consistent across all platforms
14
+ * Windows: C:\uploads\2024\01\file.jpg -> 2024/01/file.jpg
15
+ * Unix: /uploads/2024/01/file.jpg -> 2024/01/file.jpg
16
+ */
17
+ pathToUrl(filePath) {
18
+ if (!filePath)
19
+ return '';
20
+ // Remove rootPath if present
21
+ let relativePath = filePath;
22
+ if (filePath.startsWith(this.rootPath)) {
23
+ relativePath = filePath.substring(this.rootPath.length);
24
+ }
25
+ // Convert backslashes to forward slashes and remove leading slash
26
+ return relativePath.replace(/\\/g, '/').replace(/^\/+/, '');
27
+ }
28
+ /**
29
+ * Convert URL-friendly key to OS-specific file path
30
+ * This converts stored keys back to valid file system paths
31
+ * 2024/01/file.jpg -> Windows: 2024\01\file.jpg, Unix: 2024/01/file.jpg
32
+ */
33
+ urlToPath(urlKey) {
34
+ if (!urlKey)
35
+ return '';
36
+ // Split by forward slashes and rejoin with OS-specific separator
37
+ const parts = urlKey.split('/').filter(part => part.length > 0);
38
+ return parts.join(path_1.sep);
39
+ }
40
+ /**
41
+ * Get full file system path from URL-friendly key
42
+ */
43
+ getFullPath(urlKey) {
44
+ const osPath = this.urlToPath(urlKey);
45
+ return (0, path_1.join)(this.rootPath, osPath);
46
+ }
47
+ constructor(options) {
48
+ this.options = options;
49
+ // Normalize path for the current OS
50
+ this.rootPath = (0, path_1.normalize)(options.rootPath || (0, path_1.join)(process.cwd(), 'public'));
51
+ this.fileNameFunction = options.fileName || ((file, _req) => {
52
+ return `${(0, uuid_1.v4)()}-${file.originalname}`;
53
+ });
54
+ this.fileDistFunction = options.fileDist || ((_file, _req) => {
55
+ return (0, path_1.join)(this.rootPath, (0, moment_1.default)().format('YYYY'), (0, moment_1.default)().format('MM'), (0, moment_1.default)().format('DD'));
56
+ });
57
+ // Ensure the rootPath directory exists
58
+ if (!fs.existsSync(this.rootPath)) {
59
+ fs.mkdirSync(this.rootPath, { recursive: true });
60
+ }
61
+ }
62
+ async _handleFile(req, file, cb) {
63
+ try {
64
+ const dist = await this.fileDistFunction(file, req);
65
+ const fileName = await this.fileNameFunction(file, req);
66
+ const filePath = (0, path_1.join)(dist, fileName);
67
+ // Convert to URL-friendly key for storage
68
+ const urlKey = this.pathToUrl(filePath);
69
+ file.stream.pipe((0, concat_stream_1.default)({ encoding: 'buffer' }, async (buffer) => {
70
+ const uploadedFile = await this.putFile(buffer, urlKey);
71
+ const fileInfo = {
72
+ ...uploadedFile,
73
+ fieldName: file.fieldname,
74
+ originalName: file.originalname,
75
+ mimetype: file.mimetype,
76
+ };
77
+ let transformData = fileInfo;
78
+ if (this.options?.transformUploadedFileObject) {
79
+ transformData = await this.options.transformUploadedFileObject(fileInfo);
80
+ }
81
+ cb(null, transformData);
82
+ }));
83
+ }
84
+ catch (error) {
85
+ console.error('error', error);
86
+ cb(error);
87
+ }
88
+ }
89
+ _removeFile(_req, file, cb) {
90
+ const filePath = file.path;
91
+ fs.unlink(filePath, (err) => {
92
+ if (err) {
93
+ cb(err);
94
+ }
95
+ else {
96
+ cb(null);
97
+ }
98
+ });
99
+ }
100
+ getUrl(urlKey) {
101
+ if (urlKey && urlKey.startsWith('http')) {
102
+ return urlKey;
103
+ }
104
+ if (!urlKey) {
105
+ return '';
106
+ }
107
+ // Key is already in URL format (forward slashes)
108
+ // Ensure baseUrl doesn't end with slash and key doesn't start with slash
109
+ const baseUrl = this.options.baseUrl.replace(/\/$/, '');
110
+ const cleanKey = urlKey.replace(/^\//, '');
111
+ return `${baseUrl}/${cleanKey}`;
112
+ }
113
+ async getFile(urlKey) {
114
+ // Convert URL key to OS-specific path
115
+ const fullPath = this.getFullPath(urlKey);
116
+ return fs.promises.readFile(fullPath);
117
+ }
118
+ async deleteFile(urlKey) {
119
+ // Convert URL key to OS-specific path
120
+ const fullPath = this.getFullPath(urlKey);
121
+ return fs.promises.unlink(fullPath);
122
+ }
123
+ async putFile(fileContent, urlKey) {
124
+ return new Promise((putFileResolve, reject) => {
125
+ // Convert URL key to OS-specific path
126
+ const filePath = this.getFullPath(urlKey);
127
+ const directoryPath = (0, path_1.dirname)(filePath);
128
+ // Create the directory if it doesn't exist
129
+ fs.mkdirSync(directoryPath, { recursive: true });
130
+ fs.writeFile(filePath, fileContent, (err) => {
131
+ if (err) {
132
+ reject(err);
133
+ return;
134
+ }
135
+ const stats = fs.statSync(filePath);
136
+ const fileName = urlKey.split('/').pop() || urlKey;
137
+ const fileInfo = {
138
+ originalName: fileName,
139
+ size: stats.size,
140
+ fileName: fileName,
141
+ key: urlKey,
142
+ url: this.getUrl(urlKey),
143
+ fullPath: filePath,
144
+ };
145
+ putFileResolve(fileInfo);
146
+ });
147
+ });
148
+ }
149
+ path(urlKey) {
150
+ if (!urlKey) {
151
+ return '';
152
+ }
153
+ // Convert URL key to full OS-specific path
154
+ return this.getFullPath(urlKey);
155
+ }
156
+ async copyFile(oldUrlKey, newUrlKey) {
157
+ return new Promise((resolve, reject) => {
158
+ // Convert URL keys to OS-specific paths
159
+ const oldPath = this.getFullPath(oldUrlKey);
160
+ const newPath = this.getFullPath(newUrlKey);
161
+ const directoryPath = (0, path_1.dirname)(newPath);
162
+ fs.mkdirSync(directoryPath, { recursive: true });
163
+ fs.copyFile(oldPath, newPath, (err) => {
164
+ if (err) {
165
+ reject(err);
166
+ return;
167
+ }
168
+ const stats = fs.statSync(newPath);
169
+ const fileName = newUrlKey.split('/').pop() || newUrlKey;
170
+ const fileInfo = {
171
+ originalName: fileName,
172
+ size: stats.size,
173
+ fileName: fileName,
174
+ key: newUrlKey,
175
+ fullPath: newPath,
176
+ url: this.getUrl(newUrlKey),
177
+ };
178
+ resolve(fileInfo);
179
+ });
180
+ });
181
+ }
182
+ }
183
+ exports.LocalStorage = LocalStorage;
@@ -0,0 +1,195 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.S3Storage = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const client_s3_1 = require("@aws-sdk/client-s3");
6
+ const client_s3_2 = require("@aws-sdk/client-s3");
7
+ const s3_request_presigner_1 = require("@aws-sdk/s3-request-presigner");
8
+ const moment_1 = tslib_1.__importDefault(require("moment"));
9
+ const path_1 = tslib_1.__importStar(require("path"));
10
+ const stream_1 = require("stream");
11
+ const uuid_1 = require("uuid");
12
+ class S3Storage {
13
+ constructor(options) {
14
+ this.options = options;
15
+ this.fileNameFunction = options.fileName || ((file, _req) => {
16
+ return `${(0, uuid_1.v4)()}-${file.originalname}`;
17
+ });
18
+ this.fileDistFunction = options.fileDist || ((_file, _req) => {
19
+ return path_1.default.join('uploads', (0, moment_1.default)().format('YYYY'), (0, moment_1.default)().format('MM'), (0, moment_1.default)().format('DD'));
20
+ });
21
+ this.s3 = new client_s3_1.S3({
22
+ ...(this.options.endpoint ? { endpoint: this.options.endpoint } : {}),
23
+ region: this.options.region,
24
+ credentials: {
25
+ accessKeyId: this.options.accessKeyId,
26
+ secretAccessKey: this.options.secretAccessKey,
27
+ },
28
+ });
29
+ }
30
+ async _handleFile(req, file, cb) {
31
+ // Collect file chunks to determine size
32
+ const chunks = [];
33
+ file.stream.on('data', (chunk) => chunks.push(chunk));
34
+ file.stream.on('end', async () => {
35
+ const buffer = Buffer.concat(chunks);
36
+ try {
37
+ const dist = await this.fileDistFunction(file, req);
38
+ const key = await this.fileNameFunction(file, req);
39
+ const filePath = (0, path_1.join)(dist, key);
40
+ const uploadedFile = await this.putFile(buffer, filePath);
41
+ const fileInfo = {
42
+ ...uploadedFile,
43
+ fieldName: file.fieldname,
44
+ originalName: file.originalname,
45
+ mimetype: file.mimetype,
46
+ };
47
+ let transformData = fileInfo;
48
+ if (this.options?.transformUploadedFileObject) {
49
+ transformData = await this.options.transformUploadedFileObject(fileInfo);
50
+ }
51
+ cb(null, transformData);
52
+ }
53
+ catch (err) {
54
+ cb(err);
55
+ }
56
+ });
57
+ file.stream.on('error', (err) => cb(err));
58
+ }
59
+ _removeFile(_req, file, cb) {
60
+ const params = {
61
+ Bucket: this.options.bucket,
62
+ Key: file.key,
63
+ };
64
+ this.s3
65
+ .deleteObject(params)
66
+ .then(() => cb(null))
67
+ .catch((err) => cb(err));
68
+ }
69
+ getUrl(key) {
70
+ if (this.options?.cloudFrontUrl) {
71
+ return `${this.options.cloudFrontUrl}/${key}`;
72
+ }
73
+ return `https://${this.options.bucket}.s3.amazonaws.com/${key}`;
74
+ }
75
+ async getSignedUrl(key, objectConfig) {
76
+ if (key) {
77
+ const url = await (0, s3_request_presigner_1.getSignedUrl)(this.s3, new client_s3_2.GetObjectCommand({
78
+ Bucket: this.options.bucket,
79
+ Key: key,
80
+ ...(objectConfig && { ...objectConfig }),
81
+ }));
82
+ return url;
83
+ }
84
+ return '';
85
+ }
86
+ async getFile(key) {
87
+ if (!key) {
88
+ throw new Error('Key is required to fetch the file from S3.');
89
+ }
90
+ try {
91
+ const command = new client_s3_2.GetObjectCommand({
92
+ Bucket: this.options.bucket,
93
+ Key: key,
94
+ });
95
+ const data = await this.s3.send(command);
96
+ if (!data.Body) {
97
+ throw new Error('Empty response received from S3.');
98
+ }
99
+ // Handle both Buffer and Readable Stream responses
100
+ if (data.Body instanceof stream_1.Readable) {
101
+ return await this.streamToBuffer(data.Body);
102
+ }
103
+ throw new Error('Unexpected data.Body type received from S3.');
104
+ }
105
+ catch (error) {
106
+ console.error(`Error fetching file (${key}) from S3:`, error);
107
+ throw error;
108
+ }
109
+ }
110
+ async putFile(fileContent, key) {
111
+ try {
112
+ const fileName = (0, path_1.basename)(key);
113
+ const fileKey = key || (Math.random() + 1).toString(36).substring(12);
114
+ // Upload the file
115
+ const putParams = {
116
+ Bucket: this.options.bucket,
117
+ Key: fileKey,
118
+ Body: fileContent,
119
+ ContentDisposition: `inline; ${fileName}`,
120
+ };
121
+ await this.s3.send(new client_s3_2.PutObjectCommand(putParams));
122
+ // Fetch file metadata to get size
123
+ const headParams = {
124
+ Bucket: this.options.bucket,
125
+ Key: fileKey,
126
+ };
127
+ const headObject = await this.s3.send(new client_s3_2.HeadObjectCommand(headParams));
128
+ const fileSize = headObject.ContentLength || 0;
129
+ // Construct response object
130
+ const fileData = {
131
+ originalName: fileName,
132
+ fileName: fileName,
133
+ size: fileSize,
134
+ buffer: fileContent,
135
+ fullPath: fileKey,
136
+ key: fileKey,
137
+ url: this.getUrl(fileKey),
138
+ };
139
+ return fileData;
140
+ }
141
+ catch (error) {
142
+ console.error('Error uploading file to S3:', error);
143
+ throw error;
144
+ }
145
+ }
146
+ async deleteFile(key) {
147
+ if (!key) {
148
+ throw new Error('File key is required for deletion.');
149
+ }
150
+ try {
151
+ const deleteParams = {
152
+ Bucket: this.options.bucket,
153
+ Key: key,
154
+ };
155
+ await this.s3.send(new client_s3_2.DeleteObjectCommand(deleteParams));
156
+ }
157
+ catch (error) {
158
+ console.error(`Error deleting file (${key}) from S3:`, error);
159
+ throw error;
160
+ }
161
+ }
162
+ async streamToBuffer(stream) {
163
+ const chunks = [];
164
+ for await (const chunk of stream) {
165
+ chunks.push(chunk);
166
+ }
167
+ return Buffer.concat(chunks);
168
+ }
169
+ async copyFile(oldKey, newKey) {
170
+ try {
171
+ await this.s3.send(new client_s3_1.CopyObjectCommand({
172
+ Bucket: this.options.bucket,
173
+ CopySource: `/${this.options.bucket}/${oldKey}`,
174
+ Key: newKey,
175
+ }));
176
+ const headObject = await this.s3.send(new client_s3_2.HeadObjectCommand({
177
+ Bucket: this.options.bucket,
178
+ Key: newKey,
179
+ }));
180
+ return {
181
+ originalName: (0, path_1.basename)(newKey),
182
+ size: headObject.ContentLength || 0,
183
+ fileName: (0, path_1.basename)(newKey),
184
+ key: newKey,
185
+ fullPath: newKey,
186
+ url: this.getUrl(newKey),
187
+ };
188
+ }
189
+ catch (error) {
190
+ console.error('Error copying file in S3:', error);
191
+ throw error;
192
+ }
193
+ }
194
+ }
195
+ exports.S3Storage = S3Storage;
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.StorageFactory = void 0;
37
+ const types_1 = require("./types");
38
+ class StorageFactory {
39
+ static async createStorage(storageType, options) {
40
+ const cacheKey = `${storageType}-${JSON.stringify(options)}`;
41
+ // if (this.storageInstances.has(cacheKey)) {
42
+ // return this.storageInstances.get(cacheKey)!;
43
+ // }
44
+ let storageInstance;
45
+ switch (storageType) {
46
+ case types_1.FileStorageEnum.LOCAL:
47
+ const { LocalStorage } = await Promise.resolve().then(() => __importStar(require('./storage/local.storage')));
48
+ storageInstance = new LocalStorage(options);
49
+ break;
50
+ case types_1.FileStorageEnum.AZURE:
51
+ try {
52
+ const { AzureStorage } = await Promise.resolve().then(() => __importStar(require('./storage/azure.storage')));
53
+ storageInstance = new AzureStorage(options);
54
+ }
55
+ catch (error) {
56
+ throw new Error('Azure Storage SDK (@azure/storage-blob) is required when using AzureStorage. ' +
57
+ 'Please install it: npm install @azure/storage-blob');
58
+ }
59
+ break;
60
+ case types_1.FileStorageEnum.S3:
61
+ try {
62
+ const { S3Storage } = await Promise.resolve().then(() => __importStar(require('./storage/s3.storage')));
63
+ storageInstance = new S3Storage(options);
64
+ }
65
+ catch (error) {
66
+ throw new Error('AWS SDK (@aws-sdk/client-s3 and @aws-sdk/s3-request-presigner) is required when using S3Storage. ' +
67
+ 'Please install them: npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner');
68
+ }
69
+ break;
70
+ default:
71
+ throw new Error(`Unsupported storage type: ${storageType}`);
72
+ }
73
+ this.storageInstances.set(cacheKey, storageInstance);
74
+ return storageInstance;
75
+ }
76
+ static clearCache() {
77
+ this.storageInstances.clear();
78
+ }
79
+ }
80
+ exports.StorageFactory = StorageFactory;
81
+ StorageFactory.storageInstances = new Map();
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FileStorageEnum = void 0;
4
+ var FileStorageEnum;
5
+ (function (FileStorageEnum) {
6
+ FileStorageEnum["LOCAL"] = "local";
7
+ FileStorageEnum["S3"] = "s3";
8
+ FileStorageEnum["AZURE"] = "azure";
9
+ })(FileStorageEnum || (exports.FileStorageEnum = FileStorageEnum = {}));