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/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
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);
|