@arela/uploader 1.0.4 → 1.0.6
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.
|
@@ -37,7 +37,7 @@ C:\Users\Docs → C:/Users/Docs (stored) → scan_palco_local_c_users_docs
|
|
|
37
37
|
### cli_registry Table
|
|
38
38
|
- `company_slug`: Company identifier
|
|
39
39
|
- `server_id`: Server identifier
|
|
40
|
-
- `base_path_full`: Absolute path
|
|
40
|
+
- `base_path_full`: Absolute path (preserves original format for Windows paths like `O:\expediente\archivos`)
|
|
41
41
|
- `table_name`: Sanitized version (`scan_palco_local_c_users_documents_2023`)
|
|
42
42
|
- **Removed**: `base_path_label` (was redundant - same as base_path_full)
|
|
43
43
|
|
|
@@ -46,18 +46,22 @@ C:\Users\Docs → C:/Users/Docs (stored) → scan_palco_local_c_users_docs
|
|
|
46
46
|
- `relative_path`: Relative to base with forward slashes
|
|
47
47
|
- `absolute_path`: Absolute path with forward slashes
|
|
48
48
|
|
|
49
|
-
**Note**:
|
|
49
|
+
**Note**: Paths are stored consistently. Windows absolute paths (with drive letters) are preserved when the CLI runs on the same Windows machine or a different OS (macOS/Linux).
|
|
50
50
|
|
|
51
51
|
## Key Features
|
|
52
52
|
|
|
53
|
-
### 1. Cross-Platform Path
|
|
53
|
+
### 1. Cross-Platform Path Detection
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
The CLI correctly identifies Windows absolute paths even when running on macOS/Linux:
|
|
56
56
|
|
|
57
|
-
- **Windows paths**: `
|
|
58
|
-
- **Unix paths**: `/home/user/docs` →
|
|
59
|
-
- **Relative paths**:
|
|
60
|
-
|
|
57
|
+
- **Windows paths on any OS**: `O:\expediente\archivos` → recognized as absolute, preserved as-is
|
|
58
|
+
- **Unix paths**: `/home/user/docs` → recognized as absolute, normalized
|
|
59
|
+
- **Relative paths**: `./sample` → resolved against cwd
|
|
60
|
+
|
|
61
|
+
This allows:
|
|
62
|
+
- Running `arela scan` on Windows server → creates table with Windows path in `base_path_full`
|
|
63
|
+
- Running `arela identify` on macOS (for testing) → correctly uses the same Windows path
|
|
64
|
+
- Both resolve to the same table lookup
|
|
61
65
|
|
|
62
66
|
### 2. Full Path in Table Names
|
|
63
67
|
|
package/package.json
CHANGED
|
@@ -272,18 +272,17 @@ export class PushCommand {
|
|
|
272
272
|
break;
|
|
273
273
|
}
|
|
274
274
|
|
|
275
|
-
// Upload files in smaller batches
|
|
275
|
+
// Upload files in smaller batches using new CLI upload endpoint
|
|
276
276
|
for (let i = 0; i < files.length; i += uploadBatchSize) {
|
|
277
277
|
const uploadBatch = files.slice(i, i + uploadBatchSize);
|
|
278
|
-
const batchResults = await this.#
|
|
278
|
+
const batchResults = await this.#uploadBatchViaCli(
|
|
279
|
+
tableName,
|
|
279
280
|
uploadBatch,
|
|
280
281
|
uploadApiConfig,
|
|
281
282
|
);
|
|
282
283
|
|
|
283
|
-
// Update
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
// Update counters
|
|
284
|
+
// Update counters from API response
|
|
285
|
+
// Note: The CLI endpoint now handles updating the scan table directly
|
|
287
286
|
batchResults.forEach((result) => {
|
|
288
287
|
results.processed++;
|
|
289
288
|
if (result.uploaded) {
|
|
@@ -317,8 +316,160 @@ export class PushCommand {
|
|
|
317
316
|
}
|
|
318
317
|
|
|
319
318
|
/**
|
|
320
|
-
* Upload a batch of files
|
|
319
|
+
* Upload a batch of files using the new CLI upload endpoint
|
|
320
|
+
* The endpoint updates the CLI scan table directly
|
|
321
|
+
* @private
|
|
322
|
+
*/
|
|
323
|
+
async #uploadBatchViaCli(tableName, files, uploadApiConfig) {
|
|
324
|
+
const pushConfig = appConfig.getPushConfig();
|
|
325
|
+
const results = [];
|
|
326
|
+
|
|
327
|
+
// Process files one by one (simpler for now, can optimize to true batch later)
|
|
328
|
+
for (const file of files) {
|
|
329
|
+
const result = await this.#uploadFileViaCli(
|
|
330
|
+
tableName,
|
|
331
|
+
file,
|
|
332
|
+
uploadApiConfig,
|
|
333
|
+
pushConfig,
|
|
334
|
+
);
|
|
335
|
+
results.push(result);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return results;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Upload a single file using the CLI upload endpoint
|
|
343
|
+
* @private
|
|
344
|
+
*/
|
|
345
|
+
async #uploadFileViaCli(tableName, file, uploadApiConfig, pushConfig) {
|
|
346
|
+
const result = {
|
|
347
|
+
id: file.id,
|
|
348
|
+
uploaded: false,
|
|
349
|
+
uploadError: null,
|
|
350
|
+
uploadPath: null,
|
|
351
|
+
uploadedToStorageId: null,
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
try {
|
|
355
|
+
// Check if file exists
|
|
356
|
+
if (!fs.existsSync(file.absolute_path)) {
|
|
357
|
+
result.uploadError =
|
|
358
|
+
'FILE_NOT_FOUND: File does not exist on filesystem';
|
|
359
|
+
// Update the scan table with the error
|
|
360
|
+
await this.scanApiService.batchUpdateUpload(tableName, [result]);
|
|
361
|
+
return result;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Get file stats
|
|
365
|
+
const stats = fs.statSync(file.absolute_path);
|
|
366
|
+
if (!stats.isFile()) {
|
|
367
|
+
result.uploadError = 'NOT_A_FILE: Path is not a regular file';
|
|
368
|
+
await this.scanApiService.batchUpdateUpload(tableName, [result]);
|
|
369
|
+
return result;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Construct upload path using arela_path
|
|
373
|
+
// arela_path format: RFC/Year/Patente/Aduana/Pedimento/
|
|
374
|
+
const uploadPath = `${file.arela_path}${file.file_name}`;
|
|
375
|
+
result.uploadPath = uploadPath;
|
|
376
|
+
|
|
377
|
+
// Create form data for CLI upload endpoint
|
|
378
|
+
const form = new FormData();
|
|
379
|
+
|
|
380
|
+
// Encode fileId and folderStructure in the filename
|
|
381
|
+
// Format: [fileId][folderStructure]filename
|
|
382
|
+
const folderStructure = file.arela_path.endsWith('/')
|
|
383
|
+
? file.arela_path.slice(0, -1)
|
|
384
|
+
: file.arela_path;
|
|
385
|
+
|
|
386
|
+
const encodedFilename = `[${file.id}][${folderStructure}]${file.file_name}`;
|
|
387
|
+
|
|
388
|
+
// Create a read stream with the encoded filename
|
|
389
|
+
const fileStream = fs.createReadStream(file.absolute_path);
|
|
390
|
+
form.append('files', fileStream, {
|
|
391
|
+
filename: encodedFilename,
|
|
392
|
+
contentType: this.#getMimeType(file.file_extension),
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
// Add required fields for CLI upload
|
|
396
|
+
form.append('tableName', tableName);
|
|
397
|
+
form.append('rfc', file.rfc);
|
|
398
|
+
form.append('bucket', pushConfig.bucket);
|
|
399
|
+
form.append('autoDetect', 'true');
|
|
400
|
+
form.append('autoOrganize', 'false');
|
|
401
|
+
form.append('batchSize', '1');
|
|
402
|
+
form.append('clientVersion', appConfig.packageVersion);
|
|
403
|
+
|
|
404
|
+
// Upload file using new CLI upload endpoint
|
|
405
|
+
const response = await fetch(
|
|
406
|
+
`${uploadApiConfig.baseUrl}/api/storage/cli-upload`,
|
|
407
|
+
{
|
|
408
|
+
method: 'POST',
|
|
409
|
+
headers: {
|
|
410
|
+
'x-api-key': uploadApiConfig.token,
|
|
411
|
+
...form.getHeaders(),
|
|
412
|
+
},
|
|
413
|
+
body: form,
|
|
414
|
+
},
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
if (!response.ok) {
|
|
418
|
+
const errorText = await response.text();
|
|
419
|
+
result.uploadError = `HTTP ${response.status}: ${errorText}`;
|
|
420
|
+
logger.error(`✗ Failed: ${file.file_name} - ${result.uploadError}`);
|
|
421
|
+
return result;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const apiResult = await response.json();
|
|
425
|
+
|
|
426
|
+
// Check response from CLI upload endpoint
|
|
427
|
+
if (apiResult.uploaded && apiResult.uploaded.length > 0) {
|
|
428
|
+
const uploadedFile = apiResult.uploaded[0];
|
|
429
|
+
result.uploaded = true;
|
|
430
|
+
result.uploadedToStorageId = uploadedFile.storageId;
|
|
431
|
+
logger.info(`✓ Uploaded: ${file.file_name} → ${uploadPath}`);
|
|
432
|
+
} else if (apiResult.errors && apiResult.errors.length > 0) {
|
|
433
|
+
const error = apiResult.errors[0];
|
|
434
|
+
result.uploadError = `UPLOAD_FAILED: ${error.error || 'Upload failed'}`;
|
|
435
|
+
logger.error(`✗ Failed: ${file.file_name} - ${result.uploadError}`);
|
|
436
|
+
} else {
|
|
437
|
+
result.uploadError = 'Unknown upload error - no files uploaded';
|
|
438
|
+
logger.error(`✗ Failed: ${file.file_name} - ${result.uploadError}`);
|
|
439
|
+
}
|
|
440
|
+
} catch (error) {
|
|
441
|
+
result.uploadError = `UPLOAD_ERROR: ${error.message}`;
|
|
442
|
+
logger.error(`✗ Error uploading ${file.file_name}:`, error.message);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return result;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Get MIME type from file extension
|
|
450
|
+
* @private
|
|
451
|
+
*/
|
|
452
|
+
#getMimeType(extension) {
|
|
453
|
+
const mimeTypes = {
|
|
454
|
+
pdf: 'application/pdf',
|
|
455
|
+
doc: 'application/msword',
|
|
456
|
+
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
457
|
+
xls: 'application/vnd.ms-excel',
|
|
458
|
+
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
459
|
+
txt: 'text/plain',
|
|
460
|
+
jpg: 'image/jpeg',
|
|
461
|
+
jpeg: 'image/jpeg',
|
|
462
|
+
png: 'image/png',
|
|
463
|
+
gif: 'image/gif',
|
|
464
|
+
xml: 'application/xml',
|
|
465
|
+
};
|
|
466
|
+
return mimeTypes[extension?.toLowerCase()] || 'application/octet-stream';
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Upload a batch of files (legacy - kept for compatibility)
|
|
321
471
|
* @private
|
|
472
|
+
* @deprecated Use #uploadBatchViaCli instead
|
|
322
473
|
*/
|
|
323
474
|
async #uploadBatch(files, uploadApiConfig) {
|
|
324
475
|
const uploadPromises = files.map((file) =>
|
package/src/config/config.js
CHANGED
|
@@ -34,10 +34,10 @@ class Config {
|
|
|
34
34
|
const __dirname = path.dirname(__filename);
|
|
35
35
|
const packageJsonPath = path.resolve(__dirname, '../../package.json');
|
|
36
36
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
37
|
-
return packageJson.version || '1.0.
|
|
37
|
+
return packageJson.version || '1.0.6';
|
|
38
38
|
} catch (error) {
|
|
39
39
|
console.warn('⚠️ Could not read package.json version, using fallback');
|
|
40
|
-
return '1.0.
|
|
40
|
+
return '1.0.6';
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
|
|
@@ -257,11 +257,13 @@ class Config {
|
|
|
257
257
|
if (!basePathLabel && process.env.UPLOAD_BASE_PATH) {
|
|
258
258
|
const basePath = process.env.UPLOAD_BASE_PATH;
|
|
259
259
|
// Resolve to absolute path (handles ../sample vs ./sample correctly)
|
|
260
|
+
// Note: toAbsolutePath handles Windows paths (O:\...) even on macOS/Linux
|
|
260
261
|
basePathLabel = PathNormalizer.toAbsolutePath(basePath);
|
|
261
262
|
}
|
|
262
263
|
|
|
263
264
|
// If basePathLabel is provided, ensure it's absolute
|
|
264
|
-
|
|
265
|
+
// Use PathNormalizer.isAbsolutePath for cross-platform Windows path detection
|
|
266
|
+
if (basePathLabel && !PathNormalizer.isAbsolutePath(basePathLabel)) {
|
|
265
267
|
basePathLabel = PathNormalizer.toAbsolutePath(basePathLabel);
|
|
266
268
|
}
|
|
267
269
|
|
|
@@ -10,31 +10,83 @@ import { fileURLToPath } from 'url';
|
|
|
10
10
|
* 2. Store absolute paths in base_path_full
|
|
11
11
|
* 3. Normalize ONLY for table name generation (sanitize special chars)
|
|
12
12
|
* 4. Keep full path structure in table names (hash if too long)
|
|
13
|
+
*
|
|
14
|
+
* Cross-platform support:
|
|
15
|
+
* - Recognizes Windows paths (C:\, D:\, etc.) even when running on macOS/Linux
|
|
16
|
+
* - Preserves Windows paths as-is when detected (for remote server scenarios)
|
|
17
|
+
* - This allows CLI running on macOS to work with paths stored from Windows servers
|
|
13
18
|
*/
|
|
14
19
|
export class PathNormalizer {
|
|
20
|
+
/**
|
|
21
|
+
* Check if a path is a Windows-style absolute path (has drive letter)
|
|
22
|
+
* This works on any OS, not just Windows
|
|
23
|
+
*
|
|
24
|
+
* Examples:
|
|
25
|
+
* - C:\Users\Documents -> true
|
|
26
|
+
* - O:\expediente\archivos -> true
|
|
27
|
+
* - /home/user/docs -> false
|
|
28
|
+
* - ./relative/path -> false
|
|
29
|
+
*
|
|
30
|
+
* @param {string} inputPath - Path to check
|
|
31
|
+
* @returns {boolean} True if path has a Windows drive letter
|
|
32
|
+
*/
|
|
33
|
+
static isWindowsAbsolutePath(inputPath) {
|
|
34
|
+
if (!inputPath || typeof inputPath !== 'string') return false;
|
|
35
|
+
// Match drive letter followed by colon and backslash or forward slash
|
|
36
|
+
// e.g., C:\, D:/, O:\
|
|
37
|
+
return /^[A-Za-z]:[/\\]/.test(inputPath);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Check if a path is absolute (works cross-platform)
|
|
42
|
+
* Recognizes both Unix and Windows absolute paths regardless of current OS
|
|
43
|
+
*
|
|
44
|
+
* @param {string} inputPath - Path to check
|
|
45
|
+
* @returns {boolean} True if path is absolute (Unix or Windows style)
|
|
46
|
+
*/
|
|
47
|
+
static isAbsolutePath(inputPath) {
|
|
48
|
+
if (!inputPath) return false;
|
|
49
|
+
// Check native absolute path first, then Windows-style
|
|
50
|
+
return path.isAbsolute(inputPath) || this.isWindowsAbsolutePath(inputPath);
|
|
51
|
+
}
|
|
52
|
+
|
|
15
53
|
/**
|
|
16
54
|
* Resolve a path to an absolute path
|
|
17
55
|
* This ensures ../sample and ./sample are treated as different paths
|
|
18
56
|
*
|
|
57
|
+
* Cross-platform behavior:
|
|
58
|
+
* - Windows paths (C:\, O:\, etc.) are preserved as-is, even on macOS/Linux
|
|
59
|
+
* - Unix paths are normalized using native path.resolve
|
|
60
|
+
* - Relative paths are resolved against basePath or cwd
|
|
61
|
+
*
|
|
19
62
|
* Examples:
|
|
20
63
|
* - ../sample (from /data/project) -> /data/sample
|
|
21
64
|
* - ./sample (from /data/project) -> /data/project/sample
|
|
22
|
-
* - C:\Users\Documents -> C:\Users\Documents (on
|
|
65
|
+
* - C:\Users\Documents -> C:\Users\Documents (preserved on any OS)
|
|
66
|
+
* - O:\expediente\archivos -> O:\expediente\archivos (preserved on any OS)
|
|
23
67
|
* - /home/user/docs -> /home/user/docs (on Unix)
|
|
24
68
|
*
|
|
25
69
|
* @param {string} inputPath - Path to resolve
|
|
26
70
|
* @param {string} basePath - Base path for resolving relative paths (defaults to cwd)
|
|
27
|
-
* @returns {string} Absolute path
|
|
71
|
+
* @returns {string} Absolute path (native format for Unix, preserved for Windows)
|
|
28
72
|
*/
|
|
29
73
|
static toAbsolutePath(inputPath, basePath = null) {
|
|
30
74
|
if (!inputPath) return '';
|
|
31
75
|
|
|
32
|
-
//
|
|
76
|
+
// Check for Windows absolute path first (works on any OS)
|
|
77
|
+
// This prevents macOS from treating "O:\path" as relative
|
|
78
|
+
if (this.isWindowsAbsolutePath(inputPath)) {
|
|
79
|
+
// Normalize Windows path but preserve the format
|
|
80
|
+
// Replace forward slashes with backslashes for consistency
|
|
81
|
+
return inputPath.replace(/\//g, '\\');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// If native absolute path, return normalized
|
|
33
85
|
if (path.isAbsolute(inputPath)) {
|
|
34
86
|
return path.normalize(inputPath);
|
|
35
87
|
}
|
|
36
88
|
|
|
37
|
-
// Resolve relative path
|
|
89
|
+
// Resolve relative path against base
|
|
38
90
|
const base = basePath || process.cwd();
|
|
39
91
|
return path.resolve(base, inputPath);
|
|
40
92
|
}
|
|
@@ -137,7 +189,7 @@ export class PathNormalizer {
|
|
|
137
189
|
|
|
138
190
|
const basePrefix = 'scan_';
|
|
139
191
|
const prefix = `${basePrefix}${sanitizedCompany}_${sanitizedServer}_`;
|
|
140
|
-
|
|
192
|
+
|
|
141
193
|
// Check if even prefix + hash exceeds limit
|
|
142
194
|
if (prefix.length + hash.length > 63) {
|
|
143
195
|
// Company/server names are too long, need to truncate them too
|
|
@@ -145,15 +197,15 @@ export class PathNormalizer {
|
|
|
145
197
|
const halfLength = Math.floor(maxCompanyServerLength / 2);
|
|
146
198
|
const companyLength = halfLength;
|
|
147
199
|
const serverLength = maxCompanyServerLength - companyLength;
|
|
148
|
-
|
|
200
|
+
|
|
149
201
|
const truncatedCompany = sanitizedCompany.substring(0, companyLength);
|
|
150
202
|
const truncatedServer = sanitizedServer.substring(0, serverLength);
|
|
151
|
-
|
|
203
|
+
|
|
152
204
|
tableName = `${basePrefix}${truncatedCompany}_${truncatedServer}_${hash}`;
|
|
153
205
|
} else {
|
|
154
206
|
// Preserve start and end of path, put hash in middle
|
|
155
207
|
const availableSpace = 63 - prefix.length - hash.length - 2; // -2 for underscores around hash
|
|
156
|
-
|
|
208
|
+
|
|
157
209
|
if (availableSpace <= 0 || !sanitizedPath) {
|
|
158
210
|
// If no space for path or path is empty, just use hash
|
|
159
211
|
tableName = `${prefix}${hash}`;
|
|
@@ -165,11 +217,13 @@ export class PathNormalizer {
|
|
|
165
217
|
const halfSpace = Math.floor(availableSpace / 2);
|
|
166
218
|
const startLength = halfSpace;
|
|
167
219
|
const endLength = availableSpace - startLength;
|
|
168
|
-
|
|
220
|
+
|
|
169
221
|
// Extract start and end portions of the sanitized path
|
|
170
222
|
const pathStart = sanitizedPath.substring(0, startLength);
|
|
171
|
-
const pathEnd = sanitizedPath.substring(
|
|
172
|
-
|
|
223
|
+
const pathEnd = sanitizedPath.substring(
|
|
224
|
+
sanitizedPath.length - endLength,
|
|
225
|
+
);
|
|
226
|
+
|
|
173
227
|
// Build table name with start, hash, and end
|
|
174
228
|
tableName = `${prefix}${pathStart}_${hash}_${pathEnd}`;
|
|
175
229
|
}
|