sftp-push-sync 1.0.12 → 1.0.13
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 +128 -64
- 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,10 @@ 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 output.
|
|
57
|
+
|
|
54
58
|
// ---------------------------------------------------------------------------
|
|
55
59
|
// CLI arguments
|
|
56
60
|
// ---------------------------------------------------------------------------
|
|
@@ -98,7 +102,9 @@ if (!CONFIG_RAW.connections || typeof CONFIG_RAW.connections !== "object") {
|
|
|
98
102
|
|
|
99
103
|
const TARGET_CONFIG = CONFIG_RAW.connections[TARGET];
|
|
100
104
|
if (!TARGET_CONFIG) {
|
|
101
|
-
console.error(
|
|
105
|
+
console.error(
|
|
106
|
+
pc.red(`❌ Connection '${TARGET}' not found in sync.config.json.`)
|
|
107
|
+
);
|
|
102
108
|
process.exit(1);
|
|
103
109
|
}
|
|
104
110
|
|
|
@@ -119,7 +125,7 @@ const CONNECTION = {
|
|
|
119
125
|
// logLevel: "verbose", "normal", "laconic"
|
|
120
126
|
let LOG_LEVEL = (CONFIG_RAW.logLevel ?? "normal").toLowerCase();
|
|
121
127
|
|
|
122
|
-
// CLI
|
|
128
|
+
// Override config with CLI flags
|
|
123
129
|
if (cliLogLevel) {
|
|
124
130
|
LOG_LEVEL = cliLogLevel;
|
|
125
131
|
}
|
|
@@ -130,7 +136,7 @@ const IS_LACONIC = LOG_LEVEL === "laconic";
|
|
|
130
136
|
const PROGRESS = CONFIG_RAW.progress ?? {};
|
|
131
137
|
const SCAN_CHUNK = PROGRESS.scanChunk ?? (IS_VERBOSE ? 1 : 100);
|
|
132
138
|
const ANALYZE_CHUNK = PROGRESS.analyzeChunk ?? (IS_VERBOSE ? 1 : 10);
|
|
133
|
-
//
|
|
139
|
+
// For >100k files, rather 10–50, for debugging/troubleshooting 1.
|
|
134
140
|
|
|
135
141
|
// ---------------------------------------------------------------------------
|
|
136
142
|
// Shared config from JSON
|
|
@@ -139,7 +145,7 @@ const ANALYZE_CHUNK = PROGRESS.analyzeChunk ?? (IS_VERBOSE ? 1 : 10);
|
|
|
139
145
|
const INCLUDE = CONFIG_RAW.include ?? [];
|
|
140
146
|
const BASE_EXCLUDE = CONFIG_RAW.exclude ?? [];
|
|
141
147
|
|
|
142
|
-
//
|
|
148
|
+
// Special: Lists for targeted uploads/downloads
|
|
143
149
|
function normalizeList(list) {
|
|
144
150
|
if (!Array.isArray(list)) return [];
|
|
145
151
|
return list.flatMap((item) =>
|
|
@@ -158,7 +164,7 @@ const DOWNLOAD_LIST = normalizeList(CONFIG_RAW.downloadList ?? []);
|
|
|
158
164
|
// Effektive Exclude-Liste: explizites exclude + Upload/Download-Listen
|
|
159
165
|
const EXCLUDE = [...BASE_EXCLUDE, ...UPLOAD_LIST, ...DOWNLOAD_LIST];
|
|
160
166
|
|
|
161
|
-
//
|
|
167
|
+
// List of ALL files that were excluded due to uploadList/downloadList
|
|
162
168
|
const AUTO_EXCLUDED = new Set();
|
|
163
169
|
|
|
164
170
|
const TEXT_EXT = CONFIG_RAW.textExtensions ?? [
|
|
@@ -198,7 +204,7 @@ try {
|
|
|
198
204
|
}
|
|
199
205
|
} catch (err) {
|
|
200
206
|
console.warn(
|
|
201
|
-
pc.yellow("⚠️
|
|
207
|
+
pc.yellow("⚠️ Could not load cache, starting without:"),
|
|
202
208
|
err.message
|
|
203
209
|
);
|
|
204
210
|
}
|
|
@@ -292,10 +298,6 @@ function isTextFile(relPath) {
|
|
|
292
298
|
|
|
293
299
|
function shortenPathForProgress(rel) {
|
|
294
300
|
if (!rel) return "";
|
|
295
|
-
// Nur Dateinamen?
|
|
296
|
-
// return path.basename(rel);
|
|
297
|
-
|
|
298
|
-
// Letzte 2 Segmente des Pfades
|
|
299
301
|
const parts = rel.split("/");
|
|
300
302
|
if (parts.length === 1) {
|
|
301
303
|
return rel; // nur Dateiname
|
|
@@ -317,9 +319,11 @@ function updateProgress2(prefix, current, total, rel = "") {
|
|
|
317
319
|
// Fallback für Pipes / Logs
|
|
318
320
|
if (total && total > 0) {
|
|
319
321
|
const percent = ((current / total) * 100).toFixed(1);
|
|
320
|
-
console.log(
|
|
322
|
+
console.log(
|
|
323
|
+
`${tab_a()}${prefix}${current}/${total} Files (${percent}%) – ${rel}`
|
|
324
|
+
);
|
|
321
325
|
} else {
|
|
322
|
-
console.log(`${prefix}${current}
|
|
326
|
+
console.log(`${tab_a()}${prefix}${current} Files – ${rel}`);
|
|
323
327
|
}
|
|
324
328
|
return;
|
|
325
329
|
}
|
|
@@ -329,15 +333,13 @@ function updateProgress2(prefix, current, total, rel = "") {
|
|
|
329
333
|
let line1;
|
|
330
334
|
if (total && total > 0) {
|
|
331
335
|
const percent = ((current / total) * 100).toFixed(1);
|
|
332
|
-
line1 = `${prefix}${current}/${total}
|
|
336
|
+
line1 = `${tab_a()}${prefix}${current}/${total} Files (${percent}%)`;
|
|
333
337
|
} else {
|
|
334
338
|
// „unknown total“ / Scanner-Modus
|
|
335
|
-
line1 = `${prefix}${current}
|
|
339
|
+
line1 = `${tab_a()}${prefix}${current} Files`;
|
|
336
340
|
}
|
|
337
341
|
|
|
338
|
-
// Pfad einkürzen falls nötig (deine bestehende Funktion verwenden)
|
|
339
342
|
const short = rel ? shortenPathForProgress(rel) : "";
|
|
340
|
-
|
|
341
343
|
let line2 = short;
|
|
342
344
|
|
|
343
345
|
if (line1.length > width) line1 = line1.slice(0, width - 1);
|
|
@@ -371,11 +373,11 @@ async function runTasks(items, workerCount, handler, label = "Tasks") {
|
|
|
371
373
|
try {
|
|
372
374
|
await handler(item);
|
|
373
375
|
} catch (err) {
|
|
374
|
-
elog(pc.red(
|
|
376
|
+
elog(pc.red(`${tab_a()}⚠️ Error in ${label}:`), err.message || err);
|
|
375
377
|
}
|
|
376
378
|
done += 1;
|
|
377
379
|
if (done % 10 === 0 || done === total) {
|
|
378
|
-
updateProgress2(
|
|
380
|
+
updateProgress2(`${tab_a()}${label}: `, done, total);
|
|
379
381
|
}
|
|
380
382
|
}
|
|
381
383
|
}
|
|
@@ -419,7 +421,7 @@ async function walkLocal(root) {
|
|
|
419
421
|
scanned += 1;
|
|
420
422
|
const chunk = IS_VERBOSE ? 1 : SCAN_CHUNK;
|
|
421
423
|
if (scanned === 1 || scanned % chunk === 0) {
|
|
422
|
-
//
|
|
424
|
+
// totally unknown → totally = 0 → no automatic \n
|
|
423
425
|
updateProgress2(" Scan local: ", scanned, 0, rel);
|
|
424
426
|
}
|
|
425
427
|
}
|
|
@@ -429,7 +431,7 @@ async function walkLocal(root) {
|
|
|
429
431
|
await recurse(root);
|
|
430
432
|
|
|
431
433
|
if (scanned > 0) {
|
|
432
|
-
//
|
|
434
|
+
// last line + neat finish
|
|
433
435
|
updateProgress2(" Scan local: ", scanned, 0, "fertig");
|
|
434
436
|
process.stdout.write("\n");
|
|
435
437
|
progressActive = false;
|
|
@@ -561,6 +563,45 @@ async function getRemoteHash(rel, meta, sftp) {
|
|
|
561
563
|
return hash;
|
|
562
564
|
}
|
|
563
565
|
|
|
566
|
+
// ---------------------------------------------------------------------------
|
|
567
|
+
// SFTP error explanation (for clearer messages)
|
|
568
|
+
// ---------------------------------------------------------------------------
|
|
569
|
+
|
|
570
|
+
function describeSftpError(err) {
|
|
571
|
+
if (!err) return "";
|
|
572
|
+
|
|
573
|
+
const code = err.code || err.errno || "";
|
|
574
|
+
const msg = (err.message || "").toLowerCase();
|
|
575
|
+
|
|
576
|
+
// Netzwerk / DNS
|
|
577
|
+
if (code === "ENOTFOUND") {
|
|
578
|
+
return "Host not found (ENOTFOUND) – Check hostname or DNS entry.";
|
|
579
|
+
}
|
|
580
|
+
if (code === "EHOSTUNREACH") {
|
|
581
|
+
return "Host not reachable (EHOSTUNREACH) – Check network/firewall.";
|
|
582
|
+
}
|
|
583
|
+
if (code === "ECONNREFUSED") {
|
|
584
|
+
return "Connection refused (ECONNREFUSED) – Check the port or SSH service.";
|
|
585
|
+
}
|
|
586
|
+
if (code === "ECONNRESET") {
|
|
587
|
+
return "Connection was reset by the server (ECONNRESET).";
|
|
588
|
+
}
|
|
589
|
+
if (code === "ETIMEDOUT") {
|
|
590
|
+
return "Connection timeout (ETIMEDOUT) – Server is not responding or is blocked.";
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Auth / Authorisations
|
|
594
|
+
if (msg.includes("all configured authentication methods failed")) {
|
|
595
|
+
return "Authentication failed – check your username/password or SSH keys.";
|
|
596
|
+
}
|
|
597
|
+
if (msg.includes("permission denied")) {
|
|
598
|
+
return "Access denied – check permissions on the server.";
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// Fallback
|
|
602
|
+
return "";
|
|
603
|
+
}
|
|
604
|
+
|
|
564
605
|
// ---------------------------------------------------------------------------
|
|
565
606
|
// MAIN
|
|
566
607
|
// ---------------------------------------------------------------------------
|
|
@@ -568,43 +609,46 @@ async function getRemoteHash(rel, meta, sftp) {
|
|
|
568
609
|
async function main() {
|
|
569
610
|
const start = Date.now();
|
|
570
611
|
|
|
571
|
-
log(
|
|
612
|
+
log(`\n\n${hr2()}`);
|
|
572
613
|
log(
|
|
573
614
|
pc.bold(
|
|
574
615
|
`🔐 SFTP Push-Synchronisation: sftp-push-sync v${pkg.version} [logLevel=${LOG_LEVEL}]`
|
|
575
616
|
)
|
|
576
617
|
);
|
|
577
|
-
log(
|
|
578
|
-
log(`
|
|
579
|
-
log(
|
|
580
|
-
log(
|
|
618
|
+
log(`${tab_a()}Connection: ${pc.cyan(TARGET)}`);
|
|
619
|
+
log(`Worker: ${CONNECTION.workers}`);
|
|
620
|
+
log(`${tab_a()}Host: ${pc.green(CONNECTION.host)}:${pc.green(CONNECTION.port)}`);
|
|
621
|
+
log(`${tab_a()}Local: ${pc.green(CONNECTION.localRoot)}`);
|
|
622
|
+
log(`${tab_a()}Remote: ${pc.green(CONNECTION.remoteRoot)}`);
|
|
581
623
|
if (DRY_RUN) log(pc.yellow(" Mode: DRY-RUN (no changes)"));
|
|
582
624
|
if (RUN_UPLOAD_LIST || RUN_DOWNLOAD_LIST) {
|
|
583
625
|
log(
|
|
584
626
|
pc.blue(
|
|
585
|
-
|
|
627
|
+
`${tab_a()}Extra: ${RUN_UPLOAD_LIST ? "uploadList " : ""}${
|
|
586
628
|
RUN_DOWNLOAD_LIST ? "downloadList" : ""
|
|
587
629
|
}`
|
|
588
630
|
)
|
|
589
631
|
);
|
|
590
632
|
}
|
|
591
|
-
log(
|
|
633
|
+
log(`${hr1()}\n`);
|
|
592
634
|
|
|
593
635
|
const sftp = new SftpClient();
|
|
636
|
+
let connected = false;
|
|
594
637
|
|
|
595
638
|
const toAdd = [];
|
|
596
639
|
const toUpdate = [];
|
|
597
640
|
const toDelete = [];
|
|
598
641
|
|
|
599
642
|
try {
|
|
643
|
+
log(pc.cyan("🔌 Connecting to SFTP server …"));
|
|
600
644
|
await sftp.connect({
|
|
601
645
|
host: CONNECTION.host,
|
|
602
646
|
port: CONNECTION.port,
|
|
603
647
|
username: CONNECTION.user,
|
|
604
648
|
password: CONNECTION.password,
|
|
605
649
|
});
|
|
606
|
-
|
|
607
|
-
|
|
650
|
+
connected = true;
|
|
651
|
+
log(pc.green(`${tab_a()}✔ Connected to SFTP.`));
|
|
608
652
|
|
|
609
653
|
if (!fs.existsSync(CONNECTION.localRoot)) {
|
|
610
654
|
console.error(
|
|
@@ -616,20 +660,20 @@ async function main() {
|
|
|
616
660
|
|
|
617
661
|
log(pc.bold(pc.cyan("📥 Phase 1: Scan local files …")));
|
|
618
662
|
const local = await walkLocal(CONNECTION.localRoot);
|
|
619
|
-
log(
|
|
663
|
+
log(`${tab_a()}→ ${local.size} local files`);
|
|
620
664
|
|
|
621
665
|
if (AUTO_EXCLUDED.size > 0) {
|
|
622
666
|
log("");
|
|
623
667
|
log(pc.dim(" Auto-excluded (uploadList/downloadList):"));
|
|
624
668
|
[...AUTO_EXCLUDED].sort().forEach((file) => {
|
|
625
|
-
log(pc.dim(
|
|
669
|
+
log(pc.dim(`${tab_a()} - ${file}`));
|
|
626
670
|
});
|
|
627
671
|
log("");
|
|
628
672
|
}
|
|
629
673
|
|
|
630
674
|
log(pc.bold(pc.cyan("📤 Phase 2: Scan remote files …")));
|
|
631
675
|
const remote = await walkRemote(sftp, CONNECTION.remoteRoot);
|
|
632
|
-
log(
|
|
676
|
+
log(`${tab_a()}→ ${remote.size} remote files\n`);
|
|
633
677
|
|
|
634
678
|
const localKeys = new Set(local.keys());
|
|
635
679
|
const remoteKeys = new Set(remote.keys());
|
|
@@ -644,7 +688,7 @@ async function main() {
|
|
|
644
688
|
|
|
645
689
|
const chunk = IS_VERBOSE ? 1 : ANALYZE_CHUNK;
|
|
646
690
|
if (
|
|
647
|
-
checkedCount === 1 || //
|
|
691
|
+
checkedCount === 1 || // immediate first issue
|
|
648
692
|
checkedCount % chunk === 0 ||
|
|
649
693
|
checkedCount === totalToCheck
|
|
650
694
|
) {
|
|
@@ -686,19 +730,19 @@ async function main() {
|
|
|
686
730
|
).toString("utf8");
|
|
687
731
|
|
|
688
732
|
if (localStr === remoteStr) {
|
|
689
|
-
vlog(
|
|
733
|
+
vlog(`${tab_a()}${pc.dim("✓ Unchanged (Text):")} ${rel}`);
|
|
690
734
|
continue;
|
|
691
735
|
}
|
|
692
736
|
|
|
693
737
|
if (IS_VERBOSE) {
|
|
694
738
|
const diff = diffWords(remoteStr, localStr);
|
|
695
739
|
const blocks = diff.filter((d) => d.added || d.removed).length;
|
|
696
|
-
vlog(
|
|
740
|
+
vlog(`${tab_a()}${CHA} Text difference (${blocks} blocks) in ${rel}`);
|
|
697
741
|
}
|
|
698
742
|
|
|
699
743
|
toUpdate.push({ rel, local: l, remote: r, remotePath });
|
|
700
744
|
if (!IS_LACONIC) {
|
|
701
|
-
log(`${CHA} ${pc.yellow("Content changed (Text):")} ${rel}`);
|
|
745
|
+
log(`${tab_a()}${CHA} ${pc.yellow("Content changed (Text):")} ${rel}`);
|
|
702
746
|
}
|
|
703
747
|
} else {
|
|
704
748
|
// Binary: Hash comparison with cache
|
|
@@ -711,14 +755,14 @@ async function main() {
|
|
|
711
755
|
]);
|
|
712
756
|
|
|
713
757
|
if (localHash === remoteHash) {
|
|
714
|
-
vlog(
|
|
758
|
+
vlog(`${tab_a()}${pc.dim("✓ Unchanged (binary, hash):")} ${rel}`);
|
|
715
759
|
continue;
|
|
716
760
|
}
|
|
717
761
|
|
|
718
762
|
if (IS_VERBOSE) {
|
|
719
|
-
vlog(
|
|
720
|
-
vlog(
|
|
721
|
-
vlog(
|
|
763
|
+
vlog(`${tab_a()}${CHA} Hash different (binary): ${rel}`);
|
|
764
|
+
vlog(`${tab_a()} local: ${localHash}`);
|
|
765
|
+
vlog(`${tab_a()} remote: ${remoteHash}`);
|
|
722
766
|
}
|
|
723
767
|
|
|
724
768
|
toUpdate.push({ rel, local: l, remote: r, remotePath });
|
|
@@ -728,13 +772,15 @@ async function main() {
|
|
|
728
772
|
}
|
|
729
773
|
}
|
|
730
774
|
|
|
731
|
-
log(
|
|
775
|
+
log(
|
|
776
|
+
"\n" + pc.bold(pc.cyan("🧹 Phase 4: Removing orphaned remote files …"))
|
|
777
|
+
);
|
|
732
778
|
for (const rel of remoteKeys) {
|
|
733
779
|
if (!localKeys.has(rel)) {
|
|
734
780
|
const r = remote.get(rel);
|
|
735
781
|
toDelete.push({ rel, remotePath: r.remotePath });
|
|
736
782
|
if (!IS_LACONIC) {
|
|
737
|
-
log(
|
|
783
|
+
log(`${tab_a()}${DEL} ${pc.red("Remove:")} ${rel}`);
|
|
738
784
|
}
|
|
739
785
|
}
|
|
740
786
|
}
|
|
@@ -796,7 +842,11 @@ async function main() {
|
|
|
796
842
|
"Deletes"
|
|
797
843
|
);
|
|
798
844
|
} else {
|
|
799
|
-
log(
|
|
845
|
+
log(
|
|
846
|
+
pc.yellow(
|
|
847
|
+
"\n💡 DRY-RUN: Connection tested, no files transferred or deleted."
|
|
848
|
+
)
|
|
849
|
+
);
|
|
800
850
|
}
|
|
801
851
|
|
|
802
852
|
// -------------------------------------------------------------------
|
|
@@ -806,7 +856,7 @@ async function main() {
|
|
|
806
856
|
if (RUN_UPLOAD_LIST && UPLOAD_LIST.length > 0) {
|
|
807
857
|
log(
|
|
808
858
|
"\n" +
|
|
809
|
-
pc.bold(pc.cyan("⬆️
|
|
859
|
+
pc.bold(pc.cyan("⬆️ Extra Phase: Upload-List (explicit files) …"))
|
|
810
860
|
);
|
|
811
861
|
|
|
812
862
|
const tasks = UPLOAD_LIST.map((rel) => ({
|
|
@@ -817,7 +867,7 @@ async function main() {
|
|
|
817
867
|
|
|
818
868
|
if (DRY_RUN) {
|
|
819
869
|
for (const t of tasks) {
|
|
820
|
-
log(
|
|
870
|
+
log(`${tab_a()}${ADD} would upload (uploadList): ${t.rel}`);
|
|
821
871
|
}
|
|
822
872
|
} else {
|
|
823
873
|
await runTasks(
|
|
@@ -831,7 +881,7 @@ async function main() {
|
|
|
831
881
|
// ignore
|
|
832
882
|
}
|
|
833
883
|
await sftp.put(localPath, remotePath);
|
|
834
|
-
log(
|
|
884
|
+
log(`${tab_a()}${ADD} uploadList: ${rel}`);
|
|
835
885
|
},
|
|
836
886
|
"Upload-List"
|
|
837
887
|
);
|
|
@@ -841,7 +891,7 @@ async function main() {
|
|
|
841
891
|
if (RUN_DOWNLOAD_LIST && DOWNLOAD_LIST.length > 0) {
|
|
842
892
|
log(
|
|
843
893
|
"\n" +
|
|
844
|
-
pc.bold(pc.cyan("⬇️
|
|
894
|
+
pc.bold(pc.cyan("⬇️ Extra Phase: Download-List (explicit files) …"))
|
|
845
895
|
);
|
|
846
896
|
|
|
847
897
|
const tasks = DOWNLOAD_LIST.map((rel) => ({
|
|
@@ -852,7 +902,7 @@ async function main() {
|
|
|
852
902
|
|
|
853
903
|
if (DRY_RUN) {
|
|
854
904
|
for (const t of tasks) {
|
|
855
|
-
log(
|
|
905
|
+
log(`${tab_a()}${ADD} would download (downloadList): ${t.rel}`);
|
|
856
906
|
}
|
|
857
907
|
} else {
|
|
858
908
|
await runTasks(
|
|
@@ -861,7 +911,7 @@ async function main() {
|
|
|
861
911
|
async ({ remotePath, localPath, rel }) => {
|
|
862
912
|
await fsp.mkdir(path.dirname(localPath), { recursive: true });
|
|
863
913
|
await sftp.fastGet(remotePath, localPath);
|
|
864
|
-
log(
|
|
914
|
+
log(`${tab_a()}${ADD} downloadList: ${rel}`);
|
|
865
915
|
},
|
|
866
916
|
"Download-List"
|
|
867
917
|
);
|
|
@@ -875,34 +925,41 @@ async function main() {
|
|
|
875
925
|
|
|
876
926
|
// Summary
|
|
877
927
|
log("\n" + pc.bold(pc.cyan("📊 Summary:")));
|
|
878
|
-
log(
|
|
879
|
-
log(
|
|
880
|
-
log(
|
|
881
|
-
log(
|
|
928
|
+
log(`${tab_a()}Duration: ${pc.green(duration + " s")}`);
|
|
929
|
+
log(`${tab_a()}${ADD} Added : ${toAdd.length}`);
|
|
930
|
+
log(`${tab_a()}${CHA} Changed: ${toUpdate.length}`);
|
|
931
|
+
log(`${tab_a()}${DEL} Deleted: ${toDelete.length}`);
|
|
882
932
|
if (AUTO_EXCLUDED.size > 0) {
|
|
883
933
|
log(
|
|
884
|
-
|
|
934
|
+
`${tab_a()}${EXC} Excluded via uploadList | downloadList: ${AUTO_EXCLUDED.size}`
|
|
885
935
|
);
|
|
886
936
|
}
|
|
887
937
|
if (toAdd.length || toUpdate.length || toDelete.length) {
|
|
888
938
|
log("\n📄 Changes:");
|
|
889
939
|
[...toAdd.map((t) => t.rel)]
|
|
890
940
|
.sort()
|
|
891
|
-
.forEach((f) => console.log(
|
|
941
|
+
.forEach((f) => console.log(`${tab_a()}${ADD} ${f}`));
|
|
892
942
|
[...toUpdate.map((t) => t.rel)]
|
|
893
943
|
.sort()
|
|
894
|
-
.forEach((f) => console.log(
|
|
944
|
+
.forEach((f) => console.log(`${tab_a()}${CHA} ${f}`));
|
|
895
945
|
[...toDelete.map((t) => t.rel)]
|
|
896
946
|
.sort()
|
|
897
|
-
.forEach((f) => console.log(
|
|
947
|
+
.forEach((f) => console.log(`${tab_a()}${DEL} ${f}`));
|
|
898
948
|
} else {
|
|
899
949
|
log("\nNo changes.");
|
|
900
950
|
}
|
|
901
951
|
|
|
902
952
|
log("\n" + pc.bold(pc.green("✅ Sync complete.")));
|
|
903
|
-
log("==================================================================\n\n");
|
|
904
953
|
} catch (err) {
|
|
905
|
-
|
|
954
|
+
const hint = describeSftpError(err);
|
|
955
|
+
elog(pc.red("❌ Synchronisation error:"), err.message || err);
|
|
956
|
+
if (hint) {
|
|
957
|
+
wlog(pc.yellow(`${tab_a()}Mögliche Ursache:`), hint);
|
|
958
|
+
}
|
|
959
|
+
if (IS_VERBOSE) {
|
|
960
|
+
// Vollständiges Error-Objekt nur in verbose anzeigen
|
|
961
|
+
console.error(err);
|
|
962
|
+
}
|
|
906
963
|
process.exitCode = 1;
|
|
907
964
|
try {
|
|
908
965
|
await saveCache(true);
|
|
@@ -911,11 +968,18 @@ async function main() {
|
|
|
911
968
|
}
|
|
912
969
|
} finally {
|
|
913
970
|
try {
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
971
|
+
if (connected) {
|
|
972
|
+
await sftp.end();
|
|
973
|
+
log(pc.green(`${tab_a()}✔ Connection closed.`));
|
|
974
|
+
}
|
|
975
|
+
} catch (e) {
|
|
976
|
+
wlog(
|
|
977
|
+
pc.yellow("⚠️ Could not close SFTP connection cleanly:"),
|
|
978
|
+
e.message || e
|
|
979
|
+
);
|
|
917
980
|
}
|
|
918
981
|
}
|
|
982
|
+
log(`${hr2()}\n\n`);
|
|
919
983
|
}
|
|
920
984
|
|
|
921
|
-
main();
|
|
985
|
+
main();
|