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 CHANGED
@@ -216,15 +216,35 @@ Patterns support glob-style matching with the following features:
216
216
 
217
217
  ### Features
218
218
 
219
+ - **Nested folder support** - Recursively syncs files in subdirectories
219
220
  - **Multiple sync folders** - Configure multiple sync pairs in one config
220
221
  - **Local & Global configs** - Per-project or machine-wide configurations
221
- - **File patterns** - Filter files by pattern
222
+ - **File patterns** - Filter files by pattern (applied to filenames, not paths)
222
223
  - **Auto-discovery** - Finds local config in current or parent directories
223
- - **Git-style diffs** - Colored diff output showing changes
224
- - **Automatic backups** - Creates timestamped backups before download
224
+ - **Git-style diffs** - Colored diff output showing changes with full paths
225
+ - **Automatic backups** - Creates timestamped backups before download (preserves folder structure)
225
226
  - **Two-way sync** - Upload local changes or download from Drive
227
+ - **Optional deletion** - Prompts to delete orphan files/folders after sync
226
228
  - **Sync All** - Run operations on all syncs within selected config
227
229
  - **Registered Local Configs** - Run sync on multiple projects from anywhere
230
+ - **Auto-create folders** - Creates missing folders on Drive during upload
231
+
232
+ ### Sync Behavior
233
+
234
+ The sync compares files between local and Drive, showing:
235
+
236
+ | Status | Diff | Upload | Download |
237
+ |--------|------|--------|----------|
238
+ | **Modified** | Shows diff | Updates on Drive | Updates locally |
239
+ | **Local only** | Listed | Uploads to Drive | Prompts to delete |
240
+ | **Drive only** | Listed | Prompts to delete | Downloads locally |
241
+ | **Empty folders** | Listed | Prompts to delete | — |
242
+
243
+ **Delete prompts:**
244
+ - **Upload**: After uploading, prompts to delete files/folders on Drive that don't exist locally
245
+ - **Download**: After downloading, prompts to delete local files that don't exist on Drive (backup is created first)
246
+
247
+ This ensures you can keep Drive in sync with local (or vice versa) without accumulating orphan files.
228
248
 
229
249
  ### Registered Local Configs
230
250
 
