@ackplus/nest-file-storage 1.1.22 → 2.0.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/CHANGELOG.md +72 -0
- package/MIGRATION.md +220 -0
- package/README.md +353 -547
- package/dist/index.d.ts +1 -4
- package/dist/index.js +1 -4
- package/dist/index.js.map +1 -1
- package/dist/lib/constants.d.ts +3 -1
- package/dist/lib/constants.js +4 -2
- package/dist/lib/constants.js.map +1 -1
- package/dist/lib/driver-registry.d.ts +34 -0
- package/dist/lib/driver-registry.js +118 -0
- package/dist/lib/driver-registry.js.map +1 -0
- package/dist/lib/drivers/azure.driver.d.ts +21 -0
- package/dist/lib/drivers/azure.driver.js +91 -0
- package/dist/lib/drivers/azure.driver.js.map +1 -0
- package/dist/lib/drivers/driver.interface.d.ts +40 -0
- package/dist/lib/drivers/driver.interface.js +3 -0
- package/dist/lib/drivers/driver.interface.js.map +1 -0
- package/dist/lib/drivers/driver.util.d.ts +2 -0
- package/dist/lib/drivers/driver.util.js +15 -0
- package/dist/lib/drivers/driver.util.js.map +1 -0
- package/dist/lib/drivers/index.d.ts +7 -0
- package/dist/lib/drivers/index.js +39 -0
- package/dist/lib/drivers/index.js.map +1 -0
- package/dist/lib/drivers/local.driver.d.ts +15 -0
- package/dist/lib/drivers/local.driver.js +110 -0
- package/dist/lib/drivers/local.driver.js.map +1 -0
- package/dist/lib/drivers/s3.driver.d.ts +22 -0
- package/dist/lib/drivers/s3.driver.js +103 -0
- package/dist/lib/drivers/s3.driver.js.map +1 -0
- package/dist/lib/file-storage.service.d.ts +16 -5
- package/dist/lib/file-storage.service.js +60 -22
- package/dist/lib/file-storage.service.js.map +1 -1
- package/dist/lib/index.d.ts +9 -2
- package/dist/lib/index.js +15 -2
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/interceptor/file-storage.interceptor.d.ts +7 -10
- package/dist/lib/interceptor/file-storage.interceptor.js +119 -112
- package/dist/lib/interceptor/file-storage.interceptor.js.map +1 -1
- package/dist/lib/multer/driver-multer-engine.d.ts +18 -0
- package/dist/lib/multer/driver-multer-engine.js +91 -0
- package/dist/lib/multer/driver-multer-engine.js.map +1 -0
- package/dist/lib/nest-file-storage.module.d.ts +3 -3
- package/dist/lib/nest-file-storage.module.js +81 -44
- package/dist/lib/nest-file-storage.module.js.map +1 -1
- package/dist/lib/registry-holder.d.ts +6 -0
- package/dist/lib/registry-holder.js +26 -0
- package/dist/lib/registry-holder.js.map +1 -0
- package/dist/lib/tenant/tenant-from.d.ts +14 -0
- package/dist/lib/tenant/tenant-from.js +71 -0
- package/dist/lib/tenant/tenant-from.js.map +1 -0
- package/dist/lib/tenant/tenant.types.d.ts +20 -0
- package/dist/lib/tenant/tenant.types.js +3 -0
- package/dist/lib/tenant/tenant.types.js.map +1 -0
- package/dist/lib/types.d.ts +45 -35
- package/dist/lib/types.js.map +1 -1
- package/dist/lib/validation.d.ts +22 -0
- package/dist/lib/validation.js +98 -0
- package/dist/lib/validation.js.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/examples/1-basic-local-storage.example.ts +11 -7
- package/examples/10-testing.example.ts +60 -196
- package/examples/11-custom-driver.example.ts +82 -0
- package/examples/12-multi-tenant.example.ts +93 -0
- package/examples/2-s3-storage.example.ts +18 -16
- package/examples/3-azure-storage.example.ts +14 -12
- package/examples/4-upload-controller.example.ts +20 -55
- package/examples/5-custom-configuration.example.ts +37 -57
- package/examples/6-file-service.example.ts +34 -91
- package/examples/7-user-avatar.example.ts +37 -92
- package/examples/8-document-management.example.ts +45 -196
- package/examples/9-dynamic-storage.example.ts +29 -147
- package/examples/README.md +25 -107
- package/package.json +17 -4
- package/dist/lib/storage/azure.storage.d.ts +0 -18
- package/dist/lib/storage/azure.storage.js +0 -210
- package/dist/lib/storage/azure.storage.js.map +0 -1
- package/dist/lib/storage/local.storage.d.ts +0 -20
- package/dist/lib/storage/local.storage.js +0 -212
- package/dist/lib/storage/local.storage.js.map +0 -1
- package/dist/lib/storage/s3.storage.d.ts +0 -19
- package/dist/lib/storage/s3.storage.js +0 -241
- package/dist/lib/storage/s3.storage.js.map +0 -1
- package/dist/lib/storage.factory.d.ts +0 -8
- package/dist/lib/storage.factory.js +0 -46
- package/dist/lib/storage.factory.js.map +0 -1
|
@@ -1,22 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Example 1: Basic Local Storage Configuration
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
*
|
|
4
|
+
* The simplest setup — register a single local driver and make it the default.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { Module } from '@nestjs/common';
|
|
8
|
-
import { NestFileStorageModule,
|
|
8
|
+
import { NestFileStorageModule, localDriver } from '@ackplus/nest-file-storage';
|
|
9
9
|
|
|
10
10
|
@Module({
|
|
11
11
|
imports: [
|
|
12
12
|
NestFileStorageModule.forRoot({
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
default: 'local',
|
|
14
|
+
drivers: {
|
|
15
|
+
local: localDriver({
|
|
16
|
+
rootPath: './uploads', // directory where files are written
|
|
17
|
+
baseUrl: 'http://localhost:3000/uploads', // URL prefix used by getUrl()
|
|
18
|
+
}),
|
|
17
19
|
},
|
|
18
20
|
}),
|
|
19
21
|
],
|
|
20
22
|
})
|
|
21
23
|
export class AppModule {}
|
|
22
24
|
|
|
25
|
+
// Note: `baseUrl` only builds the URL string — it does not serve files over HTTP.
|
|
26
|
+
// Add static serving (e.g. @nestjs/serve-static) or stream files via your own controller.
|
|
@@ -1,233 +1,97 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Example 10: Testing File Storage
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
*
|
|
4
|
+
* Two approaches: (a) a real local driver against a temp directory, and
|
|
5
|
+
* (b) an in-memory mock driver registered with defineDriver — no disk, no network.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import { Test, TestingModule } from '@nestjs/testing';
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
import * as fs from 'fs';
|
|
11
|
-
import * as path from 'path';
|
|
12
|
-
import {
|
|
13
|
-
NestFileStorageModule,
|
|
9
|
+
import {
|
|
10
|
+
NestFileStorageModule,
|
|
14
11
|
FileStorageService,
|
|
15
|
-
|
|
12
|
+
localDriver,
|
|
13
|
+
defineDriver,
|
|
14
|
+
StorageDriver,
|
|
15
|
+
UploadedFile,
|
|
16
16
|
} from '@ackplus/nest-file-storage';
|
|
17
|
+
import * as fs from 'fs';
|
|
18
|
+
import * as path from 'path';
|
|
17
19
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
// (a) Local driver against a temp directory ---------------------------------
|
|
21
|
+
|
|
22
|
+
describe('FileStorageService (local)', () => {
|
|
23
|
+
let fileStorage: FileStorageService;
|
|
21
24
|
const testDir = './test-uploads';
|
|
22
25
|
|
|
23
26
|
beforeAll(async () => {
|
|
24
|
-
const
|
|
27
|
+
const moduleRef: TestingModule = await Test.createTestingModule({
|
|
25
28
|
imports: [
|
|
26
29
|
NestFileStorageModule.forRoot({
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
rootPath: testDir,
|
|
30
|
-
baseUrl: 'http://localhost:3000/test-uploads',
|
|
31
|
-
},
|
|
30
|
+
default: 'local',
|
|
31
|
+
drivers: { local: localDriver({ rootPath: testDir, baseUrl: 'http://localhost/test' }) },
|
|
32
32
|
}),
|
|
33
|
-
// Import your controllers and services
|
|
34
33
|
],
|
|
35
34
|
}).compile();
|
|
36
|
-
|
|
37
|
-
app = moduleFixture.createNestApplication();
|
|
38
|
-
await app.init();
|
|
39
|
-
|
|
40
|
-
storage = await FileStorageService.getStorage();
|
|
35
|
+
fileStorage = moduleRef.get(FileStorageService);
|
|
41
36
|
});
|
|
42
37
|
|
|
43
|
-
afterAll(
|
|
44
|
-
|
|
45
|
-
if (fs.existsSync(testDir)) {
|
|
46
|
-
fs.rmSync(testDir, { recursive: true, force: true });
|
|
47
|
-
}
|
|
48
|
-
await app.close();
|
|
38
|
+
afterAll(() => {
|
|
39
|
+
if (fs.existsSync(testDir)) fs.rmSync(testDir, { recursive: true, force: true });
|
|
49
40
|
});
|
|
50
41
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
.expect(201);
|
|
57
|
-
|
|
58
|
-
expect(response.body.fileKey).toBeDefined();
|
|
59
|
-
expect(response.body.message).toBe('File uploaded successfully');
|
|
60
|
-
});
|
|
42
|
+
it('puts, reads, copies, and deletes', async () => {
|
|
43
|
+
const content = Buffer.from('hello');
|
|
44
|
+
const put = await fileStorage.putFile(content, 'a/b.txt');
|
|
45
|
+
expect(put.key).toBe('a/b.txt');
|
|
46
|
+
expect(put.size).toBe(content.length);
|
|
61
47
|
|
|
62
|
-
|
|
63
|
-
const response = await request(app.getHttpServer())
|
|
64
|
-
.post('/upload/multiple')
|
|
65
|
-
.attach('files', Buffer.from('test 1'), 'test1.txt')
|
|
66
|
-
.attach('files', Buffer.from('test 2'), 'test2.txt')
|
|
67
|
-
.expect(201);
|
|
48
|
+
expect((await fileStorage.getFile('a/b.txt')).toString()).toBe('hello');
|
|
68
49
|
|
|
69
|
-
|
|
70
|
-
|
|
50
|
+
const copy = await fileStorage.copyFile('a/b.txt', 'a/c.txt');
|
|
51
|
+
expect(copy.key).toBe('a/c.txt');
|
|
71
52
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
.post('/upload/image')
|
|
75
|
-
.attach('image', Buffer.from('not an image'), 'test.txt')
|
|
76
|
-
.expect(400);
|
|
77
|
-
});
|
|
53
|
+
await fileStorage.deleteFile('a/b.txt');
|
|
54
|
+
await expect(fileStorage.getFile('a/b.txt')).rejects.toThrow();
|
|
78
55
|
});
|
|
56
|
+
});
|
|
79
57
|
|
|
80
|
-
|
|
81
|
-
const testKey = 'test/file.txt';
|
|
82
|
-
const testContent = Buffer.from('Hello, World!');
|
|
83
|
-
|
|
84
|
-
it('should upload a file', async () => {
|
|
85
|
-
const result = await storage.putFile(testContent, testKey);
|
|
86
|
-
|
|
87
|
-
expect(result.key).toBe(testKey);
|
|
88
|
-
expect(result.size).toBe(testContent.length);
|
|
89
|
-
expect(result.url).toContain(testKey);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it('should retrieve a file', async () => {
|
|
93
|
-
await storage.putFile(testContent, testKey);
|
|
94
|
-
const retrieved = await storage.getFile(testKey);
|
|
95
|
-
|
|
96
|
-
expect(retrieved.toString()).toBe(testContent.toString());
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it('should get file URL', () => {
|
|
100
|
-
const url = storage.getUrl(testKey);
|
|
101
|
-
expect(url).toContain(testKey);
|
|
102
|
-
expect(url).toContain('http://localhost:3000');
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it('should delete a file', async () => {
|
|
106
|
-
await storage.putFile(testContent, testKey);
|
|
107
|
-
await storage.deleteFile(testKey);
|
|
108
|
-
|
|
109
|
-
await expect(storage.getFile(testKey)).rejects.toThrow();
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it('should copy a file', async () => {
|
|
113
|
-
const sourceKey = 'test/source.txt';
|
|
114
|
-
const targetKey = 'test/target.txt';
|
|
115
|
-
|
|
116
|
-
await storage.putFile(testContent, sourceKey);
|
|
117
|
-
const result = await storage.copyFile(sourceKey, targetKey);
|
|
118
|
-
|
|
119
|
-
expect(result.key).toBe(targetKey);
|
|
120
|
-
|
|
121
|
-
const copiedContent = await storage.getFile(targetKey);
|
|
122
|
-
expect(copiedContent.toString()).toBe(testContent.toString());
|
|
123
|
-
|
|
124
|
-
// Cleanup
|
|
125
|
-
await storage.deleteFile(sourceKey);
|
|
126
|
-
await storage.deleteFile(targetKey);
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it('should handle multiple files', async () => {
|
|
130
|
-
const files = [
|
|
131
|
-
{ key: 'test/file1.txt', content: Buffer.from('Content 1') },
|
|
132
|
-
{ key: 'test/file2.txt', content: Buffer.from('Content 2') },
|
|
133
|
-
{ key: 'test/file3.txt', content: Buffer.from('Content 3') },
|
|
134
|
-
];
|
|
135
|
-
|
|
136
|
-
// Upload multiple files
|
|
137
|
-
await Promise.all(
|
|
138
|
-
files.map(file => storage.putFile(file.content, file.key))
|
|
139
|
-
);
|
|
140
|
-
|
|
141
|
-
// Verify all files exist
|
|
142
|
-
const results = await Promise.all(
|
|
143
|
-
files.map(file => storage.getFile(file.key))
|
|
144
|
-
);
|
|
145
|
-
|
|
146
|
-
expect(results).toHaveLength(3);
|
|
147
|
-
expect(results[0].toString()).toBe('Content 1');
|
|
58
|
+
// (b) In-memory mock driver -------------------------------------------------
|
|
148
59
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
60
|
+
class MemoryDriver implements StorageDriver {
|
|
61
|
+
private files = new Map<string, Buffer>();
|
|
62
|
+
async putFile(content: Buffer, key: string): Promise<UploadedFile> {
|
|
63
|
+
this.files.set(key, content);
|
|
64
|
+
const name = path.basename(key);
|
|
65
|
+
return { key, url: `mem://${key}`, originalName: name, fileName: name, size: content.length, fullPath: key };
|
|
66
|
+
}
|
|
67
|
+
async getFile(key: string): Promise<Buffer> {
|
|
68
|
+
const f = this.files.get(key);
|
|
69
|
+
if (!f) throw new Error('not found');
|
|
70
|
+
return f;
|
|
71
|
+
}
|
|
72
|
+
async deleteFile(key: string): Promise<void> { this.files.delete(key); }
|
|
73
|
+
async copyFile(src: string, dest: string): Promise<UploadedFile> { return this.putFile(await this.getFile(src), dest); }
|
|
74
|
+
getUrl(key: string): string { return `mem://${key}`; }
|
|
75
|
+
}
|
|
163
76
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
*/
|
|
167
|
-
describe('FileService', () => {
|
|
168
|
-
let service: any; // Your file service
|
|
77
|
+
describe('FileStorageService (mock driver)', () => {
|
|
78
|
+
let fileStorage: FileStorageService;
|
|
169
79
|
|
|
170
|
-
|
|
171
|
-
const
|
|
80
|
+
beforeAll(async () => {
|
|
81
|
+
const moduleRef = await Test.createTestingModule({
|
|
172
82
|
imports: [
|
|
173
83
|
NestFileStorageModule.forRoot({
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
rootPath: './test-uploads',
|
|
177
|
-
baseUrl: 'http://localhost:3000/test-uploads',
|
|
178
|
-
},
|
|
84
|
+
default: 'memory',
|
|
85
|
+
drivers: { memory: defineDriver(MemoryDriver) },
|
|
179
86
|
}),
|
|
180
87
|
],
|
|
181
|
-
providers: [/* Your FileService */],
|
|
182
88
|
}).compile();
|
|
183
|
-
|
|
184
|
-
service = module.get(/* Your FileService */);
|
|
89
|
+
fileStorage = moduleRef.get(FileStorageService);
|
|
185
90
|
});
|
|
186
91
|
|
|
187
|
-
it('
|
|
188
|
-
|
|
92
|
+
it('stores in memory', async () => {
|
|
93
|
+
await fileStorage.putFile(Buffer.from('x'), 'k');
|
|
94
|
+
expect((await fileStorage.getFile('k')).toString()).toBe('x');
|
|
95
|
+
expect(await fileStorage.getUrl('k')).toBe('mem://k');
|
|
189
96
|
});
|
|
190
|
-
|
|
191
|
-
// Add more unit tests...
|
|
192
97
|
});
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Mock Storage for Testing
|
|
196
|
-
*/
|
|
197
|
-
class MockStorage {
|
|
198
|
-
private files = new Map<string, Buffer>();
|
|
199
|
-
|
|
200
|
-
async putFile(content: Buffer, key: string) {
|
|
201
|
-
this.files.set(key, content);
|
|
202
|
-
return {
|
|
203
|
-
key,
|
|
204
|
-
url: `http://mock/${key}`,
|
|
205
|
-
size: content.length,
|
|
206
|
-
fileName: path.basename(key),
|
|
207
|
-
originalName: path.basename(key),
|
|
208
|
-
fullPath: key,
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
async getFile(key: string): Promise<Buffer> {
|
|
213
|
-
const file = this.files.get(key);
|
|
214
|
-
if (!file) {
|
|
215
|
-
throw new Error('File not found');
|
|
216
|
-
}
|
|
217
|
-
return file;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
async deleteFile(key: string): Promise<void> {
|
|
221
|
-
this.files.delete(key);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
async copyFile(oldKey: string, newKey: string) {
|
|
225
|
-
const file = await this.getFile(oldKey);
|
|
226
|
-
return await this.putFile(file, newKey);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
getUrl(key: string): string {
|
|
230
|
-
return `http://mock/${key}`;
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example 11: Custom Storage Driver
|
|
3
|
+
*
|
|
4
|
+
* Implement the StorageDriver interface and register it with defineDriver. A custom driver works
|
|
5
|
+
* everywhere a built-in does — in the interceptor, the service, and tenant resolution.
|
|
6
|
+
*
|
|
7
|
+
* This example sketches a Google Cloud Storage driver; swap in any backend.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Module, Injectable } from '@nestjs/common';
|
|
11
|
+
import {
|
|
12
|
+
NestFileStorageModule,
|
|
13
|
+
defineDriver,
|
|
14
|
+
FileStorageService,
|
|
15
|
+
StorageDriver,
|
|
16
|
+
UploadedFile,
|
|
17
|
+
PutFileMeta,
|
|
18
|
+
} from '@ackplus/nest-file-storage';
|
|
19
|
+
// import { Storage } from '@google-cloud/storage';
|
|
20
|
+
|
|
21
|
+
class GcsDriver implements StorageDriver {
|
|
22
|
+
// private storage = new Storage();
|
|
23
|
+
constructor(private readonly opts: { bucket: string }) {}
|
|
24
|
+
|
|
25
|
+
// private file(key: string) { return this.storage.bucket(this.opts.bucket).file(key); }
|
|
26
|
+
|
|
27
|
+
async putFile(content: Buffer, key: string, meta?: PutFileMeta): Promise<UploadedFile> {
|
|
28
|
+
// await this.file(key).save(content, { contentType: meta?.contentType });
|
|
29
|
+
void content;
|
|
30
|
+
void meta;
|
|
31
|
+
const name = key.split('/').pop()!;
|
|
32
|
+
return { key, url: this.getUrl(key), originalName: name, fileName: name, size: content.length, fullPath: key };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async getFile(key: string): Promise<Buffer> {
|
|
36
|
+
// const [buf] = await this.file(key).download();
|
|
37
|
+
// return buf;
|
|
38
|
+
throw new Error(`download ${key} not implemented in this sketch`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async deleteFile(key: string): Promise<void> {
|
|
42
|
+
// await this.file(key).delete();
|
|
43
|
+
void key;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async copyFile(src: string, dest: string): Promise<UploadedFile> {
|
|
47
|
+
// await this.file(src).copy(this.file(dest));
|
|
48
|
+
void src;
|
|
49
|
+
const name = dest.split('/').pop()!;
|
|
50
|
+
return { key: dest, url: this.getUrl(dest), originalName: name, fileName: name, size: 0, fullPath: dest };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
getUrl(key: string): string {
|
|
54
|
+
return `https://storage.googleapis.com/${this.opts.bucket}/${key}`;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@Module({
|
|
59
|
+
imports: [
|
|
60
|
+
NestFileStorageModule.forRoot({
|
|
61
|
+
default: 'gcs',
|
|
62
|
+
drivers: {
|
|
63
|
+
// defineDriver(Class, opts) is sugar for () => new Class(opts).
|
|
64
|
+
gcs: defineDriver(GcsDriver, { bucket: 'my-bucket' }),
|
|
65
|
+
// For async setup, pass a plain factory instead:
|
|
66
|
+
// gcs: async () => new GcsDriver(await loadGcsConfig()),
|
|
67
|
+
},
|
|
68
|
+
}),
|
|
69
|
+
],
|
|
70
|
+
})
|
|
71
|
+
export class AppModule {}
|
|
72
|
+
|
|
73
|
+
@Injectable()
|
|
74
|
+
export class ReportService {
|
|
75
|
+
constructor(private readonly fileStorage: FileStorageService) {}
|
|
76
|
+
|
|
77
|
+
// Use the custom driver by name — identical to a built-in.
|
|
78
|
+
async save(buffer: Buffer, key: string) {
|
|
79
|
+
const gcs = await this.fileStorage.getDriver('gcs');
|
|
80
|
+
return gcs.putFile(buffer, key);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example 12: Multi-Tenant Storage
|
|
3
|
+
*
|
|
4
|
+
* Route each upload to the right tenant's storage. "globex" gets a dedicated bucket; every other
|
|
5
|
+
* tenant shares one driver with a per-tenant key prefix. The tenant's driver is cached, so the DB
|
|
6
|
+
* lookup + client construction happen once per tenant — not per request.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Controller, Post, Body, UseInterceptors, Injectable, Module } from '@nestjs/common';
|
|
10
|
+
import {
|
|
11
|
+
NestFileStorageModule,
|
|
12
|
+
FileStorageInterceptor,
|
|
13
|
+
FileStorageService,
|
|
14
|
+
localDriver,
|
|
15
|
+
s3Driver,
|
|
16
|
+
tenantFrom,
|
|
17
|
+
} from '@ackplus/nest-file-storage';
|
|
18
|
+
|
|
19
|
+
// Your per-tenant storage config source (e.g. a database table).
|
|
20
|
+
@Injectable()
|
|
21
|
+
export class TenantStorageService {
|
|
22
|
+
async find(tenantId: string): Promise<{ dedicated: boolean; bucket?: string; region?: string; key?: string; secret?: string } | null> {
|
|
23
|
+
void tenantId;
|
|
24
|
+
return null; // look up the tenant's storage config
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@Module({
|
|
29
|
+
providers: [TenantStorageService],
|
|
30
|
+
imports: [
|
|
31
|
+
NestFileStorageModule.forRootAsync({
|
|
32
|
+
inject: [TenantStorageService],
|
|
33
|
+
useFactory: (tenants: TenantStorageService) => ({
|
|
34
|
+
default: 'local',
|
|
35
|
+
drivers: {
|
|
36
|
+
local: localDriver({ rootPath: './uploads', baseUrl: 'http://localhost:3000/uploads' }),
|
|
37
|
+
},
|
|
38
|
+
tenant: {
|
|
39
|
+
// 1) Identify the tenant — try JWT, then subdomain, then a header.
|
|
40
|
+
resolve: tenantFrom.first(
|
|
41
|
+
tenantFrom.jwt('tenantId'),
|
|
42
|
+
tenantFrom.subdomain(),
|
|
43
|
+
tenantFrom.header('x-tenant-id'),
|
|
44
|
+
),
|
|
45
|
+
// 2) Resolve a tenant -> storage. Cached by tenant id.
|
|
46
|
+
driver: async (tenantId) => {
|
|
47
|
+
const cfg = await tenants.find(tenantId);
|
|
48
|
+
if (cfg?.dedicated) {
|
|
49
|
+
return {
|
|
50
|
+
factory: s3Driver({
|
|
51
|
+
bucket: cfg.bucket!, region: cfg.region!,
|
|
52
|
+
accessKeyId: cfg.key!, secretAccessKey: cfg.secret!,
|
|
53
|
+
}),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return { use: 'local', prefix: `tenants/${tenantId}` };
|
|
57
|
+
},
|
|
58
|
+
cache: { ttlMs: 10 * 60_000, max: 500 },
|
|
59
|
+
fallback: 'default', // no tenant -> default driver ('error' -> 400)
|
|
60
|
+
},
|
|
61
|
+
}),
|
|
62
|
+
}),
|
|
63
|
+
],
|
|
64
|
+
})
|
|
65
|
+
export class AppModule {}
|
|
66
|
+
|
|
67
|
+
@Controller('files')
|
|
68
|
+
export class TenantUploadController {
|
|
69
|
+
constructor(private readonly fileStorage: FileStorageService) {}
|
|
70
|
+
|
|
71
|
+
// No tenant-specific code — the interceptor routes to the tenant's storage automatically.
|
|
72
|
+
@Post('upload')
|
|
73
|
+
@UseInterceptors(FileStorageInterceptor('file', { mapToRequestBody: (file) => file }))
|
|
74
|
+
upload(@Body() body: any) {
|
|
75
|
+
return { message: 'Uploaded to the tenant storage', file: body.file };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Programmatic access outside a request (jobs, URL generation):
|
|
80
|
+
@Injectable()
|
|
81
|
+
export class TenantReportService {
|
|
82
|
+
constructor(private readonly fileStorage: FileStorageService) {}
|
|
83
|
+
|
|
84
|
+
async urlFor(tenantId: string, key: string) {
|
|
85
|
+
const { driver } = await this.fileStorage.getTenantDriver(tenantId);
|
|
86
|
+
return driver.getUrl(key);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// When a tenant changes its storage settings, drop the cached driver:
|
|
90
|
+
invalidate(tenantId: string) {
|
|
91
|
+
this.fileStorage.getRegistry().invalidateTenant(tenantId);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -1,31 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Example 2: AWS S3 Storage Configuration
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* Requires: @aws-sdk/client-s3 @aws-sdk/s3-request-presigner
|
|
3
|
+
*
|
|
4
|
+
* Configure AWS S3 (or an S3-compatible store like MinIO / R2 / Spaces).
|
|
5
|
+
* Requires: @aws-sdk/client-s3 @aws-sdk/s3-request-presigner (loaded lazily).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { Module } from '@nestjs/common';
|
|
9
9
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
|
10
|
-
import { NestFileStorageModule,
|
|
10
|
+
import { NestFileStorageModule, s3Driver } from '@ackplus/nest-file-storage';
|
|
11
11
|
|
|
12
12
|
@Module({
|
|
13
13
|
imports: [
|
|
14
14
|
ConfigModule.forRoot(),
|
|
15
|
-
// Async configuration with ConfigService
|
|
16
15
|
NestFileStorageModule.forRootAsync({
|
|
17
16
|
imports: [ConfigModule],
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
17
|
+
inject: [ConfigService],
|
|
18
|
+
useFactory: (config: ConfigService) => ({
|
|
19
|
+
default: 's3',
|
|
20
|
+
drivers: {
|
|
21
|
+
s3: s3Driver({
|
|
22
|
+
accessKeyId: config.getOrThrow('AWS_ACCESS_KEY_ID'),
|
|
23
|
+
secretAccessKey: config.getOrThrow('AWS_SECRET_ACCESS_KEY'),
|
|
24
|
+
region: config.get('AWS_REGION', 'us-east-1'),
|
|
25
|
+
bucket: config.getOrThrow('AWS_BUCKET'),
|
|
26
|
+
cloudFrontUrl: config.get('AWS_CLOUDFRONT_URL'), // optional CDN for getUrl()
|
|
27
|
+
endpoint: config.get('S3_ENDPOINT'), // optional, for S3-compatible stores
|
|
28
|
+
}),
|
|
26
29
|
},
|
|
27
30
|
}),
|
|
28
|
-
inject: [ConfigService],
|
|
29
31
|
}),
|
|
30
32
|
],
|
|
31
33
|
})
|
|
@@ -36,5 +38,5 @@ export class AppModule {}
|
|
|
36
38
|
// AWS_SECRET_ACCESS_KEY=your-secret-key
|
|
37
39
|
// AWS_REGION=us-east-1
|
|
38
40
|
// AWS_BUCKET=your-bucket-name
|
|
39
|
-
// AWS_CLOUDFRONT_URL=https://d1234567890.cloudfront.net
|
|
40
|
-
|
|
41
|
+
// AWS_CLOUDFRONT_URL=https://d1234567890.cloudfront.net (optional)
|
|
42
|
+
// S3_ENDPOINT=https://<account>.r2.cloudflarestorage.com (optional, S3-compatible)
|
|
@@ -1,28 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Example 3: Azure Blob Storage Configuration
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* Requires: @azure/storage-blob
|
|
3
|
+
*
|
|
4
|
+
* Requires: @azure/storage-blob (loaded lazily).
|
|
6
5
|
*/
|
|
7
6
|
|
|
8
7
|
import { Module } from '@nestjs/common';
|
|
9
8
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
|
10
|
-
import { NestFileStorageModule,
|
|
9
|
+
import { NestFileStorageModule, azureDriver } from '@ackplus/nest-file-storage';
|
|
11
10
|
|
|
12
11
|
@Module({
|
|
13
12
|
imports: [
|
|
14
13
|
ConfigModule.forRoot(),
|
|
15
14
|
NestFileStorageModule.forRootAsync({
|
|
16
15
|
imports: [ConfigModule],
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
16
|
+
inject: [ConfigService],
|
|
17
|
+
useFactory: (config: ConfigService) => ({
|
|
18
|
+
default: 'azure',
|
|
19
|
+
drivers: {
|
|
20
|
+
azure: azureDriver({
|
|
21
|
+
account: config.getOrThrow('AZURE_STORAGE_ACCOUNT'),
|
|
22
|
+
accountKey: config.getOrThrow('AZURE_STORAGE_KEY'),
|
|
23
|
+
container: config.get('AZURE_CONTAINER', 'uploads'),
|
|
24
|
+
cdnUrl: config.get('AZURE_CDN_URL'), // optional CDN for getSignedUrl()
|
|
25
|
+
}),
|
|
23
26
|
},
|
|
24
27
|
}),
|
|
25
|
-
inject: [ConfigService],
|
|
26
28
|
}),
|
|
27
29
|
],
|
|
28
30
|
})
|
|
@@ -32,4 +34,4 @@ export class AppModule {}
|
|
|
32
34
|
// AZURE_STORAGE_ACCOUNT=your-account-name
|
|
33
35
|
// AZURE_STORAGE_KEY=your-account-key
|
|
34
36
|
// AZURE_CONTAINER=uploads
|
|
35
|
-
|
|
37
|
+
// AZURE_CDN_URL=https://cdn.example.com (optional)
|