@simo777/fastify-storage 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 +193 -0
- package/dist/src/app.d.ts +3 -0
- package/dist/src/app.js +93 -0
- package/dist/src/app.js.map +1 -0
- package/dist/src/config/env.d.ts +26 -0
- package/dist/src/config/env.js +28 -0
- package/dist/src/config/env.js.map +1 -0
- package/dist/src/lib/storage/index.d.ts +20 -0
- package/dist/src/lib/storage/index.js +28 -0
- package/dist/src/lib/storage/index.js.map +1 -0
- package/dist/src/lib/storage/local.d.ts +15 -0
- package/dist/src/lib/storage/local.js +55 -0
- package/dist/src/lib/storage/local.js.map +1 -0
- package/dist/src/lib/storage/provider.d.ts +23 -0
- package/dist/src/lib/storage/provider.js +2 -0
- package/dist/src/lib/storage/provider.js.map +1 -0
- package/dist/src/lib/storage/r2.d.ts +28 -0
- package/dist/src/lib/storage/r2.js +85 -0
- package/dist/src/lib/storage/r2.js.map +1 -0
- package/dist/src/modules/health/index.d.ts +2 -0
- package/dist/src/modules/health/index.js +60 -0
- package/dist/src/modules/health/index.js.map +1 -0
- package/dist/src/modules/storage/index.d.ts +2 -0
- package/dist/src/modules/storage/index.js +76 -0
- package/dist/src/modules/storage/index.js.map +1 -0
- package/dist/src/modules/storage/storage.controller.d.ts +119 -0
- package/dist/src/modules/storage/storage.controller.js +99 -0
- package/dist/src/modules/storage/storage.controller.js.map +1 -0
- package/dist/src/modules/storage/storage.service.d.ts +111 -0
- package/dist/src/modules/storage/storage.service.js +191 -0
- package/dist/src/modules/storage/storage.service.js.map +1 -0
- package/dist/src/plugins/prisma.d.ts +9 -0
- package/dist/src/plugins/prisma.js +18 -0
- package/dist/src/plugins/prisma.js.map +1 -0
- package/dist/src/plugins/queues.d.ts +14 -0
- package/dist/src/plugins/queues.js +29 -0
- package/dist/src/plugins/queues.js.map +1 -0
- package/dist/src/plugins/redis.d.ts +9 -0
- package/dist/src/plugins/redis.js +15 -0
- package/dist/src/plugins/redis.js.map +1 -0
- package/dist/src/plugins/storage.d.ts +14 -0
- package/dist/src/plugins/storage.js +15 -0
- package/dist/src/plugins/storage.js.map +1 -0
- package/dist/src/server.d.ts +1 -0
- package/dist/src/server.js +19 -0
- package/dist/src/server.js.map +1 -0
- package/dist/src/workers/index.d.ts +1 -0
- package/dist/src/workers/index.js +33 -0
- package/dist/src/workers/index.js.map +1 -0
- package/package.json +77 -0
- package/prisma/schema.prisma +28 -0
package/README.md
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# Fastify Storage Plugin
|
|
2
|
+
|
|
3
|
+
A scalable storage plugin for Fastify that supports local file storage and Cloudflare R2 (or any S3-compatible provider).
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Deduplication**: Uses SHA-256 hashing to prevent duplicate file uploads.
|
|
8
|
+
- **Media Processing**: Automatically extracts dimensions (width/height) for images/videos and duration for audio/video.
|
|
9
|
+
- **Multi-provider Support**: Switch between `local` and `r2` (S3-compatible) via typed options.
|
|
10
|
+
- **Prisma Integration**: Automatically tracks file metadata in your database.
|
|
11
|
+
- **Multipart Support**: Handles file uploads out of the box.
|
|
12
|
+
- **Local Serving**: Automatically serves local uploads via `@fastify/static`.
|
|
13
|
+
- **SDK Included**: Type-safe SDK for interacting with the storage API.
|
|
14
|
+
- **Custom Paths**: Organize files into folders (e.g., `/avatars/user-1.jpg`) across all providers.
|
|
15
|
+
|
|
16
|
+
## Database Setup
|
|
17
|
+
|
|
18
|
+
Add the following model to your `prisma/schema.prisma` file:
|
|
19
|
+
|
|
20
|
+
```prisma
|
|
21
|
+
model StoredFile {
|
|
22
|
+
id String @id @default(cuid())
|
|
23
|
+
key String @unique
|
|
24
|
+
bucket String
|
|
25
|
+
provider String // "local", "r2", etc.
|
|
26
|
+
originalName String
|
|
27
|
+
mimeType String
|
|
28
|
+
size Int
|
|
29
|
+
url String
|
|
30
|
+
type String // image | audio | video | file
|
|
31
|
+
hash String @unique // SHA-256 dedup key
|
|
32
|
+
width Int?
|
|
33
|
+
height Int?
|
|
34
|
+
duration Int?
|
|
35
|
+
createdAt DateTime @default(now())
|
|
36
|
+
updatedAt DateTime @updatedAt
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Then run:
|
|
41
|
+
```bash
|
|
42
|
+
npx prisma migrate dev --name add_stored_file
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Configuration
|
|
46
|
+
|
|
47
|
+
Add these variables to your `.env` file:
|
|
48
|
+
|
|
49
|
+
```env
|
|
50
|
+
# Storage Provider: 'local' or 'r2'
|
|
51
|
+
STORAGE_PROVIDER=local
|
|
52
|
+
|
|
53
|
+
# Local Storage Configuration
|
|
54
|
+
LOCAL_STORAGE_PATH=uploads
|
|
55
|
+
LOCAL_STORAGE_PUBLIC_URL=http://localhost:3000/uploads
|
|
56
|
+
|
|
57
|
+
# Cloudflare R2 / S3 Configuration
|
|
58
|
+
R2_ENDPOINT=https://<account-id>.r2.cloudflarestorage.com
|
|
59
|
+
R2_ACCESS_KEY_ID=your_access_key
|
|
60
|
+
R2_SECRET_ACCESS_KEY=your_secret_key
|
|
61
|
+
R2_BUCKET_NAME=your_bucket_name
|
|
62
|
+
R2_PUBLIC_URL=https://pub-<your-id>.r2.dev
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Usage
|
|
66
|
+
|
|
67
|
+
### Server-side (Fastify)
|
|
68
|
+
|
|
69
|
+
Register the plugin with your desired configuration:
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
import fastifyStorage from '@simo777/fastify-storage';
|
|
73
|
+
|
|
74
|
+
await app.register(fastifyStorage, {
|
|
75
|
+
provider: 'local', // or 'r2'
|
|
76
|
+
local: {
|
|
77
|
+
storagePath: 'uploads',
|
|
78
|
+
publicUrl: 'http://localhost:3000/uploads'
|
|
79
|
+
},
|
|
80
|
+
// r2: {
|
|
81
|
+
// endpoint: process.env.R2_ENDPOINT,
|
|
82
|
+
// accessKeyId: process.env.R2_ACCESS_KEY_ID,
|
|
83
|
+
// secretAccessKey: process.env.R2_SECRET_ACCESS_KEY,
|
|
84
|
+
// bucketName: process.env.R2_BUCKET_NAME,
|
|
85
|
+
// publicUrl: process.env.R2_PUBLIC_URL
|
|
86
|
+
// }
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The plugin decorates the Fastify instance with `storage`. You can use it in your services or controllers.
|
|
91
|
+
|
|
92
|
+
#### Uploading a File
|
|
93
|
+
```typescript
|
|
94
|
+
// Inside a route or service
|
|
95
|
+
const buffer = await data.toBuffer();
|
|
96
|
+
const key = 'avatar.jpg';
|
|
97
|
+
|
|
98
|
+
await fastify.storage.upload(buffer, {
|
|
99
|
+
key,
|
|
100
|
+
contentType: 'image/jpeg',
|
|
101
|
+
path: 'profiles/users' // Optional: results in profiles/users/avatar.jpg
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
#### Deleting a File
|
|
106
|
+
```typescript
|
|
107
|
+
await fastify.storage.delete('file-key.pdf');
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
#### Checking existence
|
|
111
|
+
```typescript
|
|
112
|
+
const exists = await fastify.storage.exists('file-key.pdf');
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### API Endpoints
|
|
116
|
+
|
|
117
|
+
The plugin provides a module with the following endpoints (prefixed by your `API_PREFIX`):
|
|
118
|
+
|
|
119
|
+
- `POST /storage/upload`: Upload a file (Multipart form-data). Supports optional `path` field.
|
|
120
|
+
- `GET /storage`: List all files in the database.
|
|
121
|
+
- `GET /storage/:id`: Get metadata for a specific file.
|
|
122
|
+
- `PUT /storage/:id`: Replace an existing file.
|
|
123
|
+
- `DELETE /storage/:id`: Delete a file from storage and database.
|
|
124
|
+
- `GET /storage/signed-url?key=...`: Get a signed upload URL (R2 only).
|
|
125
|
+
|
|
126
|
+
### Using the SDK
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
import { AppSDK } from './sdk';
|
|
130
|
+
|
|
131
|
+
const sdk = new AppSDK({
|
|
132
|
+
baseUrl: 'http://localhost:3000/api/v1'
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Upload a file from the browser to a specific folder
|
|
136
|
+
const file = fileInput.files[0];
|
|
137
|
+
const storedFile = await sdk.storage.upload(file, 'profile.jpg', 'avatars');
|
|
138
|
+
|
|
139
|
+
console.log(storedFile.url); // e.g., .../avatars/profile.jpg
|
|
140
|
+
|
|
141
|
+
// List files
|
|
142
|
+
const files = await sdk.storage.list();
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Testing with Postman
|
|
146
|
+
|
|
147
|
+
### 1. Upload a File
|
|
148
|
+
- **Method**: `POST`
|
|
149
|
+
- **URL**: `http://localhost:3000/api/v1/storage/upload`
|
|
150
|
+
- **Body**: Select `form-data`
|
|
151
|
+
- Key: `file`, Value: [Select File], Type: `File`
|
|
152
|
+
- Key: `path`, Value: `my-folder`, Type: `Text` (Optional)
|
|
153
|
+
|
|
154
|
+
### 2. List All Files
|
|
155
|
+
- **Method**: `GET`
|
|
156
|
+
- **URL**: `http://localhost:3000/api/v1/storage`
|
|
157
|
+
|
|
158
|
+
### 3. Replace a File
|
|
159
|
+
- **Method**: `PUT`
|
|
160
|
+
- **URL**: `http://localhost:3000/api/v1/storage/:id` (Replace `:id` with a file ID from the list)
|
|
161
|
+
- **Body**: `form-data`
|
|
162
|
+
- Key: `file`
|
|
163
|
+
- Value: Select a new file
|
|
164
|
+
|
|
165
|
+
### 4. Delete a File
|
|
166
|
+
- **Method**: `DELETE`
|
|
167
|
+
- **URL**: `http://localhost:3000/api/v1/storage/:id`
|
|
168
|
+
|
|
169
|
+
## Recommendations for Testing
|
|
170
|
+
|
|
171
|
+
1. **Local vs Production**:
|
|
172
|
+
* Use `STORAGE_PROVIDER=local` for development and CI to avoid costs and network latency.
|
|
173
|
+
* Use `STORAGE_PROVIDER=r2` in a staging environment that mirrors production.
|
|
174
|
+
2. **MIME Type Validation**:
|
|
175
|
+
* The current implementation accepts all file types. For production, consider adding a hook or validation logic in `src/modules/storage/index.ts` to restrict allowed MIME types (e.g., `image/jpeg`, `application/pdf`).
|
|
176
|
+
3. **File Size Limits**:
|
|
177
|
+
* Configure `@fastify/multipart` limits in `src/app.ts` to prevent large file uploads from crashing your server or filling up disk space:
|
|
178
|
+
```typescript
|
|
179
|
+
await app.register(import('@fastify/multipart'), {
|
|
180
|
+
limits: { fileSize: 10 * 1024 * 1024 } // 10MB
|
|
181
|
+
});
|
|
182
|
+
```
|
|
183
|
+
4. **Error Handling**:
|
|
184
|
+
* 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.
|
|
185
|
+
5. **Database Sync**:
|
|
186
|
+
* Verify that if a file upload fails, no entry is created in the database.
|
|
187
|
+
* Verify that if a database deletion fails, the file is not deleted from the storage provider (or handle the rollback logic).
|
|
188
|
+
|
|
189
|
+
## Adding New Providers
|
|
190
|
+
|
|
191
|
+
To add a new provider (e.g., Azure Blob Storage):
|
|
192
|
+
1. Implement the `StorageProvider` interface in `src/lib/storage/`.
|
|
193
|
+
2. Update the factory in `src/lib/storage/index.ts` to include your new provider.
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox';
|
|
2
|
+
import Fastify from 'fastify';
|
|
3
|
+
export declare function buildApp(): Promise<Fastify.FastifyInstance<import("node:http").Server<typeof import("node:http").IncomingMessage, typeof import("node:http").ServerResponse>, import("node:http").IncomingMessage, import("node:http").ServerResponse<import("node:http").IncomingMessage>, Fastify.FastifyBaseLogger, TypeBoxTypeProvider>>;
|
package/dist/src/app.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { dirname, join } from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import autoload from '@fastify/autoload';
|
|
4
|
+
import cors from '@fastify/cors';
|
|
5
|
+
import fastifyEnv from '@fastify/env';
|
|
6
|
+
import Fastify from 'fastify';
|
|
7
|
+
import { envSchema } from './config/env.js';
|
|
8
|
+
import prismaPlugin from './plugins/prisma.js';
|
|
9
|
+
import queuesPlugin from './plugins/queues.js';
|
|
10
|
+
import redisPlugin from './plugins/redis.js';
|
|
11
|
+
import storagePlugin from './plugins/storage.js';
|
|
12
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
function parseCorsOrigin(origin) {
|
|
14
|
+
if (origin === '*') {
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
return origin
|
|
18
|
+
.split(',')
|
|
19
|
+
.map((value) => value.trim())
|
|
20
|
+
.filter(Boolean);
|
|
21
|
+
}
|
|
22
|
+
export async function buildApp() {
|
|
23
|
+
const app = Fastify({
|
|
24
|
+
logger: {
|
|
25
|
+
level: process.env.LOG_LEVEL ?? 'info'
|
|
26
|
+
}
|
|
27
|
+
}).withTypeProvider();
|
|
28
|
+
await app.register(fastifyEnv, {
|
|
29
|
+
schema: envSchema,
|
|
30
|
+
dotenv: true
|
|
31
|
+
});
|
|
32
|
+
await app.register(cors, {
|
|
33
|
+
origin: parseCorsOrigin(app.config.CORS_ORIGIN),
|
|
34
|
+
credentials: true
|
|
35
|
+
});
|
|
36
|
+
await app.register(import('@fastify/multipart'), {
|
|
37
|
+
limits: { fileSize: 10 * 1024 * 1024 } // 10MB
|
|
38
|
+
});
|
|
39
|
+
if (app.config.STORAGE_PROVIDER === 'local') {
|
|
40
|
+
const { promises: fs } = await import('node:fs');
|
|
41
|
+
const storagePath = join(process.cwd(), app.config.LOCAL_STORAGE_PATH);
|
|
42
|
+
await fs.mkdir(storagePath, { recursive: true });
|
|
43
|
+
await app.register(import('@fastify/static'), {
|
|
44
|
+
root: storagePath,
|
|
45
|
+
prefix: '/uploads/',
|
|
46
|
+
decorateReply: false
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
await app.register(prismaPlugin);
|
|
50
|
+
await app.register(redisPlugin);
|
|
51
|
+
await app.register(queuesPlugin);
|
|
52
|
+
await app.register(storagePlugin, {
|
|
53
|
+
provider: app.config.STORAGE_PROVIDER,
|
|
54
|
+
defaultPath: app.config.STORAGE_DEFAULT_PATH,
|
|
55
|
+
local: {
|
|
56
|
+
storagePath: app.config.LOCAL_STORAGE_PATH,
|
|
57
|
+
publicUrl: app.config.LOCAL_STORAGE_PUBLIC_URL
|
|
58
|
+
},
|
|
59
|
+
r2: app.config.STORAGE_PROVIDER === 'r2' ? {
|
|
60
|
+
endpoint: app.config.R2_ENDPOINT,
|
|
61
|
+
accessKeyId: app.config.R2_ACCESS_KEY_ID,
|
|
62
|
+
secretAccessKey: app.config.R2_SECRET_ACCESS_KEY,
|
|
63
|
+
bucketName: app.config.R2_BUCKET_NAME,
|
|
64
|
+
publicUrl: app.config.R2_PUBLIC_URL
|
|
65
|
+
} : undefined
|
|
66
|
+
});
|
|
67
|
+
app.get('/', async () => ({
|
|
68
|
+
name: 'fastify-template',
|
|
69
|
+
status: 'ok',
|
|
70
|
+
docs: `${app.config.API_PREFIX}/health`
|
|
71
|
+
}));
|
|
72
|
+
await app.register(autoload, {
|
|
73
|
+
dir: join(__dirname, 'modules'),
|
|
74
|
+
forceESM: true,
|
|
75
|
+
options: {
|
|
76
|
+
prefix: app.config.API_PREFIX
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
app.setErrorHandler((error, request, reply) => {
|
|
80
|
+
request.log.error(error);
|
|
81
|
+
const errorWithStatus = error;
|
|
82
|
+
const statusCode = typeof errorWithStatus.statusCode === 'number' && errorWithStatus.statusCode >= 400
|
|
83
|
+
? errorWithStatus.statusCode
|
|
84
|
+
: 500;
|
|
85
|
+
const message = error instanceof Error ? error.message : 'Unexpected error';
|
|
86
|
+
return reply.status(statusCode).send({
|
|
87
|
+
error: statusCode === 500 ? 'Internal Server Error' : message,
|
|
88
|
+
statusCode
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
return app;
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=app.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app.js","sourceRoot":"","sources":["../../src/app.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type Static } from '@sinclair/typebox';
|
|
2
|
+
export declare const envSchema: import("@sinclair/typebox").TObject<{
|
|
3
|
+
NODE_ENV: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"development">, import("@sinclair/typebox").TLiteral<"test">, import("@sinclair/typebox").TLiteral<"production">]>;
|
|
4
|
+
HOST: import("@sinclair/typebox").TString;
|
|
5
|
+
PORT: import("@sinclair/typebox").TNumber;
|
|
6
|
+
LOG_LEVEL: import("@sinclair/typebox").TString;
|
|
7
|
+
API_PREFIX: import("@sinclair/typebox").TString;
|
|
8
|
+
CORS_ORIGIN: import("@sinclair/typebox").TString;
|
|
9
|
+
DATABASE_URL: import("@sinclair/typebox").TString;
|
|
10
|
+
REDIS_URL: import("@sinclair/typebox").TString;
|
|
11
|
+
STORAGE_PROVIDER: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"local">, import("@sinclair/typebox").TLiteral<"r2">]>;
|
|
12
|
+
LOCAL_STORAGE_PATH: import("@sinclair/typebox").TString;
|
|
13
|
+
LOCAL_STORAGE_PUBLIC_URL: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
14
|
+
R2_ENDPOINT: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
15
|
+
R2_ACCESS_KEY_ID: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
16
|
+
R2_SECRET_ACCESS_KEY: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
17
|
+
R2_BUCKET_NAME: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
18
|
+
R2_PUBLIC_URL: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
19
|
+
STORAGE_DEFAULT_PATH: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
20
|
+
}>;
|
|
21
|
+
export type Env = Static<typeof envSchema>;
|
|
22
|
+
declare module 'fastify' {
|
|
23
|
+
interface FastifyInstance {
|
|
24
|
+
config: Env;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Type } from '@sinclair/typebox';
|
|
2
|
+
export const envSchema = Type.Object({
|
|
3
|
+
NODE_ENV: Type.Union([
|
|
4
|
+
Type.Literal('development'),
|
|
5
|
+
Type.Literal('test'),
|
|
6
|
+
Type.Literal('production')
|
|
7
|
+
], { default: 'development' }),
|
|
8
|
+
HOST: Type.String({ default: '0.0.0.0' }),
|
|
9
|
+
PORT: Type.Number({ default: 3000 }),
|
|
10
|
+
LOG_LEVEL: Type.String({ default: 'info' }),
|
|
11
|
+
API_PREFIX: Type.String({ default: '/api/v1' }),
|
|
12
|
+
CORS_ORIGIN: Type.String({ default: '*' }),
|
|
13
|
+
DATABASE_URL: Type.String(),
|
|
14
|
+
REDIS_URL: Type.String(),
|
|
15
|
+
STORAGE_PROVIDER: Type.Union([
|
|
16
|
+
Type.Literal('local'),
|
|
17
|
+
Type.Literal('r2')
|
|
18
|
+
], { default: 'local' }),
|
|
19
|
+
LOCAL_STORAGE_PATH: Type.String({ default: 'uploads' }),
|
|
20
|
+
LOCAL_STORAGE_PUBLIC_URL: Type.Optional(Type.String()),
|
|
21
|
+
R2_ENDPOINT: Type.Optional(Type.String()),
|
|
22
|
+
R2_ACCESS_KEY_ID: Type.Optional(Type.String()),
|
|
23
|
+
R2_SECRET_ACCESS_KEY: Type.Optional(Type.String()),
|
|
24
|
+
R2_BUCKET_NAME: Type.Optional(Type.String()),
|
|
25
|
+
R2_PUBLIC_URL: Type.Optional(Type.String()),
|
|
26
|
+
STORAGE_DEFAULT_PATH: Type.Optional(Type.String()),
|
|
27
|
+
});
|
|
28
|
+
//# sourceMappingURL=env.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env.js","sourceRoot":"","sources":["../../../src/config/env.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAe,MAAM,mBAAmB,CAAC;AAEtD,MAAM,CAAC,MAAM,SAAS,GAAG,IAAI,CAAC,MAAM,CAAC;IACnC,QAAQ,EAAE,IAAI,CAAC,KAAK,CAAC;QACnB,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC;QAC3B,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC;QACpB,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC;KAC3B,EAAE,EAAE,OAAO,EAAE,aAAa,EAAE,CAAC;IAC9B,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC;IACzC,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IACpC,SAAS,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC;IAC3C,UAAU,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC;IAC/C,WAAW,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC;IAC1C,YAAY,EAAE,IAAI,CAAC,MAAM,EAAE;IAC3B,SAAS,EAAE,IAAI,CAAC,MAAM,EAAE;IACxB,gBAAgB,EAAE,IAAI,CAAC,KAAK,CAAC;QAC3B,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC;QACrB,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC;KACnB,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;IACxB,kBAAkB,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC;IACvD,wBAAwB,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;IACtD,WAAW,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;IACzC,gBAAgB,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;IAC9C,oBAAoB,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;IAClD,cAAc,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;IAC5C,aAAa,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;IAC3C,oBAAoB,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;CACnD,CAAC,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { StorageProvider } from './provider.js';
|
|
2
|
+
export * from './provider.js';
|
|
3
|
+
export * from './local.js';
|
|
4
|
+
export * from './r2.js';
|
|
5
|
+
export interface StoragePluginOptions {
|
|
6
|
+
provider: 'local' | 'r2';
|
|
7
|
+
defaultPath?: string | undefined;
|
|
8
|
+
local?: {
|
|
9
|
+
storagePath: string;
|
|
10
|
+
publicUrl?: string | undefined;
|
|
11
|
+
} | undefined;
|
|
12
|
+
r2?: {
|
|
13
|
+
endpoint: string;
|
|
14
|
+
accessKeyId: string;
|
|
15
|
+
secretAccessKey: string;
|
|
16
|
+
bucketName: string;
|
|
17
|
+
publicUrl?: string | undefined;
|
|
18
|
+
} | undefined;
|
|
19
|
+
}
|
|
20
|
+
export declare function createStorageProvider(options: StoragePluginOptions): StorageProvider;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { LocalStorageProvider } from './local.js';
|
|
2
|
+
import { R2StorageProvider } from './r2.js';
|
|
3
|
+
export * from './provider.js';
|
|
4
|
+
export * from './local.js';
|
|
5
|
+
export * from './r2.js';
|
|
6
|
+
export function createStorageProvider(options) {
|
|
7
|
+
if (options.provider === 'r2') {
|
|
8
|
+
const config = options.r2;
|
|
9
|
+
if (!config || !config.endpoint || !config.accessKeyId || !config.secretAccessKey || !config.bucketName) {
|
|
10
|
+
throw new Error('R2 storage configuration is missing');
|
|
11
|
+
}
|
|
12
|
+
return new R2StorageProvider({
|
|
13
|
+
endpoint: config.endpoint,
|
|
14
|
+
accessKeyId: config.accessKeyId,
|
|
15
|
+
secretAccessKey: config.secretAccessKey,
|
|
16
|
+
bucket: config.bucketName,
|
|
17
|
+
publicUrl: config.publicUrl,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
// Default to local
|
|
21
|
+
const config = options.local;
|
|
22
|
+
if (!config || !config.storagePath) {
|
|
23
|
+
throw new Error('Local storage configuration is missing');
|
|
24
|
+
}
|
|
25
|
+
return new LocalStorageProvider(config.storagePath, config.publicUrl || '/uploads' // Fallback
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/lib/storage/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAClD,OAAO,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAG5C,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,cAAc,SAAS,CAAC;AAoBxB,MAAM,UAAU,qBAAqB,CAAC,OAA6B;IACjE,IAAI,OAAO,CAAC,QAAQ,KAAK,IAAI,EAAE,CAAC;QAC9B,MAAM,MAAM,GAAG,OAAO,CAAC,EAAE,CAAC;QAC1B,IAAI,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,QAAQ,IAAI,CAAC,MAAM,CAAC,WAAW,IAAI,CAAC,MAAM,CAAC,eAAe,IAAI,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC;YACxG,MAAM,IAAI,KAAK,CAAC,qCAAqC,CAAC,CAAC;QACzD,CAAC;QACD,OAAO,IAAI,iBAAiB,CAAC;YAC3B,QAAQ,EAAE,MAAM,CAAC,QAAQ;YACzB,WAAW,EAAE,MAAM,CAAC,WAAW;YAC/B,eAAe,EAAE,MAAM,CAAC,eAAe;YACvC,MAAM,EAAE,MAAM,CAAC,UAAU;YACzB,SAAS,EAAE,MAAM,CAAC,SAAS;SAC5B,CAAC,CAAC;IACL,CAAC;IAED,mBAAmB;IACnB,MAAM,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC;IAC7B,IAAI,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC;QACnC,MAAM,IAAI,KAAK,CAAC,wCAAwC,CAAC,CAAC;IAC5D,CAAC;IAED,OAAO,IAAI,oBAAoB,CAC7B,MAAM,CAAC,WAAW,EAClB,MAAM,CAAC,SAAS,IAAI,UAAU,CAAC,WAAW;KAC3C,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { StorageProvider, UploadOptions } from './provider.js';
|
|
2
|
+
export declare class LocalStorageProvider implements StorageProvider {
|
|
3
|
+
private baseDir;
|
|
4
|
+
private baseUrl;
|
|
5
|
+
constructor(baseDir: string, baseUrl: string);
|
|
6
|
+
upload(file: Buffer, options: UploadOptions): Promise<string>;
|
|
7
|
+
delete(key: string): Promise<void>;
|
|
8
|
+
getPublicUrl(key: string): string;
|
|
9
|
+
exists(key: string): Promise<boolean>;
|
|
10
|
+
getMetadata(key: string): Promise<{
|
|
11
|
+
contentType?: string;
|
|
12
|
+
contentLength?: number;
|
|
13
|
+
lastModified?: Date;
|
|
14
|
+
} | null>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import { join, dirname } from 'node:path';
|
|
3
|
+
export class LocalStorageProvider {
|
|
4
|
+
baseDir;
|
|
5
|
+
baseUrl;
|
|
6
|
+
constructor(baseDir, baseUrl) {
|
|
7
|
+
this.baseDir = baseDir;
|
|
8
|
+
this.baseUrl = baseUrl;
|
|
9
|
+
}
|
|
10
|
+
async upload(file, options) {
|
|
11
|
+
const fullKey = options.path ? join(options.path, options.key) : options.key;
|
|
12
|
+
const filePath = join(this.baseDir, fullKey);
|
|
13
|
+
await fs.mkdir(dirname(filePath), { recursive: true });
|
|
14
|
+
await fs.writeFile(filePath, file);
|
|
15
|
+
return fullKey;
|
|
16
|
+
}
|
|
17
|
+
async delete(key) {
|
|
18
|
+
const filePath = join(this.baseDir, key);
|
|
19
|
+
try {
|
|
20
|
+
await fs.unlink(filePath);
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
if (error.code !== 'ENOENT') {
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
getPublicUrl(key) {
|
|
29
|
+
return `${this.baseUrl}/${key}`;
|
|
30
|
+
}
|
|
31
|
+
async exists(key) {
|
|
32
|
+
const filePath = join(this.baseDir, key);
|
|
33
|
+
try {
|
|
34
|
+
await fs.access(filePath);
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async getMetadata(key) {
|
|
42
|
+
const filePath = join(this.baseDir, key);
|
|
43
|
+
try {
|
|
44
|
+
const stats = await fs.stat(filePath);
|
|
45
|
+
return {
|
|
46
|
+
contentLength: stats.size,
|
|
47
|
+
lastModified: stats.mtime,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=local.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local.js","sourceRoot":"","sources":["../../../../src/lib/storage/local.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,IAAI,EAAE,EAAE,MAAM,SAAS,CAAC;AACzC,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAG1C,MAAM,OAAO,oBAAoB;IACX,OAAO;IAAkB,OAAO;IAApD,YAAoB,OAAe,EAAU,OAAe;uBAAxC,OAAO;uBAAkB,OAAO;IAAW,CAAC;IAEhE,KAAK,CAAC,MAAM,CAAC,IAAY,EAAE,OAAsB;QAC/C,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC;QAC7E,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QAC7C,MAAM,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACvD,MAAM,EAAE,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;QACnC,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,GAAW;QACtB,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;QACzC,IAAI,CAAC;YACH,MAAM,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAC5B,CAAC;QAAC,OAAO,KAAU,EAAE,CAAC;YACpB,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;gBAC5B,MAAM,KAAK,CAAC;YACd,CAAC;QACH,CAAC;IACH,CAAC;IAED,YAAY,CAAC,GAAW;QACtB,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,GAAG,EAAE,CAAC;IAClC,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,GAAW;QACtB,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;QACzC,IAAI,CAAC;YACH,MAAM,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;YAC1B,OAAO,IAAI,CAAC;QACd,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,GAAW;QAC3B,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;QACzC,IAAI,CAAC;YACH,MAAM,KAAK,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YACtC,OAAO;gBACL,aAAa,EAAE,KAAK,CAAC,IAAI;gBACzB,YAAY,EAAE,KAAK,CAAC,KAAK;aAC1B,CAAC;QACJ,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;CACF"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface UploadOptions {
|
|
2
|
+
key: string;
|
|
3
|
+
contentType?: string | undefined;
|
|
4
|
+
path?: string | undefined;
|
|
5
|
+
}
|
|
6
|
+
export interface FileMetadata {
|
|
7
|
+
contentType?: string | undefined;
|
|
8
|
+
contentLength?: number | undefined;
|
|
9
|
+
lastModified?: Date | undefined;
|
|
10
|
+
etag?: string | undefined;
|
|
11
|
+
metadata?: Record<string, string> | undefined;
|
|
12
|
+
}
|
|
13
|
+
export interface StorageProvider {
|
|
14
|
+
upload(file: Buffer, options: UploadOptions): Promise<string>;
|
|
15
|
+
delete(key: string): Promise<void>;
|
|
16
|
+
getPublicUrl(key: string): string;
|
|
17
|
+
exists(key: string): Promise<boolean>;
|
|
18
|
+
getMetadata(key: string): Promise<FileMetadata | null>;
|
|
19
|
+
generateSignedUploadUrl?(key: string, options: {
|
|
20
|
+
expiresIn?: number | undefined;
|
|
21
|
+
contentType?: string | undefined;
|
|
22
|
+
}): Promise<string>;
|
|
23
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider.js","sourceRoot":"","sources":["../../../../src/lib/storage/provider.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { StorageProvider, UploadOptions } from './provider.js';
|
|
2
|
+
export declare class R2StorageProvider implements StorageProvider {
|
|
3
|
+
private client;
|
|
4
|
+
private bucket;
|
|
5
|
+
private publicUrl;
|
|
6
|
+
constructor(config: {
|
|
7
|
+
endpoint: string;
|
|
8
|
+
accessKeyId: string;
|
|
9
|
+
secretAccessKey: string;
|
|
10
|
+
bucket: string;
|
|
11
|
+
publicUrl?: string | undefined;
|
|
12
|
+
});
|
|
13
|
+
upload(file: Buffer, options: UploadOptions): Promise<string>;
|
|
14
|
+
delete(key: string): Promise<void>;
|
|
15
|
+
getPublicUrl(key: string): string;
|
|
16
|
+
exists(key: string): Promise<boolean>;
|
|
17
|
+
getMetadata(key: string): Promise<{
|
|
18
|
+
contentType: string | undefined;
|
|
19
|
+
contentLength: number | undefined;
|
|
20
|
+
lastModified: Date | undefined;
|
|
21
|
+
etag: string | undefined;
|
|
22
|
+
metadata: Record<string, string> | undefined;
|
|
23
|
+
} | null>;
|
|
24
|
+
generateSignedUploadUrl(key: string, options?: {
|
|
25
|
+
expiresIn?: number | undefined;
|
|
26
|
+
contentType?: string | undefined;
|
|
27
|
+
}): Promise<string>;
|
|
28
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { S3Client, PutObjectCommand, DeleteObjectCommand, HeadObjectCommand } from '@aws-sdk/client-s3';
|
|
2
|
+
export class R2StorageProvider {
|
|
3
|
+
client;
|
|
4
|
+
bucket;
|
|
5
|
+
publicUrl;
|
|
6
|
+
constructor(config) {
|
|
7
|
+
this.client = new S3Client({
|
|
8
|
+
region: 'auto',
|
|
9
|
+
endpoint: config.endpoint,
|
|
10
|
+
credentials: {
|
|
11
|
+
accessKeyId: config.accessKeyId,
|
|
12
|
+
secretAccessKey: config.secretAccessKey,
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
this.bucket = config.bucket;
|
|
16
|
+
this.publicUrl = config.publicUrl || `${config.endpoint}/${config.bucket}`;
|
|
17
|
+
}
|
|
18
|
+
async upload(file, options) {
|
|
19
|
+
const fullKey = options.path ? `${options.path.replace(/\/$/, '')}/${options.key}` : options.key;
|
|
20
|
+
await this.client.send(new PutObjectCommand({
|
|
21
|
+
Bucket: this.bucket,
|
|
22
|
+
Key: fullKey,
|
|
23
|
+
Body: file,
|
|
24
|
+
ContentType: options.contentType,
|
|
25
|
+
}));
|
|
26
|
+
return fullKey;
|
|
27
|
+
}
|
|
28
|
+
async delete(key) {
|
|
29
|
+
await this.client.send(new DeleteObjectCommand({
|
|
30
|
+
Bucket: this.bucket,
|
|
31
|
+
Key: key,
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
34
|
+
getPublicUrl(key) {
|
|
35
|
+
return `${this.publicUrl}/${key}`;
|
|
36
|
+
}
|
|
37
|
+
async exists(key) {
|
|
38
|
+
try {
|
|
39
|
+
await this.client.send(new HeadObjectCommand({
|
|
40
|
+
Bucket: this.bucket,
|
|
41
|
+
Key: key,
|
|
42
|
+
}));
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
if (error.name === 'NotFound' || error.$metadata?.httpStatusCode === 404) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async getMetadata(key) {
|
|
53
|
+
try {
|
|
54
|
+
const response = await this.client.send(new HeadObjectCommand({
|
|
55
|
+
Bucket: this.bucket,
|
|
56
|
+
Key: key,
|
|
57
|
+
}));
|
|
58
|
+
return {
|
|
59
|
+
contentType: response.ContentType,
|
|
60
|
+
contentLength: response.ContentLength,
|
|
61
|
+
lastModified: response.LastModified,
|
|
62
|
+
etag: response.ETag,
|
|
63
|
+
metadata: response.Metadata,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
if (error.name === 'NotFound' || error.$metadata?.httpStatusCode === 404) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async generateSignedUploadUrl(key, options = {}) {
|
|
74
|
+
const { getSignedUrl } = await import('@aws-sdk/s3-request-presigner');
|
|
75
|
+
const command = new PutObjectCommand({
|
|
76
|
+
Bucket: this.bucket,
|
|
77
|
+
Key: key,
|
|
78
|
+
ContentType: options.contentType,
|
|
79
|
+
});
|
|
80
|
+
return getSignedUrl(this.client, command, {
|
|
81
|
+
expiresIn: options.expiresIn ?? 3600,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=r2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"r2.js","sourceRoot":"","sources":["../../../../src/lib/storage/r2.ts"],"names":[],"mappings":"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"}
|