@@ -0,0 +1,142 @@
1
+ # Performance Optimization Decisions
2
+
3
+ This document records design decisions made for performance optimizations, including rationale and trade-offs considered.
4
+
5
+ ## Overview
6
+
7
+ The diff/sync operations were slow with nested folders and many files (e.g., 30+ files across multiple folder levels). Root cause: all Google Drive API calls were sequential and blocking.
8
+
9
+ ---
10
+
11
+ ## Decision 1: Parallel API Calls (Implemented)
12
+
13
+ **Problem:** Sequential API calls caused O(n) latency where n = number of files + folders.
14
+
15
+ **Solution:** Use async/parallel operations with batched concurrency.
16
+
17
+ **Implementation:**
18
+ - `listAsync()` - Non-blocking folder listing using `child_process.exec` with promises
19
+ - `downloadAsync()` - Non-blocking file downloads
20
+ - `downloadParallel()` - Batch downloads with configurable concurrency
21
+ - `listDriveFilesRecursiveOptimized()` - Breadth-first traversal querying multiple folders in parallel
22
+
23
+ **Concurrency Limit:** 5 concurrent requests per batch
24
+
25
+ **Rationale for batch size of 5:**
26
+ - Google Drive API limit: ~1,000 queries per 100 seconds per user (~10/sec sustained)
27
+ - Batch size of 5 is conservative, allowing headroom for other operations
28
+ - Can be increased to 8-10 if needed, but diminishing returns due to network latency
29
+
30
+ **Expected Performance Improvement:**
31
+ | Scenario | Before (sequential) | After (parallel, batch=5) |
32
+ |----------|---------------------|---------------------------|
33
+ | 30 files in nested folders | ~10-15 seconds | ~3-5 seconds |
34
+ | Download 30 files | ~6-15 seconds | ~1.5-3 seconds |
35
+
36
+ ---
37
+
38
+ ## Decision 2: Session-Scoped Folder Caching (Implemented)
39
+
40
+ **Problem:** When uploading multiple files to the same nested folder structure (e.g., 10 files to `root/a/b/c/`), the `findOrCreateDriveFolder` function would query the same parent folders repeatedly.
41
+
42
+ **Solution:** Session-scoped folder cache that is cleared at the start of each sync operation.
43
+
44
+ **Implementation:**
45
+ - `folderCache` - Map structure: `parentId -> Map(folderName -> folderId)`
46
+ - `clearFolderCache()` - Called at start of `runSyncOperation()` to ensure fresh data
47
+ - `getCachedFolderId()` / `cacheFolderId()` - Cache lookup and storage helpers
48
+ - Cache is used in `findOrCreateDriveFolder()` to avoid redundant API calls
49
+
50
+ **Why this is safe:**
51
+ 1. **Fresh data on every operation** - Cache is cleared at start, so we always query Drive for current state
52
+ 2. **Short-lived** - Cache only exists for duration of single sync (seconds)
53
+ 3. **Consistent within operation** - Folder structure shouldn't change during a 3-5 second sync
54
+
55
+ **Benefit:** If uploading 10 files to `root/a/b/c/`:
56
+ - Without cache: Query `a`, `b`, `c` 10 times each = 30 API calls
57
+ - With cache: Query `a`, `b`, `c` once each = 3 API calls
58
+
59
+ **Trade-off considered:** Risk of stale data if folder modified mid-sync.
60
+ - Acceptable risk given short operation duration (seconds)
61
+ - User can simply re-run sync if concurrent modification occurs
62
+
63
+ ---
64
+
65
+ ## Decision 3: Breadth-First vs Depth-First Listing
66
+
67
+ **Problem:** Original `listDriveFilesRecursive` used depth-first traversal, which creates a chain of blocking calls.
68
+
69
+ **Solution:** Breadth-first traversal with level-by-level parallel queries.
70
+
71
+ **How it works:**
72
+ ```
73
+ Level 0: Query root folder (1 API call)
74
+ Level 1: Query all subfolders in parallel (e.g., 5 folders = 1 batch)
75
+ Level 2: Query all sub-subfolders in parallel (e.g., 10 folders = 2 batches)
76
+ ...
77
+ ```
78
+
79
+ **Benefit:** Folders at the same depth are queried concurrently instead of sequentially.
80
+
81
+ **Trade-off:** Slightly more memory usage to hold the queue, but negligible for typical folder structures.
82
+
83
+ ---
84
+
85
+ ## Decision 4: Delta Sync (Implemented)
86
+
87
+ **Problem:** Previously, ALL files were downloaded from Drive to a temp directory for comparison, even if they hadn't changed. With 30 files, this meant 30 downloads every sync.
88
+
89
+ **Solution:** Compare file metadata (size + modification time) before downloading. Only download files where metadata indicates a potential change.
90
+
91
+ **Implementation:**
92
+ - `parseListOutput()` now extracts `sizeBytes` and `modifiedTime` from gdrive output
93
+ - `listLocalFilesRecursive()` now includes `sizeBytes` and `modifiedTime` from `fs.statSync()`
94
+ - Comparison logic in `runSyncOperation()`:
95
+ 1. Get local files with metadata
96
+ 2. For each Drive file, check if local exists with same size AND local is not older
97
+ 3. Skip download for unchanged files
98
+ 4. Only download and compare files where metadata differs
99
+
100
+ **Unchanged file detection:**
101
+ ```javascript
102
+ const sizeMatch = driveFile.sizeBytes === localFile.sizeBytes;
103
+ const timeMatch = driveTime <= localTime + 1000; // 1 second tolerance
104
+
105
+ if (sizeMatch && timeMatch) {
106
+ // Skip download - metadata indicates unchanged
107
+ }
108
+ ```
109
+
110
+ **Performance improvement:**
111
+
112
+ | Scenario | Before | After |
113
+ |----------|--------|-------|
114
+ | 30 files, none changed | Download 30 files | Download 0 files |
115
+ | 30 files, 5 changed | Download 30 files | Download 5 files |
116
+ | 30 files, all changed | Download 30 files | Download 30 files |
117
+
118
+ **Trade-offs:**
119
+ - Relies on accurate timestamps from Google Drive
120
+ - 1-second tolerance to handle clock differences
121
+ - Files with same size but different content AND older local time could be missed (rare edge case)
122
+
123
+ **Why not use hashes?**
124
+ - Google Drive provides `md5Checksum` but requires additional API call per file
125
+ - Size + time comparison is sufficient for most use cases and much faster
126
+ - Can add optional hash verification in future if needed
127
+
128
+ ---
129
+
130
+ ## Future Considerations
131
+
132
+ 1. **Configurable concurrency** - Allow users to adjust batch size via config if they have different rate limit needs
133
+ 2. **Progress callbacks** - Add progress reporting for large sync operations
134
+ 3. **Retry with exponential backoff** - Handle transient API failures gracefully
135
+ 4. **Optional hash verification** - Add `--verify` flag to compare MD5 checksums for critical syncs
136
+
137
+ ---
138
+
139
+ ## References
140
+
141
+ - Google Drive API Quotas: https://developers.google.com/drive/api/guides/limits
142
+ - Rate limit: 1,000 queries per 100 seconds per user
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdrive-syncer",
3
- "version": "3.0.0",
3
+ "version": "3.1.1",
4
4
  "description": "Google Drive Syncer",
