@ackplus/nest-file-storage 1.1.1 → 1.1.2

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 (37) hide show
  1. package/package.json +2 -2
  2. package/src/{index.ts → index.d.ts} +1 -0
  3. package/src/index.d.ts.map +1 -0
  4. package/src/lib/constants.d.ts +2 -0
  5. package/src/lib/constants.d.ts.map +1 -0
  6. package/src/lib/file-storage.service.d.ts +8 -0
  7. package/src/lib/file-storage.service.d.ts.map +1 -0
  8. package/src/lib/{index.ts → index.d.ts} +1 -0
  9. package/src/lib/index.d.ts.map +1 -0
  10. package/src/lib/interceptor/file-storage.interceptor.d.ts +25 -0
  11. package/src/lib/interceptor/file-storage.interceptor.d.ts.map +1 -0
  12. package/src/lib/nest-file-storage.module.d.ts +9 -0
  13. package/src/lib/nest-file-storage.module.d.ts.map +1 -0
  14. package/src/lib/storage/azure.storage.d.ts +19 -0
  15. package/src/lib/storage/azure.storage.d.ts.map +1 -0
  16. package/src/lib/storage/local.storage.d.ts +35 -0
  17. package/src/lib/storage/local.storage.d.ts.map +1 -0
  18. package/src/lib/storage/s3.storage.d.ts +20 -0
  19. package/src/lib/storage/s3.storage.d.ts.map +1 -0
  20. package/src/lib/storage.factory.d.ts +9 -0
  21. package/src/lib/storage.factory.d.ts.map +1 -0
  22. package/src/lib/{types.ts → types.d.ts} +23 -35
  23. package/src/lib/types.d.ts.map +1 -0
  24. package/eslint.config.mjs +0 -22
  25. package/jest.config.ts +0 -10
  26. package/project.json +0 -38
  27. package/src/lib/constants.ts +0 -1
  28. package/src/lib/file-storage.service.ts +0 -36
  29. package/src/lib/interceptor/file-storage.interceptor.ts +0 -174
  30. package/src/lib/nest-file-storage.module.ts +0 -78
  31. package/src/lib/storage/azure.storage.ts +0 -214
  32. package/src/lib/storage/local.storage.ts +0 -233
  33. package/src/lib/storage/s3.storage.ts +0 -242
  34. package/src/lib/storage.factory.ts +0 -58
  35. package/tsconfig.json +0 -17
  36. package/tsconfig.lib.json +0 -14
  37. package/tsconfig.spec.json +0 -15
