sftp-push-sync 1.0.12 → 1.0.14
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 +27 -14
- package/bin/sftp-push-sync.mjs +149 -71
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,10 +26,16 @@ The file `sftp-push-sync.mjs` is pure JavaScript (ESM), not TypeScript. Node.js
|
|
|
26
26
|
## Install
|
|
27
27
|
|
|
28
28
|
```bash
|
|
29
|
-
npm i sftp-push-sync
|
|
29
|
+
npm i -D sftp-push-sync
|
|
30
|
+
# or
|
|
31
|
+
npm install --save-dev sftp-push-sync
|
|
32
|
+
# or
|
|
33
|
+
yarn add --dev sftp-push-sync
|
|
34
|
+
# or
|
|
35
|
+
pnpm add -D sftp-push-sync
|
|
30
36
|
```
|
|
31
37
|
|
|
32
|
-
##
|
|
38
|
+
## Setup
|
|
33
39
|
|
|
34
40
|
Create a `sync.config.json` in the root folder of your project:
|
|
35
41
|
|
|
@@ -58,23 +64,24 @@ Create a `sync.config.json` in the root folder of your project:
|
|
|
58
64
|
}
|
|
59
65
|
},
|
|
60
66
|
"include": [],
|
|
61
|
-
"exclude": [
|
|
62
|
-
"**/.DS_Store",
|
|
63
|
-
"**/.git/**",
|
|
64
|
-
"**/node_modules/**"
|
|
65
|
-
],
|
|
67
|
+
"exclude": ["**/.DS_Store", "**/.git/**", "**/node_modules/**"],
|
|
66
68
|
"textExtensions": [
|
|
67
|
-
".html",
|
|
69
|
+
".html",
|
|
70
|
+
".xml",
|
|
71
|
+
".txt",
|
|
72
|
+
".json",
|
|
73
|
+
".js",
|
|
74
|
+
".css",
|
|
75
|
+
".md",
|
|
76
|
+
".svg"
|
|
68
77
|
],
|
|
69
|
-
|
|
78
|
+
"progress": {
|
|
70
79
|
"scanChunk": 10,
|
|
71
80
|
"analyzeChunk": 1
|
|
72
81
|
},
|
|
73
|
-
"logLevel": "normal",
|
|
82
|
+
"logLevel": "normal",
|
|
74
83
|
"uploadList": [],
|
|
75
|
-
"downloadList": [
|
|
76
|
-
"download-counter.json"
|
|
77
|
-
]
|
|
84
|
+
"downloadList": ["download-counter.json"]
|
|
78
85
|
}
|
|
79
86
|
```
|
|
80
87
|
|
|
@@ -103,7 +110,13 @@ sftp-push-sync prod --download-list # then do
|
|
|
103
110
|
|
|
104
111
|
### Logging Progress
|
|
105
112
|
|
|
106
|
-
|
|
113
|
+
Logging can also be configured.
|
|
114
|
+
|
|
115
|
+
- `logLevel` - normal, verbose, laconic.
|
|
116
|
+
- `scanChunk` - After how many elements should a log output be generated during scanning?
|
|
117
|
+
- `analyzeChunk` - After how many elements should a log output be generated during analysis?
|
|
118
|
+
|
|
119
|
+
For >100k files, use analyzeChunk = 10 or 50, otherwise the TTY output itself is a relevant factor.
|
|
107
120
|
|
|
108
121
|
## NPM Scripts
|
|
109
122
|
|
package/bin/sftp-push-sync.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
** sftp-push-sync.mjs - SFTP Syncronisations Tool
|
|
4
4
|
*
|
|
5
|
-
* @author Carsten Nichte, 2025 / https://carsten-nichte.de
|
|
5
|
+
* @author Carsten Nichte, 2025 / https://carsten-nichte.de/
|
|
6
6
|
*
|
|
7
7
|
* SFTP push sync with dry run
|
|
8
8
|
* 1. Upload new files
|
|
@@ -51,6 +51,11 @@ const CHA = pc.yellow("~"); // Changed
|
|
|
51
51
|
const DEL = pc.red("-"); // Deleted
|
|
52
52
|
const EXC = pc.redBright("-"); // Excluded
|
|
53
53
|
|
|
54
|
+
const hr1 = () => "─".repeat(65); // horizontal line -
|
|
55
|
+
const hr2 = () => "=".repeat(65); // horizontal line =
|
|
56
|
+
const tab_a = () => " ".repeat(3); // indentation for formatting the terminal output.
|
|
57
|
+
const tab_b = () => " ".repeat(6);
|
|
58
|
+
|
|
54
59
|
// ---------------------------------------------------------------------------
|
|
55
60
|
// CLI arguments
|
|
56
61
|
// ---------------------------------------------------------------------------
|
|
@@ -68,7 +73,7 @@ if (args.includes("--laconic")) cliLogLevel = "laconic";
|
|
|
68
73
|
|
|
69
74
|
if (!TARGET) {
|
|
70
75
|
console.error(pc.red("❌ Please specify a connection profile:"));
|
|
71
|
-
console.error(pc.yellow(
|
|
76
|
+
console.error(pc.yellow(`${tab_a()}sftp-push-sync staging --dry-run`));
|
|
72
77
|
process.exit(1);
|
|
73
78
|
}
|
|
74
79
|
|
|
@@ -98,7 +103,9 @@ if (!CONFIG_RAW.connections || typeof CONFIG_RAW.connections !== "object") {
|
|
|
98
103
|
|
|
99
104
|
const TARGET_CONFIG = CONFIG_RAW.connections[TARGET];
|
|
100
105
|
if (!TARGET_CONFIG) {
|
|
101
|
-
console.error(
|
|
106
|
+
console.error(
|
|
107
|
+
pc.red(`❌ Connection '${TARGET}' not found in sync.config.json.`)
|
|
108
|
+
);
|
|
102
109
|
process.exit(1);
|
|
103
110
|
}
|
|
104
111
|
|
|
@@ -119,7 +126,7 @@ const CONNECTION = {
|
|
|
119
126
|
// logLevel: "verbose", "normal", "laconic"
|
|
120
127
|
let LOG_LEVEL = (CONFIG_RAW.logLevel ?? "normal").toLowerCase();
|
|
121
128
|
|
|
122
|
-
// CLI
|
|
129
|
+
// Override config with CLI flags
|
|
123
130
|
if (cliLogLevel) {
|
|
124
131
|
LOG_LEVEL = cliLogLevel;
|
|
125
132
|
}
|
|
@@ -130,7 +137,7 @@ const IS_LACONIC = LOG_LEVEL === "laconic";
|
|
|
130
137
|
const PROGRESS = CONFIG_RAW.progress ?? {};
|
|
131
138
|
const SCAN_CHUNK = PROGRESS.scanChunk ?? (IS_VERBOSE ? 1 : 100);
|
|
132
139
|
const ANALYZE_CHUNK = PROGRESS.analyzeChunk ?? (IS_VERBOSE ? 1 : 10);
|
|
133
|
-
//
|
|
140
|
+
// For >100k files, rather 10–50, for debugging/troubleshooting 1.
|
|
134
141
|
|
|
135
142
|
// ---------------------------------------------------------------------------
|
|
136
143
|
// Shared config from JSON
|
|
@@ -139,7 +146,7 @@ const ANALYZE_CHUNK = PROGRESS.analyzeChunk ?? (IS_VERBOSE ? 1 : 10);
|
|
|
139
146
|
const INCLUDE = CONFIG_RAW.include ?? [];
|
|
140
147
|
const BASE_EXCLUDE = CONFIG_RAW.exclude ?? [];
|
|
141
148
|
|
|
142
|
-
//
|
|
149
|
+
// Special: Lists for targeted uploads/downloads
|
|
143
150
|
function normalizeList(list) {
|
|
144
151
|
if (!Array.isArray(list)) return [];
|
|
145
152
|
return list.flatMap((item) =>
|
|
@@ -158,7 +165,7 @@ const DOWNLOAD_LIST = normalizeList(CONFIG_RAW.downloadList ?? []);
|
|
|
158
165
|
// Effektive Exclude-Liste: explizites exclude + Upload/Download-Listen
|
|
159
166
|
const EXCLUDE = [...BASE_EXCLUDE, ...UPLOAD_LIST, ...DOWNLOAD_LIST];
|
|
160
167
|
|
|
161
|
-
//
|
|
168
|
+
// List of ALL files that were excluded due to uploadList/downloadList
|
|
162
169
|
const AUTO_EXCLUDED = new Set();
|
|
163
170
|
|
|
164
171
|
const TEXT_EXT = CONFIG_RAW.textExtensions ?? [
|
|
@@ -198,7 +205,7 @@ try {
|
|
|
198
205
|
}
|
|
199
206
|
} catch (err) {
|
|
200
207
|
console.warn(
|
|
201
|
-
pc.yellow("⚠️
|
|
208
|
+
pc.yellow("⚠️ Could not load cache, starting without:"),
|
|
202
209
|
err.message
|
|
203
210
|
);
|
|
204
211
|
}
|
|
@@ -236,8 +243,11 @@ let progressActive = false;
|
|
|
236
243
|
function clearProgressLine() {
|
|
237
244
|
if (!process.stdout.isTTY || !progressActive) return;
|
|
238
245
|
const width = process.stdout.columns || 80;
|
|
239
|
-
const blank = " ".repeat(width
|
|
240
|
-
|
|
246
|
+
const blank = " ".repeat(width);
|
|
247
|
+
|
|
248
|
+
// Beide Progress-Zeilen leeren
|
|
249
|
+
process.stdout.write("\r" + blank + "\n" + blank + "\r");
|
|
250
|
+
|
|
241
251
|
progressActive = false;
|
|
242
252
|
}
|
|
243
253
|
|
|
@@ -292,10 +302,6 @@ function isTextFile(relPath) {
|
|
|
292
302
|
|
|
293
303
|
function shortenPathForProgress(rel) {
|
|
294
304
|
if (!rel) return "";
|
|
295
|
-
// Nur Dateinamen?
|
|
296
|
-
// return path.basename(rel);
|
|
297
|
-
|
|
298
|
-
// Letzte 2 Segmente des Pfades
|
|
299
305
|
const parts = rel.split("/");
|
|
300
306
|
if (parts.length === 1) {
|
|
301
307
|
return rel; // nur Dateiname
|
|
@@ -317,9 +323,11 @@ function updateProgress2(prefix, current, total, rel = "") {
|
|
|
317
323
|
// Fallback für Pipes / Logs
|
|
318
324
|
if (total && total > 0) {
|
|
319
325
|
const percent = ((current / total) * 100).toFixed(1);
|
|
320
|
-
console.log(
|
|
326
|
+
console.log(
|
|
327
|
+
`${tab_a()}${prefix}${current}/${total} Files (${percent}%) – ${rel}`
|
|
328
|
+
);
|
|
321
329
|
} else {
|
|
322
|
-
console.log(`${prefix}${current}
|
|
330
|
+
console.log(`${tab_a()}${prefix}${current} Files – ${rel}`);
|
|
323
331
|
}
|
|
324
332
|
return;
|
|
325
333
|
}
|
|
@@ -329,15 +337,13 @@ function updateProgress2(prefix, current, total, rel = "") {
|
|
|
329
337
|
let line1;
|
|
330
338
|
if (total && total > 0) {
|
|
331
339
|
const percent = ((current / total) * 100).toFixed(1);
|
|
332
|
-
line1 = `${prefix}${current}/${total}
|
|
340
|
+
line1 = `${tab_a()}${prefix}${current}/${total} Files (${percent}%)`;
|
|
333
341
|
} else {
|
|
334
342
|
// „unknown total“ / Scanner-Modus
|
|
335
|
-
line1 = `${prefix}${current}
|
|
343
|
+
line1 = `${tab_a()}${prefix}${current} Files`;
|
|
336
344
|
}
|
|
337
345
|
|
|
338
|
-
// Pfad einkürzen falls nötig (deine bestehende Funktion verwenden)
|
|
339
346
|
const short = rel ? shortenPathForProgress(rel) : "";
|
|
340
|
-
|
|
341
347
|
let line2 = short;
|
|
342
348
|
|
|
343
349
|
if (line1.length > width) line1 = line1.slice(0, width - 1);
|
|
@@ -371,11 +377,11 @@ async function runTasks(items, workerCount, handler, label = "Tasks") {
|
|
|
371
377
|
try {
|
|
372
378
|
await handler(item);
|
|
373
379
|
} catch (err) {
|
|
374
|
-
elog(pc.red(
|
|
380
|
+
elog(pc.red(`${tab_a()}⚠️ Error in ${label}:`), err.message || err);
|
|
375
381
|
}
|
|
376
382
|
done += 1;
|
|
377
383
|
if (done % 10 === 0 || done === total) {
|
|
378
|
-
updateProgress2(
|
|
384
|
+
updateProgress2(`${tab_a()}${label}: `, done, total);
|
|
379
385
|
}
|
|
380
386
|
}
|
|
381
387
|
}
|
|
@@ -419,8 +425,8 @@ async function walkLocal(root) {
|
|
|
419
425
|
scanned += 1;
|
|
420
426
|
const chunk = IS_VERBOSE ? 1 : SCAN_CHUNK;
|
|
421
427
|
if (scanned === 1 || scanned % chunk === 0) {
|
|
422
|
-
//
|
|
423
|
-
updateProgress2(
|
|
428
|
+
// totally unknown → total = 0 → no automatic \n
|
|
429
|
+
updateProgress2(`${tab_a()}Scan local: `, scanned, 0, rel);
|
|
424
430
|
}
|
|
425
431
|
}
|
|
426
432
|
}
|
|
@@ -429,8 +435,8 @@ async function walkLocal(root) {
|
|
|
429
435
|
await recurse(root);
|
|
430
436
|
|
|
431
437
|
if (scanned > 0) {
|
|
432
|
-
//
|
|
433
|
-
updateProgress2(
|
|
438
|
+
// last line + neat finish
|
|
439
|
+
updateProgress2(`${tab_a()}Scan local: `, scanned, 0, "fertig");
|
|
434
440
|
process.stdout.write("\n");
|
|
435
441
|
progressActive = false;
|
|
436
442
|
}
|
|
@@ -471,7 +477,7 @@ async function walkRemote(sftp, remoteRoot) {
|
|
|
471
477
|
scanned += 1;
|
|
472
478
|
const chunk = IS_VERBOSE ? 1 : SCAN_CHUNK;
|
|
473
479
|
if (scanned === 1 || scanned % chunk === 0) {
|
|
474
|
-
updateProgress2(
|
|
480
|
+
updateProgress2(`${tab_a()}Scan remote: `, scanned, 0, rel);
|
|
475
481
|
}
|
|
476
482
|
}
|
|
477
483
|
}
|
|
@@ -480,7 +486,7 @@ async function walkRemote(sftp, remoteRoot) {
|
|
|
480
486
|
await recurse(remoteRoot);
|
|
481
487
|
|
|
482
488
|
if (scanned > 0) {
|
|
483
|
-
updateProgress2(
|
|
489
|
+
updateProgress2(`${tab_a()}Scan remote: `, scanned, 0, "fertig");
|
|
484
490
|
process.stdout.write("\n");
|
|
485
491
|
progressActive = false;
|
|
486
492
|
}
|
|
@@ -561,6 +567,45 @@ async function getRemoteHash(rel, meta, sftp) {
|
|
|
561
567
|
return hash;
|
|
562
568
|
}
|
|
563
569
|
|
|
570
|
+
// ---------------------------------------------------------------------------
|
|
571
|
+
// SFTP error explanation (for clearer messages)
|
|
572
|
+
// ---------------------------------------------------------------------------
|
|
573
|
+
|
|
574
|
+
function describeSftpError(err) {
|
|
575
|
+
if (!err) return "";
|
|
576
|
+
|
|
577
|
+
const code = err.code || err.errno || "";
|
|
578
|
+
const msg = (err.message || "").toLowerCase();
|
|
579
|
+
|
|
580
|
+
// Netzwerk / DNS
|
|
581
|
+
if (code === "ENOTFOUND") {
|
|
582
|
+
return "Host not found (ENOTFOUND) – Check hostname or DNS entry.";
|
|
583
|
+
}
|
|
584
|
+
if (code === "EHOSTUNREACH") {
|
|
585
|
+
return "Host not reachable (EHOSTUNREACH) – Check network/firewall.";
|
|
586
|
+
}
|
|
587
|
+
if (code === "ECONNREFUSED") {
|
|
588
|
+
return "Connection refused (ECONNREFUSED) – Check the port or SSH service.";
|
|
589
|
+
}
|
|
590
|
+
if (code === "ECONNRESET") {
|
|
591
|
+
return "Connection was reset by the server (ECONNRESET).";
|
|
592
|
+
}
|
|
593
|
+
if (code === "ETIMEDOUT") {
|
|
594
|
+
return "Connection timeout (ETIMEDOUT) – Server is not responding or is blocked.";
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Auth / Authorisations
|
|
598
|
+
if (msg.includes("all configured authentication methods failed")) {
|
|
599
|
+
return "Authentication failed – check your username/password or SSH keys.";
|
|
600
|
+
}
|
|
601
|
+
if (msg.includes("permission denied")) {
|
|
602
|
+
return "Access denied – check permissions on the server.";
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Fallback
|
|
606
|
+
return "";
|
|
607
|
+
}
|
|
608
|
+
|
|
564
609
|
// ---------------------------------------------------------------------------
|
|
565
610
|
// MAIN
|
|
566
611
|
// ---------------------------------------------------------------------------
|
|
@@ -568,43 +613,46 @@ async function getRemoteHash(rel, meta, sftp) {
|
|
|
568
613
|
async function main() {
|
|
569
614
|
const start = Date.now();
|
|
570
615
|
|
|
571
|
-
log(
|
|
616
|
+
log(`\n\n${hr2()}`);
|
|
572
617
|
log(
|
|
573
618
|
pc.bold(
|
|
574
619
|
`🔐 SFTP Push-Synchronisation: sftp-push-sync v${pkg.version} [logLevel=${LOG_LEVEL}]`
|
|
575
620
|
)
|
|
576
621
|
);
|
|
577
|
-
log(
|
|
578
|
-
log(
|
|
579
|
-
log(
|
|
580
|
-
log(
|
|
581
|
-
|
|
622
|
+
log(`${tab_a()}Connection: ${pc.cyan(TARGET)}`);
|
|
623
|
+
log(`${tab_a()}Worker: ${CONNECTION.workers}`);
|
|
624
|
+
log(`${tab_a()}Host: ${pc.green(CONNECTION.host)}:${pc.green(CONNECTION.port)}`);
|
|
625
|
+
log(`${tab_a()}Local: ${pc.green(CONNECTION.localRoot)}`);
|
|
626
|
+
log(`${tab_a()}Remote: ${pc.green(CONNECTION.remoteRoot)}`);
|
|
627
|
+
if (DRY_RUN) log(pc.yellow(`${tab_a()}Mode: DRY-RUN (no changes)`));
|
|
582
628
|
if (RUN_UPLOAD_LIST || RUN_DOWNLOAD_LIST) {
|
|
583
629
|
log(
|
|
584
630
|
pc.blue(
|
|
585
|
-
|
|
631
|
+
`${tab_a()}Extra: ${RUN_UPLOAD_LIST ? "uploadList " : ""}${
|
|
586
632
|
RUN_DOWNLOAD_LIST ? "downloadList" : ""
|
|
587
633
|
}`
|
|
588
634
|
)
|
|
589
635
|
);
|
|
590
636
|
}
|
|
591
|
-
log(
|
|
637
|
+
log(`${hr1()}\n`);
|
|
592
638
|
|
|
593
639
|
const sftp = new SftpClient();
|
|
640
|
+
let connected = false;
|
|
594
641
|
|
|
595
642
|
const toAdd = [];
|
|
596
643
|
const toUpdate = [];
|
|
597
644
|
const toDelete = [];
|
|
598
645
|
|
|
599
646
|
try {
|
|
647
|
+
log(pc.cyan("🔌 Connecting to SFTP server …"));
|
|
600
648
|
await sftp.connect({
|
|
601
649
|
host: CONNECTION.host,
|
|
602
650
|
port: CONNECTION.port,
|
|
603
651
|
username: CONNECTION.user,
|
|
604
652
|
password: CONNECTION.password,
|
|
605
653
|
});
|
|
606
|
-
|
|
607
|
-
|
|
654
|
+
connected = true;
|
|
655
|
+
log(pc.green(`${tab_a()}✔ Connected to SFTP.`));
|
|
608
656
|
|
|
609
657
|
if (!fs.existsSync(CONNECTION.localRoot)) {
|
|
610
658
|
console.error(
|
|
@@ -616,20 +664,20 @@ async function main() {
|
|
|
616
664
|
|
|
617
665
|
log(pc.bold(pc.cyan("📥 Phase 1: Scan local files …")));
|
|
618
666
|
const local = await walkLocal(CONNECTION.localRoot);
|
|
619
|
-
log(
|
|
667
|
+
log(`${tab_a()}→ ${local.size} local files`);
|
|
620
668
|
|
|
621
669
|
if (AUTO_EXCLUDED.size > 0) {
|
|
622
670
|
log("");
|
|
623
671
|
log(pc.dim(" Auto-excluded (uploadList/downloadList):"));
|
|
624
672
|
[...AUTO_EXCLUDED].sort().forEach((file) => {
|
|
625
|
-
log(pc.dim(
|
|
673
|
+
log(pc.dim(`${tab_a()} - ${file}`));
|
|
626
674
|
});
|
|
627
675
|
log("");
|
|
628
676
|
}
|
|
629
677
|
|
|
630
678
|
log(pc.bold(pc.cyan("📤 Phase 2: Scan remote files …")));
|
|
631
679
|
const remote = await walkRemote(sftp, CONNECTION.remoteRoot);
|
|
632
|
-
log(
|
|
680
|
+
log(`${tab_a()}→ ${remote.size} remote files\n`);
|
|
633
681
|
|
|
634
682
|
const localKeys = new Set(local.keys());
|
|
635
683
|
const remoteKeys = new Set(remote.keys());
|
|
@@ -644,7 +692,7 @@ async function main() {
|
|
|
644
692
|
|
|
645
693
|
const chunk = IS_VERBOSE ? 1 : ANALYZE_CHUNK;
|
|
646
694
|
if (
|
|
647
|
-
checkedCount === 1 || //
|
|
695
|
+
checkedCount === 1 || // immediate first issue
|
|
648
696
|
checkedCount % chunk === 0 ||
|
|
649
697
|
checkedCount === totalToCheck
|
|
650
698
|
) {
|
|
@@ -686,19 +734,19 @@ async function main() {
|
|
|
686
734
|
).toString("utf8");
|
|
687
735
|
|
|
688
736
|
if (localStr === remoteStr) {
|
|
689
|
-
vlog(
|
|
737
|
+
vlog(`${tab_a()}${pc.dim("✓ Unchanged (Text):")} ${rel}`);
|
|
690
738
|
continue;
|
|
691
739
|
}
|
|
692
740
|
|
|
693
741
|
if (IS_VERBOSE) {
|
|
694
742
|
const diff = diffWords(remoteStr, localStr);
|
|
695
743
|
const blocks = diff.filter((d) => d.added || d.removed).length;
|
|
696
|
-
vlog(
|
|
744
|
+
vlog(`${tab_a()}${CHA} Text difference (${blocks} blocks) in ${rel}`);
|
|
697
745
|
}
|
|
698
746
|
|
|
699
747
|
toUpdate.push({ rel, local: l, remote: r, remotePath });
|
|
700
748
|
if (!IS_LACONIC) {
|
|
701
|
-
log(`${CHA} ${pc.yellow("Content changed (Text):")} ${rel}`);
|
|
749
|
+
log(`${tab_a()}${CHA} ${pc.yellow("Content changed (Text):")} ${rel}`);
|
|
702
750
|
}
|
|
703
751
|
} else {
|
|
704
752
|
// Binary: Hash comparison with cache
|
|
@@ -711,14 +759,14 @@ async function main() {
|
|
|
711
759
|
]);
|
|
712
760
|
|
|
713
761
|
if (localHash === remoteHash) {
|
|
714
|
-
vlog(
|
|
762
|
+
vlog(`${tab_a()}${pc.dim("✓ Unchanged (binary, hash):")} ${rel}`);
|
|
715
763
|
continue;
|
|
716
764
|
}
|
|
717
765
|
|
|
718
766
|
if (IS_VERBOSE) {
|
|
719
|
-
vlog(
|
|
720
|
-
vlog(
|
|
721
|
-
vlog(
|
|
767
|
+
vlog(`${tab_a()}${CHA} Hash different (binary): ${rel}`);
|
|
768
|
+
vlog(`${tab_b()}local: ${localHash}`);
|
|
769
|
+
vlog(`${tab_b()}remote: ${remoteHash}`);
|
|
722
770
|
}
|
|
723
771
|
|
|
724
772
|
toUpdate.push({ rel, local: l, remote: r, remotePath });
|
|
@@ -728,17 +776,29 @@ async function main() {
|
|
|
728
776
|
}
|
|
729
777
|
}
|
|
730
778
|
|
|
731
|
-
|
|
779
|
+
// Wenn Phase 3 nichts gefunden hat, explizit sagen
|
|
780
|
+
if (toAdd.length === 0 && toUpdate.length === 0) {
|
|
781
|
+
log(`${tab_a()}No differences found. Everything is up to date.`);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
log(
|
|
785
|
+
"\n" + pc.bold(pc.cyan("🧹 Phase 4: Removing orphaned remote files …"))
|
|
786
|
+
);
|
|
732
787
|
for (const rel of remoteKeys) {
|
|
733
788
|
if (!localKeys.has(rel)) {
|
|
734
789
|
const r = remote.get(rel);
|
|
735
790
|
toDelete.push({ rel, remotePath: r.remotePath });
|
|
736
791
|
if (!IS_LACONIC) {
|
|
737
|
-
log(
|
|
792
|
+
log(`${tab_a()}${DEL} ${pc.red("Remove:")} ${rel}`);
|
|
738
793
|
}
|
|
739
794
|
}
|
|
740
795
|
}
|
|
741
796
|
|
|
797
|
+
// Auch für Phase 4 eine „nix zu tun“-Meldung
|
|
798
|
+
if (toDelete.length === 0) {
|
|
799
|
+
log(`${tab_a()}No orphaned remote files found.`);
|
|
800
|
+
}
|
|
801
|
+
|
|
742
802
|
// -------------------------------------------------------------------
|
|
743
803
|
// Phase 5: Execute changes (parallel, worker-based)
|
|
744
804
|
// -------------------------------------------------------------------
|
|
@@ -796,7 +856,11 @@ async function main() {
|
|
|
796
856
|
"Deletes"
|
|
797
857
|
);
|
|
798
858
|
} else {
|
|
799
|
-
log(
|
|
859
|
+
log(
|
|
860
|
+
pc.yellow(
|
|
861
|
+
"\n💡 DRY-RUN: Connection tested, no files transferred or deleted."
|
|
862
|
+
)
|
|
863
|
+
);
|
|
800
864
|
}
|
|
801
865
|
|
|
802
866
|
// -------------------------------------------------------------------
|
|
@@ -806,7 +870,7 @@ async function main() {
|
|
|
806
870
|
if (RUN_UPLOAD_LIST && UPLOAD_LIST.length > 0) {
|
|
807
871
|
log(
|
|
808
872
|
"\n" +
|
|
809
|
-
pc.bold(pc.cyan("⬆️
|
|
873
|
+
pc.bold(pc.cyan("⬆️ Extra Phase: Upload-List (explicit files) …"))
|
|
810
874
|
);
|
|
811
875
|
|
|
812
876
|
const tasks = UPLOAD_LIST.map((rel) => ({
|
|
@@ -817,7 +881,7 @@ async function main() {
|
|
|
817
881
|
|
|
818
882
|
if (DRY_RUN) {
|
|
819
883
|
for (const t of tasks) {
|
|
820
|
-
log(
|
|
884
|
+
log(`${tab_a()}${ADD} would upload (uploadList): ${t.rel}`);
|
|
821
885
|
}
|
|
822
886
|
} else {
|
|
823
887
|
await runTasks(
|
|
@@ -831,7 +895,7 @@ async function main() {
|
|
|
831
895
|
// ignore
|
|
832
896
|
}
|
|
833
897
|
await sftp.put(localPath, remotePath);
|
|
834
|
-
log(
|
|
898
|
+
log(`${tab_a()}${ADD} uploadList: ${rel}`);
|
|
835
899
|
},
|
|
836
900
|
"Upload-List"
|
|
837
901
|
);
|
|
@@ -841,7 +905,7 @@ async function main() {
|
|
|
841
905
|
if (RUN_DOWNLOAD_LIST && DOWNLOAD_LIST.length > 0) {
|
|
842
906
|
log(
|
|
843
907
|
"\n" +
|
|
844
|
-
pc.bold(pc.cyan("⬇️
|
|
908
|
+
pc.bold(pc.cyan("⬇️ Extra Phase: Download-List (explicit files) …"))
|
|
845
909
|
);
|
|
846
910
|
|
|
847
911
|
const tasks = DOWNLOAD_LIST.map((rel) => ({
|
|
@@ -852,7 +916,7 @@ async function main() {
|
|
|
852
916
|
|
|
853
917
|
if (DRY_RUN) {
|
|
854
918
|
for (const t of tasks) {
|
|
855
|
-
log(
|
|
919
|
+
log(`${tab_a()}${ADD} would download (downloadList): ${t.rel}`);
|
|
856
920
|
}
|
|
857
921
|
} else {
|
|
858
922
|
await runTasks(
|
|
@@ -861,7 +925,7 @@ async function main() {
|
|
|
861
925
|
async ({ remotePath, localPath, rel }) => {
|
|
862
926
|
await fsp.mkdir(path.dirname(localPath), { recursive: true });
|
|
863
927
|
await sftp.fastGet(remotePath, localPath);
|
|
864
|
-
log(
|
|
928
|
+
log(`${tab_a()}${ADD} downloadList: ${rel}`);
|
|
865
929
|
},
|
|
866
930
|
"Download-List"
|
|
867
931
|
);
|
|
@@ -875,34 +939,41 @@ async function main() {
|
|
|
875
939
|
|
|
876
940
|
// Summary
|
|
877
941
|
log("\n" + pc.bold(pc.cyan("📊 Summary:")));
|
|
878
|
-
log(
|
|
879
|
-
log(
|
|
880
|
-
log(
|
|
881
|
-
log(
|
|
942
|
+
log(`${tab_a()}Duration: ${pc.green(duration + " s")}`);
|
|
943
|
+
log(`${tab_a()}${ADD} Added : ${toAdd.length}`);
|
|
944
|
+
log(`${tab_a()}${CHA} Changed: ${toUpdate.length}`);
|
|
945
|
+
log(`${tab_a()}${DEL} Deleted: ${toDelete.length}`);
|
|
882
946
|
if (AUTO_EXCLUDED.size > 0) {
|
|
883
947
|
log(
|
|
884
|
-
|
|
948
|
+
`${tab_a()}${EXC} Excluded via uploadList | downloadList: ${AUTO_EXCLUDED.size}`
|
|
885
949
|
);
|
|
886
950
|
}
|
|
887
951
|
if (toAdd.length || toUpdate.length || toDelete.length) {
|
|
888
952
|
log("\n📄 Changes:");
|
|
889
953
|
[...toAdd.map((t) => t.rel)]
|
|
890
954
|
.sort()
|
|
891
|
-
.forEach((f) => console.log(
|
|
955
|
+
.forEach((f) => console.log(`${tab_a()}${ADD} ${f}`));
|
|
892
956
|
[...toUpdate.map((t) => t.rel)]
|
|
893
957
|
.sort()
|
|
894
|
-
.forEach((f) => console.log(
|
|
958
|
+
.forEach((f) => console.log(`${tab_a()}${CHA} ${f}`));
|
|
895
959
|
[...toDelete.map((t) => t.rel)]
|
|
896
960
|
.sort()
|
|
897
|
-
.forEach((f) => console.log(
|
|
961
|
+
.forEach((f) => console.log(`${tab_a()}${DEL} ${f}`));
|
|
898
962
|
} else {
|
|
899
963
|
log("\nNo changes.");
|
|
900
964
|
}
|
|
901
965
|
|
|
902
966
|
log("\n" + pc.bold(pc.green("✅ Sync complete.")));
|
|
903
|
-
log("==================================================================\n\n");
|
|
904
967
|
} catch (err) {
|
|
905
|
-
|
|
968
|
+
const hint = describeSftpError(err);
|
|
969
|
+
elog(pc.red("❌ Synchronisation error:"), err.message || err);
|
|
970
|
+
if (hint) {
|
|
971
|
+
wlog(pc.yellow(`${tab_a()}Mögliche Ursache:`), hint);
|
|
972
|
+
}
|
|
973
|
+
if (IS_VERBOSE) {
|
|
974
|
+
// Vollständiges Error-Objekt nur in verbose anzeigen
|
|
975
|
+
console.error(err);
|
|
976
|
+
}
|
|
906
977
|
process.exitCode = 1;
|
|
907
978
|
try {
|
|
908
979
|
await saveCache(true);
|
|
@@ -911,11 +982,18 @@ async function main() {
|
|
|
911
982
|
}
|
|
912
983
|
} finally {
|
|
913
984
|
try {
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
985
|
+
if (connected) {
|
|
986
|
+
await sftp.end();
|
|
987
|
+
log(pc.green(`${tab_a()}✔ Connection closed.`));
|
|
988
|
+
}
|
|
989
|
+
} catch (e) {
|
|
990
|
+
wlog(
|
|
991
|
+
pc.yellow("⚠️ Could not close SFTP connection cleanly:"),
|
|
992
|
+
e.message || e
|
|
993
|
+
);
|
|
917
994
|
}
|
|
918
995
|
}
|
|
996
|
+
log(`${hr2()}\n\n`);
|
|
919
997
|
}
|
|
920
998
|
|
|
921
999
|
main();
|