@bloomneo/appkit 1.5.1 โ 1.5.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.
- package/AGENTS.md +195 -0
- package/CHANGELOG.md +253 -0
- package/README.md +147 -799
- package/bin/commands/generate.js +7 -7
- package/cookbook/README.md +26 -0
- package/cookbook/api-key-service.ts +106 -0
- package/cookbook/auth-protected-crud.ts +112 -0
- package/cookbook/file-upload-pipeline.ts +113 -0
- package/cookbook/multi-tenant-saas.ts +87 -0
- package/cookbook/real-time-chat.ts +121 -0
- package/dist/auth/auth.d.ts +21 -4
- package/dist/auth/auth.d.ts.map +1 -1
- package/dist/auth/auth.js +56 -44
- package/dist/auth/auth.js.map +1 -1
- package/dist/auth/defaults.d.ts +1 -1
- package/dist/auth/defaults.js +35 -35
- package/dist/cache/cache.d.ts +29 -6
- package/dist/cache/cache.d.ts.map +1 -1
- package/dist/cache/cache.js +72 -44
- package/dist/cache/cache.js.map +1 -1
- package/dist/cache/defaults.js +25 -25
- package/dist/cache/index.d.ts +19 -10
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/index.js +21 -18
- package/dist/cache/index.js.map +1 -1
- package/dist/config/defaults.d.ts +1 -1
- package/dist/config/defaults.js +8 -8
- package/dist/config/index.d.ts +3 -3
- package/dist/config/index.js +4 -4
- package/dist/database/adapters/mongoose.js +2 -2
- package/dist/database/adapters/prisma.js +2 -2
- package/dist/database/defaults.d.ts +1 -1
- package/dist/database/defaults.js +4 -4
- package/dist/database/index.js +2 -2
- package/dist/database/index.js.map +1 -1
- package/dist/email/defaults.js +20 -20
- package/dist/error/defaults.d.ts +1 -1
- package/dist/error/defaults.js +12 -12
- package/dist/error/error.d.ts +12 -0
- package/dist/error/error.d.ts.map +1 -1
- package/dist/error/error.js +19 -0
- package/dist/error/error.js.map +1 -1
- package/dist/error/index.d.ts +14 -3
- package/dist/error/index.d.ts.map +1 -1
- package/dist/error/index.js +14 -3
- package/dist/error/index.js.map +1 -1
- package/dist/event/defaults.js +30 -30
- package/dist/logger/defaults.d.ts +1 -1
- package/dist/logger/defaults.js +40 -40
- package/dist/logger/index.d.ts +1 -0
- package/dist/logger/index.d.ts.map +1 -1
- package/dist/logger/index.js.map +1 -1
- package/dist/logger/logger.d.ts +8 -0
- package/dist/logger/logger.d.ts.map +1 -1
- package/dist/logger/logger.js +13 -3
- package/dist/logger/logger.js.map +1 -1
- package/dist/logger/transports/console.js +1 -1
- package/dist/logger/transports/http.d.ts +1 -1
- package/dist/logger/transports/http.js +1 -1
- package/dist/logger/transports/webhook.d.ts +1 -1
- package/dist/logger/transports/webhook.js +1 -1
- package/dist/queue/defaults.d.ts +2 -2
- package/dist/queue/defaults.js +38 -38
- package/dist/security/defaults.d.ts +1 -1
- package/dist/security/defaults.js +29 -29
- package/dist/security/index.d.ts +1 -1
- package/dist/security/index.js +3 -3
- package/dist/security/security.d.ts +1 -1
- package/dist/security/security.js +4 -4
- package/dist/storage/defaults.js +19 -19
- package/dist/util/defaults.d.ts +1 -1
- package/dist/util/defaults.js +34 -34
- package/dist/util/env.d.ts +35 -0
- package/dist/util/env.d.ts.map +1 -0
- package/dist/util/env.js +50 -0
- package/dist/util/env.js.map +1 -0
- package/dist/util/errors.d.ts +52 -0
- package/dist/util/errors.d.ts.map +1 -0
- package/dist/util/errors.js +82 -0
- package/dist/util/errors.js.map +1 -0
- package/examples/.env.example +80 -0
- package/examples/README.md +16 -0
- package/examples/auth.ts +228 -0
- package/examples/cache.ts +36 -0
- package/examples/config.ts +45 -0
- package/examples/database.ts +69 -0
- package/examples/email.ts +53 -0
- package/examples/error.ts +50 -0
- package/examples/event.ts +42 -0
- package/examples/logger.ts +41 -0
- package/examples/queue.ts +58 -0
- package/examples/security.ts +46 -0
- package/examples/storage.ts +44 -0
- package/examples/util.ts +47 -0
- package/llms.txt +591 -0
- package/package.json +19 -10
- package/src/auth/README.md +850 -0
- package/src/cache/README.md +756 -0
- package/src/config/README.md +604 -0
- package/src/database/README.md +818 -0
- package/src/email/README.md +759 -0
- package/src/error/README.md +660 -0
- package/src/event/README.md +729 -0
- package/src/logger/README.md +435 -0
- package/src/queue/README.md +851 -0
- package/src/security/README.md +612 -0
- package/src/storage/README.md +1008 -0
- package/src/util/README.md +955 -0
- package/bin/templates/backend/docs/APPKIT_CLI.md +0 -507
- package/bin/templates/backend/docs/APPKIT_COMMENTS_GUIDELINES.md +0 -61
- package/bin/templates/backend/docs/APPKIT_LLM_GUIDE.md +0 -2539
|
@@ -0,0 +1,1008 @@
|
|
|
1
|
+
# @bloomneo/appkit - Storage Module ๐
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@bloomneo/appkit)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
|
|
6
|
+
> Ultra-simple file storage that just works with automatic Local/S3/R2 strategy
|
|
7
|
+
|
|
8
|
+
**One function** returns a storage system with automatic strategy detection.
|
|
9
|
+
Zero configuration needed, production-ready cloud integration by default, with
|
|
10
|
+
built-in CDN support and cost optimization.
|
|
11
|
+
|
|
12
|
+
## ๐ Why Choose This?
|
|
13
|
+
|
|
14
|
+
- **โก One Function** - Just `storageClass.get()`, everything else is automatic
|
|
15
|
+
- **โ๏ธ Auto Strategy** - Cloud env vars โ Distributed, No vars โ Local
|
|
16
|
+
- **๐ง Zero Configuration** - Smart defaults for everything
|
|
17
|
+
- **๐ฐ Cost Optimized** - R2 prioritized for zero egress fees
|
|
18
|
+
- **๐ CDN Ready** - Automatic CDN URL generation
|
|
19
|
+
- **๐ Security Built-in** - File type validation, size limits, signed URLs
|
|
20
|
+
- **โ๏ธ Scales Perfectly** - Development โ Production with no code changes
|
|
21
|
+
- **๐ค AI-Ready** - Optimized for LLM code generation
|
|
22
|
+
|
|
23
|
+
## ๐ฆ Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install @bloomneo/appkit
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## ๐โโ๏ธ Quick Start (30 seconds)
|
|
30
|
+
|
|
31
|
+
### Local Storage (Development)
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
import { storageClass } from '@bloomneo/appkit/storage';
|
|
35
|
+
|
|
36
|
+
const storage = storageClass.get();
|
|
37
|
+
|
|
38
|
+
// Upload files
|
|
39
|
+
await storage.put('avatars/user123.jpg', imageBuffer);
|
|
40
|
+
|
|
41
|
+
// Download files
|
|
42
|
+
const imageData = await storage.get('avatars/user123.jpg');
|
|
43
|
+
|
|
44
|
+
// Get public URL
|
|
45
|
+
const url = storage.url('avatars/user123.jpg');
|
|
46
|
+
// โ /uploads/avatars/user123.jpg
|
|
47
|
+
|
|
48
|
+
// List files
|
|
49
|
+
const files = await storage.list('avatars/');
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Cloud Storage (Production)
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# Cloudflare R2 (Recommended - Zero egress fees)
|
|
56
|
+
CLOUDFLARE_R2_BUCKET=my-bucket
|
|
57
|
+
CLOUDFLARE_ACCOUNT_ID=account123
|
|
58
|
+
CLOUDFLARE_R2_ACCESS_KEY_ID=access_key
|
|
59
|
+
CLOUDFLARE_R2_SECRET_ACCESS_KEY=secret_key
|
|
60
|
+
|
|
61
|
+
# OR AWS S3 / S3-Compatible
|
|
62
|
+
AWS_S3_BUCKET=my-bucket
|
|
63
|
+
AWS_ACCESS_KEY_ID=access_key
|
|
64
|
+
AWS_SECRET_ACCESS_KEY=secret_key
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
import { storageClass } from '@bloomneo/appkit/storage';
|
|
69
|
+
|
|
70
|
+
const storage = storageClass.get();
|
|
71
|
+
|
|
72
|
+
// Same code - now distributed across CDN!
|
|
73
|
+
await storage.put('products/item123.jpg', imageBuffer);
|
|
74
|
+
const url = storage.url('products/item123.jpg');
|
|
75
|
+
// โ https://cdn.example.com/products/item123.jpg
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**That's it!** Files automatically sync across all your servers.
|
|
79
|
+
|
|
80
|
+
## ๐ง Mental Model
|
|
81
|
+
|
|
82
|
+
### **Strategy Auto-Detection**
|
|
83
|
+
|
|
84
|
+
Environment variables determine storage backend:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
# Development/Single Server
|
|
88
|
+
# (no cloud env vars)
|
|
89
|
+
โ Local Strategy: ./uploads/ directory
|
|
90
|
+
|
|
91
|
+
# Production Cloud (Priority: R2 โ S3 โ Local)
|
|
92
|
+
CLOUDFLARE_R2_BUCKET=bucket โ R2 (zero egress fees)
|
|
93
|
+
AWS_S3_BUCKET=bucket โ S3 (AWS/Wasabi/MinIO)
|
|
94
|
+
# No cloud vars โ Local (with warning)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### **File Organization**
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
// Organize files with folder structure
|
|
101
|
+
await storage.put('users/123/avatar.jpg', imageBuffer);
|
|
102
|
+
await storage.put('products/456/gallery/1.jpg', imageBuffer);
|
|
103
|
+
await storage.put('documents/contracts/legal.pdf', pdfBuffer);
|
|
104
|
+
|
|
105
|
+
// List by folder
|
|
106
|
+
const userFiles = await storage.list('users/123/');
|
|
107
|
+
const productGallery = await storage.list('products/456/gallery/');
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## ๐ค LLM Quick Reference - Copy These Patterns
|
|
111
|
+
|
|
112
|
+
### **Basic Storage Setup (Copy Exactly)**
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
// โ
CORRECT - Complete storage setup
|
|
116
|
+
import { storageClass } from '@bloomneo/appkit/storage';
|
|
117
|
+
const storage = storageClass.get();
|
|
118
|
+
|
|
119
|
+
// Upload files
|
|
120
|
+
await storage.put('folder/file.jpg', buffer);
|
|
121
|
+
const data = await storage.get('folder/file.jpg');
|
|
122
|
+
await storage.delete('folder/file.jpg');
|
|
123
|
+
|
|
124
|
+
// URL generation
|
|
125
|
+
const publicUrl = storage.url('file.jpg');
|
|
126
|
+
const signedUrl = await storage.signedUrl('private.pdf', 3600);
|
|
127
|
+
|
|
128
|
+
// File organization
|
|
129
|
+
const files = await storage.list('images/');
|
|
130
|
+
const exists = await storage.exists('document.pdf');
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### **Helper Methods (Copy These)**
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
// โ
Quick upload with auto-naming
|
|
137
|
+
const { key, url } = await storageClass.upload(buffer, {
|
|
138
|
+
folder: 'uploads',
|
|
139
|
+
filename: 'document.pdf',
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// โ
Quick download with content type
|
|
143
|
+
const { data, contentType } = await storageClass.download('file.jpg');
|
|
144
|
+
|
|
145
|
+
// โ
Strategy detection
|
|
146
|
+
const strategy = storageClass.getStrategy(); // 'local' | 's3' | 'r2'
|
|
147
|
+
const isCloud = storageClass.hasCloudStorage(); // true if S3/R2
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### **Error Handling (Copy This Pattern)**
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
// โ
CORRECT - Comprehensive error handling
|
|
154
|
+
try {
|
|
155
|
+
await storage.put('file.jpg', buffer);
|
|
156
|
+
console.log('โ
File uploaded successfully');
|
|
157
|
+
} catch (error) {
|
|
158
|
+
if (error.message.includes('File too large')) {
|
|
159
|
+
return res.status(413).json({ error: 'File size limit exceeded' });
|
|
160
|
+
}
|
|
161
|
+
if (error.message.includes('File type not allowed')) {
|
|
162
|
+
return res.status(400).json({ error: 'Invalid file type' });
|
|
163
|
+
}
|
|
164
|
+
console.error('โ Upload failed:', error.message);
|
|
165
|
+
return res.status(500).json({ error: 'Upload failed' });
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## โ ๏ธ Common LLM Mistakes - Avoid These
|
|
170
|
+
|
|
171
|
+
### **Wrong Storage Usage**
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
// โ WRONG - Don't create StorageClass directly
|
|
175
|
+
import { StorageClass } from '@bloomneo/appkit/storage';
|
|
176
|
+
const storage = new StorageClass(config); // Wrong!
|
|
177
|
+
|
|
178
|
+
// โ WRONG - Missing await
|
|
179
|
+
storage.put('file.jpg', buffer); // Missing await!
|
|
180
|
+
|
|
181
|
+
// โ WRONG - Invalid keys
|
|
182
|
+
await storage.put('/file.jpg', buffer); // Leading slash
|
|
183
|
+
await storage.put('folder/../file.jpg', buffer); // Path traversal
|
|
184
|
+
await storage.put('folder\\file.jpg', buffer); // Backslashes
|
|
185
|
+
|
|
186
|
+
// โ
CORRECT - Use storageClass.get()
|
|
187
|
+
import { storageClass } from '@bloomneo/appkit/storage';
|
|
188
|
+
const storage = storageClass.get();
|
|
189
|
+
await storage.put('folder/file.jpg', buffer);
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### **Wrong Error Handling**
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
// โ WRONG - Ignoring errors
|
|
196
|
+
await storage.put('file.jpg', buffer); // No try-catch
|
|
197
|
+
|
|
198
|
+
// โ WRONG - Generic error handling
|
|
199
|
+
try {
|
|
200
|
+
await storage.put('file.jpg', buffer);
|
|
201
|
+
} catch (error) {
|
|
202
|
+
res.status(500).json({ error: 'Something went wrong' });
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// โ
CORRECT - Specific error handling
|
|
206
|
+
try {
|
|
207
|
+
await storage.put('file.jpg', buffer);
|
|
208
|
+
} catch (error) {
|
|
209
|
+
if (error.message.includes('File too large')) {
|
|
210
|
+
return res.status(413).json({
|
|
211
|
+
error: 'File too large',
|
|
212
|
+
maxSize: '50MB',
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
throw error;
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### **Wrong Testing**
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
// โ WRONG - No cleanup between tests
|
|
223
|
+
test('should upload file', async () => {
|
|
224
|
+
await storage.put('test.jpg', buffer);
|
|
225
|
+
// Missing: await storageClass.clear();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// โ
CORRECT - Proper test cleanup
|
|
229
|
+
afterEach(async () => {
|
|
230
|
+
await storageClass.clear(); // Essential for tests
|
|
231
|
+
});
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## ๐จ Error Handling Patterns
|
|
235
|
+
|
|
236
|
+
### **File Upload API**
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
app.post('/upload', upload.single('file'), async (req, res) => {
|
|
240
|
+
try {
|
|
241
|
+
if (!req.file) {
|
|
242
|
+
return res.status(400).json({ error: 'No file uploaded' });
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Validate file size (optional - storage handles this)
|
|
246
|
+
if (req.file.size > 50 * 1024 * 1024) {
|
|
247
|
+
// 50MB
|
|
248
|
+
return res.status(413).json({
|
|
249
|
+
error: 'File too large',
|
|
250
|
+
maxSize: '50MB',
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const key = `uploads/${Date.now()}-${req.file.originalname}`;
|
|
255
|
+
|
|
256
|
+
await storage.put(key, req.file.buffer, {
|
|
257
|
+
contentType: req.file.mimetype,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const url = storage.url(key);
|
|
261
|
+
|
|
262
|
+
res.json({
|
|
263
|
+
success: true,
|
|
264
|
+
file: { key, url, size: req.file.size },
|
|
265
|
+
});
|
|
266
|
+
} catch (error) {
|
|
267
|
+
if (error.message.includes('File type not allowed')) {
|
|
268
|
+
return res.status(400).json({
|
|
269
|
+
error: 'Invalid file type',
|
|
270
|
+
allowed: 'jpg, png, pdf, txt',
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
console.error('Upload error:', error);
|
|
275
|
+
res.status(500).json({ error: 'Upload failed' });
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### **File Download API**
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
app.get('/files/:key(*)', async (req, res) => {
|
|
284
|
+
try {
|
|
285
|
+
const key = req.params.key;
|
|
286
|
+
|
|
287
|
+
if (!(await storage.exists(key))) {
|
|
288
|
+
return res.status(404).json({ error: 'File not found' });
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const buffer = await storage.get(key);
|
|
292
|
+
|
|
293
|
+
res.setHeader('Content-Type', 'application/octet-stream');
|
|
294
|
+
res.setHeader(
|
|
295
|
+
'Content-Disposition',
|
|
296
|
+
`attachment; filename="${key.split('/').pop()}"`
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
res.send(buffer);
|
|
300
|
+
} catch (error) {
|
|
301
|
+
console.error('Download error:', error);
|
|
302
|
+
res.status(500).json({ error: 'Download failed' });
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### **Startup Validation**
|
|
308
|
+
|
|
309
|
+
```typescript
|
|
310
|
+
// โ
App startup validation
|
|
311
|
+
try {
|
|
312
|
+
storageClass.validateConfig();
|
|
313
|
+
console.log('โ
Storage validation passed');
|
|
314
|
+
} catch (error) {
|
|
315
|
+
console.error('โ Storage validation failed:', error.message);
|
|
316
|
+
process.exit(1);
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
## ๐ Security & Production
|
|
321
|
+
|
|
322
|
+
### **File Type Security**
|
|
323
|
+
|
|
324
|
+
```bash
|
|
325
|
+
# โ
SECURE - Specific file types only
|
|
326
|
+
BLOOM_STORAGE_ALLOWED_TYPES=image/jpeg,image/png,application/pdf,text/plain
|
|
327
|
+
|
|
328
|
+
# โ ๏ธ DEVELOPMENT ONLY - All file types
|
|
329
|
+
BLOOM_STORAGE_ALLOWED_TYPES=*
|
|
330
|
+
|
|
331
|
+
# โ
SECURE - File size limits
|
|
332
|
+
BLOOM_STORAGE_MAX_SIZE=52428800 # 50MB limit
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
### **Production Checklist**
|
|
336
|
+
|
|
337
|
+
- โ
**Cloud Storage**: Set `AWS_S3_BUCKET` or `CLOUDFLARE_R2_BUCKET`
|
|
338
|
+
- โ
**File Types**: Set `BLOOM_STORAGE_ALLOWED_TYPES` (never use `*`)
|
|
339
|
+
- โ
**Size Limits**: Set reasonable `BLOOM_STORAGE_MAX_SIZE`
|
|
340
|
+
- โ
**CDN**: Set `BLOOM_STORAGE_CDN_URL` for performance
|
|
341
|
+
- โ
**Error Handling**: Implement proper error responses
|
|
342
|
+
- โ
**Monitoring**: Log upload/download operations
|
|
343
|
+
|
|
344
|
+
### **Security Validation**
|
|
345
|
+
|
|
346
|
+
```typescript
|
|
347
|
+
// File type validation (automatic)
|
|
348
|
+
try {
|
|
349
|
+
await storage.put('malicious.exe', buffer);
|
|
350
|
+
} catch (error) {
|
|
351
|
+
// Error: File type not allowed: application/x-executable
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// File size validation (automatic)
|
|
355
|
+
try {
|
|
356
|
+
await storage.put('huge.zip', massiveBuffer);
|
|
357
|
+
} catch (error) {
|
|
358
|
+
// Error: File too large: 100MB (max: 50MB)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Path traversal prevention (automatic)
|
|
362
|
+
try {
|
|
363
|
+
await storage.put('../../../etc/passwd', buffer);
|
|
364
|
+
} catch (error) {
|
|
365
|
+
// Error: Storage key contains invalid path components
|
|
366
|
+
}
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
## ๐ Complete API Reference
|
|
370
|
+
|
|
371
|
+
### Core Function
|
|
372
|
+
|
|
373
|
+
```typescript
|
|
374
|
+
const storage = storageClass.get(); // One function, everything you need
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### File Operations
|
|
378
|
+
|
|
379
|
+
```typescript
|
|
380
|
+
// Upload files
|
|
381
|
+
await storage.put(key, data, options?);
|
|
382
|
+
await storage.put('file.jpg', buffer, {
|
|
383
|
+
contentType: 'image/jpeg',
|
|
384
|
+
metadata: { userId: '123' },
|
|
385
|
+
cacheControl: 'public, max-age=31536000'
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
// Download files
|
|
389
|
+
const buffer = await storage.get('file.jpg');
|
|
390
|
+
|
|
391
|
+
// Delete files
|
|
392
|
+
const success = await storage.delete('file.jpg');
|
|
393
|
+
|
|
394
|
+
// Check existence
|
|
395
|
+
const exists = await storage.exists('file.jpg');
|
|
396
|
+
|
|
397
|
+
// Copy files
|
|
398
|
+
await storage.copy('source.jpg', 'backup.jpg');
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
### URL Generation
|
|
402
|
+
|
|
403
|
+
```typescript
|
|
404
|
+
// Public URLs
|
|
405
|
+
const url = storage.url('file.jpg');
|
|
406
|
+
// Local: /uploads/file.jpg
|
|
407
|
+
// S3: https://bucket.s3.region.amazonaws.com/file.jpg
|
|
408
|
+
// R2: https://cdn.example.com/file.jpg
|
|
409
|
+
|
|
410
|
+
// Signed URLs (temporary access)
|
|
411
|
+
const signedUrl = await storage.signedUrl('private.pdf', 3600); // 1 hour
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
### File Listing
|
|
415
|
+
|
|
416
|
+
```typescript
|
|
417
|
+
// List all files
|
|
418
|
+
const allFiles = await storage.list();
|
|
419
|
+
|
|
420
|
+
// List with prefix
|
|
421
|
+
const images = await storage.list('images/');
|
|
422
|
+
|
|
423
|
+
// List with limit
|
|
424
|
+
const recent = await storage.list('logs/', 10);
|
|
425
|
+
|
|
426
|
+
// File metadata
|
|
427
|
+
files.forEach((file) => {
|
|
428
|
+
console.log(`${file.key}: ${file.size} bytes, ${file.lastModified}`);
|
|
429
|
+
});
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
### Helper Methods
|
|
433
|
+
|
|
434
|
+
```typescript
|
|
435
|
+
// Quick upload with auto-naming
|
|
436
|
+
const { key, url } = await storageClass.upload(buffer, {
|
|
437
|
+
folder: 'uploads',
|
|
438
|
+
filename: 'document.pdf',
|
|
439
|
+
contentType: 'application/pdf',
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
// Quick download with content type
|
|
443
|
+
const { data, contentType } = await storageClass.download('file.jpg');
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
### Utility Methods
|
|
447
|
+
|
|
448
|
+
```typescript
|
|
449
|
+
// Debug info
|
|
450
|
+
storageClass.getStrategy(); // 'local' | 's3' | 'r2'
|
|
451
|
+
storageClass.hasCloudStorage(); // true if S3/R2 configured
|
|
452
|
+
storageClass.isLocal(); // true if using local storage
|
|
453
|
+
storageClass.getConfig(); // Current configuration
|
|
454
|
+
storageClass.getStats(); // Usage statistics
|
|
455
|
+
|
|
456
|
+
// Cleanup
|
|
457
|
+
await storage.disconnect();
|
|
458
|
+
await storageClass.clear(); // For testing
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
## ๐ฏ Usage Examples
|
|
462
|
+
|
|
463
|
+
### **Express File Upload API**
|
|
464
|
+
|
|
465
|
+
```typescript
|
|
466
|
+
import express from 'express';
|
|
467
|
+
import multer from 'multer';
|
|
468
|
+
import { storageClass } from '@bloomneo/appkit/storage';
|
|
469
|
+
|
|
470
|
+
const app = express();
|
|
471
|
+
const storage = storageClass.get();
|
|
472
|
+
const upload = multer({ storage: multer.memoryStorage() });
|
|
473
|
+
|
|
474
|
+
// Single file upload
|
|
475
|
+
app.post('/upload', upload.single('file'), async (req, res) => {
|
|
476
|
+
try {
|
|
477
|
+
if (!req.file) {
|
|
478
|
+
return res.status(400).json({ error: 'No file uploaded' });
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const timestamp = Date.now();
|
|
482
|
+
const key = `uploads/${timestamp}-${req.file.originalname}`;
|
|
483
|
+
|
|
484
|
+
await storage.put(key, req.file.buffer, {
|
|
485
|
+
contentType: req.file.mimetype,
|
|
486
|
+
metadata: {
|
|
487
|
+
originalName: req.file.originalname,
|
|
488
|
+
uploadedBy: req.user?.id || 'anonymous',
|
|
489
|
+
uploadedAt: new Date().toISOString(),
|
|
490
|
+
},
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
const url = storage.url(key);
|
|
494
|
+
|
|
495
|
+
res.json({
|
|
496
|
+
success: true,
|
|
497
|
+
file: {
|
|
498
|
+
key,
|
|
499
|
+
url,
|
|
500
|
+
size: req.file.size,
|
|
501
|
+
contentType: req.file.mimetype,
|
|
502
|
+
},
|
|
503
|
+
});
|
|
504
|
+
} catch (error) {
|
|
505
|
+
res.status(500).json({ error: error.message });
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
// File download
|
|
510
|
+
app.get('/files/:key(*)', async (req, res) => {
|
|
511
|
+
try {
|
|
512
|
+
const key = req.params.key;
|
|
513
|
+
|
|
514
|
+
if (!(await storage.exists(key))) {
|
|
515
|
+
return res.status(404).json({ error: 'File not found' });
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const buffer = await storage.get(key);
|
|
519
|
+
|
|
520
|
+
// Set appropriate headers
|
|
521
|
+
res.setHeader('Content-Type', 'application/octet-stream');
|
|
522
|
+
res.setHeader(
|
|
523
|
+
'Content-Disposition',
|
|
524
|
+
`attachment; filename="${key.split('/').pop()}"`
|
|
525
|
+
);
|
|
526
|
+
|
|
527
|
+
res.send(buffer);
|
|
528
|
+
} catch (error) {
|
|
529
|
+
res.status(500).json({ error: error.message });
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
// Generate signed download URL
|
|
534
|
+
app.get('/files/:key(*)/signed', async (req, res) => {
|
|
535
|
+
try {
|
|
536
|
+
const key = req.params.key;
|
|
537
|
+
const expiresIn = parseInt(req.query.expires as string) || 3600; // 1 hour default
|
|
538
|
+
|
|
539
|
+
const signedUrl = await storage.signedUrl(key, expiresIn);
|
|
540
|
+
|
|
541
|
+
res.json({
|
|
542
|
+
url: signedUrl,
|
|
543
|
+
expiresIn,
|
|
544
|
+
expiresAt: new Date(Date.now() + expiresIn * 1000).toISOString(),
|
|
545
|
+
});
|
|
546
|
+
} catch (error) {
|
|
547
|
+
res.status(500).json({ error: error.message });
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
### **Image Processing Pipeline**
|
|
553
|
+
|
|
554
|
+
```typescript
|
|
555
|
+
import { storageClass } from '@bloomneo/appkit/storage';
|
|
556
|
+
import sharp from 'sharp';
|
|
557
|
+
|
|
558
|
+
const storage = storageClass.get();
|
|
559
|
+
|
|
560
|
+
export class ImageProcessor {
|
|
561
|
+
async processImage(originalKey: string) {
|
|
562
|
+
// Download original
|
|
563
|
+
const originalBuffer = await storage.get(originalKey);
|
|
564
|
+
|
|
565
|
+
// Create different sizes
|
|
566
|
+
const sizes = [
|
|
567
|
+
{ name: 'thumb', width: 150, height: 150 },
|
|
568
|
+
{ name: 'medium', width: 500, height: 500 },
|
|
569
|
+
{ name: 'large', width: 1200, height: 1200 },
|
|
570
|
+
];
|
|
571
|
+
|
|
572
|
+
const results = [];
|
|
573
|
+
|
|
574
|
+
for (const size of sizes) {
|
|
575
|
+
// Process with Sharp
|
|
576
|
+
const processedBuffer = await sharp(originalBuffer)
|
|
577
|
+
.resize(size.width, size.height, {
|
|
578
|
+
fit: 'cover',
|
|
579
|
+
withoutEnlargement: true,
|
|
580
|
+
})
|
|
581
|
+
.jpeg({ quality: 85 })
|
|
582
|
+
.toBuffer();
|
|
583
|
+
|
|
584
|
+
// Generate new key
|
|
585
|
+
const [name, ext] = originalKey.split('.');
|
|
586
|
+
const newKey = `${name}-${size.name}.${ext}`;
|
|
587
|
+
|
|
588
|
+
// Upload processed image
|
|
589
|
+
await storage.put(newKey, processedBuffer, {
|
|
590
|
+
contentType: 'image/jpeg',
|
|
591
|
+
cacheControl: 'public, max-age=31536000', // 1 year cache
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
results.push({
|
|
595
|
+
size: size.name,
|
|
596
|
+
key: newKey,
|
|
597
|
+
url: storage.url(newKey),
|
|
598
|
+
dimensions: `${size.width}x${size.height}`,
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
return results;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
async cleanupProcessedImages(originalKey: string) {
|
|
606
|
+
const [name] = originalKey.split('.');
|
|
607
|
+
const files = await storage.list(name);
|
|
608
|
+
|
|
609
|
+
for (const file of files) {
|
|
610
|
+
if (
|
|
611
|
+
file.key.includes('-thumb.') ||
|
|
612
|
+
file.key.includes('-medium.') ||
|
|
613
|
+
file.key.includes('-large.')
|
|
614
|
+
) {
|
|
615
|
+
await storage.delete(file.key);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
### **Document Management System**
|
|
623
|
+
|
|
624
|
+
```typescript
|
|
625
|
+
import { storageClass } from '@bloomneo/appkit/storage';
|
|
626
|
+
|
|
627
|
+
const storage = storageClass.get();
|
|
628
|
+
|
|
629
|
+
export class DocumentManager {
|
|
630
|
+
async uploadDocument(
|
|
631
|
+
file: Buffer,
|
|
632
|
+
metadata: {
|
|
633
|
+
userId: string;
|
|
634
|
+
category: string;
|
|
635
|
+
filename: string;
|
|
636
|
+
contentType: string;
|
|
637
|
+
}
|
|
638
|
+
) {
|
|
639
|
+
const { userId, category, filename } = metadata;
|
|
640
|
+
const timestamp = Date.now();
|
|
641
|
+
const key = `documents/${userId}/${category}/${timestamp}-${filename}`;
|
|
642
|
+
|
|
643
|
+
await storage.put(key, file, {
|
|
644
|
+
contentType: metadata.contentType,
|
|
645
|
+
metadata: {
|
|
646
|
+
userId,
|
|
647
|
+
category,
|
|
648
|
+
originalName: filename,
|
|
649
|
+
uploadedAt: new Date().toISOString(),
|
|
650
|
+
},
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
return {
|
|
654
|
+
documentId: key,
|
|
655
|
+
url: storage.url(key),
|
|
656
|
+
category,
|
|
657
|
+
uploadedAt: new Date(),
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
async getUserDocuments(userId: string, category?: string) {
|
|
662
|
+
const prefix = category
|
|
663
|
+
? `documents/${userId}/${category}/`
|
|
664
|
+
: `documents/${userId}/`;
|
|
665
|
+
|
|
666
|
+
const files = await storage.list(prefix);
|
|
667
|
+
|
|
668
|
+
return files.map((file) => ({
|
|
669
|
+
documentId: file.key,
|
|
670
|
+
filename: file.key.split('/').pop(),
|
|
671
|
+
category: file.key.split('/')[2],
|
|
672
|
+
size: file.size,
|
|
673
|
+
lastModified: file.lastModified,
|
|
674
|
+
url: storage.url(file.key),
|
|
675
|
+
}));
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
async generateShareLink(documentId: string, expiresInHours: number = 24) {
|
|
679
|
+
const expiresIn = expiresInHours * 3600; // Convert to seconds
|
|
680
|
+
const signedUrl = await storage.signedUrl(documentId, expiresIn);
|
|
681
|
+
|
|
682
|
+
return {
|
|
683
|
+
url: signedUrl,
|
|
684
|
+
expiresAt: new Date(Date.now() + expiresIn * 1000),
|
|
685
|
+
expiresInHours,
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
async deleteDocument(documentId: string) {
|
|
690
|
+
return await storage.delete(documentId);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
### **Backup & Sync System**
|
|
696
|
+
|
|
697
|
+
```typescript
|
|
698
|
+
import { storageClass } from '@bloomneo/appkit/storage';
|
|
699
|
+
|
|
700
|
+
const storage = storageClass.get();
|
|
701
|
+
|
|
702
|
+
export class BackupManager {
|
|
703
|
+
async createBackup(sourcePrefix: string) {
|
|
704
|
+
const timestamp = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
|
|
705
|
+
const backupPrefix = `backups/${timestamp}/`;
|
|
706
|
+
|
|
707
|
+
const sourceFiles = await storage.list(sourcePrefix);
|
|
708
|
+
const backupResults = [];
|
|
709
|
+
|
|
710
|
+
for (const file of sourceFiles) {
|
|
711
|
+
const relativePath = file.key.replace(sourcePrefix, '');
|
|
712
|
+
const backupKey = backupPrefix + relativePath;
|
|
713
|
+
|
|
714
|
+
try {
|
|
715
|
+
await storage.copy(file.key, backupKey);
|
|
716
|
+
backupResults.push({
|
|
717
|
+
original: file.key,
|
|
718
|
+
backup: backupKey,
|
|
719
|
+
status: 'success',
|
|
720
|
+
});
|
|
721
|
+
} catch (error) {
|
|
722
|
+
backupResults.push({
|
|
723
|
+
original: file.key,
|
|
724
|
+
backup: backupKey,
|
|
725
|
+
status: 'failed',
|
|
726
|
+
error: error.message,
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
return {
|
|
732
|
+
backupId: timestamp,
|
|
733
|
+
sourcePrefix,
|
|
734
|
+
backupPrefix,
|
|
735
|
+
totalFiles: sourceFiles.length,
|
|
736
|
+
successful: backupResults.filter((r) => r.status === 'success').length,
|
|
737
|
+
failed: backupResults.filter((r) => r.status === 'failed').length,
|
|
738
|
+
results: backupResults,
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
async restoreFromBackup(backupId: string, targetPrefix: string) {
|
|
743
|
+
const backupPrefix = `backups/${backupId}/`;
|
|
744
|
+
const backupFiles = await storage.list(backupPrefix);
|
|
745
|
+
|
|
746
|
+
for (const file of backupFiles) {
|
|
747
|
+
const relativePath = file.key.replace(backupPrefix, '');
|
|
748
|
+
const targetKey = targetPrefix + relativePath;
|
|
749
|
+
|
|
750
|
+
await storage.copy(file.key, targetKey);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
return {
|
|
754
|
+
restored: backupFiles.length,
|
|
755
|
+
targetPrefix,
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
async cleanupOldBackups(retentionDays: number = 30) {
|
|
760
|
+
const cutoffDate = new Date();
|
|
761
|
+
cutoffDate.setDate(cutoffDate.getDate() - retentionDays);
|
|
762
|
+
|
|
763
|
+
const backups = await storage.list('backups/');
|
|
764
|
+
const oldBackups = backups.filter((file) => file.lastModified < cutoffDate);
|
|
765
|
+
|
|
766
|
+
for (const backup of oldBackups) {
|
|
767
|
+
await storage.delete(backup.key);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
return {
|
|
771
|
+
deleted: oldBackups.length,
|
|
772
|
+
retentionDays,
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
```
|
|
777
|
+
|
|
778
|
+
## ๐ Environment Variables
|
|
779
|
+
|
|
780
|
+
### Strategy Selection (Auto-detected)
|
|
781
|
+
|
|
782
|
+
```bash
|
|
783
|
+
# Priority order: R2 โ S3 โ Local
|
|
784
|
+
|
|
785
|
+
# Cloudflare R2 (Highest priority - zero egress fees)
|
|
786
|
+
CLOUDFLARE_R2_BUCKET=my-bucket
|
|
787
|
+
CLOUDFLARE_ACCOUNT_ID=account_id
|
|
788
|
+
CLOUDFLARE_R2_ACCESS_KEY_ID=access_key
|
|
789
|
+
CLOUDFLARE_R2_SECRET_ACCESS_KEY=secret_key
|
|
790
|
+
CLOUDFLARE_R2_CDN_URL=https://cdn.example.com # Optional CDN
|
|
791
|
+
|
|
792
|
+
# AWS S3 / S3-Compatible (Second priority)
|
|
793
|
+
AWS_S3_BUCKET=my-bucket
|
|
794
|
+
AWS_ACCESS_KEY_ID=access_key
|
|
795
|
+
AWS_SECRET_ACCESS_KEY=secret_key
|
|
796
|
+
AWS_REGION=us-east-1 # Default: us-east-1
|
|
797
|
+
|
|
798
|
+
# S3-Compatible Services (Wasabi, MinIO, etc.)
|
|
799
|
+
S3_ENDPOINT=https://s3.wasabisys.com # Custom endpoint
|
|
800
|
+
S3_FORCE_PATH_STYLE=true # For MinIO
|
|
801
|
+
|
|
802
|
+
# Local Storage (Fallback - no cloud vars needed)
|
|
803
|
+
BLOOM_STORAGE_DIR=./uploads # Default: ./uploads
|
|
804
|
+
BLOOM_STORAGE_BASE_URL=/uploads # Default: /uploads
|
|
805
|
+
```
|
|
806
|
+
|
|
807
|
+
### Security & Limits
|
|
808
|
+
|
|
809
|
+
```bash
|
|
810
|
+
# File validation
|
|
811
|
+
BLOOM_STORAGE_MAX_SIZE=52428800 # 50MB default
|
|
812
|
+
BLOOM_STORAGE_ALLOWED_TYPES=image/*,application/pdf,text/*
|
|
813
|
+
|
|
814
|
+
# Signed URL expiration
|
|
815
|
+
BLOOM_STORAGE_SIGNED_EXPIRY=3600 # 1 hour default
|
|
816
|
+
|
|
817
|
+
# CDN configuration
|
|
818
|
+
BLOOM_STORAGE_CDN_URL=https://cdn.example.com # For any strategy
|
|
819
|
+
```
|
|
820
|
+
|
|
821
|
+
## ๐ Development vs Production
|
|
822
|
+
|
|
823
|
+
### **Development Mode**
|
|
824
|
+
|
|
825
|
+
```bash
|
|
826
|
+
# No environment variables needed
|
|
827
|
+
NODE_ENV=development
|
|
828
|
+
```
|
|
829
|
+
|
|
830
|
+
```typescript
|
|
831
|
+
const storage = storageClass.get();
|
|
832
|
+
// Strategy: Local filesystem (./uploads/)
|
|
833
|
+
// URLs: /uploads/file.jpg
|
|
834
|
+
// Features: File type validation, size limits
|
|
835
|
+
```
|
|
836
|
+
|
|
837
|
+
### **Production Mode**
|
|
838
|
+
|
|
839
|
+
```bash
|
|
840
|
+
# Cloud storage required
|
|
841
|
+
NODE_ENV=production
|
|
842
|
+
CLOUDFLARE_R2_BUCKET=prod-assets
|
|
843
|
+
# ... other cloud credentials
|
|
844
|
+
```
|
|
845
|
+
|
|
846
|
+
```typescript
|
|
847
|
+
const storage = storageClass.get();
|
|
848
|
+
// Strategy: R2 or S3 (distributed)
|
|
849
|
+
// URLs: https://cdn.example.com/file.jpg
|
|
850
|
+
// Features: CDN delivery, signed URLs, zero egress (R2)
|
|
851
|
+
```
|
|
852
|
+
|
|
853
|
+
### **Scaling Pattern**
|
|
854
|
+
|
|
855
|
+
```typescript
|
|
856
|
+
// Week 1: Local development
|
|
857
|
+
// No env vars needed - works immediately
|
|
858
|
+
|
|
859
|
+
// Month 1: Add cloud storage
|
|
860
|
+
// Set CLOUDFLARE_R2_BUCKET - zero code changes
|
|
861
|
+
|
|
862
|
+
// Year 1: Add CDN
|
|
863
|
+
// Set CLOUDFLARE_R2_CDN_URL - automatic CDN delivery
|
|
864
|
+
```
|
|
865
|
+
|
|
866
|
+
## ๐งช Testing
|
|
867
|
+
|
|
868
|
+
### **Test Setup**
|
|
869
|
+
|
|
870
|
+
```typescript
|
|
871
|
+
import { storageClass } from '@bloomneo/appkit/storage';
|
|
872
|
+
|
|
873
|
+
describe('File Storage', () => {
|
|
874
|
+
afterEach(async () => {
|
|
875
|
+
// IMPORTANT: Clear storage state between tests
|
|
876
|
+
await storageClass.clear();
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
test('should upload and download files', async () => {
|
|
880
|
+
const storage = storageClass.get();
|
|
881
|
+
|
|
882
|
+
const testData = Buffer.from('Hello, World!');
|
|
883
|
+
await storage.put('test.txt', testData);
|
|
884
|
+
|
|
885
|
+
const downloaded = await storage.get('test.txt');
|
|
886
|
+
expect(downloaded.toString()).toBe('Hello, World!');
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
test('should generate public URLs', async () => {
|
|
890
|
+
const storage = storageClass.get();
|
|
891
|
+
|
|
892
|
+
await storage.put('image.jpg', Buffer.from('fake image'));
|
|
893
|
+
const url = storage.url('image.jpg');
|
|
894
|
+
|
|
895
|
+
expect(url).toMatch(/image\.jpg$/);
|
|
896
|
+
});
|
|
897
|
+
});
|
|
898
|
+
```
|
|
899
|
+
|
|
900
|
+
### **Mock Cloud Storage for Tests**
|
|
901
|
+
|
|
902
|
+
```typescript
|
|
903
|
+
// Force local strategy for testing
|
|
904
|
+
describe('Storage with Local Strategy', () => {
|
|
905
|
+
beforeEach(() => {
|
|
906
|
+
storageClass.reset({
|
|
907
|
+
strategy: 'local',
|
|
908
|
+
local: {
|
|
909
|
+
dir: './test-uploads',
|
|
910
|
+
baseUrl: '/test-uploads',
|
|
911
|
+
maxFileSize: 1048576, // 1MB for tests
|
|
912
|
+
allowedTypes: ['*'],
|
|
913
|
+
createDirs: true,
|
|
914
|
+
},
|
|
915
|
+
});
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
afterEach(async () => {
|
|
919
|
+
await storageClass.clear();
|
|
920
|
+
// Clean up test directory
|
|
921
|
+
await fs.rm('./test-uploads', { recursive: true, force: true });
|
|
922
|
+
});
|
|
923
|
+
});
|
|
924
|
+
```
|
|
925
|
+
|
|
926
|
+
## ๐ Performance
|
|
927
|
+
|
|
928
|
+
- **Local Strategy**: ~1ms per operation (filesystem I/O)
|
|
929
|
+
- **S3 Strategy**: ~50-200ms per operation (network + AWS)
|
|
930
|
+
- **R2 Strategy**: ~50-200ms per operation (network + Cloudflare)
|
|
931
|
+
- **CDN URLs**: ~1ms generation (no network calls)
|
|
932
|
+
- **Memory Usage**: <5MB baseline per strategy
|
|
933
|
+
|
|
934
|
+
## ๐ฐ Cost Comparison
|
|
935
|
+
|
|
936
|
+
| Provider | Storage | Egress | CDN | Best For |
|
|
937
|
+
| ----------------- | ---------- | -------- | ---------- | --------------------------- |
|
|
938
|
+
| **Local** | Free | Free | None | Development, single server |
|
|
939
|
+
| **Cloudflare R2** | $0.015/GB | **FREE** | Included | High-bandwidth, global apps |
|
|
940
|
+
| **AWS S3** | $0.023/GB | $0.09/GB | Extra cost | Enterprise, AWS ecosystem |
|
|
941
|
+
| **Wasabi** | $0.0059/GB | FREE | Extra cost | Archive, backup storage |
|
|
942
|
+
|
|
943
|
+
## ๐ TypeScript Support
|
|
944
|
+
|
|
945
|
+
Full TypeScript support with comprehensive interfaces:
|
|
946
|
+
|
|
947
|
+
```typescript
|
|
948
|
+
import type {
|
|
949
|
+
Storage,
|
|
950
|
+
StorageFile,
|
|
951
|
+
PutOptions,
|
|
952
|
+
} from '@bloomneo/appkit/storage';
|
|
953
|
+
|
|
954
|
+
// Strongly typed storage operations
|
|
955
|
+
const storage: Storage = storageClass.get();
|
|
956
|
+
|
|
957
|
+
const files: StorageFile[] = await storage.list('images/');
|
|
958
|
+
const options: PutOptions = {
|
|
959
|
+
contentType: 'image/jpeg',
|
|
960
|
+
metadata: { userId: '123' },
|
|
961
|
+
};
|
|
962
|
+
|
|
963
|
+
await storage.put('image.jpg', buffer, options);
|
|
964
|
+
```
|
|
965
|
+
|
|
966
|
+
## ๐ Why Not AWS SDK/Google Cloud directly?
|
|
967
|
+
|
|
968
|
+
**Other approaches:**
|
|
969
|
+
|
|
970
|
+
```javascript
|
|
971
|
+
// AWS SDK: Complex setup, provider-specific
|
|
972
|
+
const AWS = require('aws-sdk');
|
|
973
|
+
const s3 = new AWS.S3({
|
|
974
|
+
accessKeyId: 'key',
|
|
975
|
+
secretAccessKey: 'secret',
|
|
976
|
+
region: 'us-east-1',
|
|
977
|
+
});
|
|
978
|
+
|
|
979
|
+
const params = {
|
|
980
|
+
Bucket: 'bucket',
|
|
981
|
+
Key: 'file.jpg',
|
|
982
|
+
Body: buffer,
|
|
983
|
+
ContentType: 'image/jpeg',
|
|
984
|
+
};
|
|
985
|
+
|
|
986
|
+
s3.upload(params, callback);
|
|
987
|
+
```
|
|
988
|
+
|
|
989
|
+
**This library:**
|
|
990
|
+
|
|
991
|
+
```typescript
|
|
992
|
+
// 2 lines, works with any provider
|
|
993
|
+
import { storageClass } from '@bloomneo/appkit/storage';
|
|
994
|
+
await storageClass.get().put('file.jpg', buffer);
|
|
995
|
+
```
|
|
996
|
+
|
|
997
|
+
**Same features, 90% less code, automatic provider detection.**
|
|
998
|
+
|
|
999
|
+
## ๐ License
|
|
1000
|
+
|
|
1001
|
+
MIT ยฉ [Bloomneo](https://github.com/bloomneo)
|
|
1002
|
+
|
|
1003
|
+
---
|
|
1004
|
+
|
|
1005
|
+
<p align="center">
|
|
1006
|
+
<strong>Built with โค๏ธ by the <a href="https://github.com/bloomneo">Bloomneo Team</a></strong><br>
|
|
1007
|
+
Because file storage should be simple, not a vendor nightmare.
|
|
1008
|
+
</p>
|