@simo777/fastify-storage
v1.0.4
Published
Scalable Fastify storage plugin for Local and R2/S3 storage with Prisma metadata tracking and media processing.
Readme
Fastify Storage Plugin
A scalable storage plugin for Fastify that supports local file storage and Cloudflare R2 (or any S3-compatible provider).
Features
- Deduplication: Uses SHA-256 hashing to prevent duplicate file uploads.
- Media Processing: Automatically extracts dimensions (width/height) for images/videos and duration for audio/video.
- Multi-provider Support: Switch between
localandr2(S3-compatible) via typed options. - Prisma Integration: Automatically tracks file metadata in your database.
- Multipart Support: Handles file uploads out of the box.
- Local Serving: Automatically serves local uploads via
@fastify/static. - SDK Included: Type-safe SDK for interacting with the storage API.
- Custom Paths: Organize files into folders (e.g.,
/avatars/user-1.jpg) across all providers.
Database Setup
Add the following model to your prisma/schema.prisma file:
model StoredFile {
id String @id @default(cuid())
key String @unique
bucket String
provider String // "local", "r2", etc.
originalName String
mimeType String
size Int
url String
type String // image | audio | video | file
hash String @unique // SHA-256 dedup key
width Int?
height Int?
duration Int?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}Then run:
npx prisma migrate dev --name add_stored_fileConfiguration
Add these variables to your .env file:
# Storage Provider: 'local' or 'r2'
STORAGE_PROVIDER=local
# Local Storage Configuration
LOCAL_STORAGE_PATH=uploads
LOCAL_STORAGE_PUBLIC_URL=http://localhost:3000/uploads
# Cloudflare R2 / S3 Configuration
R2_ENDPOINT=https://<account-id>.r2.cloudflarestorage.com
R2_ACCESS_KEY_ID=your_access_key
R2_SECRET_ACCESS_KEY=your_secret_key
R2_BUCKET_NAME=your_bucket_name
R2_PUBLIC_URL=https://pub-<your-id>.r2.devUsage
Server-side (Fastify)
Register the plugin with your desired configuration. The plugin automatically handles @fastify/multipart and @fastify/static (for local storage).
import fastifyStorage from '@simo777/fastify-storage';
await app.register(fastifyStorage, {
provider: 'local', // or 'r2'
routePrefix: '/storage', // Optional: defaults to /storage
defaultPath: 'uploads', // Optional: global default folder
// Optional: customize multipart limits
multipart: {
limits: { fileSize: 20 * 1024 * 1024 } // 20MB
},
// Optional: customize static file serving (for local provider)
static: {
cacheControl: true,
maxAge: 3600000
},
local: {
storagePath: 'uploads',
publicUrl: 'http://localhost:3000/uploads'
},
// r2: { ... }
});The plugin decorates the Fastify instance with storage. You can use it in your services or controllers.
Uploading a File
// Inside a route or service
const buffer = await data.toBuffer();
const key = 'avatar.jpg';
await fastify.storage.upload(buffer, {
key,
contentType: 'image/jpeg',
path: 'profiles/users' // Optional: results in profiles/users/avatar.jpg
});Deleting a File
await fastify.storage.delete('file-key.pdf');Checking existence
const exists = await fastify.storage.exists('file-key.pdf');API Endpoints
The plugin provides a module with the following endpoints (prefixed by your API_PREFIX):
POST /storage/upload: Upload a file (Multipart form-data). Supports optionalpathfield.POST /storage/upload-url: Upload a file from a remote URL. Body:{ "url": "...", "path": "..." }.GET /storage: List all files in the database.GET /storage/:id: Get metadata for a specific file.PUT /storage/:id: Replace an existing file.DELETE /storage/:id: Delete a file from storage and database.GET /storage/signed-url?key=...: Get a signed upload URL (R2 only).
Using the SDK
import { AppSDK } from './sdk';
const sdk = new AppSDK({
baseUrl: 'http://localhost:3000/api/v1'
});
// Upload a file from the browser to a specific folder
const file = fileInput.files[0];
const storedFile = await sdk.storage.upload(file, 'profile.jpg', 'avatars');
// Upload from a URL
const urlFile = await sdk.storage.uploadFromUrl('https://example.com/image.png', 'external');
console.log(storedFile.url); // e.g., .../avatars/profile.jpg
// List files
const files = await sdk.storage.list();Testing with Postman
1. Upload a File
- Method:
POST - URL:
http://localhost:3000/api/v1/storage/upload - Body: Select
form-data- Key:
file, Value: [Select File], Type:File - Key:
path, Value:my-folder, Type:Text(Optional)
- Key:
2. List All Files
- Method:
GET - URL:
http://localhost:3000/api/v1/storage
3. Replace a File
- Method:
PUT - URL:
http://localhost:3000/api/v1/storage/:id(Replace:idwith a file ID from the list) - Body:
form-data- Key:
file - Value: Select a new file
- Key:
4. Delete a File
- Method:
DELETE - URL:
http://localhost:3000/api/v1/storage/:id
Recommendations for Testing
- Local vs Production:
- Use
STORAGE_PROVIDER=localfor development and CI to avoid costs and network latency. - Use
STORAGE_PROVIDER=r2in a staging environment that mirrors production.
- Use
- MIME Type Validation:
- The current implementation accepts all file types. For production, consider adding a hook or validation logic in
src/modules/storage/index.tsto restrict allowed MIME types (e.g.,image/jpeg,application/pdf).
- The current implementation accepts all file types. For production, consider adding a hook or validation logic in
- File Size Limits:
- Configure
@fastify/multipartlimits insrc/app.tsto prevent large file uploads from crashing your server or filling up disk space:await app.register(import('@fastify/multipart'), { limits: { fileSize: 10 * 1024 * 1024 } // 10MB });
- Configure
- Error Handling:
- Test what happens when the storage provider is unreachable (e.g., wrong R2 credentials). The plugin will currently throw a 500 error, which is caught by the global error handler.
- Database Sync:
- Verify that if a file upload fails, no entry is created in the database.
- Verify that if a database deletion fails, the file is not deleted from the storage provider (or handle the rollback logic).
Adding New Providers
To add a new provider (e.g., Azure Blob Storage):
- Implement the
StorageProviderinterface insrc/lib/storage/. - Update the factory in
src/lib/storage/index.tsto include your new provider.
