sftp-push-sync 1.0.7 → 1.0.9

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
@@ -64,10 +64,40 @@ Create a `sync.config.json` in the root folder of your project:
64
64
  ],
65
65
  "textExtensions": [
66
66
  ".html", ".xml", ".txt", ".json", ".js", ".css", ".md", ".svg"
67
+ ],
68
+ "uploadList": [
69
+ "download-files.json"
70
+ ],
71
+ "downloadList": [
72
+ "download-counter.json"
67
73
  ]
68
74
  }
69
75
  ```
70
76
 
77
+ ### special cases
78
+
79
+ - uploadList
80
+ - Relativ zu localRoot "download-files.json"
81
+ - oder mit Unterordnern: "data/download-files.json"
82
+ - downloadList
83
+ - Relativ zu remoteRoot "download-counter.json"
84
+ - oder zB. "logs/download-counter.json"
85
+
86
+
87
+ ```bash
88
+ # normal synchronisation
89
+ sftp-push-sync staging
90
+
91
+ # Normal synchronisation + explicitly transfer upload list
92
+ sftp-push-sync staging --upload-list
93
+
94
+ # just fetch the download list from the server (combined with normal synchronisation)
95
+ sftp-push-sync prod --download-list --dry-run # erst ansehen
96
+ sftp-push-sync prod --download-list # dann machen
97
+ ```
98
+
99
+ ## NPM Scripts
100
+
71
101
  - Can be conveniently started via the scripts in `package.json`:
72
102
 
73
103
  ```bash
@@ -110,6 +140,10 @@ You can safely delete the local cache at any time. The first analysis will then
110
140
 
111
141
  The first run always takes a while, especially with lots of images – so be patient! Once the cache is full, it will be faster.
112
142
 
143
+ ## Example Output
144
+
145
+ ![An console output example](images/example-output-001.png)
146
+
113
147
  ## Links
114
148
 
115
149
  - <https://www.npmjs.com/package/sftp-push-sync>
@@ -20,6 +20,12 @@
20
20
  * - Parallel uploads/deletes via worker pool
21
21
  * - include/exclude patterns
22
22
  *
23
+ * Special cases:
24
+ * - Files can be excluded from synchronisation.
25
+ * - For example, log files or other special files.
26
+ * - These files can be downloaded or uploaded separately.
27
+ *
28
+ *
23
29
  * The file sftp-push-sync.mjs is pure JavaScript (ESM), not TypeScript.
24
30
  * Node.js can execute it directly as long as "type": "module" is specified in package.json
25
31
  * or the file has the extension .mjs.
@@ -34,10 +40,10 @@ import { createHash } from "crypto";
34
40
  import { Writable } from "stream";
35
41
  import pc from "picocolors";
36
42
 
37
- // Colors for the State
43
+ // Colors for the State (works on dark + light background)
38
44
  const ADD = pc.green("+"); // Added
39
45
  const CHA = pc.yellow("~"); // Changed
40
- const DEL = pc.red("-"); // deleted
46
+ const DEL = pc.red("-"); // Deleted
41
47
 
42
48
  // ---------------------------------------------------------------------------
43
49
  // CLI arguments
@@ -47,10 +53,12 @@ const args = process.argv.slice(2);
47
53
  const TARGET = args[0];
48
54
  const DRY_RUN = args.includes("--dry-run");
49
55
  const VERBOSE = args.includes("--verbose") || args.includes("-v");
56
+ const RUN_UPLOAD_LIST = args.includes("--upload-list");
57
+ const RUN_DOWNLOAD_LIST = args.includes("--download-list");
50
58
 
51
59
  if (!TARGET) {
52
60
  console.error(pc.red("❌ Please specify a connection profile:"));
53
- console.error(pc.yellow(" node sync-sftp.mjs staging --dry-run"));
61
+ console.error(pc.yellow(" sftp-push-sync staging --dry-run"));
54
62
  process.exit(1);
55
63
  }
56
64
 
@@ -111,6 +119,10 @@ const TEXT_EXT = CONFIG_RAW.textExtensions ?? [
111
119
  ".svg",
112
120
  ];
113
121
 
122
+ // SPECIAL LISTS
123
+ const UPLOAD_LIST = CONFIG_RAW.uploadList ?? [];
124
+ const DOWNLOAD_LIST = CONFIG_RAW.downloadList ?? [];
125
+
114
126
  // Cache file name per connection
