gdrive-syncer 3.0.0 → 3.1.1
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 +23 -3
- package/docs/PERFORMANCE_DECISIONS.md +142 -0
- package/package.json +1 -1
- package/run.js +12 -0
- package/src/envSync.js +638 -73
- package/src/gdriveCmd.js +156 -3
- package/src/versionCheck.js +100 -0
package/src/gdriveCmd.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
const shell = require('shelljs');
|
|
4
|
+
const { exec } = require('child_process');
|
|
5
|
+
const { promisify } = require('util');
|
|
6
|
+
const execAsync = promisify(exec);
|
|
4
7
|
|
|
5
8
|
let cachedVersion = null;
|
|
6
9
|
|
|
@@ -254,7 +257,7 @@ const syncList = () => {
|
|
|
254
257
|
* Parse list output into structured data
|
|
255
258
|
* Works with both v2 and v3 output formats
|
|
256
259
|
* @param {string} stdout - Raw stdout from list command
|
|
257
|
-
* @returns {Array<{id: string, name: string, type: string, size: string, date: string}>}
|
|
260
|
+
* @returns {Array<{id: string, name: string, type: string, size: string, sizeBytes: number, date: string, modifiedTime: Date|null}>}
|
|
258
261
|
*/
|
|
259
262
|
const parseListOutput = (stdout) => {
|
|
260
263
|
const lines = stdout.trim().split('\n').filter((line) => line.trim());
|
|
@@ -267,16 +270,64 @@ const parseListOutput = (stdout) => {
|
|
|
267
270
|
|
|
268
271
|
return lines.map((line) => {
|
|
269
272
|
const parts = line.trim().split(separator);
|
|
273
|
+
const size = parts[3] || '';
|
|
274
|
+
const date = parts[4] || '';
|
|
275
|
+
|
|
270
276
|
return {
|
|
271
277
|
id: parts[0] || '',
|
|
272
278
|
name: parts[1] || '',
|
|
273
279
|
type: parts[2] || '',
|
|
274
|
-
size
|
|
275
|
-
|
|
280
|
+
size,
|
|
281
|
+
sizeBytes: parseSizeToBytes(size),
|
|
282
|
+
date,
|
|
283
|
+
modifiedTime: parseGdriveDate(date),
|
|
276
284
|
};
|
|
277
285
|
});
|
|
278
286
|
};
|
|
279
287
|
|
|
288
|
+
/**
|
|
289
|
+
* Parse size string to bytes (e.g., "1.5 KB" -> 1536)
|
|
290
|
+
* @param {string} sizeStr - Size string from gdrive output
|
|
291
|
+
* @returns {number} - Size in bytes, or 0 if unparseable
|
|
292
|
+
*/
|
|
293
|
+
const parseSizeToBytes = (sizeStr) => {
|
|
294
|
+
if (!sizeStr) return 0;
|
|
295
|
+
|
|
296
|
+
const match = sizeStr.match(/^([\d.]+)\s*(B|KB|MB|GB|TB)?$/i);
|
|
297
|
+
if (!match) return 0;
|
|
298
|
+
|
|
299
|
+
const value = parseFloat(match[1]);
|
|
300
|
+
const unit = (match[2] || 'B').toUpperCase();
|
|
301
|
+
|
|
302
|
+
const multipliers = {
|
|
303
|
+
B: 1,
|
|
304
|
+
KB: 1024,
|
|
305
|
+
MB: 1024 * 1024,
|
|
306
|
+
GB: 1024 * 1024 * 1024,
|
|
307
|
+
TB: 1024 * 1024 * 1024 * 1024,
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
return Math.round(value * (multipliers[unit] || 1));
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Parse gdrive date string to Date object
|
|
315
|
+
* gdrive outputs dates like "2024-01-15 10:30:00" or ISO format
|
|
316
|
+
* @param {string} dateStr - Date string from gdrive output
|
|
317
|
+
* @returns {Date|null} - Date object or null if unparseable
|
|
318
|
+
*/
|
|
319
|
+
const parseGdriveDate = (dateStr) => {
|
|
320
|
+
if (!dateStr) return null;
|
|
321
|
+
|
|
322
|
+
// Try parsing as-is (handles ISO format and common formats)
|
|
323
|
+
const date = new Date(dateStr);
|
|
324
|
+
if (!isNaN(date.getTime())) {
|
|
325
|
+
return date;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return null;
|
|
329
|
+
};
|
|
330
|
+
|
|
280
331
|
/**
|
|
281
332
|
* Clear the cached version (useful for testing)
|
|
282
333
|
*/
|
|
@@ -284,12 +335,112 @@ const clearCache = () => {
|
|
|
284
335
|
cachedVersion = null;
|
|
285
336
|
};
|
|
286
337
|
|
|
338
|
+
/**
|
|
339
|
+
* Async version of list - for parallel operations
|
|
340
|
+
* @param {Object} options - Same options as list()
|
|
341
|
+
* @returns {Promise<{code: number, stdout: string, stderr: string}>}
|
|
342
|
+
*/
|
|
343
|
+
const listAsync = async (options = {}) => {
|
|
344
|
+
const version = detectVersion();
|
|
345
|
+
const { query, max = 30, noHeader = false, absolute = false, parent } = options;
|
|
346
|
+
|
|
347
|
+
let cmd;
|
|
348
|
+
if (version === 2) {
|
|
349
|
+
cmd = 'gdrive list';
|
|
350
|
+
if (max) cmd += ` --max ${max}`;
|
|
351
|
+
if (query) cmd += ` --query "${query}"`;
|
|
352
|
+
if (noHeader) cmd += ' --no-header';
|
|
353
|
+
if (absolute) cmd += ' --absolute';
|
|
354
|
+
} else {
|
|
355
|
+
cmd = 'gdrive files list';
|
|
356
|
+
if (max) cmd += ` --max ${max}`;
|
|
357
|
+
if (parent) {
|
|
358
|
+
cmd += ` --parent ${parent}`;
|
|
359
|
+
} else if (query) {
|
|
360
|
+
cmd += ` --query "${query}"`;
|
|
361
|
+
}
|
|
362
|
+
if (noHeader) cmd += ' --skip-header';
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
try {
|
|
366
|
+
const { stdout, stderr } = await execAsync(cmd);
|
|
367
|
+
return { code: 0, stdout: stdout || '', stderr: stderr || '' };
|
|
368
|
+
} catch (error) {
|
|
369
|
+
return { code: error.code || 1, stdout: error.stdout || '', stderr: error.stderr || error.message };
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Async version of download - for parallel downloads
|
|
375
|
+
* @param {string} fileId - File ID to download
|
|
376
|
+
* @param {Object} options - Same options as download()
|
|
377
|
+
* @returns {Promise<{code: number, stdout: string, stderr: string}>}
|
|
378
|
+
*/
|
|
379
|
+
const downloadAsync = async (fileId, options = {}) => {
|
|
380
|
+
const version = detectVersion();
|
|
381
|
+
const { destination, overwrite = false, recursive = false } = options;
|
|
382
|
+
|
|
383
|
+
let cmd;
|
|
384
|
+
if (version === 2) {
|
|
385
|
+
cmd = `gdrive download "${fileId}"`;
|
|
386
|
+
if (destination) cmd += ` --path "${destination}"`;
|
|
387
|
+
if (overwrite) cmd += ' --force';
|
|
388
|
+
if (recursive) cmd += ' -r';
|
|
389
|
+
} else {
|
|
390
|
+
cmd = `gdrive files download "${fileId}"`;
|
|
391
|
+
if (destination) cmd += ` --destination "${destination}"`;
|
|
392
|
+
if (overwrite) cmd += ' --overwrite';
|
|
393
|
+
if (recursive) cmd += ' --recursive';
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
try {
|
|
397
|
+
const { stdout, stderr } = await execAsync(cmd);
|
|
398
|
+
return { code: 0, stdout: stdout || '', stderr: stderr || '' };
|
|
399
|
+
} catch (error) {
|
|
400
|
+
return { code: error.code || 1, stdout: error.stdout || '', stderr: error.stderr || error.message };
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Download multiple files in parallel with concurrency limit and progress tracking
|
|
406
|
+
* @param {Array<{fileId: string, options: Object}>} downloads - Array of download requests
|
|
407
|
+
* @param {number} concurrency - Maximum concurrent downloads (default: 5)
|
|
408
|
+
* @param {Function} [onProgress] - Progress callback: (completed, total) => void
|
|
409
|
+
* @returns {Promise<Array<{code: number, stdout: string, stderr: string}>>}
|
|
410
|
+
*/
|
|
411
|
+
const downloadParallel = async (downloads, concurrency = 5, onProgress = null) => {
|
|
412
|
+
const results = [];
|
|
413
|
+
const total = downloads.length;
|
|
414
|
+
let completed = 0;
|
|
415
|
+
|
|
416
|
+
// Process in batches to limit concurrency
|
|
417
|
+
for (let i = 0; i < downloads.length; i += concurrency) {
|
|
418
|
+
const batch = downloads.slice(i, i + concurrency);
|
|
419
|
+
const batchResults = await Promise.all(
|
|
420
|
+
batch.map(async ({ fileId, options }) => {
|
|
421
|
+
const result = await downloadAsync(fileId, options);
|
|
422
|
+
completed++;
|
|
423
|
+
if (onProgress) {
|
|
424
|
+
onProgress(completed, total);
|
|
425
|
+
}
|
|
426
|
+
return result;
|
|
427
|
+
})
|
|
428
|
+
);
|
|
429
|
+
results.push(...batchResults);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return results;
|
|
433
|
+
};
|
|
434
|
+
|
|
287
435
|
module.exports = {
|
|
288
436
|
detectVersion,
|
|
289
437
|
getVersion,
|
|
290
438
|
hasSyncSupport,
|
|
291
439
|
list,
|
|
440
|
+
listAsync,
|
|
292
441
|
download,
|
|
442
|
+
downloadAsync,
|
|
443
|
+
downloadParallel,
|
|
293
444
|
upload,
|
|
294
445
|
update,
|
|
295
446
|
mkdir,
|
|
@@ -297,5 +448,7 @@ module.exports = {
|
|
|
297
448
|
syncUpload,
|
|
298
449
|
syncList,
|
|
299
450
|
parseListOutput,
|
|
451
|
+
parseSizeToBytes,
|
|
452
|
+
parseGdriveDate,
|
|
300
453
|
clearCache,
|
|
301
454
|
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version check utility - warns user if a newer version is available
|
|
3
|
+
*/
|
|
4
|
+
const https = require('https');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const color = require('picocolors');
|
|
7
|
+
|
|
8
|
+
// Get current version from package.json
|
|
9
|
+
const getLocalVersion = () => {
|
|
10
|
+
const pkg = require(path.join(__dirname, '..', 'package.json'));
|
|
11
|
+
return pkg.version;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
// Get package name from package.json
|
|
15
|
+
const getPackageName = () => {
|
|
16
|
+
const pkg = require(path.join(__dirname, '..', 'package.json'));
|
|
17
|
+
return pkg.name;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Fetch latest version from npm registry
|
|
22
|
+
* @param {string} packageName - npm package name
|
|
23
|
+
* @returns {Promise<string|null>} - Latest version or null on error
|
|
24
|
+
*/
|
|
25
|
+
const fetchLatestVersion = (packageName) => {
|
|
26
|
+
return new Promise((resolve) => {
|
|
27
|
+
const url = `https://registry.npmjs.org/${packageName}/latest`;
|
|
28
|
+
|
|
29
|
+
const req = https.get(url, { timeout: 3000 }, (res) => {
|
|
30
|
+
let data = '';
|
|
31
|
+
|
|
32
|
+
res.on('data', (chunk) => {
|
|
33
|
+
data += chunk;
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
res.on('end', () => {
|
|
37
|
+
try {
|
|
38
|
+
const json = JSON.parse(data);
|
|
39
|
+
resolve(json.version || null);
|
|
40
|
+
} catch {
|
|
41
|
+
resolve(null);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
req.on('error', () => resolve(null));
|
|
47
|
+
req.on('timeout', () => {
|
|
48
|
+
req.destroy();
|
|
49
|
+
resolve(null);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Compare semver versions
|
|
56
|
+
* @param {string} current - Current version (e.g., "1.2.3")
|
|
57
|
+
* @param {string} latest - Latest version (e.g., "1.3.0")
|
|
58
|
+
* @returns {boolean} - True if latest is newer than current
|
|
59
|
+
*/
|
|
60
|
+
const isNewerVersion = (current, latest) => {
|
|
61
|
+
const currentParts = current.split('.').map(Number);
|
|
62
|
+
const latestParts = latest.split('.').map(Number);
|
|
63
|
+
|
|
64
|
+
for (let i = 0; i < 3; i++) {
|
|
65
|
+
const c = currentParts[i] || 0;
|
|
66
|
+
const l = latestParts[i] || 0;
|
|
67
|
+
if (l > c) return true;
|
|
68
|
+
if (l < c) return false;
|
|
69
|
+
}
|
|
70
|
+
return false;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Check for updates and return message if newer version available
|
|
75
|
+
* Non-blocking - doesn't throw errors, just silently fails
|
|
76
|
+
* @returns {Promise<string|null>} - Update message or null if up to date/error
|
|
77
|
+
*/
|
|
78
|
+
const checkForUpdates = async () => {
|
|
79
|
+
try {
|
|
80
|
+
const packageName = getPackageName();
|
|
81
|
+
const currentVersion = getLocalVersion();
|
|
82
|
+
const latestVersion = await fetchLatestVersion(packageName);
|
|
83
|
+
|
|
84
|
+
if (latestVersion && isNewerVersion(currentVersion, latestVersion)) {
|
|
85
|
+
return `Update available: ${color.dim(currentVersion)} → ${color.green(latestVersion)} (${color.cyan(packageName)})`;
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
} catch {
|
|
89
|
+
// Silently ignore errors - don't disrupt user experience
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
module.exports = {
|
|
95
|
+
getLocalVersion,
|
|
96
|
+
getPackageName,
|
|
97
|
+
fetchLatestVersion,
|
|
98
|
+
isNewerVersion,
|
|
99
|
+
checkForUpdates,
|
|
100
|
+
};
|