@arela/uploader 1.0.2 → 1.0.4
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.local +316 -0
- package/.env.template +70 -0
- package/coverage/IdentifyCommand.js.html +1462 -0
- package/coverage/PropagateCommand.js.html +1507 -0
- package/coverage/PushCommand.js.html +1504 -0
- package/coverage/ScanCommand.js.html +1654 -0
- package/coverage/UploadCommand.js.html +1846 -0
- package/coverage/WatchCommand.js.html +4111 -0
- package/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +191 -0
- package/coverage/lcov-report/IdentifyCommand.js.html +1462 -0
- package/coverage/lcov-report/PropagateCommand.js.html +1507 -0
- package/coverage/lcov-report/PushCommand.js.html +1504 -0
- package/coverage/lcov-report/ScanCommand.js.html +1654 -0
- package/coverage/lcov-report/UploadCommand.js.html +1846 -0
- package/coverage/lcov-report/WatchCommand.js.html +4111 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +191 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov.info +1937 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -0
- package/docs/API_RETRY_MECHANISM.md +338 -0
- package/docs/ARELA_IDENTIFY_IMPLEMENTATION.md +489 -0
- package/docs/ARELA_IDENTIFY_QUICKREF.md +186 -0
- package/docs/ARELA_PROPAGATE_IMPLEMENTATION.md +581 -0
- package/docs/ARELA_PROPAGATE_QUICKREF.md +272 -0
- package/docs/ARELA_PUSH_IMPLEMENTATION.md +577 -0
- package/docs/ARELA_PUSH_QUICKREF.md +322 -0
- package/docs/ARELA_SCAN_IMPLEMENTATION.md +373 -0
- package/docs/ARELA_SCAN_QUICKREF.md +139 -0
- package/docs/CROSS_PLATFORM_PATH_HANDLING.md +593 -0
- package/docs/DETECTION_ATTEMPT_TRACKING.md +414 -0
- package/docs/MIGRATION_UPLOADER_TO_FILE_STATS.md +1020 -0
- package/docs/MULTI_LEVEL_DIRECTORY_SCANNING.md +494 -0
- package/docs/STATS_COMMAND_SEQUENCE_DIAGRAM.md +287 -0
- package/docs/STATS_COMMAND_SIMPLE.md +93 -0
- package/package.json +31 -3
- package/src/commands/IdentifyCommand.js +459 -0
- package/src/commands/PropagateCommand.js +474 -0
- package/src/commands/PushCommand.js +473 -0
- package/src/commands/ScanCommand.js +523 -0
- package/src/config/config.js +154 -7
- package/src/file-detection.js +9 -10
- package/src/index.js +150 -0
- package/src/services/ScanApiService.js +645 -0
- package/src/utils/PathNormalizer.js +220 -0
- package/tests/commands/IdentifyCommand.test.js +570 -0
- package/tests/commands/PropagateCommand.test.js +568 -0
- package/tests/commands/PushCommand.test.js +754 -0
- package/tests/commands/ScanCommand.test.js +382 -0
- package/tests/unit/PathAndTableNameGeneration.test.js +1211 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* PathNormalizer - Absolute path resolution and table name generation
|
|
7
|
+
*
|
|
8
|
+
* Strategy:
|
|
9
|
+
* 1. Always resolve paths to absolute paths first (resolves ../sample vs ./sample)
|
|
10
|
+
* 2. Store absolute paths in base_path_full
|
|
11
|
+
* 3. Normalize ONLY for table name generation (sanitize special chars)
|
|
12
|
+
* 4. Keep full path structure in table names (hash if too long)
|
|
13
|
+
*/
|
|
14
|
+
export class PathNormalizer {
|
|
15
|
+
/**
|
|
16
|
+
* Resolve a path to an absolute path
|
|
17
|
+
* This ensures ../sample and ./sample are treated as different paths
|
|
18
|
+
*
|
|
19
|
+
* Examples:
|
|
20
|
+
* - ../sample (from /data/project) -> /data/sample
|
|
21
|
+
* - ./sample (from /data/project) -> /data/project/sample
|
|
22
|
+
* - C:\Users\Documents -> C:\Users\Documents (on Windows)
|
|
23
|
+
* - /home/user/docs -> /home/user/docs (on Unix)
|
|
24
|
+
*
|
|
25
|
+
* @param {string} inputPath - Path to resolve
|
|
26
|
+
* @param {string} basePath - Base path for resolving relative paths (defaults to cwd)
|
|
27
|
+
* @returns {string} Absolute path in native OS format
|
|
28
|
+
*/
|
|
29
|
+
static toAbsolutePath(inputPath, basePath = null) {
|
|
30
|
+
if (!inputPath) return '';
|
|
31
|
+
|
|
32
|
+
// If already absolute, return as-is
|
|
33
|
+
if (path.isAbsolute(inputPath)) {
|
|
34
|
+
return path.normalize(inputPath);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Resolve relative path
|
|
38
|
+
const base = basePath || process.cwd();
|
|
39
|
+
return path.resolve(base, inputPath);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Normalize path separators to forward slashes for consistency
|
|
44
|
+
* Does NOT remove drive letters or convert to POSIX - just normalizes separators
|
|
45
|
+
*
|
|
46
|
+
* @param {string} inputPath - Path to normalize
|
|
47
|
+
* @returns {string} Path with forward slashes
|
|
48
|
+
*/
|
|
49
|
+
static normalizeSeparators(inputPath) {
|
|
50
|
+
if (!inputPath) return '';
|
|
51
|
+
return inputPath.replace(/\\/g, '/');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get relative path from base with normalized separators
|
|
56
|
+
*
|
|
57
|
+
* @param {string} fullPath - Full absolute path
|
|
58
|
+
* @param {string} basePath - Base absolute path to subtract
|
|
59
|
+
* @returns {string} Relative path with forward slashes
|
|
60
|
+
*/
|
|
61
|
+
static getRelativePath(fullPath, basePath) {
|
|
62
|
+
const relativePath = path.relative(basePath, fullPath);
|
|
63
|
+
return this.normalizeSeparators(relativePath);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Sanitize an absolute path for use in database table names
|
|
68
|
+
* Converts path separators and special characters to underscores
|
|
69
|
+
* Preserves the path structure for uniqueness
|
|
70
|
+
*
|
|
71
|
+
* Examples:
|
|
72
|
+
* - /data/2023 -> data_2023
|
|
73
|
+
* - C:\Users\Documents -> c_users_documents
|
|
74
|
+
* - /home/user/project/sample -> home_user_project_sample
|
|
75
|
+
*
|
|
76
|
+
* @param {string} absolutePath - Absolute path to sanitize
|
|
77
|
+
* @returns {string} Sanitized string safe for table names
|
|
78
|
+
*/
|
|
79
|
+
static sanitizeForTableName(absolutePath) {
|
|
80
|
+
if (!absolutePath) return '';
|
|
81
|
+
|
|
82
|
+
return absolutePath
|
|
83
|
+
.toLowerCase()
|
|
84
|
+
.replace(/^[a-z]:/i, '') // Remove Windows drive letter (C:, D:, etc.)
|
|
85
|
+
.replace(/[^a-z0-9]/g, '_') // Replace all non-alphanumeric with underscore
|
|
86
|
+
.replace(/_+/g, '_') // Replace multiple underscores with single
|
|
87
|
+
.replace(/^_|_$/g, ''); // Remove leading/trailing underscores
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Generate a PostgreSQL-compatible table name from scan configuration
|
|
92
|
+
* Format: scan_<company>_<server>_<sanitized_absolute_path>
|
|
93
|
+
*
|
|
94
|
+
* Uses the absolute path to ensure uniqueness:
|
|
95
|
+
* - ../sample -> /data/sample
|
|
96
|
+
* - ./sample -> /data/project/sample
|
|
97
|
+
* These create different table names!
|
|
98
|
+
*
|
|
99
|
+
* Handles the 63-character PostgreSQL limit by:
|
|
100
|
+
* 1. Trying the full name first
|
|
101
|
+
* 2. If too long, truncating and adding an 8-character hash for uniqueness
|
|
102
|
+
*
|
|
103
|
+
* @param {Object} config - Scan configuration
|
|
104
|
+
* @param {string} config.companySlug - Company identifier
|
|
105
|
+
* @param {string} config.serverId - Server identifier
|
|
106
|
+
* @param {string} config.basePathLabel - Absolute base path
|
|
107
|
+
* @returns {string} PostgreSQL-compatible table name
|
|
108
|
+
*/
|
|
109
|
+
static generateTableName(config) {
|
|
110
|
+
const { companySlug, serverId, basePathLabel } = config;
|
|
111
|
+
|
|
112
|
+
// Sanitize each component (basePathLabel should already be absolute)
|
|
113
|
+
const sanitizedCompany = companySlug
|
|
114
|
+
.toLowerCase()
|
|
115
|
+
.replace(/[^a-z0-9]/g, '_');
|
|
116
|
+
const sanitizedServer = serverId.toLowerCase().replace(/[^a-z0-9]/g, '_');
|
|
117
|
+
const sanitizedPath = this.sanitizeForTableName(basePathLabel);
|
|
118
|
+
|
|
119
|
+
// Build raw name for hashing (preserve original for uniqueness)
|
|
120
|
+
const rawName = `${companySlug}_${serverId}_${basePathLabel}`;
|
|
121
|
+
|
|
122
|
+
// Build sanitized table name
|
|
123
|
+
let tableName = `scan_${sanitizedCompany}_${sanitizedServer}`;
|
|
124
|
+
|
|
125
|
+
if (sanitizedPath) {
|
|
126
|
+
tableName += `_${sanitizedPath}`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// PostgreSQL table name limit is 63 characters
|
|
130
|
+
if (tableName.length > 63) {
|
|
131
|
+
// Generate a hash for uniqueness
|
|
132
|
+
const hash = crypto
|
|
133
|
+
.createHash('md5')
|
|
134
|
+
.update(rawName)
|
|
135
|
+
.digest('hex')
|
|
136
|
+
.substring(0, 8);
|
|
137
|
+
|
|
138
|
+
const basePrefix = 'scan_';
|
|
139
|
+
const prefix = `${basePrefix}${sanitizedCompany}_${sanitizedServer}_`;
|
|
140
|
+
|
|
141
|
+
// Check if even prefix + hash exceeds limit
|
|
142
|
+
if (prefix.length + hash.length > 63) {
|
|
143
|
+
// Company/server names are too long, need to truncate them too
|
|
144
|
+
const maxCompanyServerLength = 63 - basePrefix.length - hash.length - 2; // -2 for underscores
|
|
145
|
+
const halfLength = Math.floor(maxCompanyServerLength / 2);
|
|
146
|
+
const companyLength = halfLength;
|
|
147
|
+
const serverLength = maxCompanyServerLength - companyLength;
|
|
148
|
+
|
|
149
|
+
const truncatedCompany = sanitizedCompany.substring(0, companyLength);
|
|
150
|
+
const truncatedServer = sanitizedServer.substring(0, serverLength);
|
|
151
|
+
|
|
152
|
+
tableName = `${basePrefix}${truncatedCompany}_${truncatedServer}_${hash}`;
|
|
153
|
+
} else {
|
|
154
|
+
// Preserve start and end of path, put hash in middle
|
|
155
|
+
const availableSpace = 63 - prefix.length - hash.length - 2; // -2 for underscores around hash
|
|
156
|
+
|
|
157
|
+
if (availableSpace <= 0 || !sanitizedPath) {
|
|
158
|
+
// If no space for path or path is empty, just use hash
|
|
159
|
+
tableName = `${prefix}${hash}`;
|
|
160
|
+
} else if (sanitizedPath.length <= availableSpace) {
|
|
161
|
+
// Path fits in available space, use it all
|
|
162
|
+
tableName = `${prefix}${sanitizedPath}_${hash}`;
|
|
163
|
+
} else {
|
|
164
|
+
// Need to split: preserve start and end
|
|
165
|
+
const halfSpace = Math.floor(availableSpace / 2);
|
|
166
|
+
const startLength = halfSpace;
|
|
167
|
+
const endLength = availableSpace - startLength;
|
|
168
|
+
|
|
169
|
+
// Extract start and end portions of the sanitized path
|
|
170
|
+
const pathStart = sanitizedPath.substring(0, startLength);
|
|
171
|
+
const pathEnd = sanitizedPath.substring(sanitizedPath.length - endLength);
|
|
172
|
+
|
|
173
|
+
// Build table name with start, hash, and end
|
|
174
|
+
tableName = `${prefix}${pathStart}_${hash}_${pathEnd}`;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return tableName;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Build an absolute path from base and relative component
|
|
184
|
+
*
|
|
185
|
+
* @param {string} basePath - Absolute base path
|
|
186
|
+
* @param {string} relativePath - Relative path from base (can be empty)
|
|
187
|
+
* @returns {string} Absolute path
|
|
188
|
+
*/
|
|
189
|
+
static buildAbsolutePath(basePath, relativePath = '') {
|
|
190
|
+
if (!relativePath || relativePath === '.') {
|
|
191
|
+
return basePath;
|
|
192
|
+
}
|
|
193
|
+
return path.resolve(basePath, relativePath);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Validate that a path can be used for table name generation
|
|
198
|
+
*
|
|
199
|
+
* @param {string} pathStr - Path to validate
|
|
200
|
+
* @returns {Object} Validation result { valid: boolean, error?: string }
|
|
201
|
+
*/
|
|
202
|
+
static validatePath(pathStr) {
|
|
203
|
+
if (!pathStr || typeof pathStr !== 'string') {
|
|
204
|
+
return { valid: false, error: 'Path is required and must be a string' };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (pathStr.trim() === '') {
|
|
208
|
+
return { valid: false, error: 'Path cannot be empty' };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Check for null bytes (security)
|
|
212
|
+
if (pathStr.includes('\0')) {
|
|
213
|
+
return { valid: false, error: 'Path cannot contain null bytes' };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return { valid: true };
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export default PathNormalizer;
|