sftp-push-sync 1.0.15 → 1.0.16
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/bin/sftp-push-sync.mjs +51 -104
- package/package.json +1 -1
package/bin/sftp-push-sync.mjs
CHANGED
|
@@ -48,16 +48,13 @@ const pkg = require("../package.json");
|
|
|
48
48
|
// Colors for the State (works on dark + light background)
|
|
49
49
|
const ADD = pc.green("+"); // Added
|
|
50
50
|
const CHA = pc.yellow("~"); // Changed
|
|
51
|
-
const DEL = pc.red("-");
|
|
51
|
+
const DEL = pc.red("-"); // Deleted
|
|
52
52
|
const EXC = pc.redBright("-"); // Excluded
|
|
53
53
|
|
|
54
54
|
const hr1 = () => "─".repeat(65); // horizontal line -
|
|
55
55
|
const hr2 = () => "=".repeat(65); // horizontal line =
|
|
56
56
|
const tab_a = () => " ".repeat(3); // indentation for formatting the terminal output.
|
|
57
57
|
const tab_b = () => " ".repeat(6);
|
|
58
|
-
const cr_a = () => "\n".repeat(1);
|
|
59
|
-
const cr_b = () => "\n".repeat(2);
|
|
60
|
-
|
|
61
58
|
|
|
62
59
|
// ---------------------------------------------------------------------------
|
|
63
60
|
// CLI arguments
|
|
@@ -195,7 +192,7 @@ const CACHE_PATH = path.resolve(syncCacheName);
|
|
|
195
192
|
|
|
196
193
|
let CACHE = {
|
|
197
194
|
version: 1,
|
|
198
|
-
local: {},
|
|
195
|
+
local: {}, // key: "<TARGET>:<relPath>" -> { size, mtimeMs, hash }
|
|
199
196
|
remote: {}, // key: "<TARGET>:<relPath>" -> { size, modifyTime, hash }
|
|
200
197
|
};
|
|
201
198
|
|
|
@@ -245,11 +242,14 @@ let progressActive = false;
|
|
|
245
242
|
|
|
246
243
|
function clearProgressLine() {
|
|
247
244
|
if (!process.stdout.isTTY || !progressActive) return;
|
|
248
|
-
const width = process.stdout.columns || 80;
|
|
249
|
-
const blank = " ".repeat(width);
|
|
250
245
|
|
|
251
|
-
//
|
|
252
|
-
|
|
246
|
+
// Zwei Progress-Zeilen ohne zusätzliche Newlines leeren:
|
|
247
|
+
// Cursor steht nach updateProgress2() auf der ersten Zeile.
|
|
248
|
+
process.stdout.write("\r"); // an Zeilenanfang
|
|
249
|
+
process.stdout.write("\x1b[2K"); // erste Zeile löschen
|
|
250
|
+
process.stdout.write("\x1b[1B"); // eine Zeile nach unten
|
|
251
|
+
process.stdout.write("\x1b[2K"); // zweite Zeile löschen
|
|
252
|
+
process.stdout.write("\x1b[1A"); // wieder nach oben
|
|
253
253
|
|
|
254
254
|
progressActive = false;
|
|
255
255
|
}
|
|
@@ -353,7 +353,7 @@ function updateProgress2(prefix, current, total, rel = "") {
|
|
|
353
353
|
if (line2.length > width) line2 = line2.slice(0, width - 1);
|
|
354
354
|
|
|
355
355
|
// zwei Zeilen überschreiben
|
|
356
|
-
process.stdout.write("\r" + line1.padEnd(width) +
|
|
356
|
+
process.stdout.write("\r" + line1.padEnd(width) + "\n");
|
|
357
357
|
process.stdout.write(line2.padEnd(width));
|
|
358
358
|
|
|
359
359
|
// Cursor wieder nach oben (auf die Fortschrittszeile)
|
|
@@ -384,7 +384,7 @@ async function runTasks(items, workerCount, handler, label = "Tasks") {
|
|
|
384
384
|
}
|
|
385
385
|
done += 1;
|
|
386
386
|
if (done % 10 === 0 || done === total) {
|
|
387
|
-
updateProgress2(`${
|
|
387
|
+
updateProgress2(`${label}: `, done, total);
|
|
388
388
|
}
|
|
389
389
|
}
|
|
390
390
|
}
|
|
@@ -429,7 +429,7 @@ async function walkLocal(root) {
|
|
|
429
429
|
const chunk = IS_VERBOSE ? 1 : SCAN_CHUNK;
|
|
430
430
|
if (scanned === 1 || scanned % chunk === 0) {
|
|
431
431
|
// totally unknown → total = 0 → no automatic \n
|
|
432
|
-
updateProgress2(
|
|
432
|
+
updateProgress2("Scan local: ", scanned, 0, rel);
|
|
433
433
|
}
|
|
434
434
|
}
|
|
435
435
|
}
|
|
@@ -439,8 +439,8 @@ async function walkLocal(root) {
|
|
|
439
439
|
|
|
440
440
|
if (scanned > 0) {
|
|
441
441
|
// last line + neat finish
|
|
442
|
-
updateProgress2(
|
|
443
|
-
process.stdout.write(
|
|
442
|
+
updateProgress2("Scan local: ", scanned, 0, "fertig");
|
|
443
|
+
process.stdout.write("\n");
|
|
444
444
|
progressActive = false;
|
|
445
445
|
}
|
|
446
446
|
|
|
@@ -480,17 +480,17 @@ async function walkRemote(sftp, remoteRoot) {
|
|
|
480
480
|
scanned += 1;
|
|
481
481
|
const chunk = IS_VERBOSE ? 1 : SCAN_CHUNK;
|
|
482
482
|
if (scanned === 1 || scanned % chunk === 0) {
|
|
483
|
-
updateProgress2(
|
|
483
|
+
updateProgress2("Scan remote: ", scanned, 0, rel);
|
|
484
484
|
}
|
|
485
485
|
}
|
|
486
486
|
}
|
|
487
487
|
}
|
|
488
488
|
|
|
489
|
-
await recurse(remoteRoot);
|
|
489
|
+
await recurse(remoteRoot, "");
|
|
490
490
|
|
|
491
491
|
if (scanned > 0) {
|
|
492
|
-
updateProgress2(
|
|
493
|
-
process.stdout.write(
|
|
492
|
+
updateProgress2("Scan remote: ", scanned, 0, "fertig");
|
|
493
|
+
process.stdout.write("\n");
|
|
494
494
|
progressActive = false;
|
|
495
495
|
}
|
|
496
496
|
|
|
@@ -616,7 +616,8 @@ function describeSftpError(err) {
|
|
|
616
616
|
async function main() {
|
|
617
617
|
const start = Date.now();
|
|
618
618
|
|
|
619
|
-
|
|
619
|
+
// Header-Abstand wie gehabt: zwei Leerzeilen davor
|
|
620
|
+
log("\n\n" + hr2());
|
|
620
621
|
log(
|
|
621
622
|
pc.bold(
|
|
622
623
|
`🔐 SFTP Push-Synchronisation: sftp-push-sync v${pkg.version} [logLevel=${LOG_LEVEL}]`
|
|
@@ -637,7 +638,7 @@ async function main() {
|
|
|
637
638
|
)
|
|
638
639
|
);
|
|
639
640
|
}
|
|
640
|
-
log(
|
|
641
|
+
log(hr1());
|
|
641
642
|
|
|
642
643
|
const sftp = new SftpClient();
|
|
643
644
|
let connected = false;
|
|
@@ -647,6 +648,7 @@ async function main() {
|
|
|
647
648
|
const toDelete = [];
|
|
648
649
|
|
|
649
650
|
try {
|
|
651
|
+
log("");
|
|
650
652
|
log(pc.cyan("🔌 Connecting to SFTP server …"));
|
|
651
653
|
await sftp.connect({
|
|
652
654
|
host: CONNECTION.host,
|
|
@@ -665,7 +667,9 @@ async function main() {
|
|
|
665
667
|
process.exit(1);
|
|
666
668
|
}
|
|
667
669
|
|
|
668
|
-
|
|
670
|
+
// Phase 1 – mit exakt einer Leerzeile davor
|
|
671
|
+
log("");
|
|
672
|
+
log(pc.bold(pc.cyan("📥 Phase 1: Scan local files …")));
|
|
669
673
|
const local = await walkLocal(CONNECTION.localRoot);
|
|
670
674
|
log(`${tab_a()}→ ${local.size} local files`);
|
|
671
675
|
|
|
@@ -678,14 +682,17 @@ async function main() {
|
|
|
678
682
|
log("");
|
|
679
683
|
}
|
|
680
684
|
|
|
681
|
-
|
|
685
|
+
// Phase 2 – auch mit einer Leerzeile davor
|
|
686
|
+
log("");
|
|
687
|
+
log(pc.bold(pc.cyan("📤 Phase 2: Scan remote files …")));
|
|
682
688
|
const remote = await walkRemote(sftp, CONNECTION.remoteRoot);
|
|
683
|
-
log(`${tab_a()}→ ${remote.size} remote files
|
|
689
|
+
log(`${tab_a()}→ ${remote.size} remote files`);
|
|
690
|
+
log("");
|
|
684
691
|
|
|
685
692
|
const localKeys = new Set(local.keys());
|
|
686
693
|
const remoteKeys = new Set(remote.keys());
|
|
687
694
|
|
|
688
|
-
log(
|
|
695
|
+
log(pc.bold(pc.cyan("🔎 Phase 3: Compare & decide …")));
|
|
689
696
|
const totalToCheck = localKeys.size;
|
|
690
697
|
let checkedCount = 0;
|
|
691
698
|
|
|
@@ -699,7 +706,7 @@ async function main() {
|
|
|
699
706
|
checkedCount % chunk === 0 ||
|
|
700
707
|
checkedCount === totalToCheck
|
|
701
708
|
) {
|
|
702
|
-
updateProgress2("
|
|
709
|
+
updateProgress2("Analyse: ", checkedCount, totalToCheck, rel);
|
|
703
710
|
}
|
|
704
711
|
|
|
705
712
|
const l = local.get(rel);
|
|
@@ -781,12 +788,12 @@ async function main() {
|
|
|
781
788
|
|
|
782
789
|
// Wenn Phase 3 nichts gefunden hat, explizit sagen
|
|
783
790
|
if (toAdd.length === 0 && toUpdate.length === 0) {
|
|
791
|
+
log("");
|
|
784
792
|
log(`${tab_a()}No differences found. Everything is up to date.`);
|
|
785
793
|
}
|
|
786
794
|
|
|
787
|
-
log(
|
|
788
|
-
|
|
789
|
-
);
|
|
795
|
+
log("");
|
|
796
|
+
log(pc.bold(pc.cyan("🧹 Phase 4: Removing orphaned remote files …")));
|
|
790
797
|
for (const rel of remoteKeys) {
|
|
791
798
|
if (!localKeys.has(rel)) {
|
|
792
799
|
const r = remote.get(rel);
|
|
@@ -807,7 +814,8 @@ async function main() {
|
|
|
807
814
|
// -------------------------------------------------------------------
|
|
808
815
|
|
|
809
816
|
if (!DRY_RUN) {
|
|
810
|
-
log(
|
|
817
|
+
log("");
|
|
818
|
+
log(pc.bold(pc.cyan("🚚 Phase 5: Apply changes …")));
|
|
811
819
|
|
|
812
820
|
// Upload new files
|
|
813
821
|
await runTasks(
|
|
@@ -859,82 +867,14 @@ async function main() {
|
|
|
859
867
|
"Deletes"
|
|
860
868
|
);
|
|
861
869
|
} else {
|
|
870
|
+
log("");
|
|
862
871
|
log(
|
|
863
872
|
pc.yellow(
|
|
864
|
-
|
|
873
|
+
"💡 DRY-RUN: Connection tested, no files transferred or deleted."
|
|
865
874
|
)
|
|
866
875
|
);
|
|
867
876
|
}
|
|
868
877
|
|
|
869
|
-
// -------------------------------------------------------------------
|
|
870
|
-
// Phase 6: optional uploadList / downloadList
|
|
871
|
-
// -------------------------------------------------------------------
|
|
872
|
-
|
|
873
|
-
if (RUN_UPLOAD_LIST && UPLOAD_LIST.length > 0) {
|
|
874
|
-
log(
|
|
875
|
-
cr_a() +
|
|
876
|
-
pc.bold(pc.cyan("⬆️ Extra Phase: Upload-List (explicit files) …"))
|
|
877
|
-
);
|
|
878
|
-
|
|
879
|
-
const tasks = UPLOAD_LIST.map((rel) => ({
|
|
880
|
-
rel,
|
|
881
|
-
localPath: path.join(CONNECTION.localRoot, rel),
|
|
882
|
-
remotePath: path.posix.join(CONNECTION.remoteRoot, toPosix(rel)),
|
|
883
|
-
}));
|
|
884
|
-
|
|
885
|
-
if (DRY_RUN) {
|
|
886
|
-
for (const t of tasks) {
|
|
887
|
-
log(`${tab_a()}${ADD} would upload (uploadList): ${t.rel}`);
|
|
888
|
-
}
|
|
889
|
-
} else {
|
|
890
|
-
await runTasks(
|
|
891
|
-
tasks,
|
|
892
|
-
CONNECTION.workers,
|
|
893
|
-
async ({ localPath, remotePath, rel }) => {
|
|
894
|
-
const remoteDir = path.posix.dirname(remotePath);
|
|
895
|
-
try {
|
|
896
|
-
await sftp.mkdir(remoteDir, true);
|
|
897
|
-
} catch {
|
|
898
|
-
// ignore
|
|
899
|
-
}
|
|
900
|
-
await sftp.put(localPath, remotePath);
|
|
901
|
-
log(`${tab_a()}${ADD} uploadList: ${rel}`);
|
|
902
|
-
},
|
|
903
|
-
"Upload-List"
|
|
904
|
-
);
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
if (RUN_DOWNLOAD_LIST && DOWNLOAD_LIST.length > 0) {
|
|
909
|
-
log(
|
|
910
|
-
cr_a() +
|
|
911
|
-
pc.bold(pc.cyan("⬇️ Extra Phase: Download-List (explicit files) …"))
|
|
912
|
-
);
|
|
913
|
-
|
|
914
|
-
const tasks = DOWNLOAD_LIST.map((rel) => ({
|
|
915
|
-
rel,
|
|
916
|
-
remotePath: path.posix.join(CONNECTION.remoteRoot, toPosix(rel)),
|
|
917
|
-
localPath: path.join(CONNECTION.localRoot, rel),
|
|
918
|
-
}));
|
|
919
|
-
|
|
920
|
-
if (DRY_RUN) {
|
|
921
|
-
for (const t of tasks) {
|
|
922
|
-
log(`${tab_a()}${ADD} would download (downloadList): ${t.rel}`);
|
|
923
|
-
}
|
|
924
|
-
} else {
|
|
925
|
-
await runTasks(
|
|
926
|
-
tasks,
|
|
927
|
-
CONNECTION.workers,
|
|
928
|
-
async ({ remotePath, localPath, rel }) => {
|
|
929
|
-
await fsp.mkdir(path.dirname(localPath), { recursive: true });
|
|
930
|
-
await sftp.fastGet(remotePath, localPath);
|
|
931
|
-
log(`${tab_a()}${ADD} downloadList: ${rel}`);
|
|
932
|
-
},
|
|
933
|
-
"Download-List"
|
|
934
|
-
);
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
|
|
938
878
|
const duration = ((Date.now() - start) / 1000).toFixed(2);
|
|
939
879
|
|
|
940
880
|
// Write cache safely at the end
|
|
@@ -942,7 +882,8 @@ async function main() {
|
|
|
942
882
|
|
|
943
883
|
// Summary
|
|
944
884
|
log(hr1());
|
|
945
|
-
log(
|
|
885
|
+
log("");
|
|
886
|
+
log(pc.bold(pc.cyan("📊 Summary:")));
|
|
946
887
|
log(`${tab_a()}Duration: ${pc.green(duration + " s")}`);
|
|
947
888
|
log(`${tab_a()}${ADD} Added : ${toAdd.length}`);
|
|
948
889
|
log(`${tab_a()}${CHA} Changed: ${toUpdate.length}`);
|
|
@@ -953,7 +894,8 @@ async function main() {
|
|
|
953
894
|
);
|
|
954
895
|
}
|
|
955
896
|
if (toAdd.length || toUpdate.length || toDelete.length) {
|
|
956
|
-
log(
|
|
897
|
+
log("");
|
|
898
|
+
log("📄 Changes:");
|
|
957
899
|
[...toAdd.map((t) => t.rel)]
|
|
958
900
|
.sort()
|
|
959
901
|
.forEach((f) => console.log(`${tab_a()}${ADD} ${f}`));
|
|
@@ -964,10 +906,12 @@ async function main() {
|
|
|
964
906
|
.sort()
|
|
965
907
|
.forEach((f) => console.log(`${tab_a()}${DEL} ${f}`));
|
|
966
908
|
} else {
|
|
967
|
-
log(
|
|
909
|
+
log("");
|
|
910
|
+
log("No changes.");
|
|
968
911
|
}
|
|
969
912
|
|
|
970
|
-
log(
|
|
913
|
+
log("");
|
|
914
|
+
log(pc.bold(pc.green("✅ Sync complete.")));
|
|
971
915
|
} catch (err) {
|
|
972
916
|
const hint = describeSftpError(err);
|
|
973
917
|
elog(pc.red("❌ Synchronisation error:"), err.message || err);
|
|
@@ -997,7 +941,10 @@ async function main() {
|
|
|
997
941
|
);
|
|
998
942
|
}
|
|
999
943
|
}
|
|
1000
|
-
|
|
944
|
+
|
|
945
|
+
// Abschlusslinie + eine Leerzeile dahinter
|
|
946
|
+
log(hr2());
|
|
947
|
+
log("");
|
|
1001
948
|
}
|
|
1002
949
|
|
|
1003
950
|
main();
|