@ackplus/nest-file-storage 1.0.1 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +6 -404
  2. package/eslint.config.mjs +22 -0
  3. package/jest.config.ts +10 -0
  4. package/package.json +5 -62
  5. package/project.json +38 -0
  6. package/{dist/index.d.ts → src/index.ts} +0 -1
  7. package/src/lib/constants.ts +1 -0
  8. package/src/lib/file-storage.service.ts +36 -0
  9. package/{dist/lib/index.d.ts → src/lib/index.ts} +0 -1
  10. package/{dist/lib/interceptor/file-storage.interceptor.js → src/lib/interceptor/file-storage.interceptor.ts} +70 -37
  11. package/src/lib/nest-file-storage.module.ts +78 -0
  12. package/src/lib/storage/azure.storage.ts +214 -0
  13. package/{dist/lib/storage/local.storage.js → src/lib/storage/local.storage.ts} +96 -82
  14. package/{dist/lib/storage/s3.storage.js → src/lib/storage/s3.storage.ts} +107 -96
  15. package/src/lib/storage.factory.ts +58 -0
  16. package/{dist/lib/types.d.ts → src/lib/types.ts} +35 -23
  17. package/tsconfig.json +17 -0
  18. package/tsconfig.lib.json +14 -0
  19. package/tsconfig.spec.json +15 -0
  20. package/LICENSE +0 -21
  21. package/dist/index.d.ts.map +0 -1
  22. package/dist/index.js +0 -21
  23. package/dist/index.js.map +0 -1
  24. package/dist/lib/constants.d.ts +0 -2
  25. package/dist/lib/constants.d.ts.map +0 -1
  26. package/dist/lib/constants.js +0 -5
  27. package/dist/lib/constants.js.map +0 -1
  28. package/dist/lib/file-storage.service.d.ts +0 -8
  29. package/dist/lib/file-storage.service.d.ts.map +0 -1
  30. package/dist/lib/file-storage.service.js +0 -30
  31. package/dist/lib/file-storage.service.js.map +0 -1
  32. package/dist/lib/index.d.ts.map +0 -1
  33. package/dist/lib/index.js +0 -22
  34. package/dist/lib/index.js.map +0 -1
  35. package/dist/lib/interceptor/file-storage.interceptor.d.ts +0 -25
  36. package/dist/lib/interceptor/file-storage.interceptor.d.ts.map +0 -1
  37. package/dist/lib/interceptor/file-storage.interceptor.js.map +0 -1
  38. package/dist/lib/nest-file-storage.module.d.ts +0 -9
  39. package/dist/lib/nest-file-storage.module.d.ts.map +0 -1
  40. package/dist/lib/nest-file-storage.module.js +0 -80
  41. package/dist/lib/nest-file-storage.module.js.map +0 -1
  42. package/dist/lib/storage/azure.storage.d.ts +0 -19
  43. package/dist/lib/storage/azure.storage.d.ts.map +0 -1
  44. package/dist/lib/storage/azure.storage.js +0 -189
  45. package/dist/lib/storage/azure.storage.js.map +0 -1
  46. package/dist/lib/storage/local.storage.d.ts +0 -35
  47. package/dist/lib/storage/local.storage.d.ts.map +0 -1
  48. package/dist/lib/storage/local.storage.js.map +0 -1
  49. package/dist/lib/storage/s3.storage.d.ts +0 -20
  50. package/dist/lib/storage/s3.storage.d.ts.map +0 -1
  51. package/dist/lib/storage/s3.storage.js.map +0 -1
  52. package/dist/lib/storage.factory.d.ts +0 -9
  53. package/dist/lib/storage.factory.d.ts.map +0 -1
  54. package/dist/lib/storage.factory.js +0 -82
  55. package/dist/lib/storage.factory.js.map +0 -1
  56. package/dist/lib/types.d.ts.map +0 -1
  57. package/dist/lib/types.js +0 -10
  58. package/dist/lib/types.js.map +0 -1
