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 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
- I use the script to transfer [Hugo websites](https://gohugo.io) to the server.
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.json to save space.
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
- ![An console output example](images/example-output-001.png)
250
+ ![An console output example](images/example-output-002.jpg)
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>
@@ -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} Files`
460
- : `${prefix}${current} Files`;
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} Files (${percent}%) – ${short}`
475
+ `${tab_a()}${frame} ${prefix}${current}/${total} ${suffix} (${percent}%) – ${short}`
469
476
  );
470
477
  } else {
471
- console.log(`${tab_a()}${prefix}${current} Files – ${short}`);
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.mkdir(remoteDir, true);
571
- vlog(`${tab_a()}${pc.dim("dir ok:")} ${remoteDir}`);
572
- } catch {
573
- // Directory may already exist / keine Rechte – ignorieren
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 = toPosix(path.relative(rootDir, dir)) || ".";
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sftp-push-sync",
3
- "version": "2.1.1",
3
+ "version": "2.1.3",
4
4
  "description": "SFTP sync tool for Hugo projects (local to remote, with hash cache)",
5
5
  "type": "module",
6
6
  "bin": {