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 +17 -18
- package/bin/sftp-push-sync.mjs +96 -40
- 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
|
|
78
77
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
-
|
|
83
|
-
-
|
|
84
|
-
|
|
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 #
|
|
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,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
|
|
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
|
|
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,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("+");
|
|
44
|
+
const ADD = pc.green("+"); // Added
|
|
45
45
|
const CHA = pc.yellow("~"); // Changed
|
|
46
|
-
const DEL = pc.red("-");
|
|
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
|
|
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: {},
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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 (
|
|
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 = (
|
|
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(
|
|
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" +
|
|
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" +
|
|
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)]
|
|
722
|
-
|
|
723
|
-
|
|
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(
|
|
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();
|