sftp-push-sync 1.0.9 → 1.0.10

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,18 @@
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
+ *
28
+ *
29
29
  * 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
30
+ * Node.js can execute it directly as long as "type": "module" is specified in package.json
31
31
  * or the file has the extension .mjs.
32
32
  */
33
+ // bin/sftp-push-sync.mjs
33
34
  import fs from "fs";
34
35
  import fsp from "fs/promises";
35
36
  import path from "path";
@@ -41,9 +42,10 @@ import { Writable } from "stream";
41
42
  import pc from "picocolors";
42
43
 
43
44
  // Colors for the State (works on dark + light background)
44
- const ADD = pc.green("+"); // Added
45
+ const ADD = pc.green("+"); // Added
45
46
  const CHA = pc.yellow("~"); // Changed
46
- const DEL = pc.red("-"); // Deleted
47
+ const DEL = pc.red("-"); // Deleted
48
+ const EXC = pc.redBright("-"); // Excluded
47
49
 
48
50
  // ---------------------------------------------------------------------------
49
51
  // CLI arguments
@@ -102,9 +104,34 @@ const CONNECTION = {
102
104
  workers: TARGET_CONFIG.worker ?? 2,
103
105
  };
104
106
 
107
+ // Shared config from JSON
105
108
  // Shared config from JSON
106
109
  const INCLUDE = CONFIG_RAW.include ?? [];
