sftp-push-sync 1.0.9 → 1.0.11

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
@@ -10,8 +10,8 @@ I use the script to transfer [Hugo websites](https://gohugo.io) to the server.
10
10
 
11
11
  Features:
12
12
 
13
- - multiple connections in sync.config.json
14
- - dry-run mode
13
+ - multiple connections in `sync.config.json`
14
+ - `dry-run` mode
15
15
  - mirrors local → remote
16
16
  - adds, updates, deletes files
17
17
  - text diff detection
@@ -19,7 +19,8 @@ Features:
19
19
  - Hashes are cached in .sync-cache.json to save space.
20
20
  - Parallel uploads/deletions via worker pool
21
21
  - include/exclude patterns
22
-
22
+ - special uploads / downloads
23
+
23
24
  The file `sftp-push-sync.mjs` is pure JavaScript (ESM), not TypeScript. Node.js can execute it directly as long as "type": "module" is specified in package.json or the file has the extension .mjs.
24
25
 
25
26
  ## Install
@@ -65,23 +66,23 @@ Create a `sync.config.json` in the root folder of your project:
65
66
  "textExtensions": [
66
67
  ".html", ".xml", ".txt", ".json", ".js", ".css", ".md", ".svg"
67
68
  ],
68
- "uploadList": [
69
- "download-files.json"
70
- ],
69
+ "uploadList": [],
71
70
  "downloadList": [
72
71
  "download-counter.json"
73
72
  ]
74
73
  }
75
74
  ```
76
75
 
77
- ### special cases
76
+ ### special uploads / downloads
78
77
 
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"
78
+ A list of files that are excluded from the sync comparison and can be downloaded or uploaded separately.
79
+
80
+ - `uploadList`
81
+ - Relative to localRoot "downloads.json"
82
+ - or with subfolders: "data/downloads.json"
83
+ - `downloadList`
84
+ - Relative to remoteRoot "download-counter.json"
85
+ - or e.g. "logs/download-counter.json"
85
86
 
86
87
 
87
88
  ```bash
@@ -92,8 +93,8 @@ sftp-push-sync staging
92
93
  sftp-push-sync staging --upload-list
93
94
 
94
95
  # 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
