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/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: parts[3] || '',
275
- date: parts[4] || '',
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
+ };