107
- const EXCLUDE = CONFIG_RAW.exclude ?? [];
110
+ const BASE_EXCLUDE = CONFIG_RAW.exclude ?? [];
111
+
112
+ // Spezial: Listen für gezielte Uploads / Downloads
113
+ function normalizeList(list) {
114
+ if (!Array.isArray(list)) return [];
115
+ return list.flatMap((item) =>
116
+ typeof item === "string"
117
+ ? // erlaubt: ["a.json, b.json"] -> ["a.json", "b.json"]
118
+ item
119
+ .split(",")
120
+ .map((s) => s.trim())
121
+ .filter(Boolean)
122
+ : []
123
+ );
124
+ }
125
+
126
+ const UPLOAD_LIST = normalizeList(CONFIG_RAW.uploadList ?? []);
127
+ const DOWNLOAD_LIST = normalizeList(CONFIG_RAW.downloadList ?? []);
128
+
129
+ // Effektive Exclude-Liste: explizites exclude + Upload/Download-Listen
130
+ const EXCLUDE = [...BASE_EXCLUDE, ...UPLOAD_LIST, ...DOWNLOAD_LIST];
131
+
132
+ // Liste ALLER Dateien, die wegen uploadList/downloadList ausgeschlossen wurden
133
+ const AUTO_EXCLUDED = new Set();
134
+
108
135
  const TEXT_EXT = CONFIG_RAW.textExtensions ?? [
109
136
  ".html",
110
137
  ".htm",
@@ -118,14 +145,8 @@ const TEXT_EXT = CONFIG_RAW.textExtensions ?? [
118
145
  ".md",
119
146
  ".svg",
120
147
  ];
121
-
122
- // SPECIAL LISTS
123
- const UPLOAD_LIST = CONFIG_RAW.uploadList ?? [];
124
- const DOWNLOAD_LIST = CONFIG_RAW.downloadList ?? [];
125
-
126
148
  // Cache file name per connection
127
- const syncCacheName =
128
- TARGET_CONFIG.syncCache || `.sync-cache.${TARGET}.json`;
149
+ const syncCacheName = TARGET_CONFIG.syncCache || `.sync-cache.${TARGET}.json`;
129
150
  const CACHE_PATH = path.resolve(syncCacheName);
130
151
 
131
152
  // ---------------------------------------------------------------------------
@@ -134,7 +155,7 @@ const CACHE_PATH = path.resolve(syncCacheName);
134
155
 
135
156
  let CACHE = {
136
157
  version: 1,
137
- local: {}, // key: "<TARGET>:<relPath>" -> { size, mtimeMs, hash }
158
+ local: {}, // key: "<TARGET>:<relPath>" -> { size, mtimeMs, hash }
138
159
  remote: {}, // key: "<TARGET>:<relPath>" -> { size, modifyTime, hash }
139
160
  };
140
161
 
@@ -214,14 +235,20 @@ function wlog(...msg) {
214
235
 
215
236
  function matchesAny(patterns, relPath) {
216
237
  if (!patterns || patterns.length === 0) return false;
217
- return patterns.some((pattern) =>
218
- minimatch(relPath, pattern, { dot: true })
219
- );
238
+ return patterns.some((pattern) => minimatch(relPath, pattern, { dot: true }));
220
239
  }
221
240
 
222
241
  function isIncluded(relPath) {
242
+ // Include-Regeln
223
243
  if (INCLUDE.length > 0 && !matchesAny(INCLUDE, relPath)) return false;
224
- if (EXCLUDE.length > 0 && matchesAny(EXCLUDE, relPath)) return false;
244
+ // Exclude-Regeln
245
+ if (EXCLUDE.length > 0 && matchesAny(EXCLUDE, relPath)) {
246
+ // Falls durch Upload/Download-Liste → merken
247
+ if (UPLOAD_LIST.includes(relPath) || DOWNLOAD_LIST.includes(relPath)) {
248
+ AUTO_EXCLUDED.add(relPath);
249
+ }
250
+ return false;
251
+ }
225
252
  return true;
226
253
  }
227
254
 
@@ -357,7 +384,7 @@ async function walkRemote(sftp, remoteRoot) {
357
384
  }
358
385
 
359
386
  // ---------------------------------------------------------------------------
360
- // Hash helper for binaries (streaming, memory-efficient)
387
+ // Hash helper for binaries (streaming, memory-efficient)
361
388
  // ---------------------------------------------------------------------------
362
389
 
363
390
  function hashLocalFile(filePath) {
@@ -435,7 +462,7 @@ async function getRemoteHash(rel, meta, sftp) {
435
462
 
436
463
  async function main() {
437
464
  const start = Date.now();
438
-
465
+
439
466
  log("\n\n==================================================================");
440
467
  log(pc.bold("🔐 SFTP Push-Synchronisation: sftp-push-sync"));
441
468
  log(` Connection: ${pc.cyan(TARGET)} (Worker: ${CONNECTION.workers})`);
@@ -471,7 +498,10 @@ async function main() {
471
498
  vlog(pc.dim(" Connection established."));
472
499
 
473
500
  if (!fs.existsSync(CONNECTION.localRoot)) {
474
- console.error(pc.red("❌ Local root does not exist:"), CONNECTION.localRoot);
501
+ console.error(
502
+ pc.red("❌ Local root does not exist:"),
503
+ CONNECTION.localRoot
504
+ );
475
505
  process.exit(1);
476
506
  }
477
507
 
@@ -479,6 +509,15 @@ async function main() {
479
509
  const local = await walkLocal(CONNECTION.localRoot);
480
510
  log(` → ${local.size} local files`);
481
511
 
512
+ if (AUTO_EXCLUDED.size > 0) {
513
+ log("");
514
+ log(pc.dim(" Auto-excluded (uploadList/downloadList):"));
515
+ [...AUTO_EXCLUDED].sort().forEach((file) => {
516
+ log(pc.dim(` - ${file}`));
517
+ });
518
+ log("");
519
+ }
520
+
482
521
  log(pc.bold(pc.cyan("📤 Phase 2: Scan remote files …")));
483
522
  const remote = await walkRemote(sftp, CONNECTION.remoteRoot);
484
523
  log(` → ${remote.size} remote files\n`);
@@ -524,9 +563,8 @@ async function main() {
524
563
  ]);
525
564
 
526
565
  const localStr = localBuf.toString("utf8");
527
- const remoteStr = (Buffer.isBuffer(remoteBuf)
528
- ? remoteBuf
529
- : Buffer.from(remoteBuf)
566
+ const remoteStr = (
567
+ Buffer.isBuffer(remoteBuf) ? remoteBuf : Buffer.from(remoteBuf)
530
568
  ).toString("utf8");
531
569
 
532
570
  if (localStr === remoteStr) {
@@ -568,7 +606,9 @@ async function main() {
568
606
  }
569
607
  }
570
608
 
571
- log("\n" + pc.bold(pc.cyan("🧹 Phase 4: Removing orphaned remote files …")));
609
+ log(
610
+ "\n" + pc.bold(pc.cyan("🧹 Phase 4: Removing orphaned remote files …"))
611
+ );
572
612
  for (const rel of remoteKeys) {
573
613
  if (!localKeys.has(rel)) {
574
614
  const r = remote.get(rel);
@@ -579,7 +619,7 @@ async function main() {
579
619
 
580
620
  // -------------------------------------------------------------------
581
621
  // Phase 5: Execute changes (parallel, worker-based)
582
- // -------------------------------------------------------------------
622
+ // -------------------------------------------------------------------
583
623
 
584
624
  if (!DRY_RUN) {
585
625
  log("\n" + pc.bold(pc.cyan("🚚 Phase 5: Apply changes …")));
@@ -643,7 +683,8 @@ async function main() {
643
683
 
644
684
  if (RUN_UPLOAD_LIST && UPLOAD_LIST.length > 0) {
645
685
  log(
646
- "\n" + pc.bold(pc.cyan("⬆️ Extra Phase: Upload-List (explicit files) …"))
686
+ "\n" +
687
+ pc.bold(pc.cyan("⬆️ Extra Phase: Upload-List (explicit files) …"))
647
688
  );
648
689
 
649
690
  const tasks = UPLOAD_LIST.map((rel) => ({
@@ -677,7 +718,8 @@ async function main() {
677
718
 
678
719
  if (RUN_DOWNLOAD_LIST && DOWNLOAD_LIST.length > 0) {
679
720
  log(
680
- "\n" + pc.bold(pc.cyan("⬇️ Extra Phase: Download-List (explicit files) …"))
721
+ "\n" +
722
+ pc.bold(pc.cyan("⬇️ Extra Phase: Download-List (explicit files) …"))
681
723
  );
682
724
 
683
725
  const tasks = DOWNLOAD_LIST.map((rel) => ({
@@ -715,18 +757,28 @@ async function main() {
715
757
  log(` ${ADD} Added : ${toAdd.length}`);
716
758
  log(` ${CHA} Changed: ${toUpdate.length}`);
717
759
  log(` ${DEL} Deleted: ${toDelete.length}`);
718
-
760
+ if (AUTO_EXCLUDED.size > 0) {
761
+ log(` ${EXC} Excluded via uploadList | downloadList): ${AUTO_EXCLUDED.size}`);
762
+ }
719
763
  if (toAdd.length || toUpdate.length || toDelete.length) {
720
764
  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}`));
765
+ [...toAdd.map((t) => t.rel)]
766
+ .sort()
767
+ .forEach((f) => console.log(` ${ADD} ${f}`));
768
+ [...toUpdate.map((t) => t.rel)]
769
+ .sort()
770
+ .forEach((f) => console.log(` ${CHA} ${f}`));
771
+ [...toDelete.map((t) => t.rel)]
772
+ .sort()
773
+ .forEach((f) => console.log(` ${DEL} ${f}`));
724
774
  } else {
725
775
  log("\nNo changes.");
726
776
  }
727
777
 
728
778
  log("\n" + pc.bold(pc.green("✅ Sync complete.")));
729
- log("==================================================================\n\n");
779
+ log(
780
+ "==================================================================\n\n"
781
+ );
730
782
  } catch (err) {
731
783
  elog(pc.red("❌ Synchronisation error:"), err);
732
784
  process.exitCode = 1;
@@ -744,4 +796,4 @@ async function main() {
744
796
  }
745
797
  }
746
798
 
747
- main();
799
+ 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.10",
4
4
  "description": "SFTP sync tool for Hugo projects (local to remote, with hash cache)",
5
5
  "type": "module",
6
6
  "bin": {