sftp-push-sync 1.0.10 → 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>
@@ -25,7 +25,6 @@
25
25
  * - For example, log files or other special files.
26
26
  * - These files can be downloaded or uploaded separately.
27
27
  *
28
- *
29
28
  * The file sftp-push-sync.mjs is pure JavaScript (ESM), not TypeScript.
30
29
  * Node.js can execute it directly as long as "type": "module" is specified in package.json
31
30
  * or the file has the extension .mjs.
@@ -41,6 +40,11 @@ import { createHash } from "crypto";
41
40
  import { Writable } from "stream";
42
41
  import pc from "picocolors";
43
42
 
43
+ // get Versionsnummer
44
+ import { createRequire } from "module";
45
+ const require = createRequire(import.meta.url);
46
+ const pkg = require("../package.json");
47
+
44
48
  // Colors for the State (works on dark + light background)
45
49
  const ADD = pc.green("+"); // Added
46
50
  const CHA = pc.yellow("~"); // Changed
@@ -54,10 +58,14 @@ const EXC = pc.redBright("-"); // Excluded
54
58
  const args = process.argv.slice(2);
55
59
  const TARGET = args[0];
56
60
  const DRY_RUN = args.includes("--dry-run");
57
- const VERBOSE = args.includes("--verbose") || args.includes("-v");
58
61
  const RUN_UPLOAD_LIST = args.includes("--upload-list");
59
62
  const RUN_DOWNLOAD_LIST = args.includes("--download-list");