115
127
  const syncCacheName =
116
128
  TARGET_CONFIG.syncCache || `.sync-cache.${TARGET}.json`;
@@ -122,7 +134,7 @@ const CACHE_PATH = path.resolve(syncCacheName);
122
134
 
123
135
  let CACHE = {
124
136
  version: 1,
125
- local: {}, // key: "<TARGET>:<relPath>" -> { size, mtimeMs, hash }
137
+ local: {}, // key: "<TARGET>:<relPath>" -> { size, mtimeMs, hash }
126
138
  remote: {}, // key: "<TARGET>:<relPath>" -> { size, modifyTime, hash }
127
139
  };
128
140
 
@@ -134,7 +146,7 @@ try {
134
146
  CACHE.remote = raw.remote ?? {};
135
147
  }
136
148
  } catch (err) {
137
- wlog(pc.yellow("⚠️ Konnte Cache nicht laden, starte ohne Cache:"), err.message);
149
+ wlog(pc.yellow("⚠️ Could not load cache, starting without:"), err.message);
138
150
  }
139
151
 
140
152
  function cacheKey(relPath) {
@@ -224,7 +236,7 @@ function updateProgress(prefix, current, total) {
224
236
  const msg = `${prefix}${current}/${total} Dateien (${percent}%)`;
225
237
 
226
238
  if (!process.stdout.isTTY) {
227
- // Fallback: simply log in
239
+ // Fallback: simply log
228
240
  console.log(" " + msg);
229
241
  return;
230
242
  }
@@ -243,7 +255,7 @@ function updateProgress(prefix, current, total) {
243
255
 
244
256
  // Simple worker pool for parallel tasks
245
257
  async function runTasks(items, workerCount, handler, label = "Tasks") {
246
- if (items.length === 0) return;
258
+ if (!items || items.length === 0) return;
247
259
 
248
260
  const total = items.length;
249
261
  let done = 0;
@@ -259,7 +271,7 @@ async function runTasks(items, workerCount, handler, label = "Tasks") {
259
271
  try {
260
272
  await handler(item);
261
273
  } catch (err) {
262
- elog(pc.red(` ⚠️ Fehler in ${label}:`), err.message || err);
274
+ elog(pc.red(` ⚠️ Error in ${label}:`), err.message || err);
263
275
  }
264
276
  done += 1;
265
277
  if (done % 10 === 0 || done === total) {
@@ -309,7 +321,7 @@ async function walkLocal(root) {
309
321
  }
310
322
 
311
323
  // ---------------------------------------------------------------------------
312
- // Remote walker (recursive, all subdirectories)
324
+ // Remote walker (recursive, all subdirectories) – respects INCLUDE/EXCLUDE
313
325
  // ---------------------------------------------------------------------------
314
326
 
315
327
  async function walkRemote(sftp, remoteRoot) {
@@ -324,6 +336,9 @@ async function walkRemote(sftp, remoteRoot) {
324
336
  const full = path.posix.join(remoteDir, item.name);
325
337
  const rel = prefix ? `${prefix}/${item.name}` : item.name;
326
338
 
339
+ // Apply include/exclude rules also on remote side
340
+ if (!isIncluded(rel)) continue;
341
+
327
342
  if (item.type === "d") {
328
343
  await recurse(full, rel);
329
344
  } else {
@@ -342,7 +357,7 @@ async function walkRemote(sftp, remoteRoot) {
342
357
  }
343
358
 
344
359
  // ---------------------------------------------------------------------------
345
- // Hash helper for binaries (streaming, memory-efficient)
360
+ // Hash helper for binaries (streaming, memory-efficient)
346
361
  // ---------------------------------------------------------------------------
347
362
 
348
363
  function hashLocalFile(filePath) {
@@ -424,10 +439,19 @@ async function main() {
424
439
  log("\n\n==================================================================");
425
440
  log(pc.bold("🔐 SFTP Push-Synchronisation: sftp-push-sync"));
426
441
  log(` Connection: ${pc.cyan(TARGET)} (Worker: ${CONNECTION.workers})`);
427
- log(` Host: ${pc.green(CONNECTION.host)}:${pc.green(CONNECTION.port)}`);
428
- log(` Local: ${pc.green(CONNECTION.localRoot)}`);
442
+ log(` Host: ${pc.green(CONNECTION.host)}:${pc.green(CONNECTION.port)}`);
443
+ log(` Local: ${pc.green(CONNECTION.localRoot)}`);
429
444
  log(` Remote: ${pc.green(CONNECTION.remoteRoot)}`);
430
- if (DRY_RUN) log(pc.yellow(" Modus: DRY-RUN (no changes)"));
445
+ if (DRY_RUN) log(pc.yellow(" Mode: DRY-RUN (no changes)"));
446
+ if (RUN_UPLOAD_LIST || RUN_DOWNLOAD_LIST) {
447
+ log(
448
+ pc.blue(
449
+ ` Extra: ${RUN_UPLOAD_LIST ? "uploadList " : ""}${
450
+ RUN_DOWNLOAD_LIST ? "downloadList" : ""
451
+ }`
452
+ )
453
+ );
454
+ }
431
455
  log("-----------------------------------------------------------------\n");
432
456
 
433
457
  const sftp = new SftpClient();
@@ -457,7 +481,7 @@ async function main() {
457
481
 
458
482
  log(pc.bold(pc.cyan("📤 Phase 2: Scan remote files …")));
459
483
  const remote = await walkRemote(sftp, CONNECTION.remoteRoot);
460
- log(` → ${remote.size} remote files`);
484
+ log(` → ${remote.size} remote files\n`);
461
485
 
462
486
  const localKeys = new Set(local.keys());
463
487
  const remoteKeys = new Set(remote.keys());
@@ -487,7 +511,7 @@ async function main() {
487
511
  // 1. size comparison
488
512
  if (l.size !== r.size) {
489
513
  toUpdate.push({ rel, local: l, remote: r, remotePath });
490
- log(`${CHANGE} ${pc.yellow("Size changed:")} ${rel}`);
514
+ log(`${CHA} ${pc.yellow("Size changed:")} ${rel}`);
491
515
  continue;
492
516
  }
493
517
 
@@ -513,11 +537,11 @@ async function main() {
513
537
  if (VERBOSE) {
514
538
  const diff = diffWords(remoteStr, localStr);
515
539
  const blocks = diff.filter((d) => d.added || d.removed).length;
516
- vlog(` ${CHANGE} text difference (${blocks} Blocks) in ${rel}`);
540
+ vlog(` ${CHA} Text difference (${blocks} blocks) in ${rel}`);
517
541
  }
518
542
 
519
543
  toUpdate.push({ rel, local: l, remote: r, remotePath });
520
- log(`${CHANGE} ${pc.yellow("Content changed(Text):")} ${rel}`);
544
+ log(`${CHA} ${pc.yellow("Content changed (Text):")} ${rel}`);
521
545
  } else {
522
546
  // Binary: Hash comparison with cache
523
547
  const localMeta = l;
@@ -534,13 +558,13 @@ async function main() {
534
558
  }
535
559
 
536
560
  if (VERBOSE) {
537
- vlog(` ${CHA} Hash different(binary): ${rel}`);
561
+ vlog(` ${CHA} Hash different (binary): ${rel}`);
538
562
  vlog(` local: ${localHash}`);
539
563
  vlog(` remote: ${remoteHash}`);
540
564
  }
541
565
 
542
566
  toUpdate.push({ rel, local: l, remote: r, remotePath });
543
- log(`${CHANGE} ${pc.yellow("Content changed (Binary):")} ${rel}`);
567
+ log(`${CHA} ${pc.yellow("Content changed (Binary):")} ${rel}`);
544
568
  }
545
569
  }
546
570
 
@@ -555,16 +579,16 @@ async function main() {
555
579
 
556
580
  // -------------------------------------------------------------------
557
581
  // Phase 5: Execute changes (parallel, worker-based)
558
- // -------------------------------------------------------------------
582
+ // -------------------------------------------------------------------
559
583
 
560
584
  if (!DRY_RUN) {
561
- log("\n" + pc.bold(pc.cyan("🚚 Phase 5: Implement changes …")));
585
+ log("\n" + pc.bold(pc.cyan("🚚 Phase 5: Apply changes …")));
562
586
 
563
587
  // Upload new files
564
588
  await runTasks(
565
589
  toAdd,
566
590
  CONNECTION.workers,
567
- async ({ rel, local: l, remotePath }) => {
591
+ async ({ local: l, remotePath }) => {
568
592
  const remoteDir = path.posix.dirname(remotePath);
569
593
  try {
570
594
  await sftp.mkdir(remoteDir, true);
@@ -580,7 +604,7 @@ async function main() {
580
604
  await runTasks(
581
605
  toUpdate,
582
606
  CONNECTION.workers,
583
- async ({ rel, local: l, remotePath }) => {
607
+ async ({ local: l, remotePath }) => {
584
608
  const remoteDir = path.posix.dirname(remotePath);
585
609
  try {
586
610
  await sftp.mkdir(remoteDir, true);
@@ -613,9 +637,76 @@ async function main() {
613
637
  log(pc.yellow("\n💡 DRY-RUN: No files transferred or deleted."));
614
638
  }
615
639
 
640
+ // -------------------------------------------------------------------
641
+ // Phase 6: optional uploadList / downloadList
642
+ // -------------------------------------------------------------------
643
+
644
+ if (RUN_UPLOAD_LIST && UPLOAD_LIST.length > 0) {
645
+ log(
646
+ "\n" + pc.bold(pc.cyan("⬆️ Extra Phase: Upload-List (explicit files) …"))
647
+ );
648
+
649
+ const tasks = UPLOAD_LIST.map((rel) => ({
650
+ rel,
651
+ localPath: path.join(CONNECTION.localRoot, rel),
652
+ remotePath: path.posix.join(CONNECTION.remoteRoot, toPosix(rel)),
653
+ }));
654
+
655
+ if (DRY_RUN) {
656
+ for (const t of tasks) {
657
+ log(` ${ADD} would upload (uploadList): ${t.rel}`);
658
+ }
659
+ } else {
660
+ await runTasks(
661
+ tasks,
662
+ CONNECTION.workers,
663
+ async ({ localPath, remotePath, rel }) => {
664
+ const remoteDir = path.posix.dirname(remotePath);
665
+ try {
666
+ await sftp.mkdir(remoteDir, true);
667
+ } catch {
668
+ // ignore
669
+ }
670
+ await sftp.put(localPath, remotePath);
671
+ log(` ${ADD} uploadList: ${rel}`);
672
+ },
673
+ "Upload-List"
674
+ );
675
+ }
676
+ }
677
+
678
+ if (RUN_DOWNLOAD_LIST && DOWNLOAD_LIST.length > 0) {
679
+ log(
680
+ "\n" + pc.bold(pc.cyan("⬇️ Extra Phase: Download-List (explicit files) …"))
681
+ );
682
+
683
+ const tasks = DOWNLOAD_LIST.map((rel) => ({
684
+ rel,
685
+ remotePath: path.posix.join(CONNECTION.remoteRoot, toPosix(rel)),
686
+ localPath: path.join(CONNECTION.localRoot, rel),
687
+ }));
688
+
689
+ if (DRY_RUN) {
690
+ for (const t of tasks) {
691
+ log(` ${ADD} would download (downloadList): ${t.rel}`);
692
+ }
693
+ } else {
694
+ await runTasks(
695
+ tasks,
696
+ CONNECTION.workers,
697
+ async ({ remotePath, localPath, rel }) => {
698
+ await fsp.mkdir(path.dirname(localPath), { recursive: true });
699
+ await sftp.fastGet(remotePath, localPath);
700
+ log(` ${ADD} downloadList: ${rel}`);
701
+ },
702
+ "Download-List"
703
+ );
704
+ }
705
+ }
706
+
616
707
  const duration = ((Date.now() - start) / 1000).toFixed(2);
617
708
 
618
- // Write cache securely at the end
709
+ // Write cache safely at the end
619
710
  await saveCache(true);
620
711
 
621
712
  // Summary
@@ -635,6 +726,7 @@ async function main() {
635
726
  }
636
727
 
637
728
  log("\n" + pc.bold(pc.green("✅ Sync complete.")));
729
+ log("==================================================================\n\n");
638
730
  } catch (err) {
639
731
  elog(pc.red("❌ Synchronisation error:"), err);
640
732
  process.exitCode = 1;
@@ -646,7 +738,6 @@ async function main() {
646
738
  } finally {
647
739
  try {
648
740
  await sftp.end();
649
- log("==================================================================\n\n");
650
741
  } catch {
651
742
  // ignore
652
743
  }
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sftp-push-sync",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "SFTP sync tool for Hugo projects (local to remote, with hash cache)",
5
5
  "type": "module",
6
6
  "bin": {