@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
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
// src/utils/helpers.js
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Format file size in human-readable format
|
|
5
|
+
* @param {number} bytes - File size in bytes
|
|
6
|
+
* @returns {string}
|
|
7
|
+
*/
|
|
8
|
+
export function formatFileSize(bytes) {
|
|
9
|
+
if (bytes === 0) return "0 Bytes";
|
|
10
|
+
|
|
11
|
+
const k = 1024;
|
|
12
|
+
const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
|
|
13
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
14
|
+
|
|
15
|
+
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Generate a unique filename with timestamp
|
|
20
|
+
* @param {string} originalName - Original filename
|
|
21
|
+
* @returns {string}
|
|
22
|
+
*/
|
|
23
|
+
export function generateUniqueFilename(originalName) {
|
|
24
|
+
const timestamp = Date.now();
|
|
25
|
+
const ext = originalName.substring(originalName.lastIndexOf("."));
|
|
26
|
+
const name = originalName.substring(0, originalName.lastIndexOf("."));
|
|
27
|
+
return `${name}-${timestamp}${ext}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Validate file type
|
|
32
|
+
* @param {string} filename - Filename
|
|
33
|
+
* @param {Array<string>} allowedTypes - Allowed file extensions
|
|
34
|
+
* @returns {boolean}
|
|
35
|
+
*/
|
|
36
|
+
export function isValidFileType(filename, allowedTypes = []) {
|
|
37
|
+
if (allowedTypes.length === 0) return true;
|
|
38
|
+
|
|
39
|
+
const ext = filename.substring(filename.lastIndexOf(".") + 1).toLowerCase();
|
|
40
|
+
return allowedTypes.includes(ext);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Check if file is an image
|
|
45
|
+
* @param {string} filename - Filename
|
|
46
|
+
* @returns {boolean}
|
|
47
|
+
*/
|
|
48
|
+
export function isImage(filename) {
|
|
49
|
+
const imageExtensions = ["jpg", "jpeg", "png", "gif", "webp", "svg", "bmp", "avif"];
|
|
50
|
+
return isValidFileType(filename, imageExtensions);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Check if file is a PDF
|
|
55
|
+
* @param {string} filename - Filename
|
|
56
|
+
* @returns {boolean}
|
|
57
|
+
*/
|
|
58
|
+
export function isPdf(filename) {
|
|
59
|
+
return filename.toLowerCase().endsWith(".pdf");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Check if file is a video
|
|
64
|
+
* @param {string} filename - Filename
|
|
65
|
+
* @returns {boolean}
|
|
66
|
+
*/
|
|
67
|
+
export function isVideo(filename) {
|
|
68
|
+
const videoExtensions = ["mp4", "avi", "mov", "wmv", "flv", "webm", "mkv"];
|
|
69
|
+
return isValidFileType(filename, videoExtensions);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Sanitize filename to remove unsafe characters
|
|
74
|
+
* @param {string} filename - Original filename
|
|
75
|
+
* @returns {string}
|
|
76
|
+
*/
|
|
77
|
+
export function sanitizeFilename(filename) {
|
|
78
|
+
return filename
|
|
79
|
+
.replace(/[^a-zA-Z0-9._-]/g, "_") // Replace unsafe chars with underscore
|
|
80
|
+
.replace(/_{2,}/g, "_") // Replace multiple underscores with single
|
|
81
|
+
.toLowerCase();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Extract file extension
|
|
86
|
+
* @param {string} filename - Filename
|
|
87
|
+
* @returns {string}
|
|
88
|
+
*/
|
|
89
|
+
export function getFileExtension(filename) {
|
|
90
|
+
return filename.substring(filename.lastIndexOf(".") + 1).toLowerCase();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Get filename without extension
|
|
95
|
+
* @param {string} filename - Filename
|
|
96
|
+
* @returns {string}
|
|
97
|
+
*/
|
|
98
|
+
export function getFilenameWithoutExtension(filename) {
|
|
99
|
+
return filename.substring(0, filename.lastIndexOf("."));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Build storage key with folder structure
|
|
104
|
+
* @param {string} filename - Filename
|
|
105
|
+
* @param {object} options - Options
|
|
106
|
+
* @returns {string}
|
|
107
|
+
*/
|
|
108
|
+
export function buildStorageKey(filename, options = {}) {
|
|
109
|
+
const { folder = "", prefix = "", suffix = "", timestamp = false } = options;
|
|
110
|
+
|
|
111
|
+
let key = filename;
|
|
112
|
+
|
|
113
|
+
if (timestamp) {
|
|
114
|
+
const ext = getFileExtension(filename);
|
|
115
|
+
const name = getFilenameWithoutExtension(filename);
|
|
116
|
+
key = `${name}-${Date.now()}.${ext}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (prefix) {
|
|
120
|
+
key = `${prefix}-${key}`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (suffix) {
|
|
124
|
+
const ext = getFileExtension(key);
|
|
125
|
+
const name = getFilenameWithoutExtension(key);
|
|
126
|
+
key = `${name}-${suffix}.${ext}`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (folder) {
|
|
130
|
+
key = `${folder}/${key}`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return key;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Calculate image aspect ratio
|
|
138
|
+
* @param {number} width - Image width
|
|
139
|
+
* @param {number} height - Image height
|
|
140
|
+
* @returns {string}
|
|
141
|
+
*/
|
|
142
|
+
export function getAspectRatio(width, height) {
|
|
143
|
+
const gcd = (a, b) => (b === 0 ? a : gcd(b, a % b));
|
|
144
|
+
const divisor = gcd(width, height);
|
|
145
|
+
return `${width / divisor}:${height / divisor}`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Calculate dimensions to fit within max dimensions while preserving aspect ratio
|
|
150
|
+
* @param {number} width - Original width
|
|
151
|
+
* @param {number} height - Original height
|
|
152
|
+
* @param {number} maxWidth - Maximum width
|
|
153
|
+
* @param {number} maxHeight - Maximum height
|
|
154
|
+
* @returns {{width: number, height: number}}
|
|
155
|
+
*/
|
|
156
|
+
export function calculateFitDimensions(width, height, maxWidth, maxHeight) {
|
|
157
|
+
const aspectRatio = width / height;
|
|
158
|
+
|
|
159
|
+
let newWidth = width;
|
|
160
|
+
let newHeight = height;
|
|
161
|
+
|
|
162
|
+
if (width > maxWidth) {
|
|
163
|
+
newWidth = maxWidth;
|
|
164
|
+
newHeight = newWidth / aspectRatio;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (newHeight > maxHeight) {
|
|
168
|
+
newHeight = maxHeight;
|
|
169
|
+
newWidth = newHeight * aspectRatio;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
width: Math.round(newWidth),
|
|
174
|
+
height: Math.round(newHeight),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Parse S3 URL to get key
|
|
180
|
+
* @param {string} url - S3 URL
|
|
181
|
+
* @param {string} publicUrl - Public URL base
|
|
182
|
+
* @returns {string}
|
|
183
|
+
*/
|
|
184
|
+
export function getKeyFromUrl(url, publicUrl) {
|
|
185
|
+
return url.replace(publicUrl + "/", "");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Group files by folder
|
|
190
|
+
* @param {Array<object>} files - Array of file objects with key property
|
|
191
|
+
* @returns {object}
|
|
192
|
+
*/
|
|
193
|
+
export function groupFilesByFolder(files) {
|
|
194
|
+
return files.reduce((acc, file) => {
|
|
195
|
+
const folder = file.key.includes("/")
|
|
196
|
+
? file.key.substring(0, file.key.lastIndexOf("/"))
|
|
197
|
+
: "root";
|
|
198
|
+
|
|
199
|
+
if (!acc[folder]) {
|
|
200
|
+
acc[folder] = [];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
acc[folder].push(file);
|
|
204
|
+
return acc;
|
|
205
|
+
}, {});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Validate image dimensions
|
|
210
|
+
* @param {number} width - Image width
|
|
211
|
+
* @param {number} height - Image height
|
|
212
|
+
* @param {object} constraints - Dimension constraints
|
|
213
|
+
* @returns {boolean}
|
|
214
|
+
*/
|
|
215
|
+
export function isValidDimensions(width, height, constraints = {}) {
|
|
216
|
+
const { minWidth = 0, minHeight = 0, maxWidth = Infinity, maxHeight = Infinity } = constraints;
|
|
217
|
+
|
|
218
|
+
return width >= minWidth && width <= maxWidth && height >= minHeight && height <= maxHeight;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Generate responsive image sizes
|
|
223
|
+
* @param {number} originalWidth - Original image width
|
|
224
|
+
* @param {number} originalHeight - Original image height
|
|
225
|
+
* @returns {Array<object>}
|
|
226
|
+
*/
|
|
227
|
+
export function generateResponsiveSizes(originalWidth, originalHeight) {
|
|
228
|
+
const breakpoints = [320, 640, 768, 1024, 1280, 1536, 1920];
|
|
229
|
+
const aspectRatio = originalWidth / originalHeight;
|
|
230
|
+
|
|
231
|
+
return breakpoints
|
|
232
|
+
.filter((width) => width <= originalWidth)
|
|
233
|
+
.map((width) => ({
|
|
234
|
+
width,
|
|
235
|
+
height: Math.round(width / aspectRatio),
|
|
236
|
+
name: `w${width}`,
|
|
237
|
+
}));
|
|
238
|
+
}
|
package/src/xStorage.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// src/xStorage.js
|
|
2
|
+
import fp from "fastify-plugin";
|
|
3
|
+
import { S3Client } from "@aws-sdk/client-s3";
|
|
4
|
+
import { getStorageMethods } from "./services/storage.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* xStorage Plugin for Fastify
|
|
8
|
+
* Generic S3-compatible file storage library with simple, intuitive API
|
|
9
|
+
*
|
|
10
|
+
* Provides methods for:
|
|
11
|
+
* - Upload, download, delete, list, copy operations
|
|
12
|
+
* - Signed URLs for secure temporary access (default: private)
|
|
13
|
+
* - Public URLs for public files (optional)
|
|
14
|
+
* - Metadata retrieval and file operations
|
|
15
|
+
* - Concurrent operations (batch uploads/deletes)
|
|
16
|
+
*
|
|
17
|
+
* Supports: AWS S3, Cloudflare R2, Digital Ocean Spaces, any S3-compatible provider
|
|
18
|
+
*
|
|
19
|
+
* Usage:
|
|
20
|
+
* await fastify.xStorage.upload(buffer, 'filename.txt', { folder: 'docs' })
|
|
21
|
+
* const url = await fastify.xStorage.getSignedUrl('docs/filename.txt', 3600)
|
|
22
|
+
* await fastify.xStorage.delete('docs/filename.txt')
|
|
23
|
+
*
|
|
24
|
+
* For specialized processing:
|
|
25
|
+
* - Use @xenterprises/fastify-ximagepipeline for image processing
|
|
26
|
+
* - Use your own PDF library or service for PDF operations
|
|
27
|
+
*/
|
|
28
|
+
async function xStoragePlugin(fastify, options) {
|
|
29
|
+
const {
|
|
30
|
+
endpoint,
|
|
31
|
+
region = "us-east-1",
|
|
32
|
+
accessKeyId,
|
|
33
|
+
secretAccessKey,
|
|
34
|
+
bucket,
|
|
35
|
+
publicUrl,
|
|
36
|
+
forcePathStyle = true, // Required for Digital Ocean Spaces and some S3-compatible services
|
|
37
|
+
acl = "private", // Default to private, use signed URLs for access
|
|
38
|
+
} = options;
|
|
39
|
+
|
|
40
|
+
// Validate required options
|
|
41
|
+
if (!endpoint && !region) {
|
|
42
|
+
throw new Error("Either endpoint (for S3-compatible) or region (for AWS S3) is required");
|
|
43
|
+
}
|
|
44
|
+
if (!accessKeyId || !secretAccessKey) {
|
|
45
|
+
throw new Error("accessKeyId and secretAccessKey are required");
|
|
46
|
+
}
|
|
47
|
+
if (!bucket) {
|
|
48
|
+
throw new Error("bucket name is required");
|
|
49
|
+
}
|
|
50
|
+
if (!publicUrl) {
|
|
51
|
+
throw new Error("publicUrl is required (e.g., https://your-bucket.nyc3.digitaloceanspaces.com)");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
console.info("\n 📦 Starting xStorage...\n");
|
|
55
|
+
|
|
56
|
+
// Initialize S3 client
|
|
57
|
+
const s3Client = new S3Client({
|
|
58
|
+
endpoint,
|
|
59
|
+
region,
|
|
60
|
+
credentials: {
|
|
61
|
+
accessKeyId,
|
|
62
|
+
secretAccessKey,
|
|
63
|
+
},
|
|
64
|
+
forcePathStyle,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Store configuration
|
|
68
|
+
const config = {
|
|
69
|
+
s3Client,
|
|
70
|
+
bucket,
|
|
71
|
+
publicUrl: publicUrl.endsWith("/") ? publicUrl.slice(0, -1) : publicUrl,
|
|
72
|
+
acl,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// Get storage methods
|
|
76
|
+
const storageMethods = getStorageMethods(fastify, config);
|
|
77
|
+
|
|
78
|
+
// Decorate Fastify with xStorage namespace
|
|
79
|
+
fastify.decorate("xStorage", storageMethods);
|
|
80
|
+
|
|
81
|
+
console.info("\n 📦 xStorage Ready!\n");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export default fp(xStoragePlugin, {
|
|
85
|
+
name: "xStorage",
|
|
86
|
+
});
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
// test/storage.test.js
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import assert from "node:assert";
|
|
4
|
+
import Fastify from "fastify";
|
|
5
|
+
import xStorage from "../src/xStorage.js";
|
|
6
|
+
import { helpers } from "../src/index.js";
|
|
7
|
+
|
|
8
|
+
// Mock storage configuration
|
|
9
|
+
const mockStorageConfig = {
|
|
10
|
+
endpoint: process.env.STORAGE_ENDPOINT || "https://nyc3.digitaloceanspaces.com",
|
|
11
|
+
region: process.env.STORAGE_REGION || "nyc3",
|
|
12
|
+
accessKeyId: process.env.STORAGE_ACCESS_KEY_ID || "mock_access_key",
|
|
13
|
+
secretAccessKey: process.env.STORAGE_SECRET_ACCESS_KEY || "mock_secret_key",
|
|
14
|
+
bucket: process.env.STORAGE_BUCKET || "test-bucket",
|
|
15
|
+
publicUrl: process.env.STORAGE_PUBLIC_URL || "https://test-bucket.nyc3.digitaloceanspaces.com",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
test("Storage plugin - registers successfully", async () => {
|
|
19
|
+
const fastify = Fastify({ logger: false });
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
await fastify.register(xStorage, mockStorageConfig);
|
|
23
|
+
assert.ok(true, "Plugin registered successfully");
|
|
24
|
+
} finally {
|
|
25
|
+
await fastify.close();
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("Helper functions - file size formatting", () => {
|
|
30
|
+
assert.equal(helpers.formatFileSize(0), "0 Bytes");
|
|
31
|
+
assert.equal(helpers.formatFileSize(1024), "1 KB");
|
|
32
|
+
assert.equal(helpers.formatFileSize(1048576), "1 MB");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("Helper functions - file type detection", () => {
|
|
36
|
+
// isImage
|
|
37
|
+
assert.equal(helpers.isImage("photo.jpg"), true);
|
|
38
|
+
assert.equal(helpers.isImage("photo.png"), true);
|
|
39
|
+
assert.equal(helpers.isImage("photo.webp"), true);
|
|
40
|
+
assert.equal(helpers.isImage("document.pdf"), false);
|
|
41
|
+
|
|
42
|
+
// isPdf
|
|
43
|
+
assert.equal(helpers.isPdf("document.pdf"), true);
|
|
44
|
+
assert.equal(helpers.isPdf("photo.jpg"), false);
|
|
45
|
+
|
|
46
|
+
// isVideo
|
|
47
|
+
assert.equal(helpers.isVideo("movie.mp4"), true);
|
|
48
|
+
assert.equal(helpers.isVideo("photo.jpg"), false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("Helper functions - filename sanitization", () => {
|
|
52
|
+
// Test sanitization - the function replaces special chars with underscore
|
|
53
|
+
const result1 = helpers.sanitizeFilename("My File (2024).jpg");
|
|
54
|
+
assert.ok(result1.includes("my_file"), "should contain lowercase filename");
|
|
55
|
+
assert.ok(result1.endsWith(".jpg"), "should preserve extension");
|
|
56
|
+
|
|
57
|
+
const result2 = helpers.sanitizeFilename("Test@#$%.txt");
|
|
58
|
+
assert.ok(result2.startsWith("test"), "should be lowercase");
|
|
59
|
+
assert.ok(result2.endsWith(".txt"), "should preserve extension");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("Helper functions - file extension extraction", () => {
|
|
63
|
+
assert.equal(helpers.getFileExtension("photo.jpg"), "jpg");
|
|
64
|
+
assert.equal(helpers.getFileExtension("document.pdf"), "pdf");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("Helper functions - dimension calculations", () => {
|
|
68
|
+
const dimensions = helpers.calculateFitDimensions(4000, 3000, 1920, 1080);
|
|
69
|
+
assert.equal(dimensions.width, 1440);
|
|
70
|
+
assert.equal(dimensions.height, 1080);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("Helper functions - aspect ratio calculation", () => {
|
|
74
|
+
assert.equal(helpers.getAspectRatio(1920, 1080), "16:9");
|
|
75
|
+
assert.equal(helpers.getAspectRatio(1000, 1000), "1:1");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("Storage plugin - configuration with custom public URL", async () => {
|
|
79
|
+
const fastify = Fastify({ logger: false });
|
|
80
|
+
const customConfig = {
|
|
81
|
+
...mockStorageConfig,
|
|
82
|
+
publicUrl: "https://cdn.example.com",
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
await fastify.register(xStorage, customConfig);
|
|
87
|
+
assert.ok(true, "Plugin registered with custom public URL");
|
|
88
|
+
} finally {
|
|
89
|
+
await fastify.close();
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Generic File Storage Operations Tests
|
|
94
|
+
test("Generic file storage - upload operations available", async () => {
|
|
95
|
+
const fastify = Fastify({ logger: false });
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
await fastify.register(xStorage, mockStorageConfig);
|
|
99
|
+
|
|
100
|
+
// Verify core storage methods are available
|
|
101
|
+
assert.ok(fastify.xStorage, "xStorage namespace available");
|
|
102
|
+
assert.ok(fastify.xStorage.upload, "Upload method exists");
|
|
103
|
+
assert.ok(fastify.xStorage.download, "Download method exists");
|
|
104
|
+
assert.ok(fastify.xStorage.delete, "Delete method exists");
|
|
105
|
+
assert.ok(fastify.xStorage.list, "List method exists");
|
|
106
|
+
} finally {
|
|
107
|
+
await fastify.close();
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("Generic file storage - file operations", async () => {
|
|
112
|
+
const fastify = Fastify({ logger: false });
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
await fastify.register(xStorage, mockStorageConfig);
|
|
116
|
+
|
|
117
|
+
// Verify file operations are available
|
|
118
|
+
assert.ok(fastify.xStorage.copy, "Copy method exists");
|
|
119
|
+
assert.ok(fastify.xStorage.exists, "Exists check method exists");
|
|
120
|
+
assert.ok(fastify.xStorage.getMetadata, "Get metadata method exists");
|
|
121
|
+
} finally {
|
|
122
|
+
await fastify.close();
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("Generic file storage - URL operations", async () => {
|
|
127
|
+
const fastify = Fastify({ logger: false });
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
await fastify.register(xStorage, mockStorageConfig);
|
|
131
|
+
|
|
132
|
+
// Verify URL operations
|
|
133
|
+
assert.ok(fastify.xStorage.getSignedUrl, "Signed URL generation available");
|
|
134
|
+
assert.ok(fastify.xStorage.getPublicUrl, "Public URL generation available");
|
|
135
|
+
} finally {
|
|
136
|
+
await fastify.close();
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("Error handling - invalid credentials handling", async () => {
|
|
141
|
+
const fastify = Fastify({ logger: false });
|
|
142
|
+
const invalidConfig = {
|
|
143
|
+
...mockStorageConfig,
|
|
144
|
+
accessKeyId: "",
|
|
145
|
+
secretAccessKey: "",
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
await assert.rejects(
|
|
150
|
+
async () => {
|
|
151
|
+
await fastify.register(xStorage, invalidConfig);
|
|
152
|
+
},
|
|
153
|
+
/accessKeyId and secretAccessKey are required/
|
|
154
|
+
);
|
|
155
|
+
} finally {
|
|
156
|
+
try {
|
|
157
|
+
await fastify.close();
|
|
158
|
+
} catch {
|
|
159
|
+
// Ignore
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("Batch operations - concurrent uploads", async () => {
|
|
165
|
+
const fastify = Fastify({ logger: false });
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
await fastify.register(xStorage, mockStorageConfig);
|
|
169
|
+
|
|
170
|
+
// Verify concurrent upload support
|
|
171
|
+
assert.ok(fastify.xStorage.uploadMultiple, "Concurrent uploads supported");
|
|
172
|
+
} finally {
|
|
173
|
+
await fastify.close();
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("Batch operations - concurrent deletes", async () => {
|
|
178
|
+
const fastify = Fastify({ logger: false });
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
await fastify.register(xStorage, mockStorageConfig);
|
|
182
|
+
|
|
183
|
+
// Verify concurrent delete support
|
|
184
|
+
assert.ok(fastify.xStorage.deleteMultiple, "Concurrent deletes supported");
|
|
185
|
+
} finally {
|
|
186
|
+
await fastify.close();
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("Edge cases - handle special characters in filename", async () => {
|
|
191
|
+
const fastify = Fastify({ logger: false });
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
await fastify.register(xStorage, mockStorageConfig);
|
|
195
|
+
|
|
196
|
+
// Special character handling in filenames
|
|
197
|
+
const result = helpers.sanitizeFilename("Test@#$%^&*()_+=-[]{}|;:,.<>?/~`.jpg");
|
|
198
|
+
assert.ok(result.endsWith(".jpg"), "Special characters handled");
|
|
199
|
+
} finally {
|
|
200
|
+
await fastify.close();
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("Configuration - supports AWS S3", async () => {
|
|
205
|
+
const fastify = Fastify({ logger: false });
|
|
206
|
+
const awsConfig = {
|
|
207
|
+
region: "us-east-1",
|
|
208
|
+
accessKeyId: "mock_key",
|
|
209
|
+
secretAccessKey: "mock_secret",
|
|
210
|
+
bucket: "test-bucket",
|
|
211
|
+
publicUrl: "https://test-bucket.s3.amazonaws.com",
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
await fastify.register(xStorage, awsConfig);
|
|
216
|
+
assert.ok(true, "Plugin supports AWS S3 configuration");
|
|
217
|
+
} finally {
|
|
218
|
+
await fastify.close();
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("Configuration - supports Cloudflare R2", async () => {
|
|
223
|
+
const fastify = Fastify({ logger: false });
|
|
224
|
+
const r2Config = {
|
|
225
|
+
endpoint: "https://account-id.r2.cloudflarestorage.com",
|
|
226
|
+
accessKeyId: "mock_key",
|
|
227
|
+
secretAccessKey: "mock_secret",
|
|
228
|
+
bucket: "test-bucket",
|
|
229
|
+
publicUrl: "https://test-bucket.example.com",
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
await fastify.register(xStorage, r2Config);
|
|
234
|
+
assert.ok(true, "Plugin supports Cloudflare R2 configuration");
|
|
235
|
+
} finally {
|
|
236
|
+
await fastify.close();
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("Configuration - supports Digital Ocean Spaces", async () => {
|
|
241
|
+
const fastify = Fastify({ logger: false });
|
|
242
|
+
const dosConfig = {
|
|
243
|
+
endpoint: "https://nyc3.digitaloceanspaces.com",
|
|
244
|
+
region: "nyc3",
|
|
245
|
+
accessKeyId: "mock_key",
|
|
246
|
+
secretAccessKey: "mock_secret",
|
|
247
|
+
bucket: "test-bucket",
|
|
248
|
+
publicUrl: "https://test-bucket.nyc3.digitaloceanspaces.com",
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
try {
|
|
252
|
+
await fastify.register(xStorage, dosConfig);
|
|
253
|
+
assert.ok(true, "Plugin supports Digital Ocean Spaces configuration");
|
|
254
|
+
} finally {
|
|
255
|
+
await fastify.close();
|
|
256
|
+
}
|
|
257
|
+
});
|