@@ -1,32 +1,55 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
1
+ import { NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
2
+ import { Request } from 'express';
3
+ import multer from 'multer';
4
+ import { Observable } from 'rxjs';
5
+
6
+ import { FileStorageService } from '../file-storage.service';
7
+ import { StorageFactory } from '../storage.factory';
8
+ import { FileStorageEnum, StorageOptions, FileStorageConfigOptions, UploadedFile } from '../types';
9
+
10
+
11
+ export type FileUploadConfig = {
12
+ type: 'single' | 'array' | 'fields';
13
+ fieldName?: string;
14
+ maxCount?: number;
15
+ fields?: { name: string; maxCount?: number }[];
4
16
  };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.FileStorageInterceptor = FileStorageInterceptor;
7
- const multer_1 = __importDefault(require("multer"));
8
- const file_storage_service_1 = require("../file-storage.service");
9
- const storage_factory_1 = require("../storage.factory");
10
- const types_1 = require("../types");
17
+
18
+ export type FileStorageInterceptorOptions = {
19
+ fileName?: (file: any, req?: Request) => string;
20
+ fileDist?: (file: any, req?: Request) => string;
21
+ prefix?: string;
22
+ storageType?: FileStorageEnum;
23
+ storageOptions?: StorageOptions;
24
+
25
+ // File mapping callback - user defines what to return (defaults to file.key)
26
+ mapToRequestBody?: (file: any, fieldName: string, req?: Request) => any;
27
+ };
28
+
11
29
  // Helper function to map file object
12
- function mapFileObject(file) {
30
+ function mapFileObject(file: any) {
13
31
  return {
14
32
  fieldName: file.fieldname,
15
33
  originalName: file.originalname,
16
34
  fileName: file.filename,
17
35
  mimetype: file.mimetype,
18
36
  size: file.size,
19
- key: file.key,
20
- path: file.path,
21
- url: file.url,
37
+ key: (file as any).key,
38
+ path: (file as any).path,
39
+ url: (file as any).url,
22
40
  encoding: file.encoding,
23
- fullPath: file.fullPath
24
- };
41
+ fullPath: (file as any).fullPath
42
+ } as unknown as UploadedFile;
25
43
  }
44
+
26
45
  // Helper function to apply file mapping with callback
27
- function applyFileKeyMapping(request, fileConfig, interceptorOptions) {
46
+ function applyFileKeyMapping(
47
+ request: Request,
48
+ fileConfig: FileUploadConfig,
49
+ interceptorOptions?: FileStorageInterceptorOptions
50
+ ): void {
28
51
  // Default callback returns the file key
29
- const mapCallback = interceptorOptions?.mapToRequestBody || ((file) => {
52
+ const mapCallback = interceptorOptions?.mapToRequestBody || ((file: any) => {
30
53
  // For arrays, return array of keys
31
54
  if (Array.isArray(file)) {
32
55
  return file.map(f => f.key);
@@ -34,6 +57,7 @@ function applyFileKeyMapping(request, fileConfig, interceptorOptions) {
34
57
  // For single file, return the key
35
58
  return file.key;
36
59
  });
60
+
37
61
  if (fileConfig.type === 'single') {
38
62
  const file = request.file;
39
63
  if (file) {
@@ -41,17 +65,15 @@ function applyFileKeyMapping(request, fileConfig, interceptorOptions) {
41
65
  const mappedFile = mapFileObject(file);
42
66
  request.body[fieldName] = mapCallback(mappedFile, fieldName, request);
43
67
  }
44
- }
45
- else if (fileConfig.type === 'array') {
46
- const files = request.files;
68
+ } else if (fileConfig.type === 'array') {
69
+ const files = request.files as Express.Multer.File[];
47
70
  if (files && files.length > 0) {
48
71
  const fieldName = fileConfig.fieldName || 'files';
49
72
  const mappedFiles = files.map(file => mapFileObject(file));
50
73
  request.body[fieldName] = mapCallback(mappedFiles, fieldName, request);
51
74
  }
52
- }
53
- else if (fileConfig.type === 'fields') {
54
- const files = request.files;
75
+ } else if (fileConfig.type === 'fields') {
76
+ const files = request.files as { [fieldname: string]: Express.Multer.File[] };
55
77
  if (files) {
56
78
  Object.keys(files).forEach(fieldName => {
57
79
  const mappedFiles = files[fieldName].map(file => mapFileObject(file));
@@ -60,40 +82,47 @@ function applyFileKeyMapping(request, fileConfig, interceptorOptions) {
60
82
  }
61
83
  }
62
84
  }
85
+
63
86
  /**
64
87
  * Function-based interceptor that accepts storage options dynamically.
65
88
  */
66
- function FileStorageInterceptor(fileConfig, interceptorOptions) {
89
+ export function FileStorageInterceptor(
90
+ fileConfig: FileUploadConfig | string,
91
+ interceptorOptions?: FileStorageInterceptorOptions,
92
+ ): NestInterceptor {
67
93
  if (typeof fileConfig === 'string') {
68
94
  fileConfig = {
69
95
  type: 'single',
70
96
  fieldName: fileConfig,
71
97
  };
72
98
  }
99
+
73
100
  return {
74
- async intercept(context, next) {
75
- const options = file_storage_service_1.FileStorageService.getOptions();
101
+ async intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<any>> {
102
+ const options = FileStorageService.getOptions();
76
103
  const request = context.switchToHttp().getRequest();
77
104
  const response = context.switchToHttp().getResponse();
105
+
78
106
  // Determine storage type - handle both config approaches
79
- let storageType;
80
- let storageConfig;
107
+ let storageType: FileStorageEnum;
108
+ let storageConfig: any;
109
+
81
110
  if ('storage' in options) {
82
111
  // Configuration-based approach
83
- const configOptions = options;
112
+ const configOptions = options as FileStorageConfigOptions;
84
113
  storageType = interceptorOptions?.storageType ?? configOptions.storage;
85
- storageConfig = configOptions[`${storageType}Config`];
86
- }
87
- else {
114
+ storageConfig = (configOptions as any)[`Config`];
115
+ } else {
88
116
  // Class factory approach - default to LOCAL
89
- storageType = interceptorOptions?.storageType ?? types_1.FileStorageEnum.LOCAL;
117
+ storageType = interceptorOptions?.storageType ?? FileStorageEnum.LOCAL;
90
118
  storageConfig = {};
91
119
  }
120
+
92
121
  const storageOptions = {
93
122
  ...storageConfig,
94
123
  ...(interceptorOptions?.storageOptions || {}),
95
124
  fileName: interceptorOptions?.fileName || storageConfig?.fileName,
96
- fileDist: (file, req) => {
125
+ fileDist: (file: any, req: any) => {
97
126
  if (interceptorOptions?.fileDist) {
98
127
  return interceptorOptions.fileDist(file, req);
99
128
  }
@@ -101,9 +130,11 @@ function FileStorageInterceptor(fileConfig, interceptorOptions) {
101
130
  },
102
131
  prefix: interceptorOptions?.prefix || storageConfig?.prefix,
103
132
  };
133
+
104
134
  // Create storage instance dynamically
105
- const storage = await storage_factory_1.StorageFactory.createStorage(storageType, storageOptions);
106
- const multerInstance = (0, multer_1.default)({ storage });
135
+ const storage = await StorageFactory.createStorage(storageType, storageOptions);
136
+ const multerInstance = multer({ storage });
137
+
107
138
  // Multer setup based on fileConfig
108
139
  let multerMiddleware;
109
140
  switch (fileConfig.type) {
@@ -128,14 +159,16 @@ function FileStorageInterceptor(fileConfig, interceptorOptions) {
128
159
  default:
129
160
  throw new Error('Invalid file upload type. Use "single", "array", or "fields".');
130
161
  }
162
+
131
163
  // Execute Multer middleware
132
164
  await new Promise((resolve, reject) => {
133
165
  multerMiddleware(request, response, (err) => (err ? reject(err) : resolve(true)));
134
166
  });
167
+
135
168
  // Apply file key mapping after multer processing
136
169
  applyFileKeyMapping(request, fileConfig, interceptorOptions);
170
+
137
171
  return next.handle();
138
172
  }
139
173
  };
140
174
  }
141
- //# sourceMappingURL=file-storage.interceptor.js.map
@@ -0,0 +1,78 @@
1
+ import { Module, DynamicModule, Provider } from '@nestjs/common';
2
+
3
+ import { FILE_STORAGE_OPTIONS } from './constants';
4
+ import { FileStorageService } from './file-storage.service';
5
+ import { FileStorageAsyncOptions, FileStorageModuleOptions, FileStorageOptionsFactory } from './types';
6
+
7
+
8
+ @Module({})
9
+ export class NestFileStorageModule {
10
+
11
+ static forRoot(options: FileStorageModuleOptions): DynamicModule {
12
+ return {
13
+ module: NestFileStorageModule,
14
+ providers: [
15
+ {
16
+ provide: FILE_STORAGE_OPTIONS,
17
+ useFactory: async () => {
18
+ FileStorageService.setOptions(options); // ✅ Store globally
19
+ return options;
20
+ },
21
+ inject: [],
22
+ },
23
+ FileStorageService,
24
+ ],
25
+ exports: [],
26
+ };
27
+ }
28
+
29
+ static forRootAsync(options: FileStorageAsyncOptions): DynamicModule {
30
+ const asyncProviders: Provider[] = this.createAsyncProviders(options);
31
+
32
+ return {
33
+ module: NestFileStorageModule,
34
+ imports: options.imports || [],
35
+ providers: [...asyncProviders, FileStorageService],
36
+ exports: [],
37
+ };
38
+ }
39
+
40
+ private static createAsyncProviders(options: FileStorageAsyncOptions): Provider[] {
41
+ if (options.useExisting || options.useFactory) {
42
+ return [this.createAsyncOptionsProvider(options)];
43
+ }
44
+
45
+ return [
46
+ this.createAsyncOptionsProvider(options),
47
+ {
48
+ provide: options.useClass!,
49
+ useClass: options.useClass!,
50
+ },
51
+ ];
52
+ }
53
+
54
+ private static createAsyncOptionsProvider(options: FileStorageAsyncOptions): Provider {
55
+ if (options.useFactory) {
56
+ return {
57
+ provide: FILE_STORAGE_OPTIONS,
58
+ useFactory: async (...args: any[]) => {
59
+ const fileStorageOptions = await options.useFactory!(...args);
60
+ FileStorageService.setOptions(fileStorageOptions);
61
+ return fileStorageOptions;
62
+ },
63
+ inject: options.inject || [],
64
+ };
65
+ }
66
+
67
+ return {
68
+ provide: FILE_STORAGE_OPTIONS,
69
+ useFactory: async (optionsFactory: FileStorageOptionsFactory) => {
70
+ const fileStorageOptions = await optionsFactory.createFileStorageOptions();
71
+ FileStorageService.setOptions(fileStorageOptions);
72
+ return fileStorageOptions;
73
+ },
74
+ inject: [options.useExisting || options.useClass!],
75
+ };
76
+ }
77
+
78
+ }
@@ -0,0 +1,214 @@
1
+ import {
2
+ BlobSASPermissions,
3
+ BlobSASSignatureValues,
4
+ BlobServiceClient,
5
+ generateBlobSASQueryParameters,
6
+ SASProtocol,
7
+ StorageSharedKeyCredential,
8
+ } from '@azure/storage-blob';
9
+ import concat from 'concat-stream';
10
+ import moment from 'moment';
11
+ import { StorageEngine } from 'multer';
12
+ import path, { basename, join } from 'path';
13
+ import { v4 as uuidv4 } from 'uuid';
14
+
15
+ import { AzureStorageOptions, Storage, UploadedFile } from '../types';
16
+
17
+
18
+ export class AzureStorage implements StorageEngine, Storage {
19
+
20
+ private blobServiceClient: BlobServiceClient;
21
+
22
+ private fileNameFunction: (file: Express.Multer.File, req?: any) => string | Promise<string>;
23
+ private fileDistFunction: (file: Express.Multer.File, req?: any) => string | Promise<string>;
24
+
25
+
26
+ constructor(private options: AzureStorageOptions) {
27
+ this.fileNameFunction = options.fileName || ((file, _req) => {
28
+ return `${uuidv4()}-${file.originalname}`;
29
+ });
30
+
31
+ this.fileDistFunction = options.fileDist || ((_file, _req) => {
32
+ return path.join('uploads', moment().format('YYYY'), moment().format('MM'), moment().format('DD'));
33
+ });
34
+
35
+ const sharedKeyCredential = new StorageSharedKeyCredential(
36
+ options.account,
37
+ options.accountKey,
38
+ );
39
+
40
+ this.blobServiceClient = new BlobServiceClient(
41
+ `https://${options.account}.blob.core.windows.net`,
42
+ sharedKeyCredential,
43
+ );
44
+ }
45
+
46
+ async _handleFile(
47
+ req: any,
48
+ file: any,
49
+ cb: (error?: any, info?: any) => void,
50
+ ): Promise<void> {
51
+ try {
52
+ const dist = await this.fileDistFunction(file, req);
53
+ const key = await this.fileNameFunction(file, req);
54
+ const filePath = join(dist, key);
55
+
56
+ file.stream.pipe(concat({ encoding: 'buffer' }, async (buffer) => {
57
+ const uploadedFile = await this.putFile(buffer, filePath);
58
+
59
+ const fileInfo: UploadedFile = {
60
+ ...uploadedFile,
61
+ fieldName: file.fieldname,
62
+ originalName: file.originalname,
63
+ mimetype: file.mimetype,
64
+ };
65
+ let transformData = fileInfo;
66
+
67
+ if (this.options?.transformUploadedFileObject) {
68
+ transformData = await this.options.transformUploadedFileObject(fileInfo);
69
+ }
70
+ cb(null, transformData);
71
+ }));
72
+ } catch (error) {
73
+ cb(error);
74
+ }
75
+
76
+ file.stream.on('error', (err: any) => cb(err));
77
+ }
78
+
79
+ async _removeFile(
80
+ _req: any,
81
+ file: any,
82
+ cb: (error: Error | null) => void,
83
+ ): Promise<void> {
84
+ try {
85
+ const blobClient = this.blobServiceClient
86
+ .getContainerClient(this.options.container)
87
+ .getBlobClient(file.key);
88
+
89
+ await blobClient.delete();
90
+ cb(null);
91
+ } catch (error) {
92
+ cb(error);
93
+ }
94
+ }
95
+
96
+ getUrl(key: string): string {
97
+ return `https://${this.options.account}.blob.core.windows.net/${this.options.container}/${key}`;
98
+ }
99
+
100
+
101
+ getSignedUrl(key: string, signatureValues: Partial<Omit<BlobSASSignatureValues, 'containerName'>> = {}): string {
102
+ if (!key) return '';
103
+
104
+ const cloudFrontDomain = process.env['AZURE_CDN_DOMAIN_NAME'];
105
+
106
+ if (cloudFrontDomain) {
107
+ return `${cloudFrontDomain}/${key}`;
108
+ }
109
+
110
+ const containerClient = this.blobServiceClient.getContainerClient(this.options.container);
111
+ const blobClient = containerClient.getBlobClient(key);
112
+
113
+ const sharedKeyCredential = new StorageSharedKeyCredential(
114
+ this.options.account,
115
+ this.options.accountKey,
116
+ );
117
+
118
+ // Set the SAS token options
119
+ const expiresOn = new Date();
120
+ expiresOn.setHours(expiresOn.getHours() + 1); // Set expiration time to 1 hour from now
121
+
122
+ const sasToken = generateBlobSASQueryParameters(
123
+ {
124
+ containerName: this.options.container,
125
+ blobName: key,
126
+ permissions: BlobSASPermissions.parse('r'), // Read permissions
127
+ protocol: SASProtocol.Https, // HTTPS only
128
+ startsOn: new Date(), // Start time (now)
129
+ expiresOn, // Expiration time
130
+ ...signatureValues,
131
+ },
132
+ sharedKeyCredential,
133
+ ).toString();
134
+
135
+ return `${blobClient.url}?${sasToken}`;
136
+ }
137
+
138
+ async getFile(key: string): Promise<Buffer> {
139
+ const containerClient = this.blobServiceClient.getContainerClient(this.options.container);
140
+ const blobClient = containerClient.getBlobClient(key);
141
+ const downloadResponse = await blobClient.download();
142
+ const stream = downloadResponse.readableStreamBody;
143
+
144
+ return new Promise((resolve, reject) => {
145
+ const chunks: Buffer[] = [];
146
+ stream?.on('data', (chunk) => chunks.push(chunk));
147
+ stream?.on('end', () => resolve(Buffer.concat(chunks)));
148
+ stream?.on('error', reject);
149
+ });
150
+ }
151
+
152
+ async putFile(buffer: Buffer, key: string): Promise<UploadedFile> {
153
+ try {
154
+ const containerClient = this.blobServiceClient.getContainerClient(this.options.container);
155
+ const blockBlobClient = containerClient.getBlockBlobClient(key);
156
+
157
+ await blockBlobClient.uploadData(buffer);
158
+
159
+ const fileInfo: UploadedFile = {
160
+ originalName: basename(key),
161
+ fileName: basename(key),
162
+ size: buffer.length,
163
+ buffer,
164
+ key,
165
+ fullPath: key,
166
+ url: this.getUrl(key),
167
+ };
168
+
169
+ return fileInfo;
170
+ } catch (error) {
171
+ console.error(`Error uploading file "${key}":`, error);
172
+ throw error;
173
+ }
174
+ }
175
+
176
+ async deleteFile(key: string) {
177
+ try {
178
+ const blobClient = this.blobServiceClient
179
+ .getContainerClient(this.options.container)
180
+ .getBlobClient(key);
181
+
182
+ await blobClient.delete();
183
+ } catch (error) {
184
+ console.error(`Error deleting blob "${key}":`, error);
185
+ }
186
+ }
187
+
188
+ async copyFile(oldKey: string, newKey: string): Promise<UploadedFile> {
189
+ try {
190
+ const containerClient = this.blobServiceClient.getContainerClient(this.options.container);
191
+ const sourceBlobClient = containerClient.getBlobClient(oldKey);
192
+ const destinationBlobClient = containerClient.getBlobClient(newKey);
193
+
194
+ const copyPoller = await destinationBlobClient.beginCopyFromURL(sourceBlobClient.url);
195
+ await copyPoller.pollUntilDone();
196
+
197
+ const properties = await destinationBlobClient.getProperties();
198
+
199
+ return {
200
+ originalName: basename(newKey),
201
+ size: properties.contentLength || 0,
202
+ fileName: basename(newKey),
203
+ key: newKey,
204
+ fullPath: newKey,
205
+ url: this.getUrl(newKey),
206
+ };
207
+ } catch (error) {
208
+ console.error('Error copying file in Azure Blob Storage:', error);
209
+ throw error;
210
+ }
211
+ }
212
+
213
+
214
+ }