@arela/uploader 0.0.8 → 0.0.10
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/README.md +83 -0
- package/package.json +1 -1
- package/src/index.js +253 -44
package/README.md
CHANGED
|
@@ -1 +1,84 @@
|
|
|
1
1
|
# arela-uploader
|
|
2
|
+
|
|
3
|
+
CLI tool to upload files and directories to Supabase Storage with automatic file renaming and sanitization.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 📁 Upload entire directories or individual files
|
|
8
|
+
- 🔄 Automatic file renaming to handle problematic characters
|
|
9
|
+
- 📝 Comprehensive logging (local and remote)
|
|
10
|
+
- ⚡ Retry mechanism for failed uploads
|
|
11
|
+
- 🎯 Skip duplicate files automatically
|
|
12
|
+
- 📊 Progress bars and detailed summaries
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install -g @arela/uploader
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
### Basic Upload
|
|
23
|
+
```bash
|
|
24
|
+
arela -p "my-folder"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Upload with File Renaming
|
|
28
|
+
For files with accents, special characters, or problematic names:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# Preview what files would be renamed (dry run)
|
|
32
|
+
arela --rename-files --dry-run
|
|
33
|
+
|
|
34
|
+
# Actually rename and upload files
|
|
35
|
+
arela --rename-files -p "documents"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Options
|
|
39
|
+
|
|
40
|
+
- `-p, --prefix <prefix>`: Prefix path in bucket (default: "")
|
|
41
|
+
- `-r, --rename-files`: Rename files with problematic characters before uploading
|
|
42
|
+
- `--dry-run`: Show what files would be renamed without actually renaming them
|
|
43
|
+
- `-h, --help`: Display help information
|
|
44
|
+
- `-v, --version`: Display version number
|
|
45
|
+
|
|
46
|
+
## Environment Variables
|
|
47
|
+
|
|
48
|
+
Create a `.env` file in your project root:
|
|
49
|
+
|
|
50
|
+
```env
|
|
51
|
+
SUPABASE_URL=your_supabase_url
|
|
52
|
+
SUPABASE_KEY=your_supabase_anon_key
|
|
53
|
+
SUPABASE_BUCKET=your_bucket_name
|
|
54
|
+
UPLOAD_BASE_PATH=/path/to/your/files
|
|
55
|
+
UPLOAD_SOURCES=folder1|folder2|file.pdf
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## File Renaming
|
|
59
|
+
|
|
60
|
+
The tool automatically handles problematic characters by:
|
|
61
|
+
|
|
62
|
+
- Removing accents (á → a, ñ → n, etc.)
|
|
63
|
+
- Replacing special characters with safe alternatives
|
|
64
|
+
- Converting spaces to dashes
|
|
65
|
+
- Removing or replacing symbols like `{}[]~^`|"<>?*:`
|
|
66
|
+
- Handling Korean characters and other Unicode symbols
|
|
67
|
+
|
|
68
|
+
### Examples
|
|
69
|
+
|
|
70
|
+
| Original | Renamed |
|
|
71
|
+
|----------|---------|
|
|
72
|
+
| `Facturas Importación.pdf` | `Facturas-Importacion.pdf` |
|
|
73
|
+
| `File{with}brackets.pdf` | `File-with-brackets.pdf` |
|
|
74
|
+
| `Document ^& symbols.pdf` | `Document-and-symbols.pdf` |
|
|
75
|
+
| `CI & PL-20221212(멕시코용).xls` | `CI-and-PL-20221212.xls` |
|
|
76
|
+
|
|
77
|
+
## Logging
|
|
78
|
+
|
|
79
|
+
The tool maintains logs both locally (`upload.log`) and remotely in your Supabase database. Logs include:
|
|
80
|
+
|
|
81
|
+
- Upload status (success/error/skipped)
|
|
82
|
+
- File paths and sanitization changes
|
|
83
|
+
- Error messages and timestamps
|
|
84
|
+
- Rename operations
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -27,6 +27,52 @@ const sources = process.env.UPLOAD_SOURCES?.split('|')
|
|
|
27
27
|
|
|
28
28
|
const supabase = createClient(supabaseUrl, supabaseKey);
|
|
29
29
|
|
|
30
|
+
// Enhanced sanitization function specifically for file names
|
|
31
|
+
const sanitizeFileName = (fileName) => {
|
|
32
|
+
// Get file extension
|
|
33
|
+
const ext = path.extname(fileName);
|
|
34
|
+
const nameWithoutExt = path.basename(fileName, ext);
|
|
35
|
+
|
|
36
|
+
// Replace problematic characters with safe alternatives
|
|
37
|
+
let sanitized = nameWithoutExt
|
|
38
|
+
// First, handle common character replacements
|
|
39
|
+
.replace(/[áàâäãåāăą]/gi, 'a')
|
|
40
|
+
.replace(/[éèêëēĕėę]/gi, 'e')
|
|
41
|
+
.replace(/[íìîïīĭį]/gi, 'i')
|
|
42
|
+
.replace(/[óòôöõōŏő]/gi, 'o')
|
|
43
|
+
.replace(/[úùûüūŭů]/gi, 'u')
|
|
44
|
+
.replace(/[ñň]/gi, 'n')
|
|
45
|
+
.replace(/[ç]/gi, 'c')
|
|
46
|
+
.replace(/[ý]/gi, 'y')
|
|
47
|
+
// Handle Korean characters more comprehensively
|
|
48
|
+
.replace(/[멕]/g, 'meok')
|
|
49
|
+
.replace(/[시]/g, 'si')
|
|
50
|
+
.replace(/[코]/g, 'ko')
|
|
51
|
+
.replace(/[용]/g, 'yong')
|
|
52
|
+
.replace(/[가-힣]/g, 'kr') // Replace any remaining Korean chars with 'kr'
|
|
53
|
+
// Normalize unicode and remove diacritics
|
|
54
|
+
.normalize('NFD')
|
|
55
|
+
.replace(/[\u0300-\u036f]/g, '') // Remove diacritics
|
|
56
|
+
// Replace problematic symbols
|
|
57
|
+
.replace(/[\\?%*:|"<>[\]~`^]/g, '-')
|
|
58
|
+
.replace(/[{}]/g, '-')
|
|
59
|
+
.replace(/[&]/g, 'and')
|
|
60
|
+
.replace(/[()]/g, '') // Remove parentheses
|
|
61
|
+
// Clean up spaces and dashes
|
|
62
|
+
.replace(/\s+/g, '-') // Replace spaces with dashes
|
|
63
|
+
.replace(/-+/g, '-') // Replace multiple dashes with single dash
|
|
64
|
+
.replace(/^-+|-+$/g, '') // Remove leading/trailing dashes
|
|
65
|
+
.replace(/^\.+/, '') // Remove leading dots
|
|
66
|
+
.replace(/[^\w.-]/g, ''); // Remove any remaining non-alphanumeric chars except dots and dashes
|
|
67
|
+
|
|
68
|
+
// Ensure the filename is not empty
|
|
69
|
+
if (!sanitized) {
|
|
70
|
+
sanitized = 'unnamed_file';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return sanitized + ext;
|
|
74
|
+
};
|
|
75
|
+
|
|
30
76
|
const sanitizePath = (path) =>
|
|
31
77
|
path
|
|
32
78
|
.replace(/[\\?%*:|"<>[\]~]/g, '-')
|
|
@@ -122,16 +168,66 @@ const writeLog = (message) => {
|
|
|
122
168
|
}
|
|
123
169
|
};
|
|
124
170
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
? fs.readFileSync(logFilePath, 'utf-8').split('\n')
|
|
128
|
-
: [];
|
|
171
|
+
// Modified to fetch from Supabase first, then fallback to local log
|
|
172
|
+
const getProcessedPaths = async () => {
|
|
129
173
|
const processed = new Set();
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
174
|
+
|
|
175
|
+
// Try to fetch from Supabase first
|
|
176
|
+
try {
|
|
177
|
+
const { data, error } = await supabase
|
|
178
|
+
.from('upload_logs')
|
|
179
|
+
.select('path')
|
|
180
|
+
.in('status', ['success', 'skipped']);
|
|
181
|
+
|
|
182
|
+
if (error) {
|
|
183
|
+
console.warn(
|
|
184
|
+
`⚠️ Could not fetch processed paths from Supabase: ${error.message}. Falling back to local log.`,
|
|
185
|
+
);
|
|
186
|
+
// Fallback to local log if Supabase fetch fails
|
|
187
|
+
const lines = fs.existsSync(logFilePath)
|
|
188
|
+
? fs.readFileSync(logFilePath, 'utf-8').split('\\n')
|
|
189
|
+
: [];
|
|
190
|
+
for (const line of lines) {
|
|
191
|
+
const match = line.match(/(SUCCESS|SKIPPED): .*? -> (.+)/);
|
|
192
|
+
if (match) {
|
|
193
|
+
const [, , path] = match;
|
|
194
|
+
processed.add(path.trim());
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
} else if (data) {
|
|
198
|
+
data.forEach((log) => {
|
|
199
|
+
if (log.path) {
|
|
200
|
+
processed.add(log.path.trim());
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
// Also read from local log to ensure any paths logged before this change or during a Supabase outage are included
|
|
204
|
+
const lines = fs.existsSync(logFilePath)
|
|
205
|
+
? fs.readFileSync(logFilePath, 'utf-8').split('\\n')
|
|
206
|
+
: [];
|
|
207
|
+
for (const line of lines) {
|
|
208
|
+
const match = line.match(/(SUCCESS|SKIPPED): .*? -> (.+)/);
|
|
209
|
+
if (match) {
|
|
210
|
+
const [, , pathValue] = match;
|
|
211
|
+
if (pathValue) {
|
|
212
|
+
processed.add(pathValue.trim());
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
} catch (e) {
|
|
218
|
+
console.warn(
|
|
219
|
+
`⚠️ Error fetching from Supabase or reading local log: ${e.message}. Proceeding with an empty set of processed paths initially.`,
|
|
220
|
+
);
|
|
221
|
+
// Ensure local log is still attempted if Supabase connection itself fails
|
|
222
|
+
const lines = fs.existsSync(logFilePath)
|
|
223
|
+
? fs.readFileSync(logFilePath, 'utf-8').split('\\n')
|
|
224
|
+
: [];
|
|
225
|
+
for (const line of lines) {
|
|
226
|
+
const match = line.match(/(SUCCESS|SKIPPED): .*? -> (.+)/);
|
|
227
|
+
if (match) {
|
|
228
|
+
const [, , path] = match;
|
|
229
|
+
processed.add(path.trim());
|
|
230
|
+
}
|
|
135
231
|
}
|
|
136
232
|
}
|
|
137
233
|
return processed;
|
|
@@ -139,23 +235,68 @@ const getProcessedPaths = () => {
|
|
|
139
235
|
|
|
140
236
|
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
141
237
|
|
|
142
|
-
const uploadWithRetry = async (uploadFn, maxRetries =
|
|
238
|
+
const uploadWithRetry = async (uploadFn, maxRetries = 5, delayMs = 2000) => {
|
|
143
239
|
let attempt = 0;
|
|
240
|
+
let lastError;
|
|
241
|
+
|
|
144
242
|
while (attempt < maxRetries) {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
243
|
+
try {
|
|
244
|
+
const result = await uploadFn();
|
|
245
|
+
if (!result.error) return result;
|
|
246
|
+
lastError = result.error;
|
|
247
|
+
attempt++;
|
|
248
|
+
|
|
249
|
+
// Exponential backoff with jitter
|
|
250
|
+
if (attempt < maxRetries) {
|
|
251
|
+
const backoffDelay =
|
|
252
|
+
delayMs * Math.pow(2, attempt - 1) + Math.random() * 1000;
|
|
253
|
+
console.log(
|
|
254
|
+
`Retry ${attempt}/${maxRetries} after ${Math.round(backoffDelay)}ms...`,
|
|
255
|
+
);
|
|
256
|
+
await delay(backoffDelay);
|
|
257
|
+
}
|
|
258
|
+
} catch (error) {
|
|
259
|
+
lastError = error;
|
|
260
|
+
attempt++;
|
|
261
|
+
|
|
262
|
+
if (attempt < maxRetries) {
|
|
263
|
+
const backoffDelay =
|
|
264
|
+
delayMs * Math.pow(2, attempt - 1) + Math.random() * 1000;
|
|
265
|
+
console.log(
|
|
266
|
+
`Retry ${attempt}/${maxRetries} after ${Math.round(backoffDelay)}ms due to exception...`,
|
|
267
|
+
);
|
|
268
|
+
await delay(backoffDelay);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
149
271
|
}
|
|
150
|
-
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
error: new Error(
|
|
275
|
+
`Max retries exceeded. Last error: ${lastError?.message || 'Unknown error'}`,
|
|
276
|
+
),
|
|
277
|
+
};
|
|
151
278
|
};
|
|
152
279
|
|
|
153
280
|
program
|
|
154
281
|
.name('supabase-uploader')
|
|
155
282
|
.description('CLI to upload folders from a base path to Supabase Storage')
|
|
156
|
-
.
|
|
283
|
+
.option('-v, --version', 'output the version number')
|
|
157
284
|
.option('-p, --prefix <prefix>', 'Prefix path in bucket', '')
|
|
285
|
+
.option(
|
|
286
|
+
'-r, --rename-files',
|
|
287
|
+
'Rename files with problematic characters before uploading',
|
|
288
|
+
)
|
|
289
|
+
.option(
|
|
290
|
+
'--dry-run',
|
|
291
|
+
'Show what files would be renamed without actually renaming them',
|
|
292
|
+
)
|
|
158
293
|
.action(async (options) => {
|
|
294
|
+
// Handle version option
|
|
295
|
+
if (options.version) {
|
|
296
|
+
console.log(version);
|
|
297
|
+
process.exit(0);
|
|
298
|
+
}
|
|
299
|
+
|
|
159
300
|
if (!basePath || !sources || sources.length === 0) {
|
|
160
301
|
console.error(
|
|
161
302
|
'⚠️ UPLOAD_BASE_PATH or UPLOAD_SOURCES not defined in environment variables.',
|
|
@@ -163,7 +304,7 @@ program
|
|
|
163
304
|
process.exit(1);
|
|
164
305
|
}
|
|
165
306
|
|
|
166
|
-
const processedPaths = getProcessedPaths();
|
|
307
|
+
const processedPaths = await getProcessedPaths();
|
|
167
308
|
let globalSuccess = 0;
|
|
168
309
|
let globalFailure = 0;
|
|
169
310
|
|
|
@@ -190,41 +331,92 @@ program
|
|
|
190
331
|
|
|
191
332
|
for (const file of files) {
|
|
192
333
|
progressBar.increment();
|
|
193
|
-
|
|
334
|
+
|
|
335
|
+
let currentFile = file;
|
|
336
|
+
|
|
337
|
+
// Check if we need to rename the file
|
|
338
|
+
if (options.renameFiles) {
|
|
339
|
+
const originalName = path.basename(file);
|
|
340
|
+
const sanitizedName = sanitizeFileName(originalName);
|
|
341
|
+
|
|
342
|
+
if (originalName !== sanitizedName) {
|
|
343
|
+
const newFilePath = path.join(path.dirname(file), sanitizedName);
|
|
344
|
+
|
|
345
|
+
if (options.dryRun) {
|
|
346
|
+
console.log(`Would rename: ${originalName} → ${sanitizedName}`);
|
|
347
|
+
continue;
|
|
348
|
+
} else {
|
|
349
|
+
try {
|
|
350
|
+
fs.renameSync(file, newFilePath);
|
|
351
|
+
currentFile = newFilePath;
|
|
352
|
+
console.log(`✅ Renamed: ${originalName} → ${sanitizedName}`);
|
|
353
|
+
writeLog(`RENAMED: ${originalName} → ${sanitizedName}`);
|
|
354
|
+
await sendLogToSupabase({
|
|
355
|
+
file: originalName,
|
|
356
|
+
uploadPath: sanitizedName,
|
|
357
|
+
status: 'renamed',
|
|
358
|
+
message: `Renamed from ${originalName}`,
|
|
359
|
+
});
|
|
360
|
+
} catch (renameError) {
|
|
361
|
+
console.error(
|
|
362
|
+
`❌ Failed to rename ${originalName}: ${renameError.message}`,
|
|
363
|
+
);
|
|
364
|
+
writeLog(
|
|
365
|
+
`RENAME_ERROR: ${originalName} | ${renameError.message}`,
|
|
366
|
+
);
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const content = fs.readFileSync(currentFile);
|
|
194
374
|
const relativePathRaw = path
|
|
195
|
-
.relative(basePath,
|
|
375
|
+
.relative(basePath, currentFile)
|
|
196
376
|
.replace(/^[\\/]+/, '')
|
|
197
377
|
.replace(/\\/g, '/');
|
|
378
|
+
|
|
379
|
+
// Always sanitize the filename for upload path
|
|
380
|
+
const pathParts = relativePathRaw.split('/');
|
|
381
|
+
const originalFileName = pathParts[pathParts.length - 1];
|
|
382
|
+
const sanitizedFileName = sanitizeFileName(originalFileName);
|
|
383
|
+
pathParts[pathParts.length - 1] = sanitizedFileName;
|
|
384
|
+
const sanitizedRelativePath = pathParts.join('/');
|
|
385
|
+
|
|
198
386
|
const uploadPathRaw = options.prefix
|
|
199
|
-
? path.posix.join(options.prefix,
|
|
200
|
-
:
|
|
387
|
+
? path.posix.join(options.prefix, sanitizedRelativePath)
|
|
388
|
+
: sanitizedRelativePath;
|
|
201
389
|
const uploadPath = sanitizePath(uploadPathRaw);
|
|
202
390
|
|
|
203
|
-
if (
|
|
204
|
-
|
|
391
|
+
if (
|
|
392
|
+
uploadPath !== uploadPathRaw ||
|
|
393
|
+
originalFileName !== sanitizedFileName
|
|
394
|
+
) {
|
|
395
|
+
writeLog(`SANITIZED: ${relativePathRaw} → ${uploadPath}`);
|
|
205
396
|
await sendLogToSupabase({
|
|
206
|
-
file,
|
|
207
|
-
uploadPath:
|
|
397
|
+
file: currentFile,
|
|
398
|
+
uploadPath: relativePathRaw,
|
|
208
399
|
status: 'sanitized',
|
|
209
|
-
message: `Sanitized to ${uploadPath}`,
|
|
400
|
+
message: `Sanitized to ${uploadPath} (Arela Version: ${version})`,
|
|
210
401
|
});
|
|
211
402
|
}
|
|
212
403
|
|
|
213
404
|
if (processedPaths.has(uploadPath)) {
|
|
214
|
-
ora().info(`⏭️ Already processed (log): ${
|
|
405
|
+
ora().info(`⏭️ Already processed (log): ${currentFile}`);
|
|
215
406
|
continue;
|
|
216
407
|
}
|
|
217
408
|
|
|
218
|
-
const contentType =
|
|
409
|
+
const contentType =
|
|
410
|
+
mime.lookup(currentFile) || 'application/octet-stream';
|
|
219
411
|
|
|
220
|
-
const spinner = ora(`Checking ${
|
|
412
|
+
const spinner = ora(`Checking ${currentFile}...`).start();
|
|
221
413
|
const exists = await fileExistsInBucket(uploadPath);
|
|
222
414
|
|
|
223
415
|
if (exists) {
|
|
224
|
-
spinner.info(`⏭️ Skipped (already exists): ${
|
|
225
|
-
writeLog(`SKIPPED: ${
|
|
416
|
+
spinner.info(`⏭️ Skipped (already exists): ${currentFile}`);
|
|
417
|
+
writeLog(`SKIPPED: ${currentFile} -> ${uploadPath}`);
|
|
226
418
|
await sendLogToSupabase({
|
|
227
|
-
file,
|
|
419
|
+
file: currentFile,
|
|
228
420
|
uploadPath,
|
|
229
421
|
status: 'skipped',
|
|
230
422
|
message: 'Already exists in bucket',
|
|
@@ -235,12 +427,21 @@ program
|
|
|
235
427
|
try {
|
|
236
428
|
// await delay(5000); // TODO: Remove this delay before production
|
|
237
429
|
|
|
430
|
+
spinner.text = `Uploading ${currentFile}...`;
|
|
431
|
+
|
|
238
432
|
const { error } = await uploadWithRetry(() =>
|
|
239
433
|
supabase.storage.from(bucket).upload(uploadPath, content, {
|
|
240
434
|
upsert: true,
|
|
241
435
|
contentType,
|
|
242
436
|
metadata: {
|
|
243
|
-
originalName: path.basename(
|
|
437
|
+
originalName: path.basename(currentFile),
|
|
438
|
+
sanitizedName: path.basename(uploadPath),
|
|
439
|
+
clientPath: path.posix.join(
|
|
440
|
+
basePath,
|
|
441
|
+
folder,
|
|
442
|
+
path.relative(sourcePath, currentFile).replace(/\\/g, '/'),
|
|
443
|
+
),
|
|
444
|
+
arelaVersion: version,
|
|
244
445
|
},
|
|
245
446
|
}),
|
|
246
447
|
);
|
|
@@ -248,31 +449,39 @@ program
|
|
|
248
449
|
if (error) {
|
|
249
450
|
failureCount++;
|
|
250
451
|
globalFailure++;
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
);
|
|
254
|
-
writeLog(`ERROR: ${file} -> ${uploadPath} | ${error.message}`);
|
|
452
|
+
const errorMsg = error.message || JSON.stringify(error);
|
|
453
|
+
spinner.fail(`❌ Failed to upload ${currentFile}: ${errorMsg}`);
|
|
454
|
+
writeLog(`ERROR: ${currentFile} -> ${uploadPath} | ${errorMsg}`);
|
|
255
455
|
await sendLogToSupabase({
|
|
256
|
-
file,
|
|
456
|
+
file: currentFile,
|
|
257
457
|
uploadPath,
|
|
258
458
|
status: 'error',
|
|
259
|
-
message:
|
|
459
|
+
message: errorMsg,
|
|
260
460
|
});
|
|
261
461
|
} else {
|
|
262
462
|
successCount++;
|
|
263
463
|
globalSuccess++;
|
|
264
|
-
spinner.succeed(`✅ Uploaded ${
|
|
265
|
-
writeLog(`SUCCESS: ${
|
|
464
|
+
spinner.succeed(`✅ Uploaded ${currentFile} -> ${uploadPath}`);
|
|
465
|
+
writeLog(`SUCCESS: ${currentFile} -> ${uploadPath}`);
|
|
266
466
|
await sendLogToSupabase({
|
|
267
|
-
file,
|
|
467
|
+
file: currentFile,
|
|
268
468
|
uploadPath,
|
|
269
469
|
status: 'success',
|
|
270
470
|
message: 'Uploaded successfully',
|
|
271
471
|
});
|
|
272
472
|
}
|
|
273
473
|
} catch (err) {
|
|
274
|
-
|
|
275
|
-
|
|
474
|
+
failureCount++;
|
|
475
|
+
globalFailure++;
|
|
476
|
+
const errorMsg = err.message || JSON.stringify(err);
|
|
477
|
+
spinner.fail(`❌ Error uploading ${currentFile}: ${errorMsg}`);
|
|
478
|
+
writeLog(`ERROR: ${currentFile} -> ${uploadPath} | ${errorMsg}`);
|
|
479
|
+
await sendLogToSupabase({
|
|
480
|
+
file: currentFile,
|
|
481
|
+
uploadPath,
|
|
482
|
+
status: 'error',
|
|
483
|
+
message: errorMsg,
|
|
484
|
+
});
|
|
276
485
|
}
|
|
277
486
|
}
|
|
278
487
|
|