sftp-push-sync 2.1.1 → 2.1.3
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 +8 -3
- package/bin/sftp-push-sync.mjs +95 -15
- package/images/example-output-002.jpg +0 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,7 +6,10 @@ Implements a push syncronisation with Dry-Run. Performs the following tasks:
|
|
|
6
6
|
2. Delete remote files that no longer exist locally
|
|
7
7
|
3. Identify changes based on size or altered content and upload them
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Why?
|
|
10
|
+
|
|
11
|
+
- I use the script to transfer [Hugo websites](https://gohugo.io) to the server.
|
|
12
|
+
- This is part of the [Hugo-Toolbox](https://www.npmjs.com/package/hugo-toolbox).
|
|
10
13
|
|
|
11
14
|
Features:
|
|
12
15
|
|
|
@@ -16,7 +19,7 @@ Features:
|
|
|
16
19
|
- adds, updates, deletes files
|
|
17
20
|
- text diff detection
|
|
18
21
|
- Binary files (images, video, audio, PDF, etc.): SHA-256 hash comparison
|
|
19
|
-
- Hashes are cached in .sync-cache
|
|
22
|
+
- Hashes are cached in .sync-cache.*.json
|
|
20
23
|
- Parallel uploads/deletions via worker pool
|
|
21
24
|
- include/exclude patterns
|
|
22
25
|
- Sidecar uploads / downloads - Bypassing the sync process
|
|
@@ -244,9 +247,11 @@ Note: The first run always takes a while, especially with lots of media – so b
|
|
|
244
247
|
|
|
245
248
|
## Example Output
|
|
246
249
|
|
|
247
|
-

|
|
248
251
|
|
|
249
252
|
## Links
|
|
250
253
|
|
|
251
254
|
- <https://www.npmjs.com/package/sftp-push-sync>
|
|
252
255
|
- <https://github.com/cnichte/sftp-push-sync>
|
|
256
|
+
- <https://www.npmjs.com/package/hugo-toolbox>
|
|
257
|
+
- <https://carsten-nichte.de>
|
package/bin/sftp-push-sync.mjs
CHANGED
|
@@ -385,6 +385,10 @@ async function markCacheDirty() {
|
|
|
385
385
|
|
|
386
386
|
let progressActive = false;
|
|
387
387
|
|
|
388
|
+
// Spinner-Frames für Progress-Zeilen
|
|
389
|
+
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
390
|
+
let spinnerIndex = 0;
|
|
391
|
+
|
|
388
392
|
function clearProgressLine() {
|
|
389
393
|
if (!process.stdout.isTTY || !progressActive) return;
|
|
390
394
|
|
|
@@ -450,25 +454,28 @@ function shortenPathForProgress(rel) {
|
|
|
450
454
|
}
|
|
451
455
|
|
|
452
456
|
// Two-line progress bar (for terminal) + 1-line log entry
|
|
453
|
-
function updateProgress2(prefix, current, total, rel = "") {
|
|
457
|
+
function updateProgress2(prefix, current, total, rel = "", suffix="Files") {
|
|
454
458
|
const short = rel ? shortenPathForProgress(rel) : "";
|
|
455
459
|
|
|
456
460
|
// Log file: always as a single line with **full** rel path
|
|
457
461
|
const base =
|
|
458
462
|
total && total > 0
|
|
459
|
-
? `${prefix}${current}/${total}
|
|
460
|
-
: `${prefix}${current}
|
|
463
|
+
? `${prefix}${current}/${total} ${suffix}`
|
|
464
|
+
: `${prefix}${current} ${suffix}`;
|
|
461
465
|
writeLogLine(`[progress] ${base}${rel ? " – " + rel : ""}`);
|
|
462
466
|
|
|
467
|
+
const frame = SPINNER_FRAMES[spinnerIndex];
|
|
468
|
+
spinnerIndex = (spinnerIndex + 1) % SPINNER_FRAMES.length;
|
|
469
|
+
|
|
463
470
|
if (!process.stdout.isTTY) {
|
|
464
471
|
// Fallback-Terminal
|
|
465
472
|
if (total && total > 0) {
|
|
466
473
|
const percent = ((current / total) * 100).toFixed(1);
|
|
467
474
|
console.log(
|
|
468
|
-
`${tab_a()}${prefix}${current}/${total}
|
|
475
|
+
`${tab_a()}${frame} ${prefix}${current}/${total} ${suffix} (${percent}%) – ${short}`
|
|
469
476
|
);
|
|
470
477
|
} else {
|
|
471
|
-
console.log(`${tab_a()}${prefix}${current}
|
|
478
|
+
console.log(`${tab_a()}${frame} ${prefix}${current} ${suffix} – ${short}`);
|
|
472
479
|
}
|
|
473
480
|
return;
|
|
474
481
|
}
|
|
@@ -478,10 +485,10 @@ function updateProgress2(prefix, current, total, rel = "") {
|
|
|
478
485
|
let line1;
|
|
479
486
|
if (total && total > 0) {
|
|
480
487
|
const percent = ((current / total) * 100).toFixed(1);
|
|
481
|
-
line1 = `${tab_a()}${prefix}${current}/${total} Files (${percent}%)`;
|
|
488
|
+
line1 = `${tab_a()}${frame} ${prefix}${current}/${total} Files (${percent}%)`;
|
|
482
489
|
} else {
|
|
483
490
|
// „unknown total“ / Scanner-Modus
|
|
484
|
-
line1 = `${tab_a()}${prefix}${current} Files`;
|
|
491
|
+
line1 = `${tab_a()}${frame} ${prefix}${current} Files`;
|
|
485
492
|
}
|
|
486
493
|
|
|
487
494
|
let line2 = short;
|
|
@@ -534,6 +541,17 @@ async function runTasks(items, workerCount, handler, label = "Tasks") {
|
|
|
534
541
|
await Promise.all(workers);
|
|
535
542
|
}
|
|
536
543
|
|
|
544
|
+
// ---------------------------------------------------------------------------
|
|
545
|
+
// Directory-Statistiken (für Summary)
|
|
546
|
+
// ---------------------------------------------------------------------------
|
|
547
|
+
|
|
548
|
+
const DIR_STATS = {
|
|
549
|
+
ensuredDirs: 0, // Verzeichnisse, die wir während "Preparing remote directories" geprüft haben
|
|
550
|
+
createdDirs: 0, // Verzeichnisse, die wirklich neu angelegt wurden
|
|
551
|
+
cleanupVisited: 0, // Verzeichnisse, die während Cleanup inspiziert wurden
|
|
552
|
+
cleanupDeleted: 0, // Verzeichnisse, die gelöscht wurden
|
|
553
|
+
};
|
|
554
|
+
|
|
537
555
|
// ---------------------------------------------------------------------------
|
|
538
556
|
// Neue Helper: Verzeichnisse für Uploads/Updates vorbereiten
|
|
539
557
|
// ---------------------------------------------------------------------------
|
|
@@ -563,16 +581,42 @@ function collectDirsFromChanges(changes) {
|
|
|
563
581
|
|
|
564
582
|
async function ensureAllRemoteDirsExist(sftp, remoteRoot, toAdd, toUpdate) {
|
|
565
583
|
const dirs = collectDirsFromChanges([...toAdd, ...toUpdate]);
|
|
584
|
+
const total = dirs.length;
|
|
585
|
+
DIR_STATS.ensuredDirs += total;
|
|
586
|
+
|
|
587
|
+
if (total === 0) return;
|
|
588
|
+
|
|
589
|
+
let current = 0;
|
|
566
590
|
|
|
567
591
|
for (const relDir of dirs) {
|
|
592
|
+
current += 1;
|
|
568
593
|
const remoteDir = path.posix.join(remoteRoot, relDir);
|
|
594
|
+
|
|
595
|
+
// Fortschritt: in der zweiten Zeile den Pfad anzeigen
|
|
596
|
+
updateProgress2("Prepare dirs: ", current, total, relDir);
|
|
597
|
+
|
|
569
598
|
try {
|
|
570
|
-
await sftp.
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
599
|
+
const exists = await sftp.exists(remoteDir);
|
|
600
|
+
if (!exists) {
|
|
601
|
+
await sftp.mkdir(remoteDir, true);
|
|
602
|
+
DIR_STATS.createdDirs += 1;
|
|
603
|
+
vlog(`${tab_a()}${pc.dim("dir created:")} ${remoteDir}`);
|
|
604
|
+
} else {
|
|
605
|
+
vlog(`${tab_a()}${pc.dim("dir ok:")} ${remoteDir}`);
|
|
606
|
+
}
|
|
607
|
+
} catch (e) {
|
|
608
|
+
wlog(
|
|
609
|
+
pc.yellow("⚠️ Could not ensure directory:"),
|
|
610
|
+
remoteDir,
|
|
611
|
+
e.message || e
|
|
612
|
+
);
|
|
574
613
|
}
|
|
575
614
|
}
|
|
615
|
+
|
|
616
|
+
// Zeile „fertig“ markieren und Progress-Flag zurücksetzen
|
|
617
|
+
updateProgress2("Prepare dirs: ", total, total, "fertig");
|
|
618
|
+
process.stdout.write("\n");
|
|
619
|
+
progressActive = false;
|
|
576
620
|
}
|
|
577
621
|
|
|
578
622
|
// -----------------------------------------------------------
|
|
@@ -583,6 +627,20 @@ async function cleanupEmptyDirs(sftp, rootDir) {
|
|
|
583
627
|
// Rekursiv prüfen, ob ein Verzeichnis und seine Unterverzeichnisse
|
|
584
628
|
// KEINE Dateien enthalten. Nur dann löschen wir es.
|
|
585
629
|
async function recurse(dir, depth = 0) {
|
|
630
|
+
DIR_STATS.cleanupVisited += 1;
|
|
631
|
+
|
|
632
|
+
const relForProgress =
|
|
633
|
+
toPosix(path.relative(rootDir, dir)) || ".";
|
|
634
|
+
|
|
635
|
+
// Fortschritt: aktuelle Directory in zweiter Zeile anzeigen
|
|
636
|
+
updateProgress2(
|
|
637
|
+
"Cleanup dirs: ",
|
|
638
|
+
DIR_STATS.cleanupVisited,
|
|
639
|
+
0,
|
|
640
|
+
relForProgress,
|
|
641
|
+
"Folders"
|
|
642
|
+
);
|
|
643
|
+
|
|
586
644
|
let hasFile = false;
|
|
587
645
|
const subdirs = [];
|
|
588
646
|
|
|
@@ -626,14 +684,16 @@ async function cleanupEmptyDirs(sftp, rootDir) {
|
|
|
626
684
|
|
|
627
685
|
// Root nur löschen, wenn explizit erlaubt
|
|
628
686
|
if (isEmpty && (!isRoot || CLEANUP_EMPTY_ROOTS)) {
|
|
629
|
-
const rel =
|
|
687
|
+
const rel = relForProgress || ".";
|
|
630
688
|
if (DRY_RUN) {
|
|
631
689
|
log(`${tab_a()}${DEL} (DRY-RUN) Remove empty directory: ${rel}`);
|
|
690
|
+
DIR_STATS.cleanupDeleted += 1;
|
|
632
691
|
} else {
|
|
633
692
|
try {
|
|
634
693
|
// Nicht rekursiv: wir löschen nur, wenn unser eigener Check "leer" sagt.
|
|
635
694
|
await sftp.rmdir(dir, false);
|
|
636
695
|
log(`${tab_a()}${DEL} Removed empty directory: ${rel}`);
|
|
696
|
+
DIR_STATS.cleanupDeleted += 1;
|
|
637
697
|
} catch (e) {
|
|
638
698
|
wlog(
|
|
639
699
|
pc.yellow("⚠️ Could not remove directory:"),
|
|
@@ -650,6 +710,17 @@ async function cleanupEmptyDirs(sftp, rootDir) {
|
|
|
650
710
|
}
|
|
651
711
|
|
|
652
712
|
await recurse(rootDir, 0);
|
|
713
|
+
|
|
714
|
+
if (DIR_STATS.cleanupVisited > 0) {
|
|
715
|
+
updateProgress2(
|
|
716
|
+
"Cleanup dirs: ",
|
|
717
|
+
DIR_STATS.cleanupVisited,
|
|
718
|
+
DIR_STATS.cleanupVisited,
|
|
719
|
+
"fertig", "Folders"
|
|
720
|
+
);
|
|
721
|
+
process.stdout.write("\n");
|
|
722
|
+
progressActive = false;
|
|
723
|
+
}
|
|
653
724
|
}
|
|
654
725
|
|
|
655
726
|
// ---------------------------------------------------------------------------
|
|
@@ -1172,7 +1243,7 @@ async function main() {
|
|
|
1172
1243
|
if (!r) {
|
|
1173
1244
|
toAdd.push({ rel, local: l, remotePath });
|
|
1174
1245
|
if (!IS_LACONIC) {
|
|
1175
|
-
log(`${ADD} ${pc.green("New:")} ${rel}`);
|
|
1246
|
+
log(`${tab_a()}${ADD} ${pc.green("New:")} ${rel}`);
|
|
1176
1247
|
}
|
|
1177
1248
|
continue;
|
|
1178
1249
|
}
|
|
@@ -1181,7 +1252,7 @@ async function main() {
|
|
|
1181
1252
|
if (l.size !== r.size) {
|
|
1182
1253
|
toUpdate.push({ rel, local: l, remote: r, remotePath });
|
|
1183
1254
|
if (!IS_LACONIC) {
|
|
1184
|
-
log(`${CHA} ${pc.yellow("Size changed:")} ${rel}`);
|
|
1255
|
+
log(`${tab_a()}${CHA} ${pc.yellow("Size changed:")} ${rel}`);
|
|
1185
1256
|
}
|
|
1186
1257
|
continue;
|
|
1187
1258
|
}
|
|
@@ -1239,7 +1310,7 @@ async function main() {
|
|
|
1239
1310
|
|
|
1240
1311
|
toUpdate.push({ rel, local: l, remote: r, remotePath });
|
|
1241
1312
|
if (!IS_LACONIC) {
|
|
1242
|
-
log(`${CHA} ${pc.yellow("Content changed (Binary):")} ${rel}`);
|
|
1313
|
+
log(`${tab_a()}${CHA} ${pc.yellow("Content changed (Binary):")} ${rel}`);
|
|
1243
1314
|
}
|
|
1244
1315
|
}
|
|
1245
1316
|
}
|
|
@@ -1375,6 +1446,15 @@ async function main() {
|
|
|
1375
1446
|
}`
|
|
1376
1447
|
);
|
|
1377
1448
|
}
|
|
1449
|
+
|
|
1450
|
+
// Directory-Statistik
|
|
1451
|
+
const dirsChecked = DIR_STATS.ensuredDirs + DIR_STATS.cleanupVisited;
|
|
1452
|
+
log("");
|
|
1453
|
+
log(pc.bold("Folders:"));
|
|
1454
|
+
log(`${tab_a()}Checked : ${dirsChecked}`);
|
|
1455
|
+
log(`${tab_a()}${ADD} Created: ${DIR_STATS.createdDirs}`);
|
|
1456
|
+
log(`${tab_a()}${DEL} Deleted: ${DIR_STATS.cleanupDeleted}`);
|
|
1457
|
+
|
|
1378
1458
|
if (toAdd.length || toUpdate.length || toDelete.length) {
|
|
1379
1459
|
log("");
|
|
1380
1460
|
log("📄 Changes:");
|
|
Binary file
|