@pelatform/storage 1.0.1
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/LICENSE +21 -0
- package/README.md +10 -0
- package/dist/chunk-KJMGBTTL.js +413 -0
- package/dist/chunk-ZTZZCC52.js +169 -0
- package/dist/cloudinary.d.ts +93 -0
- package/dist/cloudinary.js +496 -0
- package/dist/helpers.d.ts +762 -0
- package/dist/helpers.js +68 -0
- package/dist/index.d.ts +281 -0
- package/dist/index.js +24 -0
- package/dist/s3.d.ts +108 -0
- package/dist/s3.js +799 -0
- package/dist/storage-interface-UizTndyU.d.ts +316 -0
- package/package.json +78 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Pelatform
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# @pelatform/storage
|
|
2
|
+
|
|
3
|
+
A comprehensive storage utilities for SaaS applications. This package provides a unified interface for working with various storage providers including AWS S3, Cloudflare R2, MinIO, DigitalOcean Spaces, Supabase, and Cloudinary Storage.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@pelatform/storage)
|
|
6
|
+
[](https://github.com/devpelatform/storage/blob/main/LICENSE)
|
|
7
|
+
|
|
8
|
+
## Getting Started
|
|
9
|
+
|
|
10
|
+
Visit https://devpelatform.github.io/storage to get started with this package.
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
// src/helpers.ts
|
|
2
|
+
import { lookup } from "mime-types";
|
|
3
|
+
function generateFileKey(originalName, prefix) {
|
|
4
|
+
const timestamp = Date.now();
|
|
5
|
+
const random = Math.random().toString(36).substring(2, 8);
|
|
6
|
+
const extension = originalName.split(".").pop();
|
|
7
|
+
const baseName = originalName.split(".").slice(0, -1).join(".");
|
|
8
|
+
const fileName = `${baseName}-${timestamp}-${random}${extension ? `.${extension}` : ""}`;
|
|
9
|
+
return prefix ? `${prefix}/${fileName}` : fileName;
|
|
10
|
+
}
|
|
11
|
+
function getMimeType(fileName) {
|
|
12
|
+
const mimeType = lookup(fileName);
|
|
13
|
+
return mimeType || "application/octet-stream";
|
|
14
|
+
}
|
|
15
|
+
function validateFileSize(fileSize, maxSize) {
|
|
16
|
+
if (fileSize > maxSize) {
|
|
17
|
+
return {
|
|
18
|
+
valid: false,
|
|
19
|
+
error: `File size ${formatFileSize(fileSize)} exceeds maximum allowed size ${formatFileSize(maxSize)}`
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
return { valid: true };
|
|
23
|
+
}
|
|
24
|
+
function validateFileType(fileName, allowedTypes) {
|
|
25
|
+
const mimeType = getMimeType(fileName);
|
|
26
|
+
const extension = fileName.split(".").pop()?.toLowerCase();
|
|
27
|
+
const isValidMime = allowedTypes.some((type) => {
|
|
28
|
+
if (type.includes("*")) {
|
|
29
|
+
const baseType = type.split("/")[0];
|
|
30
|
+
return mimeType.startsWith(baseType);
|
|
31
|
+
}
|
|
32
|
+
return mimeType === type;
|
|
33
|
+
});
|
|
34
|
+
const isValidExtension = extension && allowedTypes.includes(`.${extension}`);
|
|
35
|
+
if (!isValidMime && !isValidExtension) {
|
|
36
|
+
return {
|
|
37
|
+
valid: false,
|
|
38
|
+
error: `File type not allowed. Allowed types: ${allowedTypes.join(", ")}`
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return { valid: true };
|
|
42
|
+
}
|
|
43
|
+
function formatFileSize(bytes) {
|
|
44
|
+
if (bytes === 0) return "0 Bytes";
|
|
45
|
+
const k = 1024;
|
|
46
|
+
const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
|
|
47
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
48
|
+
return `${parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`;
|
|
49
|
+
}
|
|
50
|
+
function sanitizeFileName(fileName) {
|
|
51
|
+
return fileName.replace(/[^a-zA-Z0-9.-]/g, "_").replace(/_{2,}/g, "_").replace(/^_|_$/g, "").toLowerCase();
|
|
52
|
+
}
|
|
53
|
+
function getFileExtension(fileName) {
|
|
54
|
+
const extension = fileName.split(".").pop();
|
|
55
|
+
return extension ? `.${extension.toLowerCase()}` : "";
|
|
56
|
+
}
|
|
57
|
+
function generateCacheControl(maxAge = 31536e3, isPublic = true) {
|
|
58
|
+
const visibility = isPublic ? "public" : "private";
|
|
59
|
+
return `${visibility}, max-age=${maxAge}`;
|
|
60
|
+
}
|
|
61
|
+
function parseS3Url(url) {
|
|
62
|
+
try {
|
|
63
|
+
const urlObj = new URL(url);
|
|
64
|
+
if (urlObj.hostname.includes("amazonaws.com")) {
|
|
65
|
+
if (urlObj.hostname.startsWith("s3")) {
|
|
66
|
+
const pathParts2 = urlObj.pathname.split("/").filter(Boolean);
|
|
67
|
+
return {
|
|
68
|
+
bucket: pathParts2[0],
|
|
69
|
+
key: pathParts2.slice(1).join("/")
|
|
70
|
+
};
|
|
71
|
+
} else {
|
|
72
|
+
const bucket = urlObj.hostname.split(".")[0];
|
|
73
|
+
const key = urlObj.pathname.substring(1);
|
|
74
|
+
return { bucket, key };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const pathParts = urlObj.pathname.split("/").filter(Boolean);
|
|
78
|
+
return {
|
|
79
|
+
bucket: pathParts[0],
|
|
80
|
+
key: pathParts.slice(1).join("/")
|
|
81
|
+
};
|
|
82
|
+
} catch {
|
|
83
|
+
return {};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function buildPublicUrl(baseUrl, bucket, key) {
|
|
87
|
+
const cleanBaseUrl = baseUrl.replace(/\/$/, "");
|
|
88
|
+
const cleanKey = key.replace(/^\//, "");
|
|
89
|
+
return `${cleanBaseUrl}/${bucket}/${cleanKey}`;
|
|
90
|
+
}
|
|
91
|
+
function validateS3Key(key) {
|
|
92
|
+
if (!key || key.length === 0) {
|
|
93
|
+
return { valid: false, error: "Key cannot be empty" };
|
|
94
|
+
}
|
|
95
|
+
if (key.length > 1024) {
|
|
96
|
+
return { valid: false, error: "Key cannot exceed 1024 characters" };
|
|
97
|
+
}
|
|
98
|
+
if (key.startsWith("/") || key.endsWith("/")) {
|
|
99
|
+
return {
|
|
100
|
+
valid: false,
|
|
101
|
+
error: "Key cannot start or end with forward slash"
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
const invalidChars = /[^\w\-_./]/;
|
|
105
|
+
if (invalidChars.test(key)) {
|
|
106
|
+
return { valid: false, error: "Key contains invalid characters" };
|
|
107
|
+
}
|
|
108
|
+
return { valid: true };
|
|
109
|
+
}
|
|
110
|
+
async function fileToBuffer(file) {
|
|
111
|
+
const arrayBuffer = await file.arrayBuffer();
|
|
112
|
+
return Buffer.from(arrayBuffer);
|
|
113
|
+
}
|
|
114
|
+
function getFileInfo(file) {
|
|
115
|
+
return {
|
|
116
|
+
name: file.name,
|
|
117
|
+
size: file.size,
|
|
118
|
+
type: file.type || getMimeType(file.name),
|
|
119
|
+
lastModified: new Date(file.lastModified)
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function generateUniqueKey(fileName, prefix, includeTimestamp = true) {
|
|
123
|
+
const sanitized = sanitizeFileName(fileName);
|
|
124
|
+
const extension = getFileExtension(sanitized);
|
|
125
|
+
const baseName = sanitized.replace(extension, "");
|
|
126
|
+
let uniquePart = "";
|
|
127
|
+
if (includeTimestamp) {
|
|
128
|
+
const timestamp = Date.now();
|
|
129
|
+
const random = Math.random().toString(36).substring(2, 8);
|
|
130
|
+
uniquePart = `-${timestamp}-${random}`;
|
|
131
|
+
}
|
|
132
|
+
const finalName = `${baseName}${uniquePart}${extension}`;
|
|
133
|
+
return prefix ? `${prefix}/${finalName}` : finalName;
|
|
134
|
+
}
|
|
135
|
+
function extractS3Info(url) {
|
|
136
|
+
try {
|
|
137
|
+
const urlObj = new URL(url);
|
|
138
|
+
if (urlObj.hostname.includes("amazonaws.com")) {
|
|
139
|
+
if (urlObj.hostname.startsWith("s3")) {
|
|
140
|
+
const pathParts2 = urlObj.pathname.split("/").filter(Boolean);
|
|
141
|
+
const region = urlObj.hostname.split(".")[1];
|
|
142
|
+
return {
|
|
143
|
+
bucket: pathParts2[0],
|
|
144
|
+
key: pathParts2.slice(1).join("/"),
|
|
145
|
+
region
|
|
146
|
+
};
|
|
147
|
+
} else {
|
|
148
|
+
const hostParts = urlObj.hostname.split(".");
|
|
149
|
+
const bucket = hostParts[0];
|
|
150
|
+
const region = hostParts[2];
|
|
151
|
+
const key = urlObj.pathname.substring(1);
|
|
152
|
+
return { bucket, key, region };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const pathParts = urlObj.pathname.split("/").filter(Boolean);
|
|
156
|
+
return {
|
|
157
|
+
bucket: pathParts[0],
|
|
158
|
+
key: pathParts.slice(1).join("/")
|
|
159
|
+
};
|
|
160
|
+
} catch {
|
|
161
|
+
return {};
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function validateS3Config(config) {
|
|
165
|
+
const errors = [];
|
|
166
|
+
if (!config.provider) {
|
|
167
|
+
errors.push("Provider is required");
|
|
168
|
+
}
|
|
169
|
+
if (!config.region) {
|
|
170
|
+
errors.push("Region is required");
|
|
171
|
+
}
|
|
172
|
+
if (!config.bucket) {
|
|
173
|
+
errors.push("Bucket is required");
|
|
174
|
+
}
|
|
175
|
+
if (!config.accessKeyId) {
|
|
176
|
+
errors.push("Access Key ID is required");
|
|
177
|
+
}
|
|
178
|
+
if (!config.secretAccessKey) {
|
|
179
|
+
errors.push("Secret Access Key is required");
|
|
180
|
+
}
|
|
181
|
+
if (config.bucket && typeof config.bucket === "string") {
|
|
182
|
+
const bucketValidation = validateBucketName(config.bucket);
|
|
183
|
+
if (!bucketValidation.valid) {
|
|
184
|
+
errors.push(`Invalid bucket name: ${bucketValidation.error}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
valid: errors.length === 0,
|
|
189
|
+
errors
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function validateBucketName(bucketName) {
|
|
193
|
+
if (!bucketName || bucketName.length === 0) {
|
|
194
|
+
return { valid: false, error: "Bucket name cannot be empty" };
|
|
195
|
+
}
|
|
196
|
+
if (bucketName.length < 3 || bucketName.length > 63) {
|
|
197
|
+
return {
|
|
198
|
+
valid: false,
|
|
199
|
+
error: "Bucket name must be between 3 and 63 characters"
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
if (!/^[a-z0-9.-]+$/.test(bucketName)) {
|
|
203
|
+
return {
|
|
204
|
+
valid: false,
|
|
205
|
+
error: "Bucket name can only contain lowercase letters, numbers, dots, and hyphens"
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
if (bucketName.startsWith(".") || bucketName.endsWith(".")) {
|
|
209
|
+
return {
|
|
210
|
+
valid: false,
|
|
211
|
+
error: "Bucket name cannot start or end with a dot"
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
if (bucketName.startsWith("-") || bucketName.endsWith("-")) {
|
|
215
|
+
return {
|
|
216
|
+
valid: false,
|
|
217
|
+
error: "Bucket name cannot start or end with a hyphen"
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
if (bucketName.includes("..")) {
|
|
221
|
+
return {
|
|
222
|
+
valid: false,
|
|
223
|
+
error: "Bucket name cannot contain consecutive dots"
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
return { valid: true };
|
|
227
|
+
}
|
|
228
|
+
function getContentDisposition(fileName, disposition = "attachment") {
|
|
229
|
+
const sanitizedName = fileName.replace(/[^\w\s.-]/gi, "");
|
|
230
|
+
return `${disposition}; filename="${sanitizedName}"`;
|
|
231
|
+
}
|
|
232
|
+
function isImageFile(fileName) {
|
|
233
|
+
const mimeType = getMimeType(fileName);
|
|
234
|
+
return mimeType.startsWith("image/");
|
|
235
|
+
}
|
|
236
|
+
function isVideoFile(fileName) {
|
|
237
|
+
const mimeType = getMimeType(fileName);
|
|
238
|
+
return mimeType.startsWith("video/");
|
|
239
|
+
}
|
|
240
|
+
function isAudioFile(fileName) {
|
|
241
|
+
const mimeType = getMimeType(fileName);
|
|
242
|
+
return mimeType.startsWith("audio/");
|
|
243
|
+
}
|
|
244
|
+
function isDocumentFile(fileName) {
|
|
245
|
+
const mimeType = getMimeType(fileName);
|
|
246
|
+
const documentTypes = [
|
|
247
|
+
"application/pdf",
|
|
248
|
+
"application/msword",
|
|
249
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
250
|
+
"application/vnd.ms-excel",
|
|
251
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
252
|
+
"application/vnd.ms-powerpoint",
|
|
253
|
+
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
254
|
+
"text/plain",
|
|
255
|
+
"text/csv"
|
|
256
|
+
];
|
|
257
|
+
return documentTypes.includes(mimeType);
|
|
258
|
+
}
|
|
259
|
+
function normalizePath(path) {
|
|
260
|
+
return path.replace(/\\/g, "/").replace(/\/+/g, "/").replace(/^\/|\/$/g, "");
|
|
261
|
+
}
|
|
262
|
+
function joinPath(...segments) {
|
|
263
|
+
return segments.filter((segment) => segment && segment.trim() !== "").map((segment) => segment.replace(/^\/+|\/+$/g, "")).filter((segment) => segment !== "").join("/");
|
|
264
|
+
}
|
|
265
|
+
function getParentPath(key) {
|
|
266
|
+
const normalizedKey = normalizePath(key);
|
|
267
|
+
const lastSlashIndex = normalizedKey.lastIndexOf("/");
|
|
268
|
+
return lastSlashIndex === -1 ? "" : normalizedKey.substring(0, lastSlashIndex);
|
|
269
|
+
}
|
|
270
|
+
function getFileName(key) {
|
|
271
|
+
const normalizedKey = normalizePath(key);
|
|
272
|
+
const lastSlashIndex = normalizedKey.lastIndexOf("/");
|
|
273
|
+
return lastSlashIndex === -1 ? normalizedKey : normalizedKey.substring(lastSlashIndex + 1);
|
|
274
|
+
}
|
|
275
|
+
function base64ToBuffer(base64) {
|
|
276
|
+
const cleanBase64 = base64.includes(",") ? base64.split(",")[1] : base64;
|
|
277
|
+
return Buffer.from(cleanBase64, "base64");
|
|
278
|
+
}
|
|
279
|
+
function bufferToBase64(buffer, includeDataUrl = false, mimeType) {
|
|
280
|
+
const base64 = buffer.toString("base64");
|
|
281
|
+
if (includeDataUrl) {
|
|
282
|
+
if (!mimeType) {
|
|
283
|
+
throw new Error("MIME type is required when includeDataUrl is true");
|
|
284
|
+
}
|
|
285
|
+
return `data:${mimeType};base64,${base64}`;
|
|
286
|
+
}
|
|
287
|
+
return base64;
|
|
288
|
+
}
|
|
289
|
+
async function generateFileHash(content, algorithm = "md5") {
|
|
290
|
+
const crypto = await import("crypto");
|
|
291
|
+
const hash = crypto.createHash(algorithm);
|
|
292
|
+
hash.update(content);
|
|
293
|
+
return hash.digest("hex");
|
|
294
|
+
}
|
|
295
|
+
function generateBatchKeys(fileNames, prefix) {
|
|
296
|
+
return fileNames.map((fileName) => generateFileKey(fileName, prefix));
|
|
297
|
+
}
|
|
298
|
+
function validateBatchFiles(files, options) {
|
|
299
|
+
const results = [];
|
|
300
|
+
if (options.maxFiles && files.length > options.maxFiles) {
|
|
301
|
+
return files.map((file) => ({
|
|
302
|
+
valid: false,
|
|
303
|
+
fileName: file.name,
|
|
304
|
+
errors: [`Maximum ${options.maxFiles} files allowed, but ${files.length} files provided`]
|
|
305
|
+
}));
|
|
306
|
+
}
|
|
307
|
+
for (const file of files) {
|
|
308
|
+
const errors = [];
|
|
309
|
+
if (options.maxSize) {
|
|
310
|
+
const sizeResult = validateFileSize(file.size, options.maxSize);
|
|
311
|
+
if (!sizeResult.valid && sizeResult.error) {
|
|
312
|
+
errors.push(sizeResult.error);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
if (options.allowedTypes) {
|
|
316
|
+
const typeResult = validateFileType(file.name, options.allowedTypes);
|
|
317
|
+
if (!typeResult.valid && typeResult.error) {
|
|
318
|
+
errors.push(typeResult.error);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
results.push({
|
|
322
|
+
valid: errors.length === 0,
|
|
323
|
+
fileName: file.name,
|
|
324
|
+
errors: errors.length > 0 ? errors : void 0
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
return results;
|
|
328
|
+
}
|
|
329
|
+
function detectFileTypeFromContent(buffer) {
|
|
330
|
+
if (buffer.length === 0) {
|
|
331
|
+
return "application/octet-stream";
|
|
332
|
+
}
|
|
333
|
+
const header = buffer.subarray(0, 16);
|
|
334
|
+
if (header.subarray(0, 4).toString() === "%PDF") {
|
|
335
|
+
return "application/pdf";
|
|
336
|
+
}
|
|
337
|
+
if (header[0] === 255 && header[1] === 216 && header[2] === 255) {
|
|
338
|
+
return "image/jpeg";
|
|
339
|
+
}
|
|
340
|
+
if (header.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]))) {
|
|
341
|
+
return "image/png";
|
|
342
|
+
}
|
|
343
|
+
if (header.subarray(0, 6).toString() === "GIF87a" || header.subarray(0, 6).toString() === "GIF89a") {
|
|
344
|
+
return "image/gif";
|
|
345
|
+
}
|
|
346
|
+
if (header.subarray(0, 4).toString() === "RIFF" && header.subarray(8, 12).toString() === "WEBP") {
|
|
347
|
+
return "image/webp";
|
|
348
|
+
}
|
|
349
|
+
if (header[0] === 80 && header[1] === 75 && (header[2] === 3 || header[2] === 5)) {
|
|
350
|
+
const zipContent = buffer.toString("utf8", 0, Math.min(buffer.length, 1e3));
|
|
351
|
+
if (zipContent.includes("word/"))
|
|
352
|
+
return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
|
|
353
|
+
if (zipContent.includes("xl/"))
|
|
354
|
+
return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
|
|
355
|
+
if (zipContent.includes("ppt/"))
|
|
356
|
+
return "application/vnd.openxmlformats-officedocument.presentationml.presentation";
|
|
357
|
+
return "application/zip";
|
|
358
|
+
}
|
|
359
|
+
if (header.subarray(4, 8).toString() === "ftyp") {
|
|
360
|
+
return "video/mp4";
|
|
361
|
+
}
|
|
362
|
+
if (header[0] === 255 && (header[1] & 224) === 224 || header.subarray(0, 3).toString() === "ID3") {
|
|
363
|
+
return "audio/mpeg";
|
|
364
|
+
}
|
|
365
|
+
let isText = true;
|
|
366
|
+
const sampleSize = Math.min(buffer.length, 512);
|
|
367
|
+
for (let i = 0; i < sampleSize; i++) {
|
|
368
|
+
const byte = buffer[i];
|
|
369
|
+
if (byte === 0 || byte < 32 && byte !== 9 && byte !== 10 && byte !== 13) {
|
|
370
|
+
isText = false;
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
if (isText) {
|
|
375
|
+
return "text/plain";
|
|
376
|
+
}
|
|
377
|
+
return "application/octet-stream";
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export {
|
|
381
|
+
generateFileKey,
|
|
382
|
+
getMimeType,
|
|
383
|
+
validateFileSize,
|
|
384
|
+
validateFileType,
|
|
385
|
+
formatFileSize,
|
|
386
|
+
sanitizeFileName,
|
|
387
|
+
getFileExtension,
|
|
388
|
+
generateCacheControl,
|
|
389
|
+
parseS3Url,
|
|
390
|
+
buildPublicUrl,
|
|
391
|
+
validateS3Key,
|
|
392
|
+
fileToBuffer,
|
|
393
|
+
getFileInfo,
|
|
394
|
+
generateUniqueKey,
|
|
395
|
+
extractS3Info,
|
|
396
|
+
validateS3Config,
|
|
397
|
+
validateBucketName,
|
|
398
|
+
getContentDisposition,
|
|
399
|
+
isImageFile,
|
|
400
|
+
isVideoFile,
|
|
401
|
+
isAudioFile,
|
|
402
|
+
isDocumentFile,
|
|
403
|
+
normalizePath,
|
|
404
|
+
joinPath,
|
|
405
|
+
getParentPath,
|
|
406
|
+
getFileName,
|
|
407
|
+
base64ToBuffer,
|
|
408
|
+
bufferToBase64,
|
|
409
|
+
generateFileHash,
|
|
410
|
+
generateBatchKeys,
|
|
411
|
+
validateBatchFiles,
|
|
412
|
+
detectFileTypeFromContent
|
|
413
|
+
};
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// src/config.ts
|
|
2
|
+
var ENV_VARS = {
|
|
3
|
+
// S3 Configuration
|
|
4
|
+
PELATFORM_S3_PROVIDER: "PELATFORM_S3_PROVIDER",
|
|
5
|
+
PELATFORM_S3_REGION: "PELATFORM_S3_REGION",
|
|
6
|
+
PELATFORM_S3_BUCKET: "PELATFORM_S3_BUCKET",
|
|
7
|
+
PELATFORM_S3_ACCESS_KEY_ID: "PELATFORM_S3_ACCESS_KEY_ID",
|
|
8
|
+
PELATFORM_S3_SECRET_ACCESS_KEY: "PELATFORM_S3_SECRET_ACCESS_KEY",
|
|
9
|
+
PELATFORM_S3_ENDPOINT: "PELATFORM_S3_ENDPOINT",
|
|
10
|
+
PELATFORM_S3_FORCE_PATH_STYLE: "PELATFORM_S3_FORCE_PATH_STYLE",
|
|
11
|
+
PELATFORM_S3_PUBLIC_URL: "PELATFORM_S3_PUBLIC_URL",
|
|
12
|
+
// Cloudinary Configuration
|
|
13
|
+
PELATFORM_CLOUDINARY_CLOUD_NAME: "PELATFORM_CLOUDINARY_CLOUD_NAME",
|
|
14
|
+
PELATFORM_CLOUDINARY_API_KEY: "PELATFORM_CLOUDINARY_API_KEY",
|
|
15
|
+
PELATFORM_CLOUDINARY_API_SECRET: "PELATFORM_CLOUDINARY_API_SECRET",
|
|
16
|
+
PELATFORM_CLOUDINARY_SECURE: "PELATFORM_CLOUDINARY_SECURE",
|
|
17
|
+
PELATFORM_CLOUDINARY_FOLDER: "PELATFORM_CLOUDINARY_FOLDER"
|
|
18
|
+
};
|
|
19
|
+
function getEnvVar(key) {
|
|
20
|
+
if (typeof process !== "undefined" && process.env) {
|
|
21
|
+
return process.env[key];
|
|
22
|
+
}
|
|
23
|
+
return void 0;
|
|
24
|
+
}
|
|
25
|
+
function getRequiredEnvVar(key) {
|
|
26
|
+
const value = getEnvVar(key);
|
|
27
|
+
if (!value) {
|
|
28
|
+
throw new Error(`Missing required environment variable: ${key}`);
|
|
29
|
+
}
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
function getBooleanEnvVar(key, defaultValue = false) {
|
|
33
|
+
const value = getEnvVar(key);
|
|
34
|
+
if (!value) return defaultValue;
|
|
35
|
+
return value.toLowerCase() === "true" || value === "1";
|
|
36
|
+
}
|
|
37
|
+
function loadS3Config() {
|
|
38
|
+
const provider = getRequiredEnvVar(ENV_VARS.PELATFORM_S3_PROVIDER);
|
|
39
|
+
const validProviders = [
|
|
40
|
+
"aws",
|
|
41
|
+
"cloudflare-r2",
|
|
42
|
+
"minio",
|
|
43
|
+
"digitalocean",
|
|
44
|
+
"supabase",
|
|
45
|
+
"custom"
|
|
46
|
+
];
|
|
47
|
+
if (!validProviders.includes(provider)) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`Invalid S3 provider: ${provider}. Must be one of: ${validProviders.join(", ")}`
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
provider,
|
|
54
|
+
region: getRequiredEnvVar(ENV_VARS.PELATFORM_S3_REGION),
|
|
55
|
+
bucket: getRequiredEnvVar(ENV_VARS.PELATFORM_S3_BUCKET),
|
|
56
|
+
accessKeyId: getRequiredEnvVar(ENV_VARS.PELATFORM_S3_ACCESS_KEY_ID),
|
|
57
|
+
secretAccessKey: getRequiredEnvVar(ENV_VARS.PELATFORM_S3_SECRET_ACCESS_KEY),
|
|
58
|
+
endpoint: getEnvVar(ENV_VARS.PELATFORM_S3_ENDPOINT),
|
|
59
|
+
forcePathStyle: getBooleanEnvVar(ENV_VARS.PELATFORM_S3_FORCE_PATH_STYLE),
|
|
60
|
+
publicUrl: getEnvVar(ENV_VARS.PELATFORM_S3_PUBLIC_URL)
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function loadCloudinaryConfig() {
|
|
64
|
+
return {
|
|
65
|
+
provider: "cloudinary",
|
|
66
|
+
cloudName: getRequiredEnvVar(ENV_VARS.PELATFORM_CLOUDINARY_CLOUD_NAME),
|
|
67
|
+
apiKey: getRequiredEnvVar(ENV_VARS.PELATFORM_CLOUDINARY_API_KEY),
|
|
68
|
+
apiSecret: getRequiredEnvVar(ENV_VARS.PELATFORM_CLOUDINARY_API_SECRET),
|
|
69
|
+
secure: getBooleanEnvVar(ENV_VARS.PELATFORM_CLOUDINARY_SECURE, true),
|
|
70
|
+
folder: getEnvVar(ENV_VARS.PELATFORM_CLOUDINARY_FOLDER)
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function loadStorageConfig() {
|
|
74
|
+
const provider = getEnvVar(ENV_VARS.PELATFORM_S3_PROVIDER);
|
|
75
|
+
if (provider) {
|
|
76
|
+
if (provider === "cloudinary") {
|
|
77
|
+
return loadCloudinaryConfig();
|
|
78
|
+
} else {
|
|
79
|
+
return loadS3Config();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const hasCloudinaryVars = getEnvVar(ENV_VARS.PELATFORM_CLOUDINARY_CLOUD_NAME) && getEnvVar(ENV_VARS.PELATFORM_CLOUDINARY_API_KEY) && getEnvVar(ENV_VARS.PELATFORM_CLOUDINARY_API_SECRET);
|
|
83
|
+
const hasS3Vars = getEnvVar(ENV_VARS.PELATFORM_S3_BUCKET) && getEnvVar(ENV_VARS.PELATFORM_S3_ACCESS_KEY_ID) && getEnvVar(ENV_VARS.PELATFORM_S3_SECRET_ACCESS_KEY);
|
|
84
|
+
if (hasCloudinaryVars) {
|
|
85
|
+
return loadCloudinaryConfig();
|
|
86
|
+
} else if (hasS3Vars) {
|
|
87
|
+
const config = loadS3Config();
|
|
88
|
+
if (!config.provider) {
|
|
89
|
+
config.provider = "aws";
|
|
90
|
+
}
|
|
91
|
+
return config;
|
|
92
|
+
}
|
|
93
|
+
throw new Error(
|
|
94
|
+
"No storage configuration found. Please set either:\n- S3: PELATFORM_S3_PROVIDER, PELATFORM_S3_REGION, PELATFORM_S3_BUCKET, PELATFORM_S3_ACCESS_KEY_ID, PELATFORM_S3_SECRET_ACCESS_KEY\n- Cloudinary: PELATFORM_CLOUDINARY_CLOUD_NAME, PELATFORM_CLOUDINARY_API_KEY, PELATFORM_CLOUDINARY_API_SECRET"
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
function hasStorageConfig() {
|
|
98
|
+
try {
|
|
99
|
+
loadStorageConfig();
|
|
100
|
+
return true;
|
|
101
|
+
} catch {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function isStorageConfigured() {
|
|
106
|
+
return hasStorageConfig();
|
|
107
|
+
}
|
|
108
|
+
function getStorageProvider() {
|
|
109
|
+
const provider = getEnvVar(ENV_VARS.PELATFORM_S3_PROVIDER);
|
|
110
|
+
if (provider) return provider;
|
|
111
|
+
const hasCloudinaryVars = getEnvVar(ENV_VARS.PELATFORM_CLOUDINARY_CLOUD_NAME);
|
|
112
|
+
const hasS3Vars = getEnvVar(ENV_VARS.PELATFORM_S3_BUCKET);
|
|
113
|
+
if (hasCloudinaryVars) return "cloudinary";
|
|
114
|
+
if (hasS3Vars) return "aws";
|
|
115
|
+
return void 0;
|
|
116
|
+
}
|
|
117
|
+
function validateS3EnvVars() {
|
|
118
|
+
const required = [
|
|
119
|
+
ENV_VARS.PELATFORM_S3_PROVIDER,
|
|
120
|
+
ENV_VARS.PELATFORM_S3_REGION,
|
|
121
|
+
ENV_VARS.PELATFORM_S3_BUCKET,
|
|
122
|
+
ENV_VARS.PELATFORM_S3_ACCESS_KEY_ID,
|
|
123
|
+
ENV_VARS.PELATFORM_S3_SECRET_ACCESS_KEY
|
|
124
|
+
];
|
|
125
|
+
const missing = required.filter((key) => !getEnvVar(key));
|
|
126
|
+
return {
|
|
127
|
+
valid: missing.length === 0,
|
|
128
|
+
missing
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function validateCloudinaryEnvVars() {
|
|
132
|
+
const required = [
|
|
133
|
+
ENV_VARS.PELATFORM_CLOUDINARY_CLOUD_NAME,
|
|
134
|
+
ENV_VARS.PELATFORM_CLOUDINARY_API_KEY,
|
|
135
|
+
ENV_VARS.PELATFORM_CLOUDINARY_API_SECRET
|
|
136
|
+
];
|
|
137
|
+
const missing = required.filter((key) => !getEnvVar(key));
|
|
138
|
+
return {
|
|
139
|
+
valid: missing.length === 0,
|
|
140
|
+
missing
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function getStorageEnvVars() {
|
|
144
|
+
const result = {};
|
|
145
|
+
Object.values(ENV_VARS).forEach((key) => {
|
|
146
|
+
const value = getEnvVar(key);
|
|
147
|
+
if (value) {
|
|
148
|
+
if (key.includes("SECRET") || key.includes("API_SECRET")) {
|
|
149
|
+
result[key] = `${value.substring(0, 4)}***`;
|
|
150
|
+
} else {
|
|
151
|
+
result[key] = value;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export {
|
|
159
|
+
ENV_VARS,
|
|
160
|
+
loadS3Config,
|
|
161
|
+
loadCloudinaryConfig,
|
|
162
|
+
loadStorageConfig,
|
|
163
|
+
hasStorageConfig,
|
|
164
|
+
isStorageConfigured,
|
|
165
|
+
getStorageProvider,
|
|
166
|
+
validateS3EnvVars,
|
|
167
|
+
validateCloudinaryEnvVars,
|
|
168
|
+
getStorageEnvVars
|
|
169
|
+
};
|