@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/server/app.js ADDED
@@ -0,0 +1,136 @@
1
+ // server/app.js
2
+ import Fastify from "fastify";
3
+ import multipart from "@fastify/multipart";
4
+ import xStorage from "../src/xStorage.js";
5
+
6
+ const fastify = Fastify({
7
+ logger: true,
8
+ });
9
+
10
+ // xStorage is a library of methods, not a server with routes
11
+ // The routes below are EXAMPLES of how to use the fastify.xStorage methods
12
+ // You can implement your own routes or use xStorage methods directly in your application
13
+
14
+ // Register multipart for file uploads
15
+ await fastify.register(multipart, {
16
+ limits: {
17
+ fileSize: 10 * 1024 * 1024, // 10MB
18
+ },
19
+ });
20
+
21
+ // Register xStorage
22
+ await fastify.register(xStorage, {
23
+ endpoint: process.env.STORAGE_ENDPOINT,
24
+ region: process.env.STORAGE_REGION,
25
+ accessKeyId: process.env.STORAGE_ACCESS_KEY_ID,
26
+ secretAccessKey: process.env.STORAGE_SECRET_ACCESS_KEY,
27
+ bucket: process.env.STORAGE_BUCKET,
28
+ publicUrl: process.env.STORAGE_PUBLIC_URL,
29
+ forcePathStyle: true,
30
+ // Default ACL is "private" - use signed URLs for secure access
31
+ // acl: "private", // Uncomment to override default
32
+ });
33
+
34
+ // Basic file upload endpoint
35
+ fastify.post("/upload", async (request, reply) => {
36
+ const data = await request.file();
37
+
38
+ if (!data) {
39
+ return reply.code(400).send({ error: "No file uploaded" });
40
+ }
41
+
42
+ const buffer = await data.toBuffer();
43
+
44
+ const result = await fastify.xStorage.upload(buffer, data.filename, {
45
+ folder: "uploads",
46
+ });
47
+
48
+ return {
49
+ success: true,
50
+ file: result,
51
+ };
52
+ });
53
+
54
+ // For image processing, use @xenterprises/fastify-ximagepipeline
55
+ // Example:
56
+ // import xImagePipeline from "@xenterprises/fastify-ximagepipeline";
57
+ // await fastify.register(xImagePipeline, { r2, db, ... });
58
+
59
+ // Multiple file upload
60
+ fastify.post("/upload/multiple", async (request, reply) => {
61
+ const parts = request.parts();
62
+ const files = [];
63
+
64
+ for await (const part of parts) {
65
+ if (part.type === "file") {
66
+ const buffer = await part.toBuffer();
67
+ files.push({
68
+ file: buffer,
69
+ filename: part.filename,
70
+ });
71
+ }
72
+ }
73
+
74
+ const results = await fastify.xStorage.uploadMultiple(files, {
75
+ folder: "uploads",
76
+ });
77
+
78
+ return {
79
+ success: true,
80
+ files: results,
81
+ };
82
+ });
83
+
84
+ // List files
85
+ fastify.get("/files", async (request, reply) => {
86
+ const { folder = "" } = request.query;
87
+
88
+ const files = await fastify.xStorage.list(folder);
89
+
90
+ return {
91
+ success: true,
92
+ files,
93
+ };
94
+ });
95
+
96
+ // Delete file
97
+ fastify.delete("/files/:key", async (request, reply) => {
98
+ const { key } = request.params;
99
+
100
+ await fastify.xStorage.delete(key);
101
+
102
+ return {
103
+ success: true,
104
+ message: "File deleted successfully",
105
+ };
106
+ });
107
+
108
+ // Get signed URL
109
+ fastify.get("/files/:key/signed-url", async (request, reply) => {
110
+ const { key } = request.params;
111
+ const { expires = 3600 } = request.query;
112
+
113
+ const url = await fastify.xStorage.getSignedUrl(key, Number(expires));
114
+
115
+ return {
116
+ success: true,
117
+ url,
118
+ };
119
+ });
120
+
121
+ // Health check
122
+ fastify.get("/health", async () => {
123
+ return { status: "ok", timestamp: new Date().toISOString() };
124
+ });
125
+
126
+ // Start server
127
+ const start = async () => {
128
+ try {
129
+ await fastify.listen({ port: process.env.PORT || 3000, host: "0.0.0.0" });
130
+ } catch (err) {
131
+ fastify.log.error(err);
132
+ process.exit(1);
133
+ }
134
+ };
135
+
136
+ start();
package/src/index.js ADDED
@@ -0,0 +1,9 @@
1
+ // src/index.js
2
+
3
+ /**
4
+ * Main exports for xStorage plugin
5
+ */
6
+
7
+ export { default } from "./xStorage.js";
8
+ export { default as xStorage } from "./xStorage.js";
9
+ export * as helpers from "./utils/helpers.js";
@@ -0,0 +1,352 @@
1
+ // src/services/storage.js
2
+ import {
3
+ PutObjectCommand,
4
+ GetObjectCommand,
5
+ DeleteObjectCommand,
6
+ ListObjectsV2Command,
7
+ CopyObjectCommand,
8
+ HeadObjectCommand,
9
+ } from "@aws-sdk/client-s3";
10
+ import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
11
+ import { lookup } from "mime-types";
12
+ import { randomBytes } from "crypto";
13
+ import path from "path";
14
+
15
+ export function getStorageMethods(fastify, config) {
16
+ const { s3Client, bucket, publicUrl, acl } = config;
17
+
18
+ return {
19
+ /**
20
+ * Upload a file to S3-compatible storage
21
+ * @param {Buffer|Stream} file - File buffer or stream
22
+ * @param {string} filename - Original filename
23
+ * @param {object} options - Upload options
24
+ * @returns {Promise<{key: string, url: string, size: number, contentType: string}>}
25
+ */
26
+ upload: async (file, filename, options = {}) => {
27
+ try {
28
+ const {
29
+ folder = "",
30
+ key = null,
31
+ contentType = null,
32
+ metadata = {},
33
+ useRandomName = true,
34
+ acl: fileAcl = acl, // Allow per-file ACL override
35
+ } = options;
36
+
37
+ // Generate file key
38
+ let fileKey;
39
+ if (key) {
40
+ fileKey = key;
41
+ } else {
42
+ const ext = path.extname(filename);
43
+ const baseName = path.basename(filename, ext);
44
+ const randomName = useRandomName
45
+ ? `${baseName}-${randomBytes(8).toString("hex")}${ext}`
46
+ : filename;
47
+ fileKey = folder ? `${folder}/${randomName}` : randomName;
48
+ }
49
+
50
+ // Determine content type
51
+ const mimeType = contentType || lookup(filename) || "application/octet-stream";
52
+
53
+ // Upload to S3 with per-file ACL
54
+ const command = new PutObjectCommand({
55
+ Bucket: bucket,
56
+ Key: fileKey,
57
+ Body: file,
58
+ ContentType: mimeType,
59
+ ACL: fileAcl,
60
+ Metadata: metadata,
61
+ });
62
+
63
+ await s3Client.send(command);
64
+
65
+ // Get file size
66
+ const fileSize = Buffer.isBuffer(file) ? file.length : null;
67
+
68
+ const result = {
69
+ key: fileKey,
70
+ url: `${publicUrl}/${fileKey}`,
71
+ size: fileSize,
72
+ contentType: mimeType,
73
+ bucket,
74
+ };
75
+
76
+ fastify.log.info(`File uploaded: ${fileKey}`);
77
+ return result;
78
+ } catch (error) {
79
+ fastify.log.error("Storage upload failed:", error);
80
+ throw new Error(`Failed to upload file: ${error.message}`);
81
+ }
82
+ },
83
+
84
+ /**
85
+ * Upload multiple files
86
+ * @param {Array<{file: Buffer, filename: string}>} files - Array of files
87
+ * @param {object} options - Upload options
88
+ * @returns {Promise<Array>}
89
+ */
90
+ uploadMultiple: async (files, options = {}) => {
91
+ try {
92
+ const {
93
+ folder = "",
94
+ key = null,
95
+ contentType = null,
96
+ metadata = {},
97
+ useRandomName = true,
98
+ acl: batchAcl = acl, // Allow batch ACL override
99
+ } = options;
100
+
101
+ const uploadPromises = files.map((fileData) => {
102
+ // Determine per-file ACL (file can override batch ACL)
103
+ const fileAcl = fileData.acl || batchAcl;
104
+
105
+ // Generate file key
106
+ let fileKey;
107
+ if (key) {
108
+ fileKey = key;
109
+ } else {
110
+ const ext = path.extname(fileData.filename);
111
+ const baseName = path.basename(fileData.filename, ext);
112
+ const randomName = useRandomName
113
+ ? `${baseName}-${randomBytes(8).toString("hex")}${ext}`
114
+ : fileData.filename;
115
+ fileKey = folder ? `${folder}/${randomName}` : randomName;
116
+ }
117
+
118
+ // Determine content type
119
+ const mimeType = contentType || lookup(fileData.filename) || "application/octet-stream";
120
+
121
+ // Upload to S3 with per-file ACL support
122
+ const command = new PutObjectCommand({
123
+ Bucket: bucket,
124
+ Key: fileKey,
125
+ Body: fileData.file,
126
+ ContentType: mimeType,
127
+ ACL: fileAcl,
128
+ Metadata: metadata,
129
+ });
130
+
131
+ return s3Client.send(command).then(() => {
132
+ const fileSize = Buffer.isBuffer(fileData.file) ? fileData.file.length : null;
133
+ return {
134
+ key: fileKey,
135
+ url: `${publicUrl}/${fileKey}`,
136
+ size: fileSize,
137
+ contentType: mimeType,
138
+ bucket,
139
+ };
140
+ });
141
+ });
142
+
143
+ const results = await Promise.all(uploadPromises);
144
+ fastify.log.info(`${results.length} files uploaded successfully`);
145
+ return results;
146
+ } catch (error) {
147
+ fastify.log.error("Multiple upload failed:", error);
148
+ throw new Error(`Failed to upload multiple files: ${error.message}`);
149
+ }
150
+ },
151
+
152
+ /**
153
+ * Download a file from storage
154
+ * @param {string} key - File key
155
+ * @returns {Promise<Buffer>}
156
+ */
157
+ download: async (key) => {
158
+ try {
159
+ const command = new GetObjectCommand({
160
+ Bucket: bucket,
161
+ Key: key,
162
+ });
163
+
164
+ const response = await s3Client.send(command);
165
+ const chunks = [];
166
+
167
+ for await (const chunk of response.Body) {
168
+ chunks.push(chunk);
169
+ }
170
+
171
+ return Buffer.concat(chunks);
172
+ } catch (error) {
173
+ fastify.log.error("Storage download failed:", error);
174
+ throw new Error(`Failed to download file: ${error.message}`);
175
+ }
176
+ },
177
+
178
+ /**
179
+ * Delete a file from storage
180
+ * @param {string} key - File key
181
+ * @returns {Promise<boolean>}
182
+ */
183
+ delete: async (key) => {
184
+ try {
185
+ const command = new DeleteObjectCommand({
186
+ Bucket: bucket,
187
+ Key: key,
188
+ });
189
+
190
+ await s3Client.send(command);
191
+ fastify.log.info(`File deleted: ${key}`);
192
+ return true;
193
+ } catch (error) {
194
+ fastify.log.error("Storage delete failed:", error);
195
+ throw new Error(`Failed to delete file: ${error.message}`);
196
+ }
197
+ },
198
+
199
+ /**
200
+ * Delete multiple files
201
+ * @param {Array<string>} keys - Array of file keys
202
+ * @returns {Promise<boolean>}
203
+ */
204
+ deleteMultiple: async (keys) => {
205
+ try {
206
+ const deletePromises = keys.map((key) => fastify.storage.delete(key));
207
+ await Promise.all(deletePromises);
208
+ return true;
209
+ } catch (error) {
210
+ fastify.log.error("Multiple delete failed:", error);
211
+ throw new Error(`Failed to delete multiple files: ${error.message}`);
212
+ }
213
+ },
214
+
215
+ /**
216
+ * Copy a file within storage
217
+ * @param {string} sourceKey - Source file key
218
+ * @param {string} destinationKey - Destination file key
219
+ * @returns {Promise<{key: string, url: string}>}
220
+ */
221
+ copy: async (sourceKey, destinationKey) => {
222
+ try {
223
+ const command = new CopyObjectCommand({
224
+ Bucket: bucket,
225
+ CopySource: `${bucket}/${sourceKey}`,
226
+ Key: destinationKey,
227
+ ACL: acl,
228
+ });
229
+
230
+ await s3Client.send(command);
231
+
232
+ return {
233
+ key: destinationKey,
234
+ url: `${publicUrl}/${destinationKey}`,
235
+ };
236
+ } catch (error) {
237
+ fastify.log.error("Storage copy failed:", error);
238
+ throw new Error(`Failed to copy file: ${error.message}`);
239
+ }
240
+ },
241
+
242
+ /**
243
+ * Check if a file exists
244
+ * @param {string} key - File key
245
+ * @returns {Promise<boolean>}
246
+ */
247
+ exists: async (key) => {
248
+ try {
249
+ const command = new HeadObjectCommand({
250
+ Bucket: bucket,
251
+ Key: key,
252
+ });
253
+
254
+ await s3Client.send(command);
255
+ return true;
256
+ } catch (error) {
257
+ if (error.name === "NotFound") {
258
+ return false;
259
+ }
260
+ throw error;
261
+ }
262
+ },
263
+
264
+ /**
265
+ * Get file metadata
266
+ * @param {string} key - File key
267
+ * @returns {Promise<object>}
268
+ */
269
+ getMetadata: async (key) => {
270
+ try {
271
+ const command = new HeadObjectCommand({
272
+ Bucket: bucket,
273
+ Key: key,
274
+ });
275
+
276
+ const response = await s3Client.send(command);
277
+
278
+ return {
279
+ contentType: response.ContentType,
280
+ contentLength: response.ContentLength,
281
+ lastModified: response.LastModified,
282
+ etag: response.ETag,
283
+ metadata: response.Metadata,
284
+ };
285
+ } catch (error) {
286
+ fastify.log.error("Get metadata failed:", error);
287
+ throw new Error(`Failed to get file metadata: ${error.message}`);
288
+ }
289
+ },
290
+
291
+ /**
292
+ * List files in a folder
293
+ * @param {string} prefix - Folder prefix
294
+ * @param {number} maxKeys - Maximum number of keys to return
295
+ * @returns {Promise<Array>}
296
+ */
297
+ list: async (prefix = "", maxKeys = 1000) => {
298
+ try {
299
+ const command = new ListObjectsV2Command({
300
+ Bucket: bucket,
301
+ Prefix: prefix,
302
+ MaxKeys: maxKeys,
303
+ });
304
+
305
+ const response = await s3Client.send(command);
306
+
307
+ return (
308
+ response.Contents?.map((item) => ({
309
+ key: item.Key,
310
+ url: `${publicUrl}/${item.Key}`,
311
+ size: item.Size,
312
+ lastModified: item.LastModified,
313
+ etag: item.ETag,
314
+ })) || []
315
+ );
316
+ } catch (error) {
317
+ fastify.log.error("Storage list failed:", error);
318
+ throw new Error(`Failed to list files: ${error.message}`);
319
+ }
320
+ },
321
+
322
+ /**
323
+ * Generate a pre-signed URL for temporary access
324
+ * @param {string} key - File key
325
+ * @param {number} expiresIn - Expiration time in seconds (default: 1 hour)
326
+ * @returns {Promise<string>}
327
+ */
328
+ getSignedUrl: async (key, expiresIn = 3600) => {
329
+ try {
330
+ const command = new GetObjectCommand({
331
+ Bucket: bucket,
332
+ Key: key,
333
+ });
334
+
335
+ const url = await getSignedUrl(s3Client, command, { expiresIn });
336
+ return url;
337
+ } catch (error) {
338
+ fastify.log.error("Generate signed URL failed:", error);
339
+ throw new Error(`Failed to generate signed URL: ${error.message}`);
340
+ }
341
+ },
342
+
343
+ /**
344
+ * Get public URL for a file
345
+ * @param {string} key - File key
346
+ * @returns {string}
347
+ */
348
+ getPublicUrl: (key) => {
349
+ return `${publicUrl}/${key}`;
350
+ },
351
+ };
352
+ }