@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/TESTING.md ADDED
@@ -0,0 +1,567 @@
1
+ # Testing Guide
2
+
3
+ Complete guide for testing xStorage plugin locally and in production.
4
+
5
+ ## Local Development Setup
6
+
7
+ ### 1. Install Dependencies
8
+
9
+ ```bash
10
+ npm install
11
+ ```
12
+
13
+ ### 2. Setup Storage Provider
14
+
15
+ Choose one of the following:
16
+
17
+ #### Digital Ocean Spaces
18
+
19
+ 1. Create a Space at https://cloud.digitalocean.com/spaces
20
+ 2. Create API keys at https://cloud.digitalocean.com/account/api/tokens
21
+ 3. Configure CORS (allow your domain)
22
+ 4. Set permissions to public-read
23
+
24
+ #### AWS S3
25
+
26
+ 1. Create S3 bucket at https://console.aws.amazon.com/s3
27
+ 2. Create IAM user with S3 permissions
28
+ 3. Configure bucket policy for public access
29
+ 4. Enable CORS
30
+
31
+ #### Cloudflare R2
32
+
33
+ 1. Create R2 bucket at https://dash.cloudflare.com/r2
34
+ 2. Create API token
35
+ 3. Set up custom domain
36
+ 4. Configure CORS
37
+
38
+ ### 3. Configure Environment
39
+
40
+ Create `.env` file:
41
+
42
+ ```bash
43
+ cp .env.example .env
44
+ ```
45
+
46
+ Edit `.env` with your credentials:
47
+
48
+ ```bash
49
+ STORAGE_ENDPOINT=https://nyc3.digitaloceanspaces.com
50
+ STORAGE_REGION=us-east-1
51
+ STORAGE_ACCESS_KEY_ID=your_key
52
+ STORAGE_SECRET_ACCESS_KEY=your_secret
53
+ STORAGE_BUCKET=your-bucket
54
+ STORAGE_PUBLIC_URL=https://your-bucket.nyc3.digitaloceanspaces.com
55
+ PORT=3000
56
+ ```
57
+
58
+ ### 4. Start Development Server
59
+
60
+ ```bash
61
+ npm run dev
62
+ ```
63
+
64
+ ## Manual Testing
65
+
66
+ ### Test File Upload
67
+
68
+ ```bash
69
+ # Upload a file
70
+ curl -X POST http://localhost:3000/upload \
71
+ -F "file=@./test-image.jpg"
72
+
73
+ # Response:
74
+ {
75
+ "success": true,
76
+ "file": {
77
+ "key": "uploads/test-image-a1b2c3d4.jpg",
78
+ "url": "https://your-bucket.nyc3.digitaloceanspaces.com/uploads/test-image-a1b2c3d4.jpg",
79
+ "size": 245678,
80
+ "contentType": "image/jpeg"
81
+ }
82
+ }
83
+ ```
84
+
85
+ ### Test Image Upload with Thumbnails
86
+
87
+ ```bash
88
+ curl -X POST http://localhost:3000/upload/image \
89
+ -F "file=@./photo.jpg"
90
+
91
+ # Response includes original + thumbnails
92
+ {
93
+ "success": true,
94
+ "image": {
95
+ "original": {
96
+ "url": "https://.../images/photo.webp",
97
+ "width": 1920,
98
+ "height": 1080
99
+ },
100
+ "thumbnails": [
101
+ {
102
+ "size": "small",
103
+ "url": "https://.../thumbnails/photo-small.webp",
104
+ "width": 150,
105
+ "height": 150
106
+ }
107
+ ]
108
+ }
109
+ }
110
+ ```
111
+
112
+ ### Test PDF Upload
113
+
114
+ ```bash
115
+ curl -X POST http://localhost:3000/upload/pdf \
116
+ -F "file=@./document.pdf"
117
+
118
+ # Response:
119
+ {
120
+ "success": true,
121
+ "pdf": {
122
+ "key": "pdfs/document-a1b2c3d4.pdf",
123
+ "url": "https://.../pdfs/document-a1b2c3d4.pdf",
124
+ "size": 456789,
125
+ "pageCount": 12
126
+ }
127
+ }
128
+ ```
129
+
130
+ ### Test Multiple Files
131
+
132
+ ```bash
133
+ curl -X POST http://localhost:3000/upload/multiple \
134
+ -F "file1=@./photo1.jpg" \
135
+ -F "file2=@./photo2.jpg" \
136
+ -F "file3=@./photo3.jpg"
137
+ ```
138
+
139
+ ### List Files
140
+
141
+ ```bash
142
+ curl http://localhost:3000/files?folder=uploads
143
+
144
+ # Response:
145
+ {
146
+ "success": true,
147
+ "files": [
148
+ {
149
+ "key": "uploads/photo1.jpg",
150
+ "url": "https://.../uploads/photo1.jpg",
151
+ "size": 123456,
152
+ "lastModified": "2024-01-15T10:30:00.000Z"
153
+ }
154
+ ]
155
+ }
156
+ ```
157
+
158
+ ### Delete File
159
+
160
+ ```bash
161
+ curl -X DELETE "http://localhost:3000/files/uploads%2Fphoto1.jpg"
162
+ ```
163
+
164
+ ### Get Signed URL
165
+
166
+ ```bash
167
+ curl "http://localhost:3000/files/uploads%2Fphoto1.jpg/signed-url?expires=3600"
168
+
169
+ # Response:
170
+ {
171
+ "success": true,
172
+ "url": "https://...?X-Amz-Signature=..."
173
+ }
174
+ ```
175
+
176
+ ## Unit Testing
177
+
178
+ Create `test/storage.test.js`:
179
+
180
+ ```javascript
181
+ import { test } from "node:test";
182
+ import assert from "node:assert";
183
+ import Fastify from "fastify";
184
+ import xStorage from "../src/xStorage.js";
185
+ import fs from "fs";
186
+
187
+ test("Storage plugin registration", async () => {
188
+ const fastify = Fastify();
189
+
190
+ await fastify.register(xStorage, {
191
+ endpoint: process.env.STORAGE_ENDPOINT,
192
+ region: process.env.STORAGE_REGION,
193
+ accessKeyId: process.env.STORAGE_ACCESS_KEY_ID,
194
+ secretAccessKey: process.env.STORAGE_SECRET_ACCESS_KEY,
195
+ bucket: process.env.STORAGE_BUCKET,
196
+ publicUrl: process.env.STORAGE_PUBLIC_URL,
197
+ });
198
+
199
+ assert.ok(fastify.storage);
200
+ assert.ok(fastify.images);
201
+ assert.ok(fastify.pdfs);
202
+ });
203
+
204
+ test("Upload file", async () => {
205
+ const fastify = Fastify();
206
+
207
+ await fastify.register(xStorage, {
208
+ endpoint: process.env.STORAGE_ENDPOINT,
209
+ region: process.env.STORAGE_REGION,
210
+ accessKeyId: process.env.STORAGE_ACCESS_KEY_ID,
211
+ secretAccessKey: process.env.STORAGE_SECRET_ACCESS_KEY,
212
+ bucket: process.env.STORAGE_BUCKET,
213
+ publicUrl: process.env.STORAGE_PUBLIC_URL,
214
+ });
215
+
216
+ const buffer = Buffer.from("test content");
217
+ const result = await fastify.storage.upload(buffer, "test.txt", {
218
+ folder: "test",
219
+ });
220
+
221
+ assert.ok(result.key);
222
+ assert.ok(result.url);
223
+ assert.equal(result.contentType, "text/plain");
224
+
225
+ // Cleanup
226
+ await fastify.storage.delete(result.key);
227
+ });
228
+
229
+ test("Upload and generate thumbnails", async () => {
230
+ const fastify = Fastify();
231
+
232
+ await fastify.register(xStorage, {
233
+ endpoint: process.env.STORAGE_ENDPOINT,
234
+ region: process.env.STORAGE_REGION,
235
+ accessKeyId: process.env.STORAGE_ACCESS_KEY_ID,
236
+ secretAccessKey: process.env.STORAGE_SECRET_ACCESS_KEY,
237
+ bucket: process.env.STORAGE_BUCKET,
238
+ publicUrl: process.env.STORAGE_PUBLIC_URL,
239
+ });
240
+
241
+ const imageBuffer = fs.readFileSync("./test-image.jpg");
242
+
243
+ const result = await fastify.images.uploadWithThumbnails(imageBuffer, "test.jpg", {
244
+ imageOptions: { folder: "test/images" },
245
+ thumbnailOptions: {
246
+ folder: "test/thumbnails",
247
+ sizes: [{ name: "small", width: 150, height: 150 }],
248
+ },
249
+ });
250
+
251
+ assert.ok(result.original.url);
252
+ assert.equal(result.thumbnails.length, 1);
253
+ assert.equal(result.thumbnails[0].size, "small");
254
+ assert.equal(result.thumbnails[0].width, 150);
255
+
256
+ // Cleanup
257
+ await fastify.storage.delete(result.original.key);
258
+ await fastify.storage.delete(result.thumbnails[0].key);
259
+ });
260
+
261
+ test("PDF upload and metadata", async () => {
262
+ const fastify = Fastify();
263
+
264
+ await fastify.register(xStorage, {
265
+ endpoint: process.env.STORAGE_ENDPOINT,
266
+ region: process.env.STORAGE_REGION,
267
+ accessKeyId: process.env.STORAGE_ACCESS_KEY_ID,
268
+ secretAccessKey: process.env.STORAGE_SECRET_ACCESS_KEY,
269
+ bucket: process.env.STORAGE_BUCKET,
270
+ publicUrl: process.env.STORAGE_PUBLIC_URL,
271
+ });
272
+
273
+ const pdfBuffer = fs.readFileSync("./test-document.pdf");
274
+
275
+ const result = await fastify.pdfs.upload(pdfBuffer, "test.pdf", {
276
+ folder: "test/pdfs",
277
+ });
278
+
279
+ assert.ok(result.url);
280
+ assert.ok(result.pageCount > 0);
281
+
282
+ // Cleanup
283
+ await fastify.storage.delete(result.key);
284
+ });
285
+ ```
286
+
287
+ Run tests:
288
+
289
+ ```bash
290
+ npm test
291
+ ```
292
+
293
+ ## Integration Testing
294
+
295
+ Create `test/integration.test.js`:
296
+
297
+ ```javascript
298
+ import { test } from "node:test";
299
+ import assert from "node:assert";
300
+ import Fastify from "fastify";
301
+ import multipart from "@fastify/multipart";
302
+ import xStorage from "../src/xStorage.js";
303
+ import FormData from "form-data";
304
+ import fs from "fs";
305
+
306
+ test("End-to-end file upload", async () => {
307
+ const fastify = Fastify();
308
+
309
+ await fastify.register(multipart);
310
+ await fastify.register(xStorage, {
311
+ endpoint: process.env.STORAGE_ENDPOINT,
312
+ region: process.env.STORAGE_REGION,
313
+ accessKeyId: process.env.STORAGE_ACCESS_KEY_ID,
314
+ secretAccessKey: process.env.STORAGE_SECRET_ACCESS_KEY,
315
+ bucket: process.env.STORAGE_BUCKET,
316
+ publicUrl: process.env.STORAGE_PUBLIC_URL,
317
+ });
318
+
319
+ fastify.post("/upload", async (request) => {
320
+ const data = await request.file();
321
+ const buffer = await data.toBuffer();
322
+
323
+ return await fastify.storage.upload(buffer, data.filename, {
324
+ folder: "test",
325
+ });
326
+ });
327
+
328
+ await fastify.listen({ port: 0 });
329
+
330
+ const form = new FormData();
331
+ form.append("file", fs.createReadStream("./test-image.jpg"));
332
+
333
+ const response = await fastify.inject({
334
+ method: "POST",
335
+ url: "/upload",
336
+ headers: form.getHeaders(),
337
+ payload: form,
338
+ });
339
+
340
+ assert.equal(response.statusCode, 200);
341
+
342
+ const result = JSON.parse(response.payload);
343
+ assert.ok(result.url);
344
+ assert.ok(result.key);
345
+
346
+ // Cleanup
347
+ await fastify.storage.delete(result.key);
348
+ await fastify.close();
349
+ });
350
+ ```
351
+
352
+ ## Testing Helpers
353
+
354
+ Create `test/helpers.test.js`:
355
+
356
+ ```javascript
357
+ import { test } from "node:test";
358
+ import assert from "node:assert";
359
+ import { helpers } from "../src/index.js";
360
+
361
+ test("formatFileSize", () => {
362
+ assert.equal(helpers.formatFileSize(0), "0 Bytes");
363
+ assert.equal(helpers.formatFileSize(1024), "1 KB");
364
+ assert.equal(helpers.formatFileSize(1048576), "1 MB");
365
+ assert.equal(helpers.formatFileSize(1073741824), "1 GB");
366
+ });
367
+
368
+ test("isImage", () => {
369
+ assert.equal(helpers.isImage("photo.jpg"), true);
370
+ assert.equal(helpers.isImage("photo.png"), true);
371
+ assert.equal(helpers.isImage("photo.webp"), true);
372
+ assert.equal(helpers.isImage("document.pdf"), false);
373
+ });
374
+
375
+ test("isPdf", () => {
376
+ assert.equal(helpers.isPdf("document.pdf"), true);
377
+ assert.equal(helpers.isPdf("photo.jpg"), false);
378
+ });
379
+
380
+ test("sanitizeFilename", () => {
381
+ assert.equal(helpers.sanitizeFilename("My File (2024).jpg"), "my_file_2024.jpg");
382
+ assert.equal(helpers.sanitizeFilename("Test@#$%.txt"), "test____.txt");
383
+ });
384
+
385
+ test("calculateFitDimensions", () => {
386
+ const result = helpers.calculateFitDimensions(4000, 3000, 1920, 1080);
387
+ assert.equal(result.width, 1440);
388
+ assert.equal(result.height, 1080);
389
+ });
390
+ ```
391
+
392
+ ## Performance Testing
393
+
394
+ Test upload performance with larger files:
395
+
396
+ ```javascript
397
+ import { test } from "node:test";
398
+ import Fastify from "fastify";
399
+ import xStorage from "../src/xStorage.js";
400
+
401
+ test("Upload large file performance", async () => {
402
+ const fastify = Fastify();
403
+
404
+ await fastify.register(xStorage, {
405
+ endpoint: process.env.STORAGE_ENDPOINT,
406
+ region: process.env.STORAGE_REGION,
407
+ accessKeyId: process.env.STORAGE_ACCESS_KEY_ID,
408
+ secretAccessKey: process.env.STORAGE_SECRET_ACCESS_KEY,
409
+ bucket: process.env.STORAGE_BUCKET,
410
+ publicUrl: process.env.STORAGE_PUBLIC_URL,
411
+ });
412
+
413
+ // Create 10MB buffer
414
+ const largeBuffer = Buffer.alloc(10 * 1024 * 1024);
415
+
416
+ const start = Date.now();
417
+ const result = await fastify.storage.upload(largeBuffer, "large-file.bin", {
418
+ folder: "test",
419
+ });
420
+ const duration = Date.now() - start;
421
+
422
+ console.log(`Upload took ${duration}ms`);
423
+
424
+ // Cleanup
425
+ await fastify.storage.delete(result.key);
426
+ });
427
+ ```
428
+
429
+ ## Production Testing
430
+
431
+ ### 1. Test in Staging Environment
432
+
433
+ Deploy to staging with production-like configuration:
434
+
435
+ ```bash
436
+ # staging.env
437
+ STORAGE_ENDPOINT=https://nyc3.digitaloceanspaces.com
438
+ STORAGE_REGION=us-east-1
439
+ STORAGE_ACCESS_KEY_ID=staging_key
440
+ STORAGE_SECRET_ACCESS_KEY=staging_secret
441
+ STORAGE_BUCKET=staging-bucket
442
+ STORAGE_PUBLIC_URL=https://staging-bucket.nyc3.digitaloceanspaces.com
443
+ ```
444
+
445
+ ### 2. Monitor Uploads
446
+
447
+ Check logs for errors:
448
+
449
+ ```bash
450
+ # Check Fastify logs
451
+ tail -f logs/app.log | grep "Storage"
452
+
453
+ # Look for:
454
+ # - Upload failures
455
+ # - Slow uploads
456
+ # - Invalid file types
457
+ # - Permission errors
458
+ ```
459
+
460
+ ### 3. Verify Public URLs
461
+
462
+ Test that uploaded files are accessible:
463
+
464
+ ```bash
465
+ # Upload file
466
+ curl -X POST https://staging.example.com/upload \
467
+ -F "file=@./test.jpg"
468
+
469
+ # Get URL from response
470
+ # Visit URL in browser to verify it works
471
+ ```
472
+
473
+ ### 4. Test CORS
474
+
475
+ Verify CORS is configured correctly:
476
+
477
+ ```bash
478
+ curl -H "Origin: https://your-domain.com" \
479
+ -H "Access-Control-Request-Method: GET" \
480
+ -H "Access-Control-Request-Headers: Content-Type" \
481
+ -X OPTIONS \
482
+ https://your-bucket.nyc3.digitaloceanspaces.com/test.jpg
483
+ ```
484
+
485
+ ### 5. Load Testing
486
+
487
+ Use tools like `autocannon` for load testing:
488
+
489
+ ```bash
490
+ npm install -g autocannon
491
+
492
+ # Test upload endpoint
493
+ autocannon -c 10 -d 30 \
494
+ -m POST \
495
+ -H "Content-Type: multipart/form-data" \
496
+ http://localhost:3000/upload
497
+ ```
498
+
499
+ ## Common Issues
500
+
501
+ ### Issue: Signature Mismatch
502
+
503
+ **Cause**: Incorrect credentials or endpoint
504
+
505
+ **Fix**:
506
+ ```bash
507
+ # Verify credentials
508
+ echo $STORAGE_ACCESS_KEY_ID
509
+ echo $STORAGE_SECRET_ACCESS_KEY
510
+
511
+ # Check endpoint format
512
+ # Correct: https://nyc3.digitaloceanspaces.com
513
+ # Wrong: https://nyc3.digitaloceanspaces.com/
514
+ ```
515
+
516
+ ### Issue: Access Denied
517
+
518
+ **Cause**: Bucket permissions or ACL
519
+
520
+ **Fix**:
521
+ - Check bucket is public or has correct ACL
522
+ - Verify IAM user has `s3:PutObject` permission
523
+ - Check CORS configuration
524
+
525
+ ### Issue: File Not Found
526
+
527
+ **Cause**: Incorrect public URL or key
528
+
529
+ **Fix**:
530
+ ```javascript
531
+ // Ensure publicUrl doesn't have trailing slash
532
+ publicUrl: "https://bucket.nyc3.digitaloceanspaces.com" // ✓
533
+ publicUrl: "https://bucket.nyc3.digitaloceanspaces.com/" // ✗
534
+
535
+ // Verify key format
536
+ const url = fastify.storage.getPublicUrl(key);
537
+ console.log(url); // Should be valid URL
538
+ ```
539
+
540
+ ### Issue: Sharp Installation Error
541
+
542
+ **Cause**: Platform-specific binary issues
543
+
544
+ **Fix**:
545
+ ```bash
546
+ # Rebuild sharp
547
+ npm rebuild sharp
548
+
549
+ # Or install with specific platform
550
+ npm install --platform=linux --arch=x64 sharp
551
+ ```
552
+
553
+ ## Best Practices
554
+
555
+ 1. **Use test bucket** - Never test with production bucket
556
+ 2. **Clean up test files** - Delete uploaded files after tests
557
+ 3. **Test all file types** - Images, PDFs, documents, etc.
558
+ 4. **Test error cases** - Invalid files, large files, etc.
559
+ 5. **Monitor logs** - Check for warnings and errors
560
+ 6. **Test CORS** - Verify cross-origin access works
561
+ 7. **Load test** - Ensure performance under load
562
+ 8. **Test URLs** - Verify all generated URLs are accessible
563
+
564
+ ## Next Steps
565
+
566
+ - See [README.md](./README.md) for API documentation
567
+ - See [EXAMPLES.md](./EXAMPLES.md) for real-world examples
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@xenterprises/fastify-xstorage",
3
+ "type": "module",
4
+ "version": "1.0.0",
5
+ "description": "Fastify plugin for S3-compatible storage with simple, intuitive API. Supports AWS S3, Cloudflare R2, Digital Ocean Spaces and any S3-compatible provider.",
6
+ "main": "src/index.js",
7
+ "exports": {
8
+ ".": "./src/index.js",
9
+ "./helpers": "./src/utils/helpers.js"
10
+ },
11
+ "scripts": {
12
+ "start": "fastify start -l info server/app.js",
13
+ "dev": "fastify start -w -l info -P server/app.js",
14
+ "test": "node --test test/storage.test.js"
15
+ },
16
+ "keywords": [
17
+ "fastify",
18
+ "s3",
19
+ "storage",
20
+ "upload",
21
+ "download",
22
+ "file",
23
+ "spaces",
24
+ "r2",
25
+ "cloudflare",
26
+ "digitalocean",
27
+ "aws"
28
+ ],
29
+ "author": "Tim Mushen",
30
+ "license": "ISC",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git@gitlab.com:x-enterprises/fastify-plugins/fastify-x-storage.git"
34
+ },
35
+ "bugs": {
36
+ "url": "https://gitlab.com/x-enterprises/fastify-plugins/fastify-x-storage/-/issues"
37
+ },
38
+ "dependencies": {
39
+ "@aws-sdk/client-s3": "^3.700.0",
40
+ "@aws-sdk/s3-request-presigner": "^3.700.0",
41
+ "fastify-plugin": "^5.0.0",
42
+ "mime-types": "^2.1.35"
43
+ },
44
+ "devDependencies": {
45
+ "@fastify/multipart": "^9.0.0",
46
+ "fastify": "^5.1.0"
47
+ },
48
+ "peerDependencies": {
49
+ "fastify": "^5.0.0"
50
+ }
51
+ }