@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/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
|