sftp-push-sync 1.0.21 → 2.1.0
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/CHANGELOG.md +18 -0
- package/README.md +63 -32
- package/bin/sftp-push-sync.mjs +220 -36
- package/package.json +1 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [2.0.0] - 2025-11-18
|
|
4
|
+
|
|
5
|
+
### Breaking
|
|
6
|
+
|
|
7
|
+
- CLI flags renamed:
|
|
8
|
+
- `--upload-list` → `--sidecar-upload`
|
|
9
|
+
- `--download-list` → `--sidecar-download`
|
|
10
|
+
- Configuration per connection restructured:
|
|
11
|
+
- `localRoot` / `remoteRoot` now under `sync`
|
|
12
|
+
- `sidecar` block for sidecar uploads/downloads
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- Separate `sidecar.localRoot` / `sidecar.remoteRoot` für Upload-/Download-Listen.
|
|
17
|
+
|
|
18
|
+
---
|
package/README.md
CHANGED
|
@@ -23,6 +23,12 @@ Features:
|
|
|
23
23
|
|
|
24
24
|
The file `sftp-push-sync.mjs` is pure JavaScript (ESM), not TypeScript. Node.js can execute it directly as long as "type": "module" is specified in package.json or the file has the extension .mjs.
|
|
25
25
|
|
|
26
|
+
## Breaking changes in 2.0.0
|
|
27
|
+
|
|
28
|
+
- The flags `--upload-list` / `--download-list` have been replaced by
|
|
29
|
+
`--sidecar-upload` / `--sidecar-download`.
|
|
30
|
+
- The settings for sidecars are now located in the `sidecar` block of the connection.
|
|
31
|
+
|
|
26
32
|
## Install
|
|
27
33
|
|
|
28
34
|
```bash
|
|
@@ -47,22 +53,40 @@ Create a `sync.config.json` in the root folder of your project:
|
|
|
47
53
|
"port": 23,
|
|
48
54
|
"user": "ftpuser",
|
|
49
55
|
"password": "mypassword",
|
|
50
|
-
"remoteRoot": "/folder/",
|
|
51
|
-
"localRoot": "public",
|
|
52
56
|
"syncCache": ".sync-cache.prod.json",
|
|
53
|
-
"worker": 3
|
|
57
|
+
"worker": 3,
|
|
58
|
+
"cleanupEmptyDirs": true,
|
|
59
|
+
"sync": {
|
|
60
|
+
"localRoot": "public",
|
|
61
|
+
"remoteRoot": "/folder/"
|
|
62
|
+
},
|
|
63
|
+
"sidecar": {
|
|
64
|
+
"localRoot": "sidecar-local",
|
|
65
|
+
"remoteRoot": "/sidecar-remote/",
|
|
66
|
+
"uploadList": [],
|
|
67
|
+
"downloadList": []
|
|
68
|
+
}
|
|
54
69
|
},
|
|
55
70
|
"staging": {
|
|
56
71
|
"host": "ftpserver02",
|
|
57
72
|
"port": 22,
|
|
58
73
|
"user": "ftp_user",
|
|
59
74
|
"password": "total_secret",
|
|
60
|
-
"remoteRoot": "/web/my-page/",
|
|
61
|
-
"localRoot": "public",
|
|
62
75
|
"syncCache": ".sync-cache.staging.json",
|
|
63
|
-
"worker": 1
|
|
76
|
+
"worker": 1,
|
|
77
|
+
"sync": {
|
|
78
|
+
"localRoot": "public",
|
|
79
|
+
"remoteRoot": "/web/my-page/"
|
|
80
|
+
},
|
|
81
|
+
"sidecar": {
|
|
82
|
+
"localRoot": "sidecar-local",
|
|
83
|
+
"remoteRoot": "/sidecar-remote/",
|
|
84
|
+
"uploadList": [],
|
|
85
|
+
"downloadList": []
|
|
86
|
+
}
|
|
64
87
|
}
|
|
65
88
|
},
|
|
89
|
+
"cleanupEmptyDirs": true,
|
|
66
90
|
"include": [],
|
|
67
91
|
"exclude": ["**/.DS_Store", "**/.git/**", "**/node_modules/**"],
|
|
68
92
|
"textExtensions": [".html",".xml",".txt",".json",".js",".css",".md",".svg"],
|
|
@@ -72,9 +96,7 @@ Create a `sync.config.json` in the root folder of your project:
|
|
|
72
96
|
"analyzeChunk": 1
|
|
73
97
|
},
|
|
74
98
|
"logLevel": "normal",
|
|
75
|
-
"logFile": ".sftp-push-sync.{target}.log"
|
|
76
|
-
"uploadList": [],
|
|
77
|
-
"downloadList": ["download-counter.json"]
|
|
99
|
+
"logFile": ".sftp-push-sync.{target}.log"
|
|
78
100
|
}
|
|
79
101
|
```
|
|
80
102
|
|
|
@@ -84,16 +106,18 @@ Create a `sync.config.json` in the root folder of your project:
|
|
|
84
106
|
# Normal synchronisation
|
|
85
107
|
node bin/sftp-push-sync.mjs staging
|
|
86
108
|
|
|
87
|
-
#
|
|
88
|
-
node bin/sftp-push-sync.mjs staging --upload
|
|
109
|
+
# Normal synchronisation + sidecar upload list
|
|
110
|
+
node bin/sftp-push-sync.mjs staging --sidecar-upload
|
|
111
|
+
|
|
112
|
+
# Normal synchronisation + sidecar download list
|
|
113
|
+
node bin/sftp-push-sync.mjs staging --sidecar-download
|
|
89
114
|
|
|
90
|
-
# Only lists, no standard synchronisation
|
|
91
|
-
node bin/sftp-push-sync.mjs staging --skip-sync --upload
|
|
92
|
-
node bin/sftp-push-sync.mjs staging --skip-sync --download
|
|
93
|
-
node bin/sftp-push-sync.mjs staging --skip-sync --upload-list --download-list
|
|
115
|
+
# Only sidecar lists, no standard synchronisation
|
|
116
|
+
node bin/sftp-push-sync.mjs staging --skip-sync --sidecar-upload
|
|
117
|
+
node bin/sftp-push-sync.mjs staging --skip-sync --sidecar-download
|
|
94
118
|
|
|
95
119
|
# (optional) only run lists dry
|
|
96
|
-
node bin/sftp-push-sync.mjs staging --skip-sync --upload
|
|
120
|
+
node bin/sftp-push-sync.mjs staging --skip-sync --sidecar-upload --dry-run
|
|
97
121
|
```
|
|
98
122
|
|
|
99
123
|
- Can be conveniently started via the scripts in `package.json`:
|
|
@@ -128,30 +152,29 @@ The dry run is a great way to compare files and fill the cache.
|
|
|
128
152
|
|
|
129
153
|
A list of files that are excluded from the sync comparison and can be downloaded or uploaded separately.
|
|
130
154
|
|
|
131
|
-
- `uploadList`
|
|
132
|
-
- Relative to localRoot "downloads.json"
|
|
133
|
-
|
|
134
|
-
-
|
|
135
|
-
- Relative to remoteRoot "download-counter.json"
|
|
136
|
-
- or e.g. "logs/download-counter.json"
|
|
155
|
+
- `sidecar.uploadList`
|
|
156
|
+
- Relative to sidecar.localRoot, e.g. "downloads.json" or "data/downloads.json"
|
|
157
|
+
- `sidecar.downloadList`
|
|
158
|
+
- Relative to sidecar.remoteRoot, e.g. "download-counter.json" or "logs/download-counter.json"
|
|
137
159
|
|
|
138
160
|
```bash
|
|
139
161
|
# normal synchronisation
|
|
140
162
|
sftp-push-sync staging
|
|
141
163
|
|
|
142
|
-
# Normal synchronisation + explicitly transfer upload list
|
|
143
|
-
sftp-push-sync staging --upload
|
|
164
|
+
# Normal synchronisation + explicitly transfer sidecar upload list
|
|
165
|
+
sftp-push-sync staging --sidecar-upload
|
|
144
166
|
|
|
145
|
-
# just fetch the download list from the server
|
|
146
|
-
|
|
147
|
-
sftp-push-sync prod --download-
|
|
167
|
+
# just fetch the sidecar download list from the server
|
|
168
|
+
# combined with normal synchronisation
|
|
169
|
+
sftp-push-sync prod --sidecar-download --dry-run # view first
|
|
170
|
+
sftp-push-sync prod --sidecar-download # then do
|
|
148
171
|
```
|
|
149
172
|
|
|
150
|
-
- The
|
|
151
|
-
-
|
|
173
|
+
- The sidecar is always executed together with sync when using `--sidecar-download` or `--sidecar-upload`.
|
|
174
|
+
- With `--skip-sync`, you can exclude the sync process and only process the sidecar:
|
|
152
175
|
|
|
153
176
|
```bash
|
|
154
|
-
sftp-push-sync prod --download
|
|
177
|
+
sftp-push-sync prod --sidecar-download --skip-sync
|
|
155
178
|
```
|
|
156
179
|
|
|
157
180
|
### Logging Progress
|
|
@@ -198,6 +221,14 @@ practical excludes:
|
|
|
198
221
|
]
|
|
199
222
|
```
|
|
200
223
|
|
|
224
|
+
### Folder handling
|
|
225
|
+
|
|
226
|
+
Sync only handles files and creates missing directories during upload.
|
|
227
|
+
However, it should also manage directories:
|
|
228
|
+
|
|
229
|
+
- If, for example, a directory is empty because all files have been deleted from it.
|
|
230
|
+
- Or if a directory no longer exists locally.
|
|
231
|
+
|
|
201
232
|
## Which files are needed?
|
|
202
233
|
|
|
203
234
|
- `sync.config.json` - The configuration file (with passwords in plain text, so please leave it out of the git repository)
|
|
@@ -207,9 +238,9 @@ practical excludes:
|
|
|
207
238
|
- The cache files: `.sync-cache.*.json`
|
|
208
239
|
- The log file: `.sftp-push-sync.{target}.log` (Optional, overwritten with each run)
|
|
209
240
|
|
|
210
|
-
You can safely delete the local cache at any time. The first analysis will then take longer
|
|
241
|
+
You can safely delete the local cache at any time. The first analysis will then take longer, because remote hashes will be streamed again. After that, everything will run fast.
|
|
211
242
|
|
|
212
|
-
Note: The first run always takes a while, especially with lots of
|
|
243
|
+
Note: The first run always takes a while, especially with lots of media – so be patient! Once the cache is full, it will be faster.
|
|
213
244
|
|
|
214
245
|
## Example Output
|
|
215
246
|
|
package/bin/sftp-push-sync.mjs
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* 1. Upload new files
|
|
9
9
|
* 2. Delete remote files that no longer exist locally
|
|
10
10
|
* 3. Detect changes based on size or modified content and upload them
|
|
11
|
+
* 4. Supports separate sidecar upload/download lists for special files
|
|
11
12
|
*
|
|
12
13
|
* Features:
|
|
13
14
|
* - multiple connections in sync.config.json
|
|
@@ -25,6 +26,11 @@
|
|
|
25
26
|
* - For example, log files or other special files.
|
|
26
27
|
* - These files can be downloaded or uploaded separately.
|
|
27
28
|
*
|
|
29
|
+
* Folder handling:
|
|
30
|
+
* Delete Folders if
|
|
31
|
+
* - If, for example, a directory is empty because all files have been deleted from it.
|
|
32
|
+
* - Or if a directory no longer exists locally.
|
|
33
|
+
*
|
|
28
34
|
* The file sftp-push-sync.mjs is pure JavaScript (ESM), not TypeScript.
|
|
29
35
|
* Node.js can execute it directly as long as "type": "module" is specified in package.json
|
|
30
36
|
* or the file has the extension .mjs.
|
|
@@ -63,8 +69,8 @@ const tab_b = () => " ".repeat(6);
|
|
|
63
69
|
const args = process.argv.slice(2);
|
|
64
70
|
const TARGET = args[0];
|
|
65
71
|
const DRY_RUN = args.includes("--dry-run");
|
|
66
|
-
const RUN_UPLOAD_LIST = args.includes("--upload
|
|
67
|
-
const RUN_DOWNLOAD_LIST = args.includes("--download
|
|
72
|
+
const RUN_UPLOAD_LIST = args.includes("--sidecar-upload");
|
|
73
|
+
const RUN_DOWNLOAD_LIST = args.includes("--sidecar-download");
|
|
68
74
|
const SKIP_SYNC = args.includes("--skip-sync");
|
|
69
75
|
|
|
70
76
|
// logLevel override via CLI (optional)
|
|
@@ -81,7 +87,9 @@ if (!TARGET) {
|
|
|
81
87
|
// Wenn jemand --skip-sync ohne Listen benutzt → sinnlos, also abbrechen
|
|
82
88
|
if (SKIP_SYNC && !RUN_UPLOAD_LIST && !RUN_DOWNLOAD_LIST) {
|
|
83
89
|
console.error(
|
|
84
|
-
pc.red(
|
|
90
|
+
pc.red(
|
|
91
|
+
"❌ --skip-sync requires at least --sidecar-upload or --sidecar-download."
|
|
92
|
+
)
|
|
85
93
|
);
|
|
86
94
|
process.exit(1);
|
|
87
95
|
}
|
|
@@ -113,12 +121,11 @@ if (!CONFIG_RAW.connections || typeof CONFIG_RAW.connections !== "object") {
|
|
|
113
121
|
// ---------------------------------------------------------------------------
|
|
114
122
|
// Logging helpers (Terminal + optional Logfile)
|
|
115
123
|
// ---------------------------------------------------------------------------
|
|
124
|
+
|
|
116
125
|
// Default: .sync.{TARGET}.log, kann via config.logFile überschrieben werden
|
|
117
126
|
const DEFAULT_LOG_FILE = `.sync.${TARGET}.log`;
|
|
118
127
|
const rawLogFilePattern = CONFIG_RAW.logFile || DEFAULT_LOG_FILE;
|
|
119
|
-
const LOG_FILE = path.resolve(
|
|
120
|
-
rawLogFilePattern.replace("{target}", TARGET)
|
|
121
|
-
);
|
|
128
|
+
const LOG_FILE = path.resolve(rawLogFilePattern.replace("{target}", TARGET));
|
|
122
129
|
let LOG_STREAM = null;
|
|
123
130
|
|
|
124
131
|
/** einmalig Logfile-Stream öffnen */
|
|
@@ -175,7 +182,7 @@ function rawConsoleWarn(...msg) {
|
|
|
175
182
|
writeLogLine("[WARN] " + line);
|
|
176
183
|
}
|
|
177
184
|
|
|
178
|
-
// High-level Helfer
|
|
185
|
+
// High-level Helfer
|
|
179
186
|
function log(...msg) {
|
|
180
187
|
rawConsoleLog(...msg);
|
|
181
188
|
}
|
|
@@ -205,13 +212,30 @@ if (!TARGET_CONFIG) {
|
|
|
205
212
|
process.exit(1);
|
|
206
213
|
}
|
|
207
214
|
|
|
215
|
+
// Haupt-Sync-Config + Sidecar
|
|
216
|
+
const SYNC_CFG = TARGET_CONFIG.sync ?? TARGET_CONFIG;
|
|
217
|
+
const SIDECAR_CFG = TARGET_CONFIG.sidecar ?? {};
|
|
218
|
+
|
|
219
|
+
if (!SYNC_CFG.localRoot || !SYNC_CFG.remoteRoot) {
|
|
220
|
+
console.error(
|
|
221
|
+
pc.red(
|
|
222
|
+
`❌ Connection '${TARGET}' is missing sync.localRoot or sync.remoteRoot.`
|
|
223
|
+
)
|
|
224
|
+
);
|
|
225
|
+
process.exit(1);
|
|
226
|
+
}
|
|
227
|
+
|
|
208
228
|
const CONNECTION = {
|
|
209
229
|
host: TARGET_CONFIG.host,
|
|
210
230
|
port: TARGET_CONFIG.port ?? 22,
|
|
211
231
|
user: TARGET_CONFIG.user,
|
|
212
232
|
password: TARGET_CONFIG.password,
|
|
213
|
-
|
|
214
|
-
|
|
233
|
+
// Main sync roots
|
|
234
|
+
localRoot: path.resolve(SYNC_CFG.localRoot),
|
|
235
|
+
remoteRoot: SYNC_CFG.remoteRoot,
|
|
236
|
+
// Sidecar roots (für sidecar-upload / sidecar-download)
|
|
237
|
+
sidecarLocalRoot: path.resolve(SIDECAR_CFG.localRoot ?? SYNC_CFG.localRoot),
|
|
238
|
+
sidecarRemoteRoot: SIDECAR_CFG.remoteRoot ?? SYNC_CFG.remoteRoot,
|
|
215
239
|
workers: TARGET_CONFIG.worker ?? 2,
|
|
216
240
|
};
|
|
217
241
|
|
|
@@ -235,6 +259,10 @@ const SCAN_CHUNK = PROGRESS.scanChunk ?? (IS_VERBOSE ? 1 : 100);
|
|
|
235
259
|
const ANALYZE_CHUNK = PROGRESS.analyzeChunk ?? (IS_VERBOSE ? 1 : 10);
|
|
236
260
|
// For >100k files, rather 10–50, for debugging/troubleshooting 1.
|
|
237
261
|
|
|
262
|
+
// Leere Verzeichnisse nach dem Sync entfernen?
|
|
263
|
+
const CLEANUP_EMPTY_DIRS = CONFIG_RAW.cleanupEmptyDirs ?? true;
|
|
264
|
+
const CLEANUP_EMPTY_ROOTS = CONFIG_RAW.cleanupEmptyRoots ?? false;
|
|
265
|
+
|
|
238
266
|
// ---------------------------------------------------------------------------
|
|
239
267
|
// Shared config from JSON
|
|
240
268
|
// ---------------------------------------------------------------------------
|
|
@@ -274,7 +302,7 @@ const MEDIA_EXT = CONFIG_RAW.mediaExtensions ?? [
|
|
|
274
302
|
".pdf",
|
|
275
303
|
];
|
|
276
304
|
|
|
277
|
-
// Special: Lists for targeted uploads/downloads
|
|
305
|
+
// Special: Lists for targeted uploads/downloads (per-connection sidecar)
|
|
278
306
|
function normalizeList(list) {
|
|
279
307
|
if (!Array.isArray(list)) return [];
|
|
280
308
|
return list.flatMap((item) =>
|
|
@@ -287,15 +315,16 @@ function normalizeList(list) {
|
|
|
287
315
|
);
|
|
288
316
|
}
|
|
289
317
|
|
|
290
|
-
|
|
291
|
-
const
|
|
318
|
+
// Lists from sidecar config (relative to sidecar.localRoot / sidecar.remoteRoot)
|
|
319
|
+
const UPLOAD_LIST = normalizeList(SIDECAR_CFG.uploadList ?? []);
|
|
320
|
+
const DOWNLOAD_LIST = normalizeList(SIDECAR_CFG.downloadList ?? []);
|
|
292
321
|
|
|
293
322
|
// Effektive Exclude-Liste: explizites exclude + Upload/Download-Listen
|
|
294
323
|
// → diese Dateien werden im „normalen“ Sync nicht angerührt,
|
|
295
|
-
// sondern nur über die
|
|
324
|
+
// sondern nur über die Sidecar-Mechanik behandelt.
|
|
296
325
|
const EXCLUDE = [...BASE_EXCLUDE, ...UPLOAD_LIST, ...DOWNLOAD_LIST];
|
|
297
326
|
|
|
298
|
-
// List of ALL files that were
|
|
327
|
+
// List of ALL files that were ausgeschlossen durch uploadList/downloadList
|
|
299
328
|
const AUTO_EXCLUDED = new Set();
|
|
300
329
|
|
|
301
330
|
// Cache file name per connection
|
|
@@ -384,7 +413,7 @@ function isIncluded(relPath) {
|
|
|
384
413
|
if (INCLUDE.length > 0 && !matchesAny(INCLUDE, relPath)) return false;
|
|
385
414
|
// Exclude-Regeln
|
|
386
415
|
if (EXCLUDE.length > 0 && matchesAny(EXCLUDE, relPath)) {
|
|
387
|
-
// Falls durch
|
|
416
|
+
// Falls durch Sidecar-Listen → merken
|
|
388
417
|
if (UPLOAD_LIST.includes(relPath) || DOWNLOAD_LIST.includes(relPath)) {
|
|
389
418
|
AUTO_EXCLUDED.add(relPath);
|
|
390
419
|
}
|
|
@@ -424,14 +453,12 @@ function shortenPathForProgress(rel) {
|
|
|
424
453
|
function updateProgress2(prefix, current, total, rel = "") {
|
|
425
454
|
const short = rel ? shortenPathForProgress(rel) : "";
|
|
426
455
|
|
|
427
|
-
//Log file: always as a single line with **full** rel path
|
|
456
|
+
// Log file: always as a single line with **full** rel path
|
|
428
457
|
const base =
|
|
429
458
|
total && total > 0
|
|
430
459
|
? `${prefix}${current}/${total} Files`
|
|
431
460
|
: `${prefix}${current} Files`;
|
|
432
|
-
writeLogLine(
|
|
433
|
-
`[progress] ${base}${rel ? " – " + rel : ""}`
|
|
434
|
-
);
|
|
461
|
+
writeLogLine(`[progress] ${base}${rel ? " – " + rel : ""}`);
|
|
435
462
|
|
|
436
463
|
if (!process.stdout.isTTY) {
|
|
437
464
|
// Fallback-Terminal
|
|
@@ -507,6 +534,124 @@ async function runTasks(items, workerCount, handler, label = "Tasks") {
|
|
|
507
534
|
await Promise.all(workers);
|
|
508
535
|
}
|
|
509
536
|
|
|
537
|
+
// ---------------------------------------------------------------------------
|
|
538
|
+
// Neue Helper: Verzeichnisse für Uploads/Updates vorbereiten
|
|
539
|
+
// ---------------------------------------------------------------------------
|
|
540
|
+
|
|
541
|
+
function collectDirsFromChanges(changes) {
|
|
542
|
+
const dirs = new Set();
|
|
543
|
+
|
|
544
|
+
for (const item of changes) {
|
|
545
|
+
const rel = item.rel;
|
|
546
|
+
if (!rel) continue;
|
|
547
|
+
|
|
548
|
+
const parts = rel.split("/");
|
|
549
|
+
if (parts.length <= 1) continue; // Dateien im Root
|
|
550
|
+
|
|
551
|
+
let acc = "";
|
|
552
|
+
for (let i = 0; i < parts.length - 1; i += 1) {
|
|
553
|
+
acc = acc ? `${acc}/${parts[i]}` : parts[i];
|
|
554
|
+
dirs.add(acc);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// flachere Pfade zuerst, damit Eltern vor Kindern angelegt werden
|
|
559
|
+
return [...dirs].sort(
|
|
560
|
+
(a, b) => a.split("/").length - b.split("/").length
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
async function ensureAllRemoteDirsExist(sftp, remoteRoot, toAdd, toUpdate) {
|
|
565
|
+
const dirs = collectDirsFromChanges([...toAdd, ...toUpdate]);
|
|
566
|
+
|
|
567
|
+
for (const relDir of dirs) {
|
|
568
|
+
const remoteDir = path.posix.join(remoteRoot, relDir);
|
|
569
|
+
try {
|
|
570
|
+
await sftp.mkdir(remoteDir, true);
|
|
571
|
+
vlog(`${tab_a()}${pc.dim("dir ok:")} ${remoteDir}`);
|
|
572
|
+
} catch {
|
|
573
|
+
// Directory may already exist / keine Rechte – ignorieren
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// -----------------------------------------------------------
|
|
579
|
+
// Cleanup: remove *only truly empty* directories on remote
|
|
580
|
+
// -----------------------------------------------------------
|
|
581
|
+
|
|
582
|
+
async function cleanupEmptyDirs(sftp, rootDir) {
|
|
583
|
+
// Rekursiv prüfen, ob ein Verzeichnis und seine Unterverzeichnisse
|
|
584
|
+
// KEINE Dateien enthalten. Nur dann löschen wir es.
|
|
585
|
+
async function recurse(dir, depth = 0) {
|
|
586
|
+
let hasFile = false;
|
|
587
|
+
const subdirs = [];
|
|
588
|
+
|
|
589
|
+
let items;
|
|
590
|
+
try {
|
|
591
|
+
items = await sftp.list(dir);
|
|
592
|
+
} catch (e) {
|
|
593
|
+
// Falls das Verzeichnis inzwischen weg ist o.ä., brechen wir hier ab.
|
|
594
|
+
wlog(
|
|
595
|
+
pc.yellow("⚠️ Could not list directory during cleanup:"),
|
|
596
|
+
dir,
|
|
597
|
+
e.message || e
|
|
598
|
+
);
|
|
599
|
+
return false;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
for (const item of items) {
|
|
603
|
+
if (!item.name || item.name === "." || item.name === "..") continue;
|
|
604
|
+
|
|
605
|
+
if (item.type === "d") {
|
|
606
|
+
subdirs.push(item);
|
|
607
|
+
} else {
|
|
608
|
+
// Jede Datei (egal ob sie nach INCLUDE/EXCLUDE
|
|
609
|
+
// sonst ignoriert würde) verhindert das Löschen.
|
|
610
|
+
hasFile = true;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Erst alle Unterverzeichnisse aufräumen (post-order)
|
|
615
|
+
let allSubdirsEmpty = true;
|
|
616
|
+
for (const sub of subdirs) {
|
|
617
|
+
const full = path.posix.join(dir, sub.name);
|
|
618
|
+
const subEmpty = await recurse(full, depth + 1);
|
|
619
|
+
if (!subEmpty) {
|
|
620
|
+
allSubdirsEmpty = false;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const isRoot = dir === rootDir;
|
|
625
|
+
const isEmpty = !hasFile && allSubdirsEmpty;
|
|
626
|
+
|
|
627
|
+
// Root nur löschen, wenn explizit erlaubt
|
|
628
|
+
if (isEmpty && (!isRoot || CLEANUP_EMPTY_ROOTS)) {
|
|
629
|
+
const rel = toPosix(path.relative(rootDir, dir)) || ".";
|
|
630
|
+
if (DRY_RUN) {
|
|
631
|
+
log(`${tab_a()}${DEL} (DRY-RUN) Remove empty directory: ${rel}`);
|
|
632
|
+
} else {
|
|
633
|
+
try {
|
|
634
|
+
// Nicht rekursiv: wir löschen nur, wenn unser eigener Check "leer" sagt.
|
|
635
|
+
await sftp.rmdir(dir, false);
|
|
636
|
+
log(`${tab_a()}${DEL} Removed empty directory: ${rel}`);
|
|
637
|
+
} catch (e) {
|
|
638
|
+
wlog(
|
|
639
|
+
pc.yellow("⚠️ Could not remove directory:"),
|
|
640
|
+
dir,
|
|
641
|
+
e.message || e
|
|
642
|
+
);
|
|
643
|
+
// Falls rmdir scheitert, betrachten wir das Verzeichnis als "nicht leer"
|
|
644
|
+
return false;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
return isEmpty;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
await recurse(rootDir, 0);
|
|
653
|
+
}
|
|
654
|
+
|
|
510
655
|
// ---------------------------------------------------------------------------
|
|
511
656
|
// Local file walker (recursive, all subdirectories)
|
|
512
657
|
// ---------------------------------------------------------------------------
|
|
@@ -597,7 +742,7 @@ async function walkRemote(sftp, remoteRoot) {
|
|
|
597
742
|
const full = path.posix.join(remoteDir, item.name);
|
|
598
743
|
const rel = prefix ? `${prefix}/${item.name}` : item.name;
|
|
599
744
|
|
|
600
|
-
//
|
|
745
|
+
// Include/Exclude-Regeln auch auf Remote anwenden
|
|
601
746
|
if (!isIncluded(rel)) continue;
|
|
602
747
|
|
|
603
748
|
if (item.type === "d") {
|
|
@@ -771,16 +916,16 @@ function describeSftpError(err) {
|
|
|
771
916
|
}
|
|
772
917
|
|
|
773
918
|
// ---------------------------------------------------------------------------
|
|
774
|
-
// Bypass-only Mode (
|
|
919
|
+
// Bypass-only Mode (sidecar-upload / sidecar-download ohne normalen Sync)
|
|
775
920
|
// ---------------------------------------------------------------------------
|
|
776
921
|
|
|
777
922
|
async function collectUploadTargets() {
|
|
778
|
-
const all = await walkLocalPlain(CONNECTION.
|
|
923
|
+
const all = await walkLocalPlain(CONNECTION.sidecarLocalRoot);
|
|
779
924
|
const results = [];
|
|
780
925
|
|
|
781
926
|
for (const [rel, meta] of all.entries()) {
|
|
782
927
|
if (matchesAny(UPLOAD_LIST, rel)) {
|
|
783
|
-
const remotePath = path.posix.join(CONNECTION.
|
|
928
|
+
const remotePath = path.posix.join(CONNECTION.sidecarRemoteRoot, rel);
|
|
784
929
|
results.push({
|
|
785
930
|
rel,
|
|
786
931
|
localPath: meta.localPath,
|
|
@@ -793,12 +938,12 @@ async function collectUploadTargets() {
|
|
|
793
938
|
}
|
|
794
939
|
|
|
795
940
|
async function collectDownloadTargets(sftp) {
|
|
796
|
-
const all = await walkRemotePlain(sftp, CONNECTION.
|
|
941
|
+
const all = await walkRemotePlain(sftp, CONNECTION.sidecarRemoteRoot);
|
|
797
942
|
const results = [];
|
|
798
943
|
|
|
799
944
|
for (const [rel, meta] of all.entries()) {
|
|
800
945
|
if (matchesAny(DOWNLOAD_LIST, rel)) {
|
|
801
|
-
const localPath = path.join(CONNECTION.
|
|
946
|
+
const localPath = path.join(CONNECTION.sidecarLocalRoot, rel);
|
|
802
947
|
results.push({
|
|
803
948
|
rel,
|
|
804
949
|
remotePath: meta.remotePath,
|
|
@@ -813,10 +958,20 @@ async function collectDownloadTargets(sftp) {
|
|
|
813
958
|
async function performBypassOnly(sftp) {
|
|
814
959
|
log("");
|
|
815
960
|
log(pc.bold(pc.cyan("🚀 Bypass-Only Mode (skip-sync)")));
|
|
961
|
+
log(`${tab_a()}Sidecar Local: ${pc.green(CONNECTION.sidecarLocalRoot)}`);
|
|
962
|
+
log(`${tab_a()}Sidecar Remote: ${pc.green(CONNECTION.sidecarRemoteRoot)}`);
|
|
963
|
+
|
|
964
|
+
if (RUN_UPLOAD_LIST && !fs.existsSync(CONNECTION.sidecarLocalRoot)) {
|
|
965
|
+
elog(
|
|
966
|
+
pc.red("❌ Sidecar local root does not exist:"),
|
|
967
|
+
CONNECTION.sidecarLocalRoot
|
|
968
|
+
);
|
|
969
|
+
process.exit(1);
|
|
970
|
+
}
|
|
816
971
|
|
|
817
972
|
if (RUN_UPLOAD_LIST) {
|
|
818
973
|
log("");
|
|
819
|
-
log(pc.bold(pc.cyan("⬆️ Upload-Bypass (
|
|
974
|
+
log(pc.bold(pc.cyan("⬆️ Upload-Bypass (sidecar-upload) …")));
|
|
820
975
|
const targets = await collectUploadTargets();
|
|
821
976
|
log(`${tab_a()}→ ${targets.length} files from uploadList`);
|
|
822
977
|
|
|
@@ -845,7 +1000,7 @@ async function performBypassOnly(sftp) {
|
|
|
845
1000
|
|
|
846
1001
|
if (RUN_DOWNLOAD_LIST) {
|
|
847
1002
|
log("");
|
|
848
|
-
log(pc.bold(pc.cyan("⬇️ Download-Bypass (
|
|
1003
|
+
log(pc.bold(pc.cyan("⬇️ Download-Bypass (sidecar-download) …")));
|
|
849
1004
|
const targets = await collectDownloadTargets(sftp);
|
|
850
1005
|
log(`${tab_a()}→ ${targets.length} files from downloadList`);
|
|
851
1006
|
|
|
@@ -902,17 +1057,24 @@ async function main() {
|
|
|
902
1057
|
);
|
|
903
1058
|
log(`${tab_a()}Local: ${pc.green(CONNECTION.localRoot)}`);
|
|
904
1059
|
log(`${tab_a()}Remote: ${pc.green(CONNECTION.remoteRoot)}`);
|
|
1060
|
+
if (RUN_UPLOAD_LIST || RUN_DOWNLOAD_LIST || SKIP_SYNC) {
|
|
1061
|
+
log(`${tab_a()}Sidecar Local: ${pc.green(CONNECTION.sidecarLocalRoot)}`);
|
|
1062
|
+
log(`${tab_a()}Sidecar Remote: ${pc.green(CONNECTION.sidecarRemoteRoot)}`);
|
|
1063
|
+
}
|
|
905
1064
|
if (DRY_RUN) log(pc.yellow(`${tab_a()}Mode: DRY-RUN (no changes)`));
|
|
906
1065
|
if (SKIP_SYNC) log(pc.yellow(`${tab_a()}Mode: SKIP-SYNC (bypass only)`));
|
|
907
1066
|
if (RUN_UPLOAD_LIST || RUN_DOWNLOAD_LIST) {
|
|
908
1067
|
log(
|
|
909
1068
|
pc.blue(
|
|
910
|
-
`${tab_a()}Extra: ${
|
|
911
|
-
|
|
912
|
-
}`
|
|
1069
|
+
`${tab_a()}Extra: ${
|
|
1070
|
+
RUN_UPLOAD_LIST ? "sidecar-upload " : ""
|
|
1071
|
+
}${RUN_DOWNLOAD_LIST ? "sidecar-download" : ""}`
|
|
913
1072
|
)
|
|
914
1073
|
);
|
|
915
1074
|
}
|
|
1075
|
+
if (CLEANUP_EMPTY_DIRS) {
|
|
1076
|
+
log(`${tab_a()}Cleanup empty dirs: ${pc.green("enabled")}`);
|
|
1077
|
+
}
|
|
916
1078
|
if (LOG_FILE) {
|
|
917
1079
|
log(`${tab_a()}LogFile: ${pc.cyan(LOG_FILE)}`);
|
|
918
1080
|
}
|
|
@@ -937,7 +1099,7 @@ async function main() {
|
|
|
937
1099
|
connected = true;
|
|
938
1100
|
log(pc.green(`${tab_a()}✔ Connected to SFTP.`));
|
|
939
1101
|
|
|
940
|
-
if (!fs.existsSync(CONNECTION.localRoot)) {
|
|
1102
|
+
if (!SKIP_SYNC && !fs.existsSync(CONNECTION.localRoot)) {
|
|
941
1103
|
console.error(
|
|
942
1104
|
pc.red("❌ Local root does not exist:"),
|
|
943
1105
|
CONNECTION.localRoot
|
|
@@ -946,7 +1108,7 @@ async function main() {
|
|
|
946
1108
|
}
|
|
947
1109
|
|
|
948
1110
|
// -------------------------------------------------------------
|
|
949
|
-
// SKIP-SYNC-Modus → nur
|
|
1111
|
+
// SKIP-SYNC-Modus → nur Sidecar-Listen
|
|
950
1112
|
// -------------------------------------------------------------
|
|
951
1113
|
if (SKIP_SYNC) {
|
|
952
1114
|
await performBypassOnly(sftp);
|
|
@@ -958,7 +1120,7 @@ async function main() {
|
|
|
958
1120
|
}
|
|
959
1121
|
|
|
960
1122
|
// -------------------------------------------------------------
|
|
961
|
-
// Normaler Sync (inkl. evtl. paralleler
|
|
1123
|
+
// Normaler Sync (inkl. evtl. paralleler Sidecar-Excludes)
|
|
962
1124
|
// -------------------------------------------------------------
|
|
963
1125
|
|
|
964
1126
|
// Phase 1 – mit exakt einer Leerzeile davor
|
|
@@ -969,7 +1131,7 @@ async function main() {
|
|
|
969
1131
|
|
|
970
1132
|
if (AUTO_EXCLUDED.size > 0) {
|
|
971
1133
|
log("");
|
|
972
|
-
log(pc.dim(" Auto-excluded (
|
|
1134
|
+
log(pc.dim(" Auto-excluded (sidecar upload/download):"));
|
|
973
1135
|
[...AUTO_EXCLUDED].sort().forEach((file) => {
|
|
974
1136
|
log(pc.dim(`${tab_a()} - ${file}`));
|
|
975
1137
|
});
|
|
@@ -1105,6 +1267,20 @@ async function main() {
|
|
|
1105
1267
|
log(`${tab_a()}No orphaned remote files found.`);
|
|
1106
1268
|
}
|
|
1107
1269
|
|
|
1270
|
+
// -------------------------------------------------------------------
|
|
1271
|
+
// Verzeichnisse vorab anlegen (damit Worker sich nicht ins Gehege kommen)
|
|
1272
|
+
// -------------------------------------------------------------------
|
|
1273
|
+
if (!DRY_RUN && (toAdd.length || toUpdate.length)) {
|
|
1274
|
+
log("");
|
|
1275
|
+
log(pc.bold(pc.cyan("📁 Preparing remote directories …")));
|
|
1276
|
+
await ensureAllRemoteDirsExist(
|
|
1277
|
+
sftp,
|
|
1278
|
+
CONNECTION.remoteRoot,
|
|
1279
|
+
toAdd,
|
|
1280
|
+
toUpdate
|
|
1281
|
+
);
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1108
1284
|
// -------------------------------------------------------------------
|
|
1109
1285
|
// Phase 5: Execute changes (parallel, worker-based)
|
|
1110
1286
|
// -------------------------------------------------------------------
|
|
@@ -1118,6 +1294,7 @@ async function main() {
|
|
|
1118
1294
|
toAdd,
|
|
1119
1295
|
CONNECTION.workers,
|
|
1120
1296
|
async ({ local: l, remotePath }) => {
|
|
1297
|
+
// Verzeichnisse sollten bereits existieren – mkdir hier nur als Fallback
|
|
1121
1298
|
const remoteDir = path.posix.dirname(remotePath);
|
|
1122
1299
|
try {
|
|
1123
1300
|
await sftp.mkdir(remoteDir, true);
|
|
@@ -1171,6 +1348,13 @@ async function main() {
|
|
|
1171
1348
|
);
|
|
1172
1349
|
}
|
|
1173
1350
|
|
|
1351
|
+
// Optional: leere Verzeichnisse aufräumen
|
|
1352
|
+
if (!DRY_RUN && CLEANUP_EMPTY_DIRS) {
|
|
1353
|
+
log("");
|
|
1354
|
+
log(pc.bold(pc.cyan("🧹 Cleaning up empty remote directories …")));
|
|
1355
|
+
await cleanupEmptyDirs(sftp, CONNECTION.remoteRoot);
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1174
1358
|
const duration = ((Date.now() - start) / 1000).toFixed(2);
|
|
1175
1359
|
|
|
1176
1360
|
// Write cache safely at the end
|
|
@@ -1186,7 +1370,7 @@ async function main() {
|
|
|
1186
1370
|
log(`${tab_a()}${DEL} Deleted: ${toDelete.length}`);
|
|
1187
1371
|
if (AUTO_EXCLUDED.size > 0) {
|
|
1188
1372
|
log(
|
|
1189
|
-
`${tab_a()}${EXC} Excluded via
|
|
1373
|
+
`${tab_a()}${EXC} Excluded via sidecar upload/download: ${
|
|
1190
1374
|
AUTO_EXCLUDED.size
|
|
1191
1375
|
}`
|
|
1192
1376
|
);
|
|
@@ -1249,4 +1433,4 @@ async function main() {
|
|
|
1249
1433
|
}
|
|
1250
1434
|
}
|
|
1251
1435
|
|
|
1252
|
-
main();
|
|
1436
|
+
main();
|