sftp-push-sync 1.0.8 → 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 +21 -18
- package/bin/sftp-push-sync.mjs +90 -38
- package/images/example-output-001.png +0 -0
- package/package.json +1 -1
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
|
-
|
|
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
|
|
76
|
+
### special uploads / downloads
|
|
77
|
+
|
|
78
|
+
A list of files that are excluded from the sync comparison and can be downloaded or uploaded separately.
|
|
78
79
|
|
|
79
|
-
- uploadList
|
|
80
|
-
-
|
|
81
|
-
-
|
|
82
|
-
- downloadList
|
|
83
|
-
-
|
|
84
|
-
-
|
|
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 #
|
|
96
|
-
sftp-push-sync prod --download-list #
|
|
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,12 +135,14 @@ 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
|
|
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
|
|
|
142
|
+
## Example Output
|
|
143
|
+
|
|
144
|
+

|
|
145
|
+
|
|
143
146
|
## Links
|
|
144
147
|
|
|
145
148
|
- <https://www.npmjs.com/package/sftp-push-sync>
|
package/bin/sftp-push-sync.mjs
CHANGED
|
@@ -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("+");
|
|
45
|
+
const ADD = pc.green("+"); // Added
|
|
45
46
|
const CHA = pc.yellow("~"); // Changed
|
|
46
|
-
const DEL = pc.red("-");
|
|
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
|
|
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: {},
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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 = (
|
|
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(
|
|
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" +
|
|
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" +
|
|
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)]
|
|
722
|
-
|
|
723
|
-
|
|
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(
|
|
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();
|
|
Binary file
|