5
5
  "main": "./index.js",
6
6
  "bin": "./run.js",
package/run.js CHANGED
@@ -6,6 +6,7 @@ const { cfgAdd, cfgRm, cfgShow } = require('./src/cfgManager');
6
6
  const { runSync } = require('./src/sync');
7
7
  const { runList, runSearch, runDelete, runMkdir, runListSync } = require('./src/list');
8
8
  const { envInit, envRun, envShow, envRemove, envRegister, envUnregister, envMigrate } = require('./src/envSync');
9
+ const { checkForUpdates } = require('./src/versionCheck');
9
10
 
10
11
  const [, , ...args] = process.argv;
11
12
  const [firstArg] = args;
@@ -81,10 +82,17 @@ const showHelp = () => {
81
82
  console.clear();
82
83
  intro(color.bgCyan(color.black(' GDrive Syncer ')));
83
84
 
85
+ // Check for updates and show at start if available
86
+ const updateMessage = await checkForUpdates();
87
+ if (updateMessage) {
88
+ note(updateMessage, 'Update Available');
89
+ }
90
+
84
91
  try {
85
92
  // Check for help
86
93
  if (firstArg === 'help' || firstArg === '--help' || firstArg === '-h') {
87
94
  showHelp();
95
+ if (updateMessage) note(updateMessage, 'Update Available');
88
96
  outro(color.green('Done!'));
89
97
  return;
90
98
  }
@@ -92,6 +100,7 @@ const showHelp = () => {
92
100
  // Check for direct command
93
101
  if (firstArg && commands[firstArg]) {
94
102
  await commands[firstArg].handler();
103
+ if (updateMessage) note(updateMessage, 'Update Available');
95
104
  outro(color.green('Done!'));
96
105
  return;
97
106
  }
@@ -119,6 +128,7 @@ const showHelp = () => {
119
128
 
120
129
  if (category === 'help') {
121
130
  showHelp();
131
+ if (updateMessage) note(updateMessage, 'Update Available');
122
132
  outro(color.green('Done!'));
123
133
  return;
124
134
  }
@@ -176,6 +186,8 @@ const showHelp = () => {
176
186
  await commands[action].handler();
177
187
  }
178
188
 
189
+ // Show update message at exit
190
+ if (updateMessage) note(updateMessage, 'Update Available');
179
191
  outro(color.green('Done!'));
180
192
  } catch (e) {
181
193
  log.error(e.message);