@intranefr/superbackend 1.4.3
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/.commiat +4 -0
- package/.env.example +47 -0
- package/README.md +110 -0
- package/index.js +94 -0
- package/package.json +67 -0
- package/public/css/styles.css +139 -0
- package/public/js/animations.js +41 -0
- package/sdk/error-tracking/browser/package.json +16 -0
- package/sdk/error-tracking/browser/src/core.js +270 -0
- package/sdk/error-tracking/browser/src/embed.js +18 -0
- package/sdk/error-tracking/browser/src/index.js +1 -0
- package/server.js +5 -0
- package/src/admin/endpointRegistry.js +300 -0
- package/src/controllers/admin.controller.js +321 -0
- package/src/controllers/adminAssets.controller.js +530 -0
- package/src/controllers/adminAssetsStorage.controller.js +260 -0
- package/src/controllers/adminEjsVirtual.controller.js +354 -0
- package/src/controllers/adminFeatureFlags.controller.js +155 -0
- package/src/controllers/adminHeadless.controller.js +1071 -0
- package/src/controllers/adminI18n.controller.js +604 -0
- package/src/controllers/adminJsonConfigs.controller.js +97 -0
- package/src/controllers/adminLlm.controller.js +273 -0
- package/src/controllers/adminMigration.controller.js +257 -0
- package/src/controllers/adminSeoConfig.controller.js +515 -0
- package/src/controllers/adminStats.controller.js +121 -0
- package/src/controllers/adminUploadNamespaces.controller.js +208 -0
- package/src/controllers/assets.controller.js +248 -0
- package/src/controllers/auth.controller.js +93 -0
- package/src/controllers/billing.controller.js +223 -0
- package/src/controllers/featureFlags.controller.js +35 -0
- package/src/controllers/forms.controller.js +217 -0
- package/src/controllers/globalSettings.controller.js +252 -0
- package/src/controllers/headlessCrud.controller.js +126 -0
- package/src/controllers/i18n.controller.js +12 -0
- package/src/controllers/invite.controller.js +249 -0
- package/src/controllers/jsonConfigs.controller.js +19 -0
- package/src/controllers/metrics.controller.js +149 -0
- package/src/controllers/notificationAdmin.controller.js +264 -0
- package/src/controllers/notifications.controller.js +131 -0
- package/src/controllers/org.controller.js +357 -0
- package/src/controllers/orgAdmin.controller.js +491 -0
- package/src/controllers/stripeAdmin.controller.js +410 -0
- package/src/controllers/user.controller.js +361 -0
- package/src/controllers/userAdmin.controller.js +277 -0
- package/src/controllers/waitingList.controller.js +167 -0
- package/src/controllers/webhook.controller.js +200 -0
- package/src/middleware/auth.js +66 -0
- package/src/middleware/errorCapture.js +170 -0
- package/src/middleware/headlessApiTokenAuth.js +57 -0
- package/src/middleware/org.js +108 -0
- package/src/middleware.js +901 -0
- package/src/models/ActionEvent.js +31 -0
- package/src/models/ActivityLog.js +41 -0
- package/src/models/Asset.js +84 -0
- package/src/models/AuditEvent.js +93 -0
- package/src/models/EmailLog.js +28 -0
- package/src/models/ErrorAggregate.js +72 -0
- package/src/models/FormSubmission.js +41 -0
- package/src/models/GlobalSetting.js +38 -0
- package/src/models/HeadlessApiToken.js +24 -0
- package/src/models/HeadlessModelDefinition.js +41 -0
- package/src/models/I18nEntry.js +77 -0
- package/src/models/I18nLocale.js +33 -0
- package/src/models/Invite.js +70 -0
- package/src/models/JsonConfig.js +46 -0
- package/src/models/Notification.js +60 -0
- package/src/models/Organization.js +57 -0
- package/src/models/OrganizationMember.js +43 -0
- package/src/models/StripeCatalogItem.js +77 -0
- package/src/models/StripeWebhookEvent.js +57 -0
- package/src/models/User.js +89 -0
- package/src/models/VirtualEjsFile.js +60 -0
- package/src/models/VirtualEjsFileVersion.js +43 -0
- package/src/models/VirtualEjsGroupChange.js +32 -0
- package/src/models/WaitingList.js +41 -0
- package/src/models/Webhook.js +63 -0
- package/src/models/Workflow.js +29 -0
- package/src/models/WorkflowExecution.js +12 -0
- package/src/routes/admin.routes.js +26 -0
- package/src/routes/adminAssets.routes.js +28 -0
- package/src/routes/adminAssetsStorage.routes.js +13 -0
- package/src/routes/adminAudit.routes.js +196 -0
- package/src/routes/adminEjsVirtual.routes.js +17 -0
- package/src/routes/adminErrors.routes.js +164 -0
- package/src/routes/adminFeatureFlags.routes.js +12 -0
- package/src/routes/adminHeadless.routes.js +38 -0
- package/src/routes/adminI18n.routes.js +22 -0
- package/src/routes/adminJsonConfigs.routes.js +15 -0
- package/src/routes/adminLlm.routes.js +12 -0
- package/src/routes/adminMigration.routes.js +81 -0
- package/src/routes/adminSeoConfig.routes.js +20 -0
- package/src/routes/adminUploadNamespaces.routes.js +13 -0
- package/src/routes/assets.routes.js +21 -0
- package/src/routes/auth.routes.js +12 -0
- package/src/routes/billing.routes.js +11 -0
- package/src/routes/errorTracking.routes.js +31 -0
- package/src/routes/featureFlags.routes.js +9 -0
- package/src/routes/forms.routes.js +9 -0
- package/src/routes/formsAdmin.routes.js +13 -0
- package/src/routes/globalSettings.routes.js +18 -0
- package/src/routes/headless.routes.js +15 -0
- package/src/routes/i18n.routes.js +8 -0
- package/src/routes/invite.routes.js +9 -0
- package/src/routes/jsonConfigs.routes.js +8 -0
- package/src/routes/log.routes.js +111 -0
- package/src/routes/metrics.routes.js +9 -0
- package/src/routes/notificationAdmin.routes.js +15 -0
- package/src/routes/notifications.routes.js +12 -0
- package/src/routes/org.routes.js +31 -0
- package/src/routes/orgAdmin.routes.js +20 -0
- package/src/routes/publicAssets.routes.js +7 -0
- package/src/routes/stripeAdmin.routes.js +20 -0
- package/src/routes/user.routes.js +22 -0
- package/src/routes/userAdmin.routes.js +15 -0
- package/src/routes/waitingList.routes.js +13 -0
- package/src/routes/waitingListAdmin.routes.js +9 -0
- package/src/routes/webhook.routes.js +32 -0
- package/src/routes/workflowWebhook.routes.js +54 -0
- package/src/routes/workflows.routes.js +110 -0
- package/src/services/assets.service.js +110 -0
- package/src/services/audit.service.js +62 -0
- package/src/services/auditLogger.js +165 -0
- package/src/services/ejsVirtual.service.js +614 -0
- package/src/services/email.service.js +351 -0
- package/src/services/errorLogger.js +221 -0
- package/src/services/featureFlags.service.js +202 -0
- package/src/services/forms.service.js +214 -0
- package/src/services/globalSettings.service.js +49 -0
- package/src/services/headlessApiTokens.service.js +158 -0
- package/src/services/headlessCrypto.service.js +31 -0
- package/src/services/headlessModels.service.js +356 -0
- package/src/services/i18n.service.js +314 -0
- package/src/services/i18nInferredKeys.service.js +337 -0
- package/src/services/jsonConfigs.service.js +392 -0
- package/src/services/llm.service.js +749 -0
- package/src/services/migration.service.js +581 -0
- package/src/services/migrationAssets/fsLocal.js +58 -0
- package/src/services/migrationAssets/index.js +134 -0
- package/src/services/migrationAssets/s3.js +75 -0
- package/src/services/migrationAssets/sftp.js +92 -0
- package/src/services/notification.service.js +212 -0
- package/src/services/objectStorage.service.js +514 -0
- package/src/services/seoConfig.service.js +402 -0
- package/src/services/storage.js +150 -0
- package/src/services/stripe.service.js +185 -0
- package/src/services/stripeHelper.service.js +264 -0
- package/src/services/uploadNamespaces.service.js +326 -0
- package/src/services/webhook.service.js +157 -0
- package/src/services/workflow.service.js +271 -0
- package/src/utils/asyncHandler.js +5 -0
- package/src/utils/encryption.js +80 -0
- package/src/utils/jwt.js +40 -0
- package/src/utils/orgRoles.js +156 -0
- package/src/utils/validation.js +26 -0
- package/src/utils/webhookRetry.js +93 -0
- package/views/admin-assets.ejs +444 -0
- package/views/admin-audit.ejs +283 -0
- package/views/admin-coolify-deploy.ejs +207 -0
- package/views/admin-dashboard-home.ejs +291 -0
- package/views/admin-dashboard.ejs +397 -0
- package/views/admin-ejs-virtual.ejs +280 -0
- package/views/admin-errors.ejs +368 -0
- package/views/admin-feature-flags.ejs +390 -0
- package/views/admin-forms.ejs +526 -0
- package/views/admin-global-settings.ejs +436 -0
- package/views/admin-headless.ejs +2020 -0
- package/views/admin-i18n-locales.ejs +221 -0
- package/views/admin-i18n.ejs +728 -0
- package/views/admin-json-configs.ejs +410 -0
- package/views/admin-llm.ejs +884 -0
- package/views/admin-metrics.ejs +274 -0
- package/views/admin-migration.ejs +814 -0
- package/views/admin-notifications.ejs +430 -0
- package/views/admin-organizations.ejs +984 -0
- package/views/admin-seo-config.ejs +673 -0
- package/views/admin-stripe-pricing.ejs +558 -0
- package/views/admin-test.ejs +342 -0
- package/views/admin-users.ejs +452 -0
- package/views/admin-waiting-list.ejs +547 -0
- package/views/admin-webhooks.ejs +329 -0
- package/views/admin-workflows.ejs +310 -0
- package/views/partials/admin-assets-script.ejs +2022 -0
- package/views/partials/admin-test-sidebar.ejs +14 -0
- package/views/partials/dashboard/nav-items.ejs +66 -0
- package/views/partials/dashboard/palette.ejs +63 -0
- package/views/partials/dashboard/sidebar.ejs +21 -0
- package/views/partials/dashboard/tab-bar.ejs +26 -0
- package/views/partials/footer.ejs +3 -0
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
|
|
5
|
+
const globalSettingsService = require('./globalSettings.service');
|
|
6
|
+
|
|
7
|
+
let s3Client = null;
|
|
8
|
+
let s3Config = null;
|
|
9
|
+
|
|
10
|
+
let ensuredBucketCache = null;
|
|
11
|
+
|
|
12
|
+
let activeBackendCache = null;
|
|
13
|
+
|
|
14
|
+
const STORAGE_BACKEND_SETTING_KEY = 'STORAGE_BACKEND';
|
|
15
|
+
const STORAGE_S3_CONFIG_SETTING_KEY = 'STORAGE_S3_CONFIG';
|
|
16
|
+
|
|
17
|
+
const getEnvS3Config = () => {
|
|
18
|
+
const endpoint = process.env.S3_ENDPOINT;
|
|
19
|
+
const accessKeyId = process.env.S3_ACCESS_KEY_ID;
|
|
20
|
+
const secretAccessKey = process.env.S3_SECRET_ACCESS_KEY;
|
|
21
|
+
const bucket = process.env.S3_BUCKET;
|
|
22
|
+
|
|
23
|
+
if (!endpoint || !accessKeyId || !secretAccessKey || !bucket) return null;
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
endpoint,
|
|
27
|
+
region: process.env.S3_REGION || 'us-east-1',
|
|
28
|
+
accessKeyId,
|
|
29
|
+
secretAccessKey,
|
|
30
|
+
bucket,
|
|
31
|
+
forcePathStyle: process.env.S3_FORCE_PATH_STYLE === 'true'
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const validateS3Config = (cfg) => {
|
|
36
|
+
if (!cfg || typeof cfg !== 'object') return null;
|
|
37
|
+
|
|
38
|
+
const endpoint = String(cfg.endpoint || '').trim();
|
|
39
|
+
const region = String(cfg.region || 'us-east-1').trim() || 'us-east-1';
|
|
40
|
+
const accessKeyId = String(cfg.accessKeyId || '').trim();
|
|
41
|
+
const secretAccessKey = String(cfg.secretAccessKey || '').trim();
|
|
42
|
+
const bucket = String(cfg.bucket || '').trim();
|
|
43
|
+
|
|
44
|
+
if (!endpoint || !accessKeyId || !secretAccessKey || !bucket) return null;
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
endpoint,
|
|
48
|
+
region,
|
|
49
|
+
accessKeyId,
|
|
50
|
+
secretAccessKey,
|
|
51
|
+
bucket,
|
|
52
|
+
forcePathStyle: Boolean(cfg.forcePathStyle),
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const getS3Config = async () => {
|
|
57
|
+
if (s3Config !== null) return s3Config;
|
|
58
|
+
|
|
59
|
+
const raw = await globalSettingsService.getSettingValue(STORAGE_S3_CONFIG_SETTING_KEY, null);
|
|
60
|
+
if (raw) {
|
|
61
|
+
try {
|
|
62
|
+
const parsed = JSON.parse(String(raw));
|
|
63
|
+
const validated = validateS3Config(parsed);
|
|
64
|
+
s3Config = validated || false;
|
|
65
|
+
return s3Config;
|
|
66
|
+
} catch (e) {
|
|
67
|
+
s3Config = false;
|
|
68
|
+
return s3Config;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const envCfg = getEnvS3Config();
|
|
73
|
+
s3Config = envCfg ? validateS3Config(envCfg) : false;
|
|
74
|
+
return s3Config;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const getS3Client = async () => {
|
|
78
|
+
const config = await getS3Config();
|
|
79
|
+
if (!config) return null;
|
|
80
|
+
|
|
81
|
+
if (s3Client) return s3Client;
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const { S3Client } = await import('@aws-sdk/client-s3');
|
|
85
|
+
s3Client = new S3Client({
|
|
86
|
+
endpoint: config.endpoint,
|
|
87
|
+
region: config.region,
|
|
88
|
+
credentials: {
|
|
89
|
+
accessKeyId: config.accessKeyId,
|
|
90
|
+
secretAccessKey: config.secretAccessKey
|
|
91
|
+
},
|
|
92
|
+
forcePathStyle: config.forcePathStyle
|
|
93
|
+
});
|
|
94
|
+
return s3Client;
|
|
95
|
+
} catch (err) {
|
|
96
|
+
console.warn('⚠️ @aws-sdk/client-s3 not installed, falling back to filesystem storage');
|
|
97
|
+
s3Config = false;
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const getActiveBackend = async () => {
|
|
103
|
+
if (activeBackendCache) return activeBackendCache;
|
|
104
|
+
const fromSetting = await globalSettingsService.getSettingValue(STORAGE_BACKEND_SETTING_KEY, null);
|
|
105
|
+
const normalized = String(fromSetting || '').trim().toLowerCase();
|
|
106
|
+
if (normalized === 'fs' || normalized === 's3') {
|
|
107
|
+
activeBackendCache = normalized;
|
|
108
|
+
return activeBackendCache;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const cfg = await getS3Config();
|
|
112
|
+
activeBackendCache = cfg ? 's3' : 'fs';
|
|
113
|
+
return activeBackendCache;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const clearStorageConfigCache = () => {
|
|
117
|
+
s3Client = null;
|
|
118
|
+
s3Config = null;
|
|
119
|
+
activeBackendCache = null;
|
|
120
|
+
ensuredBucketCache = null;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const ensureS3BucketExists = async () => {
|
|
124
|
+
const cfg = await getS3Config();
|
|
125
|
+
if (!cfg) {
|
|
126
|
+
const err = new Error('S3 is not configured');
|
|
127
|
+
err.code = 'S3_NOT_CONFIGURED';
|
|
128
|
+
throw err;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const cacheKey = `${cfg.endpoint}::${cfg.region}::${cfg.bucket}`;
|
|
132
|
+
if (ensuredBucketCache === cacheKey) {
|
|
133
|
+
return { ok: true, bucket: cfg.bucket, ensured: true };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const client = await getS3Client();
|
|
137
|
+
if (!client) {
|
|
138
|
+
const err = new Error('S3 client not available');
|
|
139
|
+
err.code = 'S3_CLIENT_NOT_AVAILABLE';
|
|
140
|
+
throw err;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const { HeadBucketCommand, CreateBucketCommand } = await import('@aws-sdk/client-s3');
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
await client.send(new HeadBucketCommand({ Bucket: cfg.bucket }));
|
|
147
|
+
ensuredBucketCache = cacheKey;
|
|
148
|
+
return { ok: true, bucket: cfg.bucket, ensured: false };
|
|
149
|
+
} catch (err) {
|
|
150
|
+
const status = err?.$metadata?.httpStatusCode;
|
|
151
|
+
const name = err?.name;
|
|
152
|
+
if (status !== 404 && name !== 'NotFound') {
|
|
153
|
+
throw err;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
await client.send(new CreateBucketCommand({ Bucket: cfg.bucket }));
|
|
158
|
+
} catch (createErr) {
|
|
159
|
+
const createName = createErr?.name;
|
|
160
|
+
const createCode = createErr?.Code || createErr?.code;
|
|
161
|
+
const createStatus = createErr?.$metadata?.httpStatusCode;
|
|
162
|
+
if (
|
|
163
|
+
createName === 'BucketAlreadyOwnedByYou' ||
|
|
164
|
+
createName === 'BucketAlreadyExists' ||
|
|
165
|
+
createCode === 'BucketAlreadyOwnedByYou' ||
|
|
166
|
+
createCode === 'BucketAlreadyExists' ||
|
|
167
|
+
createStatus === 409
|
|
168
|
+
) {
|
|
169
|
+
// ok
|
|
170
|
+
} else {
|
|
171
|
+
throw createErr;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
await client.send(new HeadBucketCommand({ Bucket: cfg.bucket }));
|
|
176
|
+
ensuredBucketCache = cacheKey;
|
|
177
|
+
return { ok: true, bucket: cfg.bucket, ensured: true };
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const isS3Enabled = async () => {
|
|
182
|
+
return (await getS3Config()) !== false;
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const getProvider = async () => {
|
|
186
|
+
return getActiveBackend();
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const getBucket = async () => {
|
|
190
|
+
const backend = await getActiveBackend();
|
|
191
|
+
if (backend !== 's3') return 'fs';
|
|
192
|
+
const cfg = await getS3Config();
|
|
193
|
+
return cfg ? cfg.bucket : 'fs';
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const getUploadDir = () => {
|
|
197
|
+
return process.env.UPLOAD_DIR || 'uploads';
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const getMaxFileSize = () => {
|
|
201
|
+
return parseInt(process.env.MAX_FILE_SIZE || '10485760', 10);
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const DEFAULT_ALLOWED_TYPES = [
|
|
205
|
+
'image/jpeg',
|
|
206
|
+
'image/png',
|
|
207
|
+
'image/gif',
|
|
208
|
+
'image/webp',
|
|
209
|
+
'image/svg+xml',
|
|
210
|
+
'application/pdf',
|
|
211
|
+
'video/mp4',
|
|
212
|
+
'video/webm'
|
|
213
|
+
];
|
|
214
|
+
|
|
215
|
+
const getAllowedContentTypes = () => {
|
|
216
|
+
const envTypes = process.env.ALLOWED_CONTENT_TYPES;
|
|
217
|
+
if (envTypes) {
|
|
218
|
+
return envTypes.split(',').map(t => t.trim()).filter(Boolean);
|
|
219
|
+
}
|
|
220
|
+
return DEFAULT_ALLOWED_TYPES;
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const validateContentType = (contentType) => {
|
|
224
|
+
const allowed = getAllowedContentTypes();
|
|
225
|
+
return allowed.includes(contentType);
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const validateFileSize = (sizeBytes) => {
|
|
229
|
+
return sizeBytes <= getMaxFileSize();
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const generateKey = (originalName, prefix = 'assets') => {
|
|
233
|
+
const ext = path.extname(originalName).toLowerCase();
|
|
234
|
+
const hash = crypto.randomBytes(12).toString('hex');
|
|
235
|
+
const date = new Date();
|
|
236
|
+
const year = date.getFullYear();
|
|
237
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
238
|
+
return `${prefix}/${year}/${month}/${hash}${ext}`;
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const ensureDir = (dirPath) => {
|
|
242
|
+
if (!fs.existsSync(dirPath)) {
|
|
243
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const sha256 = (buf) => {
|
|
248
|
+
return crypto.createHash('sha256').update(buf).digest('hex');
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const buildFsPath = (key) => {
|
|
252
|
+
const uploadDir = getUploadDir();
|
|
253
|
+
return path.join(process.cwd(), uploadDir, key);
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const putObjectFs = async ({ key, body }) => {
|
|
257
|
+
const filePath = buildFsPath(key);
|
|
258
|
+
const dirPath = path.dirname(filePath);
|
|
259
|
+
ensureDir(dirPath);
|
|
260
|
+
fs.writeFileSync(filePath, body);
|
|
261
|
+
return { provider: 'fs', bucket: 'fs', key };
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const getObjectFs = async ({ key }) => {
|
|
265
|
+
const filePath = buildFsPath(key);
|
|
266
|
+
if (!fs.existsSync(filePath)) return null;
|
|
267
|
+
const body = fs.readFileSync(filePath);
|
|
268
|
+
return { body, contentType: null };
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
const deleteObjectFs = async ({ key }) => {
|
|
272
|
+
const filePath = buildFsPath(key);
|
|
273
|
+
if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
|
|
274
|
+
return true;
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const objectExistsFs = async ({ key }) => {
|
|
278
|
+
const filePath = buildFsPath(key);
|
|
279
|
+
return fs.existsSync(filePath);
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const putObjectS3 = async ({ key, body, contentType }) => {
|
|
283
|
+
const client = await getS3Client();
|
|
284
|
+
if (!client) {
|
|
285
|
+
const err = new Error('S3 is not configured');
|
|
286
|
+
err.code = 'S3_NOT_CONFIGURED';
|
|
287
|
+
throw err;
|
|
288
|
+
}
|
|
289
|
+
const { PutObjectCommand } = await import('@aws-sdk/client-s3');
|
|
290
|
+
const config = await getS3Config();
|
|
291
|
+
|
|
292
|
+
await ensureS3BucketExists();
|
|
293
|
+
|
|
294
|
+
await client.send(new PutObjectCommand({
|
|
295
|
+
Bucket: config.bucket,
|
|
296
|
+
Key: key,
|
|
297
|
+
Body: body,
|
|
298
|
+
ContentType: contentType
|
|
299
|
+
}));
|
|
300
|
+
return { provider: 's3', bucket: config.bucket, key };
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const getObjectS3 = async ({ key }) => {
|
|
304
|
+
const client = await getS3Client();
|
|
305
|
+
if (!client) return null;
|
|
306
|
+
const { GetObjectCommand } = await import('@aws-sdk/client-s3');
|
|
307
|
+
const config = await getS3Config();
|
|
308
|
+
|
|
309
|
+
const response = await client.send(new GetObjectCommand({
|
|
310
|
+
Bucket: config.bucket,
|
|
311
|
+
Key: key
|
|
312
|
+
}));
|
|
313
|
+
|
|
314
|
+
const chunks = [];
|
|
315
|
+
for await (const chunk of response.Body) {
|
|
316
|
+
chunks.push(chunk);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
body: Buffer.concat(chunks),
|
|
321
|
+
contentType: response.ContentType
|
|
322
|
+
};
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
const deleteObjectS3 = async ({ key }) => {
|
|
326
|
+
const client = await getS3Client();
|
|
327
|
+
if (!client) return true;
|
|
328
|
+
const { DeleteObjectCommand } = await import('@aws-sdk/client-s3');
|
|
329
|
+
const config = await getS3Config();
|
|
330
|
+
await client.send(new DeleteObjectCommand({
|
|
331
|
+
Bucket: config.bucket,
|
|
332
|
+
Key: key
|
|
333
|
+
}));
|
|
334
|
+
return true;
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
const moveObjectFs = async ({ sourceKey, destKey }) => {
|
|
338
|
+
const sourcePath = buildFsPath(sourceKey);
|
|
339
|
+
const destPath = buildFsPath(destKey);
|
|
340
|
+
if (!fs.existsSync(sourcePath)) {
|
|
341
|
+
const err = new Error('Source object not found');
|
|
342
|
+
err.code = 'NOT_FOUND';
|
|
343
|
+
throw err;
|
|
344
|
+
}
|
|
345
|
+
ensureDir(path.dirname(destPath));
|
|
346
|
+
fs.copyFileSync(sourcePath, destPath);
|
|
347
|
+
fs.unlinkSync(sourcePath);
|
|
348
|
+
return true;
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
const moveObjectS3 = async ({ sourceKey, destKey }) => {
|
|
352
|
+
const client = await getS3Client();
|
|
353
|
+
if (!client) {
|
|
354
|
+
const err = new Error('S3 is not configured');
|
|
355
|
+
err.code = 'S3_NOT_CONFIGURED';
|
|
356
|
+
throw err;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
await ensureS3BucketExists();
|
|
360
|
+
const config = await getS3Config();
|
|
361
|
+
const { CopyObjectCommand, DeleteObjectCommand } = await import('@aws-sdk/client-s3');
|
|
362
|
+
|
|
363
|
+
await client.send(new CopyObjectCommand({
|
|
364
|
+
Bucket: config.bucket,
|
|
365
|
+
CopySource: `${encodeURIComponent(config.bucket)}/${encodeURIComponent(sourceKey)}`,
|
|
366
|
+
Key: destKey,
|
|
367
|
+
}));
|
|
368
|
+
|
|
369
|
+
await client.send(new DeleteObjectCommand({
|
|
370
|
+
Bucket: config.bucket,
|
|
371
|
+
Key: sourceKey,
|
|
372
|
+
}));
|
|
373
|
+
|
|
374
|
+
return true;
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
const objectExistsS3 = async ({ key }) => {
|
|
378
|
+
const client = await getS3Client();
|
|
379
|
+
if (!client) return false;
|
|
380
|
+
const { HeadObjectCommand } = await import('@aws-sdk/client-s3');
|
|
381
|
+
const config = await getS3Config();
|
|
382
|
+
|
|
383
|
+
try {
|
|
384
|
+
await client.send(new HeadObjectCommand({
|
|
385
|
+
Bucket: config.bucket,
|
|
386
|
+
Key: key
|
|
387
|
+
}));
|
|
388
|
+
return true;
|
|
389
|
+
} catch (err) {
|
|
390
|
+
if (err.name === 'NotFound' || err.$metadata?.httpStatusCode === 404) {
|
|
391
|
+
return false;
|
|
392
|
+
}
|
|
393
|
+
throw err;
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
const putObject = async ({ key, body, contentType, backend }) => {
|
|
398
|
+
const target = backend || await getActiveBackend();
|
|
399
|
+
if (target === 's3') {
|
|
400
|
+
return putObjectS3({ key, body, contentType });
|
|
401
|
+
}
|
|
402
|
+
return putObjectFs({ key, body });
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
const getObject = async ({ key, backend }) => {
|
|
406
|
+
const target = backend || await getActiveBackend();
|
|
407
|
+
if (target === 's3') {
|
|
408
|
+
return getObjectS3({ key });
|
|
409
|
+
}
|
|
410
|
+
return getObjectFs({ key });
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
const deleteObject = async ({ key, backend }) => {
|
|
414
|
+
const target = backend || await getActiveBackend();
|
|
415
|
+
if (target === 's3') {
|
|
416
|
+
return deleteObjectS3({ key });
|
|
417
|
+
}
|
|
418
|
+
return deleteObjectFs({ key });
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
const moveObject = async ({ sourceKey, destKey, backend }) => {
|
|
422
|
+
const target = backend || await getActiveBackend();
|
|
423
|
+
if (!sourceKey || !destKey) {
|
|
424
|
+
const err = new Error('sourceKey and destKey are required');
|
|
425
|
+
err.code = 'INVALID_INPUT';
|
|
426
|
+
throw err;
|
|
427
|
+
}
|
|
428
|
+
if (target === 's3') {
|
|
429
|
+
return moveObjectS3({ sourceKey, destKey });
|
|
430
|
+
}
|
|
431
|
+
return moveObjectFs({ sourceKey, destKey });
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
const objectExists = async ({ key, backend }) => {
|
|
435
|
+
const target = backend || await getActiveBackend();
|
|
436
|
+
if (target === 's3') {
|
|
437
|
+
return objectExistsS3({ key });
|
|
438
|
+
}
|
|
439
|
+
return objectExistsFs({ key });
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
const checkS3Connection = async () => {
|
|
443
|
+
const cfg = await getS3Config();
|
|
444
|
+
if (!cfg) {
|
|
445
|
+
const err = new Error('S3 is not configured');
|
|
446
|
+
err.code = 'S3_NOT_CONFIGURED';
|
|
447
|
+
throw err;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const client = await getS3Client();
|
|
451
|
+
if (!client) {
|
|
452
|
+
const err = new Error('S3 client not available');
|
|
453
|
+
err.code = 'S3_CLIENT_NOT_AVAILABLE';
|
|
454
|
+
throw err;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
try {
|
|
458
|
+
const ensured = await ensureS3BucketExists();
|
|
459
|
+
return {
|
|
460
|
+
ok: true,
|
|
461
|
+
bucket: cfg.bucket,
|
|
462
|
+
region: cfg.region,
|
|
463
|
+
endpoint: cfg.endpoint,
|
|
464
|
+
ensuredBucket: Boolean(ensured?.ensured),
|
|
465
|
+
};
|
|
466
|
+
} catch (err) {
|
|
467
|
+
const status = err?.$metadata?.httpStatusCode;
|
|
468
|
+
const name = err?.name;
|
|
469
|
+
const e = new Error(err?.message ? String(err.message) : 'S3 check failed');
|
|
470
|
+
e.code = err?.code || 'S3_CHECK_FAILED';
|
|
471
|
+
e.details = err?.details || { status, name, bucket: cfg.bucket, endpoint: cfg.endpoint, region: cfg.region };
|
|
472
|
+
throw e;
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
const compareObjectBytes = async ({ key, sourceBackend, destBackend }) => {
|
|
477
|
+
const [src, dst] = await Promise.all([
|
|
478
|
+
getObject({ key, backend: sourceBackend }),
|
|
479
|
+
getObject({ key, backend: destBackend }),
|
|
480
|
+
]);
|
|
481
|
+
|
|
482
|
+
if (!src || !dst) {
|
|
483
|
+
return { comparable: false, same: false };
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (src.body.length !== dst.body.length) {
|
|
487
|
+
return { comparable: true, same: false };
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return { comparable: true, same: sha256(src.body) === sha256(dst.body) };
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
module.exports = {
|
|
494
|
+
isS3Enabled,
|
|
495
|
+
getProvider,
|
|
496
|
+
getBucket,
|
|
497
|
+
getActiveBackend,
|
|
498
|
+
clearStorageConfigCache,
|
|
499
|
+
getS3Config,
|
|
500
|
+
validateS3Config,
|
|
501
|
+
checkS3Connection,
|
|
502
|
+
compareObjectBytes,
|
|
503
|
+
getUploadDir,
|
|
504
|
+
getMaxFileSize,
|
|
505
|
+
getAllowedContentTypes,
|
|
506
|
+
validateContentType,
|
|
507
|
+
validateFileSize,
|
|
508
|
+
generateKey,
|
|
509
|
+
putObject,
|
|
510
|
+
getObject,
|
|
511
|
+
deleteObject,
|
|
512
|
+
moveObject,
|
|
513
|
+
objectExists
|
|
514
|
+
};
|