96
+ sftp-push-sync prod --download-list --dry-run # view first
97
+ sftp-push-sync prod --download-list # then do
97
98
  ```
98
99
 
99
100
  ## NPM Scripts
@@ -134,9 +135,7 @@ The dry run is a great way to compare files and fill the cache.
134
135
 
135
136
  - The cache files: `.sync-cache.*.json`
136
137
 
137
- You can safely delete the local cache at any time. The first analysis will then take longer again (because remote hashes will be streamed again). After that, everything will run extremely fast again.
138
-
139
- ## special features
138
+ You can safely delete the local cache at any time. The first analysis will then take longer again (because remote hashes will be streamed again). After that, everything will run fast.
140
139
 
141
140
  The first run always takes a while, especially with lots of images – so be patient! Once the cache is full, it will be faster.
142
141
 
@@ -3,12 +3,12 @@
3
3
  ** sftp-push-sync.mjs - SFTP Syncronisations Tool
4
4
  *
5
5
  * @author Carsten Nichte, 2025 / https://carsten-nichte.de
6
- *
6
+ *
7
7
  * SFTP push sync with dry run
8
8
  * 1. Upload new files
9
9
  * 2. Delete remote files that no longer exist locally
10
10
  * 3. Detect changes based on size or modified content and upload them
11
- *
11
+ *
12
12
  * Features:
13
13
  * - multiple connections in sync.config.json
14
14
  * - dry-run mode
@@ -19,17 +19,17 @@
19
19
  * - Hashes are cached in .sync-cache.json to save space.
20
20
  * - Parallel uploads/deletes via worker pool
21
21
  * - include/exclude patterns
22
- *
23
- * Special cases:
22
+ *
23
+ * Special cases:
24
24
  * - Files can be excluded from synchronisation.
25
25
  * - For example, log files or other special files.
26
26
  * - These files can be downloaded or uploaded separately.
27
- *
28
- *
27
+ *
29
28
  * The file sftp-push-sync.mjs is pure JavaScript (ESM), not TypeScript.
30
- * Node.js can execute it directly as long as "type": "module" is specified in package.json
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.
32
31
  */
32
+ // bin/sftp-push-sync.mjs
33
33
  import fs from "fs";
34
34
  import fsp from "fs/promises";
35
35
  import path from "path";
@@ -41,9 +41,10 @@ import { Writable } from "stream";
41
41
  import pc from "picocolors";
42
42
 
43
43
  // Colors for the State (works on dark + light background)
44
- const ADD = pc.green("+"); // Added
44
+ const ADD = pc.green("+"); // Added
45
45
  const CHA = pc.yellow("~"); // Changed
46
- const DEL = pc.red("-"); // Deleted
46
+ const DEL = pc.red("-"); // Deleted
47
+ const EXC = pc.redBright("-"); // Excluded
47
48
 
48
49
  // ---------------------------------------------------------------------------
49
50
  // CLI arguments
@@ -102,9 +103,34 @@ const CONNECTION = {
102
103
  workers: TARGET_CONFIG.worker ?? 2,
103
104
  };
104
105
 
106
+ // Shared config from JSON
105
107
  // Shared config from JSON
106
108
  const INCLUDE = CONFIG_RAW.include ?? [];
107
- const EXCLUDE = CONFIG_RAW.exclude ?? [];
109
+ const BASE_EXCLUDE = CONFIG_RAW.exclude ?? [];
110
+
111
+ // Spezial: Listen für gezielte Uploads / Downloads
112
+ function normalizeList(list) {
113
+ if (!Array.isArray(list)) return [];
114
+ return list.flatMap((item) =>
115
+ typeof item === "string"
116
+ ? // erlaubt: ["a.json, b.json"] -> ["a.json", "b.json"]
117
+ item
118
+ .split(",")
119
+ .map((s) => s.trim())
120
+ .filter(Boolean)
121
+ : []
122
+ );
123
+ }
124
+
125
+ const UPLOAD_LIST = normalizeList(CONFIG_RAW.uploadList ?? []);
126
+ const DOWNLOAD_LIST = normalizeList(CONFIG_RAW.downloadList ?? []);
127
+
128
+ // Effektive Exclude-Liste: explizites exclude + Upload/Download-Listen
129
+ const EXCLUDE = [...BASE_EXCLUDE, ...UPLOAD_LIST, ...DOWNLOAD_LIST];
130
+
131
+ // Liste ALLER Dateien, die wegen uploadList/downloadList ausgeschlossen wurden
132
+ const AUTO_EXCLUDED = new Set();
133
+
108
134
  const TEXT_EXT = CONFIG_RAW.textExtensions ?? [
109
135
  ".html",
110
136
  ".htm",
@@ -118,14 +144,8 @@ const TEXT_EXT = CONFIG_RAW.textExtensions ?? [
118
144
  ".md",
119
145
  ".svg",
120
146
  ];
121
-
122
- // SPECIAL LISTS
123
- const UPLOAD_LIST = CONFIG_RAW.uploadList ?? [];
124
- const DOWNLOAD_LIST = CONFIG_RAW.downloadList ?? [];
125
-
126
147
  // Cache file name per connection
127
- const syncCacheName =
128
- TARGET_CONFIG.syncCache || `.sync-cache.${TARGET}.json`;
148
+ const syncCacheName = TARGET_CONFIG.syncCache || `.sync-cache.${TARGET}.json`;
129
149
  const CACHE_PATH = path.resolve(syncCacheName);
130
150
 
131
151
  // ---------------------------------------------------------------------------
@@ -134,7 +154,7 @@ const CACHE_PATH = path.resolve(syncCacheName);
134
154
 
135
155
  let CACHE = {
136
156
  version: 1,
137
- local: {}, // key: "<TARGET>:<relPath>" -> { size, mtimeMs, hash }
157
+ local: {}, // key: "<TARGET>:<relPath>" -> { size, mtimeMs, hash }
138
158
  remote: {}, // key: "<TARGET>:<relPath>" -> { size, modifyTime, hash }
139
159
  };
140
160
 
@@ -214,14 +234,20 @@ function wlog(...msg) {
214
234
 
215
235
  function matchesAny(patterns, relPath) {
216
236
  if (!patterns || patterns.length === 0) return false;
217
- return patterns.some((pattern) =>
218
- minimatch(relPath, pattern, { dot: true })
219
- );
237
+ return patterns.some((pattern) => minimatch(relPath, pattern, { dot: true }));
220
238
  }
221
239
 
222
240
  function isIncluded(relPath) {
241
+ // Include-Regeln
223
242
  if (INCLUDE.length > 0 && !matchesAny(INCLUDE, relPath)) return false;
224
- if (EXCLUDE.length > 0 && matchesAny(EXCLUDE, relPath)) return false;
243
+ // Exclude-Regeln
244
+ if (EXCLUDE.length > 0 && matchesAny(EXCLUDE, relPath)) {
245
+ // Falls durch Upload/Download-Liste → merken
246
+ if (UPLOAD_LIST.includes(relPath) || DOWNLOAD_LIST.includes(relPath)) {
247
+ AUTO_EXCLUDED.add(relPath);
248
+ }
249
+ return false;
250
+ }
225
251
  return true;
226
252
  }
227
253
 
@@ -357,7 +383,7 @@ async function walkRemote(sftp, remoteRoot) {
357
383
  }
358
384
 
359
385
  // ---------------------------------------------------------------------------
360
- // Hash helper for binaries (streaming, memory-efficient)
386
+ // Hash helper for binaries (streaming, memory-efficient)
361
387
  // ---------------------------------------------------------------------------
362
388
 
363
389
  function hashLocalFile(filePath) {
@@ -435,7 +461,7 @@ async function getRemoteHash(rel, meta, sftp) {
435
461
 
436
462
  async function main() {
437
463
  const start = Date.now();
438
-
464
+
439
465
  log("\n\n==================================================================");
440
466
  log(pc.bold("🔐 SFTP Push-Synchronisation: sftp-push-sync"));
441
467
  log(` Connection: ${pc.cyan(TARGET)} (Worker: ${CONNECTION.workers})`);
@@ -471,7 +497,10 @@ async function main() {
471
497
  vlog(pc.dim(" Connection established."));
472
498
 
473
499
  if (!fs.existsSync(CONNECTION.localRoot)) {
474
- console.error(pc.red("❌ Local root does not exist:"), CONNECTION.localRoot);
500
+ console.error(
501
+ pc.red("❌ Local root does not exist:"),
502
+ CONNECTION.localRoot
503
+ );
475
504
  process.exit(1);
476
505
  }
477
506
 
@@ -479,6 +508,15 @@ async function main() {
479
508
  const local = await walkLocal(CONNECTION.localRoot);
480
509
  log(` → ${local.size} local files`);
481
510
 
511
+ if (AUTO_EXCLUDED.size > 0) {
512
+ log("");
513
+ log(pc.dim(" Auto-excluded (uploadList/downloadList):"));
514
+ [...AUTO_EXCLUDED].sort().forEach((file) => {
515
+ log(pc.dim(` - ${file}`));
516
+ });
517
+ log("");
518
+ }
519
+
482
520
  log(pc.bold(pc.cyan("📤 Phase 2: Scan remote files …")));
483
521
  const remote = await walkRemote(sftp, CONNECTION.remoteRoot);
484
522
  log(` → ${remote.size} remote files\n`);
@@ -493,10 +531,13 @@ async function main() {
493
531
  // Analysis: just decide, don't upload/delete anything yet
494
532
  for (const rel of localKeys) {
495
533
  checkedCount += 1;
496
- if (checkedCount % 500 === 0 || checkedCount === totalToCheck) {
534
+ if (
535
+ checkedCount === 1 || // sofortige erste Ausgabe
536
+ checkedCount % 100 === 0 || // aktualisieren alle 100
537
+ checkedCount === totalToCheck // letzte Ausgabe immer
538
+ ) {
497
539
  updateProgress(" Analyse: ", checkedCount, totalToCheck);
498
540
  }
499
-
500
541
  const l = local.get(rel);
501
542
  const r = remote.get(rel);
502
543
 
@@ -524,9 +565,8 @@ async function main() {
524
565
  ]);
525
566
 
526
567
  const localStr = localBuf.toString("utf8");
527
- const remoteStr = (Buffer.isBuffer(remoteBuf)
528
- ? remoteBuf
529
- : Buffer.from(remoteBuf)
568
+ const remoteStr = (
569
+ Buffer.isBuffer(remoteBuf) ? remoteBuf : Buffer.from(remoteBuf)
530
570
  ).toString("utf8");
531
571
 
532
572
  if (localStr === remoteStr) {
@@ -568,7 +608,9 @@ async function main() {
568
608
  }
569
609
  }
570
610
 
571
- log("\n" + pc.bold(pc.cyan("🧹 Phase 4: Removing orphaned remote files …")));
611
+ log(
612
+ "\n" + pc.bold(pc.cyan("🧹 Phase 4: Removing orphaned remote files …"))
613
+ );
572
614
  for (const rel of remoteKeys) {
573
615
  if (!localKeys.has(rel)) {
574
616
  const r = remote.get(rel);
@@ -579,7 +621,7 @@ async function main() {
579
621
 
580
622
  // -------------------------------------------------------------------
581
623
  // Phase 5: Execute changes (parallel, worker-based)
582
- // -------------------------------------------------------------------
624
+ // -------------------------------------------------------------------
583
625
 
584
626
  if (!DRY_RUN) {
585
627
  log("\n" + pc.bold(pc.cyan("🚚 Phase 5: Apply changes …")));
@@ -643,7 +685,8 @@ async function main() {
643
685
 
644
686
  if (RUN_UPLOAD_LIST && UPLOAD_LIST.length > 0) {
645
687
  log(
646
- "\n" + pc.bold(pc.cyan("⬆️ Extra Phase: Upload-List (explicit files) …"))
688
+ "\n" +
689
+ pc.bold(pc.cyan("⬆️ Extra Phase: Upload-List (explicit files) …"))
647
690
  );
648
691
 
649
692
  const tasks = UPLOAD_LIST.map((rel) => ({
@@ -677,7 +720,8 @@ async function main() {
677
720
 
678
721
  if (RUN_DOWNLOAD_LIST && DOWNLOAD_LIST.length > 0) {
679
722
  log(
680
- "\n" + pc.bold(pc.cyan("⬇️ Extra Phase: Download-List (explicit files) …"))
723
+ "\n" +
724
+ pc.bold(pc.cyan("⬇️ Extra Phase: Download-List (explicit files) …"))
681
725
  );
682
726
 
683
727
  const tasks = DOWNLOAD_LIST.map((rel) => ({
@@ -715,18 +759,30 @@ async function main() {
715
759
  log(` ${ADD} Added : ${toAdd.length}`);
716
760
  log(` ${CHA} Changed: ${toUpdate.length}`);
717
761
  log(` ${DEL} Deleted: ${toDelete.length}`);
718
-
762
+ if (AUTO_EXCLUDED.size > 0) {
763
+ log(
764
+ ` ${EXC} Excluded via uploadList | downloadList): ${AUTO_EXCLUDED.size}`
765
+ );
766
+ }
719
767
  if (toAdd.length || toUpdate.length || toDelete.length) {
720
768
  log("\n📄 Changes:");
721
- [...toAdd.map((t) => t.rel)].sort().forEach((f) => console.log(` ${ADD} ${f}`));
722
- [...toUpdate.map((t) => t.rel)].sort().forEach((f) => console.log(` ${CHA} ${f}`));
723
- [...toDelete.map((t) => t.rel)].sort().forEach((f) => console.log(` ${DEL} ${f}`));
769
+ [...toAdd.map((t) => t.rel)]
770
+ .sort()
771
+ .forEach((f) => console.log(` ${ADD} ${f}`));
772
+ [...toUpdate.map((t) => t.rel)]
773
+ .sort()
774
+ .forEach((f) => console.log(` ${CHA} ${f}`));
775
+ [...toDelete.map((t) => t.rel)]
776
+ .sort()
777
+ .forEach((f) => console.log(` ${DEL} ${f}`));
724
778
  } else {
725
779
  log("\nNo changes.");
726
780
  }
727
781
 
728
782
  log("\n" + pc.bold(pc.green("✅ Sync complete.")));
729
- log("==================================================================\n\n");
783
+ log(
784
+ "==================================================================\n\n"
785
+ );
730
786
  } catch (err) {
731
787
  elog(pc.red("❌ Synchronisation error:"), err);
732
788
  process.exitCode = 1;
@@ -744,4 +800,4 @@ async function main() {
744
800
  }
745
801
  }
746
802
 
747
- main();
803
+ main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sftp-push-sync",
3
- "version": "1.0.9",
3
+ "version": "1.0.11",
4
4
  "description": "SFTP sync tool for Hugo projects (local to remote, with hash cache)",
5
5
  "type": "module",
6
6
  "bin": {