60
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
+
61
69
  if (!TARGET) {
62
70
  console.error(pc.red("❌ Please specify a connection profile:"));
63
71
  console.error(pc.yellow(" sftp-push-sync staging --dry-run"));
@@ -79,18 +87,18 @@ let CONFIG_RAW;
79
87
  try {
80
88
  CONFIG_RAW = JSON.parse(await fsp.readFile(CONFIG_PATH, "utf8"));
81
89
  } catch (err) {
82
- elog(pc.red("❌ Error reading sync.config.json:"), err.message);
90
+ console.error(pc.red("❌ Error reading sync.config.json:"), err.message);
83
91
  process.exit(1);
84
92
  }
85
93
 
86
94
  if (!CONFIG_RAW.connections || typeof CONFIG_RAW.connections !== "object") {
87
- elog(pc.red("❌ sync.config.json must have a 'connections' field."));
95
+ console.error(pc.red("❌ sync.config.json must have a 'connections' field."));
88
96
  process.exit(1);
89
97
  }
90
98
 
91
99
  const TARGET_CONFIG = CONFIG_RAW.connections[TARGET];
92
100
  if (!TARGET_CONFIG) {
93
- elog(pc.red(`❌ Connection '${TARGET}' not found in sync.config.json.`));
101
+ console.error(pc.red(`❌ Connection '${TARGET}' not found in sync.config.json.`));
94
102
  process.exit(1);
95
103
  }
96
104
 
@@ -104,8 +112,30 @@ const CONNECTION = {
104
112
  workers: TARGET_CONFIG.worker ?? 2,
105
113
  };
106
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
+ // ---------------------------------------------------------------------------
107
136
  // Shared config from JSON
108
- // Shared config from JSON
137
+ // ---------------------------------------------------------------------------
138
+
109
139
  const INCLUDE = CONFIG_RAW.include ?? [];
110
140
  const BASE_EXCLUDE = CONFIG_RAW.exclude ?? [];
111
141
 
@@ -114,8 +144,7 @@ function normalizeList(list) {
114
144
  if (!Array.isArray(list)) return [];
115
145
  return list.flatMap((item) =>
116
146
  typeof item === "string"
117
- ? // erlaubt: ["a.json, b.json"] -> ["a.json", "b.json"]
118
- item
147
+ ? item
119
148
  .split(",")
120
149
  .map((s) => s.trim())
121
150
  .filter(Boolean)
@@ -145,6 +174,7 @@ const TEXT_EXT = CONFIG_RAW.textExtensions ?? [
145
174
  ".md",
146
175
  ".svg",
147
176
  ];
177
+
148
178
  // Cache file name per connection
149
179
  const syncCacheName = TARGET_CONFIG.syncCache || `.sync-cache.${TARGET}.json`;
150
180
  const CACHE_PATH = path.resolve(syncCacheName);
@@ -167,7 +197,10 @@ try {
167
197
  CACHE.remote = raw.remote ?? {};
168
198
  }
169
199
  } catch (err) {
170
- 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
+ );
171
204
  }
172
205
 
173
206
  function cacheKey(relPath) {
@@ -218,7 +251,7 @@ function log(...msg) {
218
251
  }
219
252
 
220
253
  function vlog(...msg) {
221
- if (!VERBOSE) return;
254
+ if (!IS_VERBOSE) return;
222
255
  clearProgressLine();
223
256
  console.log(...msg);
224
257
  }
@@ -257,27 +290,67 @@ function isTextFile(relPath) {
257
290
  return TEXT_EXT.includes(ext);
258
291
  }
259
292
 
260
- // Single-line progress bar
261
- function updateProgress(prefix, current, total) {
262
- const percent = total > 0 ? ((current / total) * 100).toFixed(1) : "0.0";
263
- const msg = `${prefix}${current}/${total} Dateien (${percent}%)`;
293
+ function shortenPathForProgress(rel) {
294
+ if (!rel) return "";
295
+ // Nur Dateinamen?
296
+ // return path.basename(rel);
264
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 = "") {
265
316
  if (!process.stdout.isTTY) {
266
- // Fallback: simply log
267
- 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
+ }
268
324
  return;
269
325
  }
270
326
 
271
327
  const width = process.stdout.columns || 80;
272
- const line = msg.padEnd(width - 1);
273
328
 
274
- progressActive = true;
275
- process.stdout.write("\r" + line);
276
-
277
- if (current === total) {
278
- process.stdout.write("\n");
279
- 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`;
280
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;
281
354
  }
282
355
 
283
356
  // Simple worker pool for parallel tasks
@@ -302,7 +375,7 @@ async function runTasks(items, workerCount, handler, label = "Tasks") {
302
375
  }
303
376
  done += 1;
304
377
  if (done % 10 === 0 || done === total) {
305
- updateProgress(` ${label}: `, done, total);
378
+ updateProgress2(` ${label}: `, done, total);
306
379
  }
307
380
  }
308
381
  }
@@ -321,6 +394,7 @@ async function runTasks(items, workerCount, handler, label = "Tasks") {
321
394
 
322
395
  async function walkLocal(root) {
323
396
  const result = new Map();
397
+ let scanned = 0;
324
398
 
325
399
  async function recurse(current) {
326
400
  const entries = await fsp.readdir(current, { withFileTypes: true });
@@ -330,7 +404,9 @@ async function walkLocal(root) {
330
404
  await recurse(full);
331
405
  } else if (entry.isFile()) {
332
406
  const rel = toPosix(path.relative(root, full));
407
+
333
408
  if (!isIncluded(rel)) continue;
409
+
334
410
  const stat = await fsp.stat(full);
335
411
  result.set(rel, {
336
412
  rel,
@@ -339,11 +415,26 @@ async function walkLocal(root) {
339
415
  mtimeMs: stat.mtimeMs,
340
416
  isText: isTextFile(rel),
341
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
+ }
342
425
  }
343
426
  }
344
427
  }
345
428
 
346
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
+
347
438
  return result;
348
439
  }
349
440
 
@@ -353,6 +444,7 @@ async function walkLocal(root) {
353
444
 
354
445
  async function walkRemote(sftp, remoteRoot) {
355
446
  const result = new Map();
447
+ let scanned = 0;
356
448
 
357
449
  async function recurse(remoteDir, prefix) {
358
450
  const items = await sftp.list(remoteDir);
@@ -375,11 +467,24 @@ async function walkRemote(sftp, remoteRoot) {
375
467
  size: Number(item.size),
376
468
  modifyTime: item.modifyTime ?? 0,
377
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
+ }
378
476
  }
379
477
  }
380
478
  }
381
479
 
382
- 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
+
383
488
  return result;
384
489
  }
385
490
 
@@ -464,7 +569,11 @@ async function main() {
464
569
  const start = Date.now();
465
570
 
466
571
  log("\n\n==================================================================");
467
- 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
+ );
468
577
  log(` Connection: ${pc.cyan(TARGET)} (Worker: ${CONNECTION.workers})`);
469
578
  log(` Host: ${pc.green(CONNECTION.host)}:${pc.green(CONNECTION.port)}`);
470
579
  log(` Local: ${pc.green(CONNECTION.localRoot)}`);
@@ -532,25 +641,34 @@ async function main() {
532
641
  // Analysis: just decide, don't upload/delete anything yet
533
642
  for (const rel of localKeys) {
534
643
  checkedCount += 1;
535
- if (checkedCount % 500 === 0 || checkedCount === totalToCheck) {
536
- updateProgress(" Analyse: ", checkedCount, totalToCheck);
644
+
645
+ const chunk = IS_VERBOSE ? 1 : ANALYZE_CHUNK;
646
+ if (
647
+ checkedCount === 1 || // sofortige erste Ausgabe
648
+ checkedCount % chunk === 0 ||
649
+ checkedCount === totalToCheck
650
+ ) {
651
+ updateProgress2(" Analyse: ", checkedCount, totalToCheck, rel);
537
652
  }
538
653
 
539
654
  const l = local.get(rel);
540
655
  const r = remote.get(rel);
541
-
542
656
  const remotePath = path.posix.join(CONNECTION.remoteRoot, rel);
543
657
 
544
658
  if (!r) {
545
659
  toAdd.push({ rel, local: l, remotePath });
546
- log(`${ADD} ${pc.green("New:")} ${rel}`);
660
+ if (!IS_LACONIC) {
661
+ log(`${ADD} ${pc.green("New:")} ${rel}`);
662
+ }
547
663
  continue;
548
664
  }
549
665
 
550
666
  // 1. size comparison
551
667
  if (l.size !== r.size) {
552
668
  toUpdate.push({ rel, local: l, remote: r, remotePath });
553
- log(`${CHA} ${pc.yellow("Size changed:")} ${rel}`);
669
+ if (!IS_LACONIC) {
670
+ log(`${CHA} ${pc.yellow("Size changed:")} ${rel}`);
671
+ }
554
672
  continue;
555
673
  }
556
674
 
@@ -572,14 +690,16 @@ async function main() {
572
690
  continue;
573
691
  }
574
692
 
575
- if (VERBOSE) {
693
+ if (IS_VERBOSE) {
576
694
  const diff = diffWords(remoteStr, localStr);
577
695
  const blocks = diff.filter((d) => d.added || d.removed).length;
578
696
  vlog(` ${CHA} Text difference (${blocks} blocks) in ${rel}`);
579
697
  }
580
698
 
581
699
  toUpdate.push({ rel, local: l, remote: r, remotePath });
582
- log(`${CHA} ${pc.yellow("Content changed (Text):")} ${rel}`);
700
+ if (!IS_LACONIC) {
701
+ log(`${CHA} ${pc.yellow("Content changed (Text):")} ${rel}`);
702
+ }
583
703
  } else {
584
704
  // Binary: Hash comparison with cache
585
705
  const localMeta = l;
@@ -595,25 +715,27 @@ async function main() {
595
715
  continue;
596
716
  }
597
717
 
598
- if (VERBOSE) {
718
+ if (IS_VERBOSE) {
599
719
  vlog(` ${CHA} Hash different (binary): ${rel}`);
600
720
  vlog(` local: ${localHash}`);
601
721
  vlog(` remote: ${remoteHash}`);
602
722
  }
603
723
 
604
724
  toUpdate.push({ rel, local: l, remote: r, remotePath });
605
- log(`${CHA} ${pc.yellow("Content changed (Binary):")} ${rel}`);
725
+ if (!IS_LACONIC) {
726
+ log(`${CHA} ${pc.yellow("Content changed (Binary):")} ${rel}`);
727
+ }
606
728
  }
607
729
  }
608
730
 
609
- log(
610
- "\n" + pc.bold(pc.cyan("🧹 Phase 4: Removing orphaned remote files …"))
611
- );
731
+ log("\n" + pc.bold(pc.cyan("🧹 Phase 4: Removing orphaned remote files …")));
612
732
  for (const rel of remoteKeys) {
613
733
  if (!localKeys.has(rel)) {
614
734
  const r = remote.get(rel);
615
735
  toDelete.push({ rel, remotePath: r.remotePath });
616
- log(` ${DEL} ${pc.red("Remove:")} ${rel}`);
736
+ if (!IS_LACONIC) {
737
+ log(` ${DEL} ${pc.red("Remove:")} ${rel}`);
738
+ }
617
739
  }
618
740
  }
619
741
 
@@ -758,7 +880,9 @@ async function main() {
758
880
  log(` ${CHA} Changed: ${toUpdate.length}`);
759
881
  log(` ${DEL} Deleted: ${toDelete.length}`);
760
882
  if (AUTO_EXCLUDED.size > 0) {
761
- log(` ${EXC} Excluded via uploadList | downloadList): ${AUTO_EXCLUDED.size}`);
883
+ log(
884
+ ` ${EXC} Excluded via uploadList | downloadList: ${AUTO_EXCLUDED.size}`
885
+ );
762
886
  }
763
887
  if (toAdd.length || toUpdate.length || toDelete.length) {
764
888
  log("\n📄 Changes:");
@@ -776,9 +900,7 @@ async function main() {
776
900
  }
777
901
 
778
902
  log("\n" + pc.bold(pc.green("✅ Sync complete.")));
779
- log(
780
- "==================================================================\n\n"
781
- );
903
+ log("==================================================================\n\n");
782
904
  } catch (err) {
783
905
  elog(pc.red("❌ Synchronisation error:"), err);
784
906
  process.exitCode = 1;
@@ -796,4 +918,4 @@ async function main() {
796
918
  }
797
919
  }
798
920
 
799
- main();
921
+ main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sftp-push-sync",
3
- "version": "1.0.10",
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": {