sftp-push-sync 1.0.11 → 1.0.12

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
@@ -66,6 +66,11 @@ Create a `sync.config.json` in the root folder of your project:
66
66
  "textExtensions": [
67
67
  ".html", ".xml", ".txt", ".json", ".js", ".css", ".md", ".svg"
68
68
  ],
69
+ "progress": {
70
+ "scanChunk": 10,
71
+ "analyzeChunk": 1
72
+ },
73
+ "logLevel": "normal", // or: "verbose", "laconic"
69
74
  "uploadList": [],
70
75
  "downloadList": [
71
76
  "download-counter.json"
@@ -84,7 +89,6 @@ A list of files that are excluded from the sync comparison and can be downloaded
84
89
  - Relative to remoteRoot "download-counter.json"
85
90
  - or e.g. "logs/download-counter.json"
86
91
 
87
-
88
92
  ```bash
89
93
  # normal synchronisation
90
94
  sftp-push-sync staging
@@ -97,6 +101,10 @@ sftp-push-sync prod --download-list --dry-run # view first
97
101
  sftp-push-sync prod --download-list # then do
98
102
  ```
99
103
 
104
+ ### Logging Progress
105
+
106
+ For >100k files, use ANALYZE_CHUNKS = 10 or 50, otherwise the TTY output itself is a relevant factor.
107
+
100
108
  ## NPM Scripts
101
109
 
102
110
  - Can be conveniently started via the scripts in `package.json`:
@@ -146,4 +154,4 @@ The first run always takes a while, especially with lots of images – so be pat
146
154
  ## Links
147
155
 
148
156
  - <https://www.npmjs.com/package/sftp-push-sync>
149
- - <https://github.com/cnichte/sftp-push-sync>
157
+ - <https://github.com/cnichte/sftp-push-sync>
@@ -40,6 +40,11 @@ import { createHash } from "crypto";
40
40
  import { Writable } from "stream";
41
41
  import pc from "picocolors";
42
42
 
43
+ // get Versionsnummer
44
+ import { createRequire } from "module";
45
+ const require = createRequire(import.meta.url);
46
+ const pkg = require("../package.json");
47
+
43
48
  // Colors for the State (works on dark + light background)
44
49
  const ADD = pc.green("+"); // Added
45
50
  const CHA = pc.yellow("~"); // Changed
@@ -53,10 +58,14 @@ const EXC = pc.redBright("-"); // Excluded
53
58
  const args = process.argv.slice(2);
54
59
  const TARGET = args[0];
55
60
  const DRY_RUN = args.includes("--dry-run");
56
- const VERBOSE = args.includes("--verbose") || args.includes("-v");
57
61
  const RUN_UPLOAD_LIST = args.includes("--upload-list");
58
62
  const RUN_DOWNLOAD_LIST = args.includes("--download-list");
59
63
 
64
+ // logLevel override via CLI (optional)
65
+ let cliLogLevel = null;
66
+ if (args.includes("--verbose")) cliLogLevel = "verbose";
67
+ if (args.includes("--laconic")) cliLogLevel = "laconic";
68
+
60
69
  if (!TARGET) {
61
70
  console.error(pc.red("❌ Please specify a connection profile:"));
62
71
  console.error(pc.yellow(" sftp-push-sync staging --dry-run"));
@@ -78,18 +87,18 @@ let CONFIG_RAW;
78
87
  try {
79
88
  CONFIG_RAW = JSON.parse(await fsp.readFile(CONFIG_PATH, "utf8"));
80
89
  } catch (err) {
81
- elog(pc.red("❌ Error reading sync.config.json:"), err.message);
90
+ console.error(pc.red("❌ Error reading sync.config.json:"), err.message);
82
91
  process.exit(1);
83
92
  }
84
93
 
85
94
  if (!CONFIG_RAW.connections || typeof CONFIG_RAW.connections !== "object") {
86
- elog(pc.red("❌ sync.config.json must have a 'connections' field."));
95
+ console.error(pc.red("❌ sync.config.json must have a 'connections' field."));
87
96
  process.exit(1);
88
97
  }
89
98
 
90
99
  const TARGET_CONFIG = CONFIG_RAW.connections[TARGET];
91
100
  if (!TARGET_CONFIG) {
92
- elog(pc.red(`❌ Connection '${TARGET}' not found in sync.config.json.`));
101
+ console.error(pc.red(`❌ Connection '${TARGET}' not found in sync.config.json.`));
93
102
  process.exit(1);
94
103
  }
95
104
 
@@ -103,8 +112,30 @@ const CONNECTION = {
103
112
  workers: TARGET_CONFIG.worker ?? 2,
104
113
  };
105
114
 
115
+ // ---------------------------------------------------------------------------
116
+ // LogLevel + Progress aus Config
117
+ // ---------------------------------------------------------------------------
118
+
119
+ // logLevel: "verbose", "normal", "laconic"
120
+ let LOG_LEVEL = (CONFIG_RAW.logLevel ?? "normal").toLowerCase();
121
+
122
+ // CLI-Flags überschreiben Config
123
+ if (cliLogLevel) {
124
+ LOG_LEVEL = cliLogLevel;
125
+ }
126
+
127
+ const IS_VERBOSE = LOG_LEVEL === "verbose";
128
+ const IS_LACONIC = LOG_LEVEL === "laconic";
129
+
130
+ const PROGRESS = CONFIG_RAW.progress ?? {};
131
+ const SCAN_CHUNK = PROGRESS.scanChunk ?? (IS_VERBOSE ? 1 : 100);
132
+ const ANALYZE_CHUNK = PROGRESS.analyzeChunk ?? (IS_VERBOSE ? 1 : 10);
133
+ // Für >100k Files eher 10–50, bei Debug/Fehlersuche 1.
134
+
135
+ // ---------------------------------------------------------------------------
106
136
  // Shared config from JSON
107
- // Shared config from JSON
137
+ // ---------------------------------------------------------------------------
138
+
108
139
  const INCLUDE = CONFIG_RAW.include ?? [];
109
140
  const BASE_EXCLUDE = CONFIG_RAW.exclude ?? [];
110
141
 
@@ -113,8 +144,7 @@ function normalizeList(list) {
113
144
  if (!Array.isArray(list)) return [];
114
145
  return list.flatMap((item) =>
115
146
  typeof item === "string"
116
- ? // erlaubt: ["a.json, b.json"] -> ["a.json", "b.json"]
117
- item
147
+ ? item
118
148
  .split(",")
119
149
  .map((s) => s.trim())
120
150
  .filter(Boolean)
@@ -144,6 +174,7 @@ const TEXT_EXT = CONFIG_RAW.textExtensions ?? [
144
174
  ".md",
145
175
  ".svg",
146
176
  ];
177
+
147
178
  // Cache file name per connection
148
179
  const syncCacheName = TARGET_CONFIG.syncCache || `.sync-cache.${TARGET}.json`;
149
180
  const CACHE_PATH = path.resolve(syncCacheName);
@@ -166,7 +197,10 @@ try {
166
197
  CACHE.remote = raw.remote ?? {};
167
198
  }
168
199
  } catch (err) {
169
- wlog(pc.yellow("⚠️ Could not load cache, starting without:"), err.message);
200
+ console.warn(
201
+ pc.yellow("⚠️ Could not load cache, starting without:"),
202
+ err.message
203
+ );
170
204
  }
171
205
 
172
206
  function cacheKey(relPath) {
@@ -217,7 +251,7 @@ function log(...msg) {
217
251
  }
218
252
 
219
253
  function vlog(...msg) {
220
- if (!VERBOSE) return;
254
+ if (!IS_VERBOSE) return;
221
255
  clearProgressLine();
222
256
  console.log(...msg);
223
257
  }
@@ -256,27 +290,67 @@ function isTextFile(relPath) {
256
290
  return TEXT_EXT.includes(ext);
257
291
  }
258
292
 
259
- // Single-line progress bar
260
- function updateProgress(prefix, current, total) {
261
- const percent = total > 0 ? ((current / total) * 100).toFixed(1) : "0.0";
262
- const msg = `${prefix}${current}/${total} Dateien (${percent}%)`;
293
+ function shortenPathForProgress(rel) {
294
+ if (!rel) return "";
295
+ // Nur Dateinamen?
296
+ // return path.basename(rel);
263
297
 
298
+ // Letzte 2 Segmente des Pfades
299
+ const parts = rel.split("/");
300
+ if (parts.length === 1) {
301
+ return rel; // nur Dateiname
302
+ }
303
+ if (parts.length === 2) {
304
+ return rel; // schon kurz genug
305
+ }
306
+
307
+ const last = parts[parts.length - 1];
308
+ const prev = parts[parts.length - 2];
309
+
310
+ // z.B. …/images/foo.jpg
311
+ return `…/${prev}/${last}`;
312
+ }
313
+
314
+ // Two-line progress bar
315
+ function updateProgress2(prefix, current, total, rel = "") {
264
316
  if (!process.stdout.isTTY) {
265
- // Fallback: simply log
266
- console.log(" " + msg);
317
+ // Fallback für Pipes / Logs
318
+ if (total && total > 0) {
319
+ const percent = ((current / total) * 100).toFixed(1);
320
+ console.log(`${prefix}${current}/${total} Dateien (${percent}%) – ${rel}`);
321
+ } else {
322
+ console.log(`${prefix}${current} Dateien – ${rel}`);
323
+ }
267
324
  return;
268
325
  }
269
326
 
270
327
  const width = process.stdout.columns || 80;
271
- const line = msg.padEnd(width - 1);
272
328
 
273
- progressActive = true;
274
- process.stdout.write("\r" + line);
275
-
276
- if (current === total) {
277
- process.stdout.write("\n");
278
- progressActive = false;
329
+ let line1;
330
+ if (total && total > 0) {
331
+ const percent = ((current / total) * 100).toFixed(1);
332
+ line1 = `${prefix}${current}/${total} Dateien (${percent}%)`;
333
+ } else {
334
+ // „unknown total“ / Scanner-Modus
335
+ line1 = `${prefix}${current} Dateien`;
279
336
  }
337
+
338
+ // Pfad einkürzen falls nötig (deine bestehende Funktion verwenden)
339
+ const short = rel ? shortenPathForProgress(rel) : "";
340
+
341
+ let line2 = short;
342
+
343
+ if (line1.length > width) line1 = line1.slice(0, width - 1);
344
+ if (line2.length > width) line2 = line2.slice(0, width - 1);
345
+
346
+ // zwei Zeilen überschreiben
347
+ process.stdout.write("\r" + line1.padEnd(width) + "\n");
348
+ process.stdout.write(line2.padEnd(width));
349
+
350
+ // Cursor wieder nach oben (auf die Fortschrittszeile)
351
+ process.stdout.write("\x1b[1A");
352
+
353
+ progressActive = true;
280
354
  }
281
355
 
282
356
  // Simple worker pool for parallel tasks
@@ -301,7 +375,7 @@ async function runTasks(items, workerCount, handler, label = "Tasks") {
301
375
  }
302
376
  done += 1;
303
377
  if (done % 10 === 0 || done === total) {
304
- updateProgress(` ${label}: `, done, total);
378
+ updateProgress2(` ${label}: `, done, total);
305
379
  }
306
380
  }
307
381
  }
@@ -320,6 +394,7 @@ async function runTasks(items, workerCount, handler, label = "Tasks") {
320
394
 
321
395
  async function walkLocal(root) {
322
396
  const result = new Map();
397
+ let scanned = 0;
323
398
 
324
399
  async function recurse(current) {
325
400
  const entries = await fsp.readdir(current, { withFileTypes: true });
@@ -329,7 +404,9 @@ async function walkLocal(root) {
329
404
  await recurse(full);
330
405
  } else if (entry.isFile()) {
331
406
  const rel = toPosix(path.relative(root, full));
407
+
332
408
  if (!isIncluded(rel)) continue;
409
+
333
410
  const stat = await fsp.stat(full);
334
411
  result.set(rel, {
335
412
  rel,
@@ -338,11 +415,26 @@ async function walkLocal(root) {
338
415
  mtimeMs: stat.mtimeMs,
339
416
  isText: isTextFile(rel),
340
417
  });
418
+
419
+ scanned += 1;
420
+ const chunk = IS_VERBOSE ? 1 : SCAN_CHUNK;
421
+ if (scanned === 1 || scanned % chunk === 0) {
422
+ // total unbekannt → total = 0 → kein automatisches \n
423
+ updateProgress2(" Scan local: ", scanned, 0, rel);
424
+ }
341
425
  }
342
426
  }
343
427
  }
344
428
 
345
429
  await recurse(root);
430
+
431
+ if (scanned > 0) {
432
+ // letzte Zeile + sauberer Abschluss
433
+ updateProgress2(" Scan local: ", scanned, 0, "fertig");
434
+ process.stdout.write("\n");
435
+ progressActive = false;
436
+ }
437
+
346
438
  return result;
347
439
  }
348
440
 
@@ -352,6 +444,7 @@ async function walkLocal(root) {
352
444
 
353
445
  async function walkRemote(sftp, remoteRoot) {
354
446
  const result = new Map();
447
+ let scanned = 0;
355
448
 
356
449
  async function recurse(remoteDir, prefix) {
357
450
  const items = await sftp.list(remoteDir);
@@ -374,11 +467,24 @@ async function walkRemote(sftp, remoteRoot) {
374
467
  size: Number(item.size),
375
468
  modifyTime: item.modifyTime ?? 0,
376
469
  });
470
+
471
+ scanned += 1;
472
+ const chunk = IS_VERBOSE ? 1 : SCAN_CHUNK;
473
+ if (scanned === 1 || scanned % chunk === 0) {
474
+ updateProgress2(" Scan remote: ", scanned, 0, rel);
475
+ }
377
476
  }
378
477
  }
379
478
  }
380
479
 
381
- await recurse(remoteRoot, "");
480
+ await recurse(remoteRoot);
481
+
482
+ if (scanned > 0) {
483
+ updateProgress2(" Scan remote: ", scanned, 0, "fertig");
484
+ process.stdout.write("\n");
485
+ progressActive = false;
486
+ }
487
+
382
488
  return result;
383
489
  }
384
490
 
@@ -463,7 +569,11 @@ async function main() {
463
569
  const start = Date.now();
464
570
 
465
571
  log("\n\n==================================================================");
466
- log(pc.bold("🔐 SFTP Push-Synchronisation: sftp-push-sync"));
572
+ log(
573
+ pc.bold(
574
+ `🔐 SFTP Push-Synchronisation: sftp-push-sync v${pkg.version} [logLevel=${LOG_LEVEL}]`
575
+ )
576
+ );
467
577
  log(` Connection: ${pc.cyan(TARGET)} (Worker: ${CONNECTION.workers})`);
468
578
  log(` Host: ${pc.green(CONNECTION.host)}:${pc.green(CONNECTION.port)}`);
469
579
  log(` Local: ${pc.green(CONNECTION.localRoot)}`);
@@ -531,28 +641,34 @@ async function main() {
531
641
  // Analysis: just decide, don't upload/delete anything yet
532
642
  for (const rel of localKeys) {
533
643
  checkedCount += 1;
644
+
645
+ const chunk = IS_VERBOSE ? 1 : ANALYZE_CHUNK;
534
646
  if (
535
647
  checkedCount === 1 || // sofortige erste Ausgabe
536
- checkedCount % 100 === 0 || // aktualisieren alle 100
537
- checkedCount === totalToCheck // letzte Ausgabe immer
648
+ checkedCount % chunk === 0 ||
649
+ checkedCount === totalToCheck
538
650
  ) {
539
- updateProgress(" Analyse: ", checkedCount, totalToCheck);
651
+ updateProgress2(" Analyse: ", checkedCount, totalToCheck, rel);
540
652
  }
653
+
541
654
  const l = local.get(rel);
542
655
  const r = remote.get(rel);
543
-
544
656
  const remotePath = path.posix.join(CONNECTION.remoteRoot, rel);
545
657
 
546
658
  if (!r) {
547
659
  toAdd.push({ rel, local: l, remotePath });
548
- log(`${ADD} ${pc.green("New:")} ${rel}`);
660
+ if (!IS_LACONIC) {
661
+ log(`${ADD} ${pc.green("New:")} ${rel}`);
662
+ }
549
663
  continue;
550
664
  }
551
665
 
552
666
  // 1. size comparison
553
667
  if (l.size !== r.size) {
554
668
  toUpdate.push({ rel, local: l, remote: r, remotePath });
555
- log(`${CHA} ${pc.yellow("Size changed:")} ${rel}`);
669
+ if (!IS_LACONIC) {
670
+ log(`${CHA} ${pc.yellow("Size changed:")} ${rel}`);
671
+ }
556
672
  continue;
557
673
  }
558
674
 
@@ -574,14 +690,16 @@ async function main() {
574
690
  continue;
575
691
  }
576
692
 
577
- if (VERBOSE) {
693
+ if (IS_VERBOSE) {
578
694
  const diff = diffWords(remoteStr, localStr);
579
695
  const blocks = diff.filter((d) => d.added || d.removed).length;
580
696
  vlog(` ${CHA} Text difference (${blocks} blocks) in ${rel}`);
581
697
  }
582
698
 
583
699
  toUpdate.push({ rel, local: l, remote: r, remotePath });
584
- log(`${CHA} ${pc.yellow("Content changed (Text):")} ${rel}`);
700
+ if (!IS_LACONIC) {
701
+ log(`${CHA} ${pc.yellow("Content changed (Text):")} ${rel}`);
702
+ }
585
703
  } else {
586
704
  // Binary: Hash comparison with cache
587
705
  const localMeta = l;
@@ -597,25 +715,27 @@ async function main() {
597
715
  continue;
598
716
  }
599
717
 
600
- if (VERBOSE) {
718
+ if (IS_VERBOSE) {
601
719
  vlog(` ${CHA} Hash different (binary): ${rel}`);
602
720
  vlog(` local: ${localHash}`);
603
721
  vlog(` remote: ${remoteHash}`);
604
722
  }
605
723
 
606
724
  toUpdate.push({ rel, local: l, remote: r, remotePath });
607
- log(`${CHA} ${pc.yellow("Content changed (Binary):")} ${rel}`);
725
+ if (!IS_LACONIC) {
726
+ log(`${CHA} ${pc.yellow("Content changed (Binary):")} ${rel}`);
727
+ }
608
728
  }
609
729
  }
610
730
 
611
- log(
612
- "\n" + pc.bold(pc.cyan("🧹 Phase 4: Removing orphaned remote files …"))
613
- );
731
+ log("\n" + pc.bold(pc.cyan("🧹 Phase 4: Removing orphaned remote files …")));
614
732
  for (const rel of remoteKeys) {
615
733
  if (!localKeys.has(rel)) {
616
734
  const r = remote.get(rel);
617
735
  toDelete.push({ rel, remotePath: r.remotePath });
618
- log(` ${DEL} ${pc.red("Remove:")} ${rel}`);
736
+ if (!IS_LACONIC) {
737
+ log(` ${DEL} ${pc.red("Remove:")} ${rel}`);
738
+ }
619
739
  }
620
740
  }
621
741
 
@@ -761,7 +881,7 @@ async function main() {
761
881
  log(` ${DEL} Deleted: ${toDelete.length}`);
762
882
  if (AUTO_EXCLUDED.size > 0) {
763
883
  log(
764
- ` ${EXC} Excluded via uploadList | downloadList): ${AUTO_EXCLUDED.size}`
884
+ ` ${EXC} Excluded via uploadList | downloadList: ${AUTO_EXCLUDED.size}`
765
885
  );
766
886
  }
767
887
  if (toAdd.length || toUpdate.length || toDelete.length) {
@@ -780,9 +900,7 @@ async function main() {
780
900
  }
781
901
 
782
902
  log("\n" + pc.bold(pc.green("✅ Sync complete.")));
783
- log(
784
- "==================================================================\n\n"
785
- );
903
+ log("==================================================================\n\n");
786
904
  } catch (err) {
787
905
  elog(pc.red("❌ Synchronisation error:"), err);
788
906
  process.exitCode = 1;
@@ -800,4 +918,4 @@ async function main() {
800
918
  }
801
919
  }
802
920
 
803
- main();
921
+ main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sftp-push-sync",
3
- "version": "1.0.11",
3
+ "version": "1.0.12",
4
4
  "description": "SFTP sync tool for Hugo projects (local to remote, with hash cache)",
5
5
  "type": "module",
6
6
  "bin": {