@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.
Files changed (61) hide show
  1. package/.env.local +316 -0
  2. package/.env.template +70 -0
  3. package/coverage/IdentifyCommand.js.html +1462 -0
  4. package/coverage/PropagateCommand.js.html +1507 -0
  5. package/coverage/PushCommand.js.html +1504 -0
  6. package/coverage/ScanCommand.js.html +1654 -0
  7. package/coverage/UploadCommand.js.html +1846 -0
  8. package/coverage/WatchCommand.js.html +4111 -0
  9. package/coverage/base.css +224 -0
  10. package/coverage/block-navigation.js +87 -0
  11. package/coverage/favicon.png +0 -0
  12. package/coverage/index.html +191 -0
  13. package/coverage/lcov-report/IdentifyCommand.js.html +1462 -0
  14. package/coverage/lcov-report/PropagateCommand.js.html +1507 -0
  15. package/coverage/lcov-report/PushCommand.js.html +1504 -0
  16. package/coverage/lcov-report/ScanCommand.js.html +1654 -0
  17. package/coverage/lcov-report/UploadCommand.js.html +1846 -0
  18. package/coverage/lcov-report/WatchCommand.js.html +4111 -0
  19. package/coverage/lcov-report/base.css +224 -0
  20. package/coverage/lcov-report/block-navigation.js +87 -0
  21. package/coverage/lcov-report/favicon.png +0 -0
  22. package/coverage/lcov-report/index.html +191 -0
  23. package/coverage/lcov-report/prettify.css +1 -0
  24. package/coverage/lcov-report/prettify.js +2 -0
  25. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  26. package/coverage/lcov-report/sorter.js +210 -0
  27. package/coverage/lcov.info +1937 -0
  28. package/coverage/prettify.css +1 -0
  29. package/coverage/prettify.js +2 -0
  30. package/coverage/sort-arrow-sprite.png +0 -0
  31. package/coverage/sorter.js +210 -0
  32. package/docs/API_RETRY_MECHANISM.md +338 -0
  33. package/docs/ARELA_IDENTIFY_IMPLEMENTATION.md +489 -0
  34. package/docs/ARELA_IDENTIFY_QUICKREF.md +186 -0
  35. package/docs/ARELA_PROPAGATE_IMPLEMENTATION.md +581 -0
  36. package/docs/ARELA_PROPAGATE_QUICKREF.md +272 -0
  37. package/docs/ARELA_PUSH_IMPLEMENTATION.md +577 -0
  38. package/docs/ARELA_PUSH_QUICKREF.md +322 -0
  39. package/docs/ARELA_SCAN_IMPLEMENTATION.md +373 -0
  40. package/docs/ARELA_SCAN_QUICKREF.md +139 -0
  41. package/docs/CROSS_PLATFORM_PATH_HANDLING.md +593 -0
  42. package/docs/DETECTION_ATTEMPT_TRACKING.md +414 -0
  43. package/docs/MIGRATION_UPLOADER_TO_FILE_STATS.md +1020 -0
  44. package/docs/MULTI_LEVEL_DIRECTORY_SCANNING.md +494 -0
  45. package/docs/STATS_COMMAND_SEQUENCE_DIAGRAM.md +287 -0
  46. package/docs/STATS_COMMAND_SIMPLE.md +93 -0
  47. package/package.json +31 -3
  48. package/src/commands/IdentifyCommand.js +459 -0
  49. package/src/commands/PropagateCommand.js +474 -0
  50. package/src/commands/PushCommand.js +473 -0
  51. package/src/commands/ScanCommand.js +523 -0
  52. package/src/config/config.js +154 -7
  53. package/src/file-detection.js +9 -10
  54. package/src/index.js +150 -0
  55. package/src/services/ScanApiService.js +645 -0
  56. package/src/utils/PathNormalizer.js +220 -0
  57. package/tests/commands/IdentifyCommand.test.js +570 -0
  58. package/tests/commands/PropagateCommand.test.js +568 -0
  59. package/tests/commands/PushCommand.test.js +754 -0
  60. package/tests/commands/ScanCommand.test.js +382 -0
  61. 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;