@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/.env.example +28 -0
- package/EXAMPLES.md +797 -0
- package/QUICK_START.md +345 -0
- package/README.md +492 -0
- package/TESTING.md +567 -0
- package/package.json +51 -0
- package/server/app.js +136 -0
- package/src/index.js +9 -0
- package/src/services/storage.js +352 -0
- package/src/utils/helpers.js +238 -0
- package/src/xStorage.js +86 -0
- package/test/storage.test.js +257 -0
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
|
+
}
|