@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.
Files changed (51) hide show
  1. package/README.md +193 -0
  2. package/dist/src/app.d.ts +3 -0
  3. package/dist/src/app.js +93 -0
  4. package/dist/src/app.js.map +1 -0
  5. package/dist/src/config/env.d.ts +26 -0
  6. package/dist/src/config/env.js +28 -0
  7. package/dist/src/config/env.js.map +1 -0
  8. package/dist/src/lib/storage/index.d.ts +20 -0
  9. package/dist/src/lib/storage/index.js +28 -0
  10. package/dist/src/lib/storage/index.js.map +1 -0
  11. package/dist/src/lib/storage/local.d.ts +15 -0
  12. package/dist/src/lib/storage/local.js +55 -0
  13. package/dist/src/lib/storage/local.js.map +1 -0
  14. package/dist/src/lib/storage/provider.d.ts +23 -0
  15. package/dist/src/lib/storage/provider.js +2 -0
  16. package/dist/src/lib/storage/provider.js.map +1 -0
  17. package/dist/src/lib/storage/r2.d.ts +28 -0
  18. package/dist/src/lib/storage/r2.js +85 -0
  19. package/dist/src/lib/storage/r2.js.map +1 -0
  20. package/dist/src/modules/health/index.d.ts +2 -0
  21. package/dist/src/modules/health/index.js +60 -0
  22. package/dist/src/modules/health/index.js.map +1 -0
  23. package/dist/src/modules/storage/index.d.ts +2 -0
  24. package/dist/src/modules/storage/index.js +76 -0
  25. package/dist/src/modules/storage/index.js.map +1 -0
  26. package/dist/src/modules/storage/storage.controller.d.ts +119 -0
  27. package/dist/src/modules/storage/storage.controller.js +99 -0
  28. package/dist/src/modules/storage/storage.controller.js.map +1 -0
  29. package/dist/src/modules/storage/storage.service.d.ts +111 -0
  30. package/dist/src/modules/storage/storage.service.js +191 -0
  31. package/dist/src/modules/storage/storage.service.js.map +1 -0
  32. package/dist/src/plugins/prisma.d.ts +9 -0
  33. package/dist/src/plugins/prisma.js +18 -0
  34. package/dist/src/plugins/prisma.js.map +1 -0
  35. package/dist/src/plugins/queues.d.ts +14 -0
  36. package/dist/src/plugins/queues.js +29 -0
  37. package/dist/src/plugins/queues.js.map +1 -0
  38. package/dist/src/plugins/redis.d.ts +9 -0
  39. package/dist/src/plugins/redis.js +15 -0
  40. package/dist/src/plugins/redis.js.map +1 -0
  41. package/dist/src/plugins/storage.d.ts +14 -0
  42. package/dist/src/plugins/storage.js +15 -0
  43. package/dist/src/plugins/storage.js.map +1 -0
  44. package/dist/src/server.d.ts +1 -0
  45. package/dist/src/server.js +19 -0
  46. package/dist/src/server.js.map +1 -0
  47. package/dist/src/workers/index.d.ts +1 -0
  48. package/dist/src/workers/index.js +33 -0
  49. package/dist/src/workers/index.js.map +1 -0
  50. package/package.json +77 -0
  51. 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>>;
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=provider.js.map
@@ -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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"}
@@ -0,0 +1,2 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ export default function (fastify: FastifyInstance): Promise<void>;