@@ -1,78 +0,0 @@
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
- }
@@ -1,214 +0,0 @@
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
- }
@@ -1,233 +0,0 @@
1
- import concat from 'concat-stream';
2
- import * as fs from 'fs';
3
- import moment from 'moment';
4
- import { StorageEngine } from 'multer';
5
- import * as path from 'path';
6
- import { basename, dirname, join, normalize, sep } from 'path';
7
- import { join as posixJoin } from 'path/posix';
8
- import { v4 as uuidv4 } from 'uuid';
9
-
10
- import { LocalStorageOptions, Storage, UploadedFile } from '../types';
11
-
12
-
13
- export class LocalStorage implements StorageEngine, Storage {
14
-
15
- private rootPath: string;
16
- private fileNameFunction: (file: Express.Multer.File, req?: any) => string | Promise<string>;
17
- private fileDistFunction: (file: Express.Multer.File, req?: any) => string | Promise<string>;
18
-
19
- /**
20
- * Convert OS-specific file path to URL-friendly key
21
- * This ensures keys are consistent across all platforms
22
- * Windows: C:\uploads\2024\01\file.jpg -> 2024/01/file.jpg
23
- * Unix: /uploads/2024/01/file.jpg -> 2024/01/file.jpg
24
- */
25
- private pathToUrl(filePath: string): string {
26
- if (!filePath) return '';
27
-
28
- // Remove rootPath if present
29
- let relativePath = filePath;
30
- if (filePath.startsWith(this.rootPath)) {
31
- relativePath = filePath.substring(this.rootPath.length);
32
- }
33
-
34
- // Convert backslashes to forward slashes and remove leading slash
35
- return relativePath.replace(/\\/g, '/').replace(/^\/+/, '');
36
- }
37
-
38
- /**
39
- * Convert URL-friendly key to OS-specific file path
40
- * This converts stored keys back to valid file system paths
41
- * 2024/01/file.jpg -> Windows: 2024\01\file.jpg, Unix: 2024/01/file.jpg
42
- */
43
- private urlToPath(urlKey: string): string {
44
- if (!urlKey) return '';
45
-
46
- // Split by forward slashes and rejoin with OS-specific separator
47
- const parts = urlKey.split('/').filter(part => part.length > 0);
48
- return parts.join(sep);
49
- }
50
-
51
- /**
52
- * Get full file system path from URL-friendly key
53
- */
54
- private getFullPath(urlKey: string): string {
55
- const osPath = this.urlToPath(urlKey);
56
- return join(this.rootPath, osPath);
57
- }
58
-
59
- constructor(private options: LocalStorageOptions) {
60
- // Normalize path for the current OS
61
- this.rootPath = normalize(options.rootPath || join(process.cwd(), 'public'));
62
-
63
- this.fileNameFunction = options.fileName || ((file, _req) => {
64
- return `${uuidv4()}-${file.originalname}`;
65
- });
66
-
67
- this.fileDistFunction = options.fileDist || ((_file, _req) => {
68
- return join(this.rootPath, moment().format('YYYY'), moment().format('MM'), moment().format('DD'));
69
- });
70
-
71
-
72
- // Ensure the rootPath directory exists
73
- if (!fs.existsSync(this.rootPath)) {
74
- fs.mkdirSync(this.rootPath, { recursive: true });
75
- }
76
- }
77
-
78
- async _handleFile(
79
- req: any,
80
- file: Express.Multer.File,
81
- cb: (error?: any, info?: any) => void,
82
- ) {
83
- try {
84
- const dist = await this.fileDistFunction(file, req);
85
- const fileName = await this.fileNameFunction(file, req);
86
-
87
- const filePath = join(dist, fileName);
88
- // Convert to URL-friendly key for storage
89
- const urlKey = this.pathToUrl(filePath);
90
-
91
- file.stream.pipe(concat({ encoding: 'buffer' }, async (buffer) => {
92
- const uploadedFile = await this.putFile(buffer, urlKey);
93
-
94
- const fileInfo: UploadedFile = {
95
- ...uploadedFile,
96
- fieldName: file.fieldname,
97
- originalName: file.originalname,
98
- mimetype: file.mimetype,
99
- };
100
- let transformData = fileInfo;
101
-
102
- if (this.options?.transformUploadedFileObject) {
103
- transformData = await this.options.transformUploadedFileObject(fileInfo);
104
- }
105
- cb(null, transformData);
106
- }));
107
- } catch (error) {
108
- console.error('error', error);
109
- cb(error);
110
- }
111
- }
112
-
113
- _removeFile(
114
- _req: any,
115
- file: any,
116
- cb: (error: Error | null) => void,
117
- ) {
118
- const filePath = file.path;
119
-
120
- fs.unlink(filePath, (err) => {
121
- if (err) {
122
- cb(err);
123
- } else {
124
- cb(null);
125
- }
126
- });
127
- }
128
-
129
- getUrl(urlKey: string): string {
130
- if (urlKey && urlKey.startsWith('http')) {
131
- return urlKey;
132
- }
133
- if (!urlKey) {
134
- return '';
135
- }
136
-
137
- // Key is already in URL format (forward slashes)
138
- // Ensure baseUrl doesn't end with slash and key doesn't start with slash
139
- const baseUrl = this.options.baseUrl.replace(/\/$/, '');
140
- const cleanKey = urlKey.replace(/^\//, '');
141
-
142
- return `${baseUrl}/${cleanKey}`;
143
- }
144
-
145
- async getFile(urlKey: string): Promise<Buffer> {
146
- // Convert URL key to OS-specific path
147
- const fullPath = this.getFullPath(urlKey);
148
- return fs.promises.readFile(fullPath);
149
- }
150
-
151
- async deleteFile(urlKey: string): Promise<void> {
152
- // Convert URL key to OS-specific path
153
- const fullPath = this.getFullPath(urlKey);
154
- return fs.promises.unlink(fullPath);
155
- }
156
-
157
- async putFile(
158
- fileContent: Buffer,
159
- urlKey: string,
160
- ): Promise<UploadedFile> {
161
- return new Promise((putFileResolve, reject) => {
162
- // Convert URL key to OS-specific path
163
- const filePath = this.getFullPath(urlKey);
164
-
165
- const directoryPath = dirname(filePath);
166
-
167
- // Create the directory if it doesn't exist
168
- fs.mkdirSync(directoryPath, { recursive: true });
169
-
170
- fs.writeFile(filePath, fileContent, (err) => {
171
- if (err) {
172
- reject(err);
173
- return;
174
- }
175
-
176
- const stats = fs.statSync(filePath);
177
- const fileName = urlKey.split('/').pop() || urlKey;
178
-
179
- const fileInfo: UploadedFile = {
180
- originalName: fileName,
181
- size: stats.size,
182
- fileName: fileName,
183
- key: urlKey,
184
- url: this.getUrl(urlKey),
185
- fullPath: filePath,
186
- };
187
- putFileResolve(fileInfo);
188
- });
189
- });
190
- }
191
-
192
-
193
- path(urlKey: string): string {
194
- if (!urlKey) {
195
- return '';
196
- }
197
- // Convert URL key to full OS-specific path
198
- return this.getFullPath(urlKey);
199
- }
200
-
201
- async copyFile(oldUrlKey: string, newUrlKey: string): Promise<UploadedFile> {
202
- return new Promise((resolve, reject) => {
203
- // Convert URL keys to OS-specific paths
204
- const oldPath = this.getFullPath(oldUrlKey);
205
- const newPath = this.getFullPath(newUrlKey);
206
-
207
- const directoryPath = dirname(newPath);
208
- fs.mkdirSync(directoryPath, { recursive: true });
209
-
210
- fs.copyFile(oldPath, newPath, (err) => {
211
- if (err) {
212
- reject(err);
213
- return;
214
- }
215
-
216
- const stats = fs.statSync(newPath);
217
- const fileName = newUrlKey.split('/').pop() || newUrlKey;
218
-
219
- const fileInfo: UploadedFile = {
220
- originalName: fileName,
221
- size: stats.size,
222
- fileName: fileName,
223
- key: newUrlKey,
224
- fullPath: newPath,
225
- url: this.getUrl(newUrlKey),
226
- };
227
- resolve(fileInfo);
228
- });
229
- });
230
- }
231
-
232
-
233
- }