@xenterprises/fastify-xstorage 1.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/README.md ADDED
@@ -0,0 +1,492 @@
1
+ # xStorage
2
+
3
+ > Fastify v5 plugin providing a simple, intuitive library of methods for S3-compatible storage.
4
+
5
+ Manage file uploads, downloads, and storage operations with Digital Ocean Spaces, AWS S3, Cloudflare R2, and any S3-compatible storage service. **For image processing, use [@xenterprises/fastify-ximagepipeline](https://github.com/x-enterprises/fastify-plugins/tree/main/fastify-ximagepipeline).**
6
+
7
+ ## Requirements
8
+
9
+ - **Fastify v5.0.0+**
10
+ - **Node.js v20+**
11
+
12
+ ## Features
13
+
14
+ - 📦 **S3-Compatible Storage** - Works with Digital Ocean Spaces, AWS S3, Cloudflare R2
15
+ - 🔒 **Secure by Default** - Private ACL with signed URLs for access control
16
+ - 🔗 **Signed URLs** - Generate temporary access URLs for private files
17
+ - 📊 **File Operations** - Upload, download, delete, list, copy, and metadata retrieval
18
+ - ⚡ **Concurrent Operations** - Batch uploads and deletes for efficiency
19
+ - 🎯 **Simple API** - Intuitive methods decorated on Fastify instance
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ npm install @xenterprises/fastify-xstorage @aws-sdk/client-s3 @aws-sdk/s3-request-presigner fastify@5
25
+ ```
26
+
27
+ For file uploads via HTTP, also install:
28
+
29
+ ```bash
30
+ npm install @fastify/multipart@9
31
+ ```
32
+
33
+ ## Quick Start
34
+
35
+ ```javascript
36
+ import Fastify from "fastify";
37
+ import xStorage from "@xenterprises/fastify-xstorage";
38
+
39
+ const fastify = Fastify({ logger: true });
40
+
41
+ // Register xStorage
42
+ await fastify.register(xStorage, {
43
+ endpoint: "https://nyc3.digitaloceanspaces.com", // Digital Ocean Spaces
44
+ region: "us-east-1",
45
+ accessKeyId: process.env.STORAGE_ACCESS_KEY_ID,
46
+ secretAccessKey: process.env.STORAGE_SECRET_ACCESS_KEY,
47
+ bucket: "your-bucket-name",
48
+ publicUrl: "https://your-bucket-name.nyc3.digitaloceanspaces.com",
49
+ // Default ACL is "private" - use signed URLs for secure access
50
+ });
51
+
52
+ // Upload a file programmatically
53
+ const buffer = await fs.readFile("path/to/file.pdf");
54
+ const result = await fastify.xStorage.upload(buffer, "file.pdf", {
55
+ folder: "documents",
56
+ });
57
+
58
+ console.log(result);
59
+ // {
60
+ // key: "documents/file-a1b2c3d4.pdf",
61
+ // url: "https://your-bucket-name.nyc3.digitaloceanspaces.com/documents/file-a1b2c3d4.pdf",
62
+ // size: 123456,
63
+ // contentType: "application/pdf"
64
+ // }
65
+
66
+ // Generate a signed URL for temporary private file access
67
+ const signedUrl = await fastify.xStorage.getSignedUrl(
68
+ result.key,
69
+ 3600 // Expires in 1 hour
70
+ );
71
+
72
+ // Use in your application
73
+ await fastify.prisma.document.create({
74
+ data: {
75
+ filename: "file.pdf",
76
+ storageKey: result.key,
77
+ size: result.size,
78
+ },
79
+ });
80
+
81
+ await fastify.listen({ port: 3000 });
82
+ ```
83
+
84
+ ## Configuration
85
+
86
+ ### Digital Ocean Spaces
87
+
88
+ ```javascript
89
+ await fastify.register(xStorage, {
90
+ endpoint: "https://nyc3.digitaloceanspaces.com",
91
+ region: "nyc3",
92
+ accessKeyId: process.env.DO_SPACES_KEY,
93
+ secretAccessKey: process.env.DO_SPACES_SECRET,
94
+ bucket: "your-bucket",
95
+ publicUrl: "https://your-bucket.nyc3.digitaloceanspaces.com",
96
+ forcePathStyle: true,
97
+ // acl: "private", // Default - use signed URLs for access
98
+ });
99
+ ```
100
+
101
+ ### AWS S3
102
+
103
+ ```javascript
104
+ await fastify.register(xStorage, {
105
+ region: "us-east-1",
106
+ accessKeyId: process.env.AWS_ACCESS_KEY_ID,
107
+ secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
108
+ bucket: "your-bucket",
109
+ publicUrl: "https://your-bucket.s3.us-east-1.amazonaws.com",
110
+ forcePathStyle: false,
111
+ // acl: "private", // Default - use signed URLs for access
112
+ });
113
+ ```
114
+
115
+ ### Cloudflare R2
116
+
117
+ ```javascript
118
+ await fastify.register(xStorage, {
119
+ endpoint: "https://your-account-id.r2.cloudflarestorage.com",
120
+ region: "auto",
121
+ accessKeyId: process.env.R2_ACCESS_KEY_ID,
122
+ secretAccessKey: process.env.R2_SECRET_ACCESS_KEY,
123
+ bucket: "your-bucket",
124
+ publicUrl: "https://your-custom-domain.com",
125
+ forcePathStyle: true,
126
+ // acl: "private", // Default - use signed URLs for access
127
+ });
128
+ ```
129
+
130
+ ## Core Storage API
131
+
132
+ All methods are available on the `fastify.xStorage` namespace.
133
+
134
+ ### `fastify.xStorage.upload(file, filename, options)`
135
+
136
+ Upload a file to storage.
137
+
138
+ ```javascript
139
+ const result = await fastify.xStorage.upload(buffer, "document.pdf", {
140
+ folder: "documents",
141
+ useRandomName: true,
142
+ });
143
+
144
+ console.log(result);
145
+ // {
146
+ // key: "documents/document-a1b2c3d4.pdf",
147
+ // url: "https://your-bucket.nyc3.digitaloceanspaces.com/documents/document-a1b2c3d4.pdf",
148
+ // size: 245678,
149
+ // contentType: "application/pdf"
150
+ // }
151
+
152
+ // Store in database
153
+ await db.documents.create({
154
+ data: {
155
+ filename: "document.pdf",
156
+ storageKey: result.key,
157
+ size: result.size,
158
+ },
159
+ });
160
+ ```
161
+
162
+ **Options:**
163
+ - `folder` - Folder path (e.g., "documents", "docs/2024")
164
+ - `key` - Custom storage key (overrides folder/filename)
165
+ - `contentType` - MIME type (auto-detected if not provided)
166
+ - `metadata` - Custom metadata object
167
+ - `useRandomName` - Add random ID to filename (default: true)
168
+ - `acl` - File ACL override (default: uses plugin's configured ACL)
169
+ - `"private"` - Only accessible via signed URLs
170
+ - `"public-read"` - Publicly accessible
171
+
172
+ **Example with per-file ACL:**
173
+ ```javascript
174
+ // Private file (default)
175
+ await fastify.xStorage.upload(buffer, "private.pdf", {
176
+ folder: "documents",
177
+ });
178
+
179
+ // Public file
180
+ await fastify.xStorage.upload(buffer, "public.pdf", {
181
+ folder: "documents",
182
+ acl: "public-read",
183
+ });
184
+ ```
185
+
186
+ ### `fastify.xStorage.uploadMultiple(files, options)`
187
+
188
+ Upload multiple files at once with optional per-file ACL control.
189
+
190
+ ```javascript
191
+ const files = [
192
+ { file: buffer1, filename: "private.pdf" },
193
+ { file: buffer2, filename: "public.pdf", acl: "public-read" },
194
+ ];
195
+
196
+ const results = await fastify.xStorage.uploadMultiple(files, {
197
+ folder: "documents",
198
+ acl: "private", // Default ACL for all files
199
+ });
200
+
201
+ // Returns array of upload results
202
+ // Files can override batch ACL with per-file acl property
203
+ ```
204
+
205
+ ### `fastify.xStorage.delete(key)`
206
+
207
+ Delete a file.
208
+
209
+ ```javascript
210
+ await fastify.xStorage.delete("documents/document-a1b2c3d4.pdf");
211
+ ```
212
+
213
+ ### `fastify.xStorage.deleteMultiple(keys)`
214
+
215
+ Delete multiple files at once.
216
+
217
+ ```javascript
218
+ const keys = ["documents/doc1.pdf", "documents/doc2.pdf"];
219
+ await fastify.xStorage.deleteMultiple(keys);
220
+ ```
221
+
222
+ ### `fastify.xStorage.download(key)`
223
+
224
+ Download a file as a buffer.
225
+
226
+ ```javascript
227
+ const buffer = await fastify.xStorage.download("documents/document-a1b2c3d4.pdf");
228
+ ```
229
+
230
+ ### `fastify.xStorage.list(prefix, maxKeys)`
231
+
232
+ List files in a folder.
233
+
234
+ ```javascript
235
+ const files = await fastify.xStorage.list("documents/", 100);
236
+
237
+ console.log(files);
238
+ // [
239
+ // {
240
+ // key: "documents/doc1.pdf",
241
+ // url: "https://...",
242
+ // size: 123456,
243
+ // lastModified: Date,
244
+ // etag: "..."
245
+ // }
246
+ // ]
247
+ ```
248
+
249
+ ### `fastify.xStorage.getSignedUrl(key, expiresIn)`
250
+
251
+ Generate a temporary signed URL for private file access.
252
+
253
+ ```javascript
254
+ const url = await fastify.xStorage.getSignedUrl("documents/document-a1b2c3d4.pdf", 3600); // 1 hour
255
+ ```
256
+
257
+ ### `fastify.xStorage.getPublicUrl(key)`
258
+
259
+ Get public URL for a file. Note: This only works if file ACL is set to public.
260
+
261
+ ```javascript
262
+ const url = fastify.xStorage.getPublicUrl("documents/document.pdf");
263
+ // Returns: "https://your-bucket.nyc3.digitaloceanspaces.com/documents/document.pdf"
264
+ ```
265
+
266
+ ### `fastify.xStorage.copy(sourceKey, destinationKey)`
267
+
268
+ Copy a file to a new location.
269
+
270
+ ```javascript
271
+ await fastify.xStorage.copy("documents/doc1.pdf", "documents/backup/doc1.pdf");
272
+ ```
273
+
274
+ ### `fastify.xStorage.exists(key)`
275
+
276
+ Check if a file exists.
277
+
278
+ ```javascript
279
+ const exists = await fastify.xStorage.exists("documents/document.pdf");
280
+ ```
281
+
282
+ ### `fastify.xStorage.getMetadata(key)`
283
+
284
+ Get file metadata.
285
+
286
+ ```javascript
287
+ const metadata = await fastify.xStorage.getMetadata("documents/document.pdf");
288
+ // Returns: { size, contentType, lastModified, etag, etc }
289
+ ```
290
+
291
+ ## Image Processing
292
+
293
+ For image processing, optimization, resizing, thumbnail generation, and format conversion, use **[@xenterprises/fastify-ximagepipeline](https://github.com/x-enterprises/fastify-plugins/tree/main/fastify-ximagepipeline)**.
294
+
295
+ xImagePipeline integrates with xStorage and provides:
296
+ - 🖼️ Image optimization and format conversion
297
+ - 🎯 Multiple variant generation (webp, avif, etc)
298
+ - 📐 Intelligent resizing and cropping
299
+ - 🔍 EXIF metadata extraction and stripping
300
+ - 💫 Blur hash generation for progressive loading
301
+ - 📊 Compressed original image storage
302
+
303
+ ```javascript
304
+ import Fastify from "fastify";
305
+ import xImagePipeline from "@xenterprises/fastify-ximagepipeline";
306
+ import xStorage from "@xenterprises/fastify-xstorage";
307
+
308
+ const fastify = Fastify();
309
+
310
+ // Register xStorage first
311
+ await fastify.register(xStorage, { /* config */ });
312
+
313
+ // Register xImagePipeline
314
+ await fastify.register(xImagePipeline, { /* config */ });
315
+
316
+ // Use for image processing
317
+ const result = await fastify.ximagepipeline.processImage(buffer, "photo.jpg", {
318
+ sourceType: "avatar", // Uses configured variants for avatar
319
+ });
320
+ ```
321
+
322
+ ## Helper Utilities
323
+
324
+ ```javascript
325
+ import { helpers } from "@xenterprises/fastify-xstorage";
326
+
327
+ // Format file size
328
+ helpers.formatFileSize(1234567); // "1.18 MB"
329
+
330
+ // Check file types
331
+ helpers.isImage("photo.jpg"); // true
332
+ helpers.isPdf("document.pdf"); // true
333
+ helpers.isVideo("movie.mp4"); // true
334
+
335
+ // Sanitize filename
336
+ helpers.sanitizeFilename("My File (2024).jpg"); // "my_file_2024.jpg"
337
+
338
+ // Calculate dimensions
339
+ helpers.calculateFitDimensions(4000, 3000, 1920, 1080);
340
+ // { width: 1440, height: 1080 }
341
+
342
+ // Generate responsive sizes
343
+ helpers.generateResponsiveSizes(1920, 1080);
344
+ // [
345
+ // { width: 320, height: 180, name: "w320" },
346
+ // { width: 640, height: 360, name: "w640" },
347
+ // // ...
348
+ // ]
349
+ ```
350
+
351
+ ## Usage Examples
352
+
353
+ ### Document Upload
354
+
355
+ ```javascript
356
+ import multipart from "@fastify/multipart";
357
+
358
+ // Register multipart for file uploads
359
+ await fastify.register(multipart);
360
+
361
+ // HTTP endpoint for document upload
362
+ fastify.post("/documents", async (request, reply) => {
363
+ const data = await request.file();
364
+
365
+ if (!data) {
366
+ return reply.code(400).send({ error: "No file uploaded" });
367
+ }
368
+
369
+ const buffer = await data.toBuffer();
370
+
371
+ // Upload file
372
+ const result = await fastify.xStorage.upload(buffer, data.filename, {
373
+ folder: "documents",
374
+ useRandomName: true,
375
+ });
376
+
377
+ // Save to database
378
+ await fastify.db.document.create({
379
+ data: {
380
+ filename: data.filename,
381
+ storageKey: result.key,
382
+ size: result.size,
383
+ contentType: result.contentType,
384
+ },
385
+ });
386
+
387
+ return { success: true, file: result };
388
+ });
389
+
390
+ // Download document with signed URL
391
+ fastify.get("/documents/:id", async (request, reply) => {
392
+ const { id } = request.params;
393
+
394
+ const document = await fastify.db.document.findUnique({ where: { id } });
395
+
396
+ // Generate signed URL valid for 1 hour
397
+ const signedUrl = await fastify.xStorage.getSignedUrl(document.storageKey, 3600);
398
+
399
+ return { download: signedUrl };
400
+ });
401
+ ```
402
+
403
+ ### Batch File Operations
404
+
405
+ ```javascript
406
+ // Upload multiple files
407
+ fastify.post("/batch-upload", async (request, reply) => {
408
+ const parts = request.parts();
409
+ const files = [];
410
+
411
+ for await (const part of parts) {
412
+ if (part.type === "file") {
413
+ const buffer = await part.toBuffer();
414
+ files.push({ file: buffer, filename: part.filename });
415
+ }
416
+ }
417
+
418
+ const results = await fastify.xStorage.uploadMultiple(files, {
419
+ folder: "batch-uploads",
420
+ });
421
+
422
+ return { success: true, files: results };
423
+ });
424
+
425
+ // Delete multiple files
426
+ fastify.post("/batch-delete", async (request, reply) => {
427
+ const { keys } = request.body;
428
+
429
+ await fastify.xStorage.deleteMultiple(keys);
430
+
431
+ return { success: true, deleted: keys.length };
432
+ });
433
+ ```
434
+
435
+ ### File Organization
436
+
437
+ ```javascript
438
+ // List files in a folder
439
+ fastify.get("/files/:folder", async (request, reply) => {
440
+ const { folder } = request.params;
441
+
442
+ const files = await fastify.xStorage.list(`${folder}/`, 100);
443
+
444
+ return { folder, files };
445
+ });
446
+
447
+ // Move file (copy then delete)
448
+ fastify.post("/files/move", async (request, reply) => {
449
+ const { sourceKey, destinationKey } = request.body;
450
+
451
+ await fastify.xStorage.copy(sourceKey, destinationKey);
452
+ await fastify.xStorage.delete(sourceKey);
453
+
454
+ return { success: true, newLocation: destinationKey };
455
+ });
456
+ ```
457
+
458
+ ## Best Practices
459
+
460
+ 1. **Use signed URLs by default** - Default ACL is private; always use signed URLs for file access
461
+ 2. **Validate file types** - Check file types before accepting uploads
462
+ 3. **Use random filenames** - Prevents accidental overwrites of existing files
463
+ 4. **Store storage keys in database** - Keep reference to the storage key, not just the URL
464
+ 5. **Organize with folders** - Use logical folder structure (e.g., "documents/2024/january")
465
+ 6. **Set reasonable expiration times** - Use appropriate TTL for signed URLs (shorter for sensitive data)
466
+ 7. **Handle errors gracefully** - Files might be deleted externally; implement proper error handling
467
+ 8. **Batch operations for efficiency** - Use uploadMultiple/deleteMultiple for better performance
468
+
469
+ ## Plugin Options
470
+
471
+ | Option | Type | Default | Description |
472
+ |--------|------|---------|-------------|
473
+ | `endpoint` | string | - | S3 endpoint URL (required for non-AWS) |
474
+ | `region` | string | `"us-east-1"` | AWS region |
475
+ | `accessKeyId` | string | - | Access key ID (required) |
476
+ | `secretAccessKey` | string | - | Secret access key (required) |
477
+ | `bucket` | string | - | Bucket name (required) |
478
+ | `publicUrl` | string | - | Public URL base (required) |
479
+ | `forcePathStyle` | boolean | `true` | Use path-style URLs |
480
+ | `acl` | string | `"private"` | Default ACL for uploads |
481
+
482
+ ## Testing
483
+
484
+ See [TESTING.md](./TESTING.md) for comprehensive testing guide.
485
+
486
+ ## Examples
487
+
488
+ See [EXAMPLES.md](./EXAMPLES.md) for complete real-world examples.
489
+
490
+ ## License
491
+
492
+ ISC