sftp-push-sync 1.0.11 → 1.0.13
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 +34 -13
- package/bin/sftp-push-sync.mjs +263 -81
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,10 +26,16 @@ The file `sftp-push-sync.mjs` is pure JavaScript (ESM), not TypeScript. Node.js
|
|
|
26
26
|
## Install
|
|
27
27
|
|
|
28
28
|
```bash
|
|
29
|
-
npm i sftp-push-sync
|
|
29
|
+
npm i -D sftp-push-sync
|
|
30
|
+
# or
|
|
31
|
+
npm install --save-dev sftp-push-sync
|
|
32
|
+
# or
|
|
33
|
+
yarn add --dev sftp-push-sync
|
|
34
|
+
# or
|
|
35
|
+
pnpm add -D sftp-push-sync
|
|
30
36
|
```
|
|
31
37
|
|
|
32
|
-
##
|
|
38
|
+
## Setup
|
|
33
39
|
|
|
34
40
|
Create a `sync.config.json` in the root folder of your project:
|
|
35
41
|
|
|
@@ -58,18 +64,24 @@ Create a `sync.config.json` in the root folder of your project:
|
|
|
58
64
|
}
|
|
59
65
|
},
|
|
60
66
|
"include": [],
|
|
61
|
-
"exclude": [
|
|
62
|
-
"**/.DS_Store",
|
|
63
|
-
"**/.git/**",
|
|
64
|
-
"**/node_modules/**"
|
|
65
|
-
],
|
|
67
|
+
"exclude": ["**/.DS_Store", "**/.git/**", "**/node_modules/**"],
|
|
66
68
|
"textExtensions": [
|
|
67
|
-
".html",
|
|
69
|
+
".html",
|
|
70
|
+
".xml",
|
|
71
|
+
".txt",
|
|
72
|
+
".json",
|
|
73
|
+
".js",
|
|
74
|
+
".css",
|
|
75
|
+
".md",
|
|
76
|
+
".svg"
|
|
68
77
|
],
|
|
78
|
+
"progress": {
|
|
79
|
+
"scanChunk": 10,
|
|
80
|
+
"analyzeChunk": 1
|
|
81
|
+
},
|
|
82
|
+
"logLevel": "normal",
|
|
69
83
|
"uploadList": [],
|
|
70
|
-
"downloadList": [
|
|
71
|
-
"download-counter.json"
|
|
72
|
-
]
|
|
84
|
+
"downloadList": ["download-counter.json"]
|
|
73
85
|
}
|
|
74
86
|
```
|
|
75
87
|
|
|
@@ -84,7 +96,6 @@ A list of files that are excluded from the sync comparison and can be downloaded
|
|
|
84
96
|
- Relative to remoteRoot "download-counter.json"
|
|
85
97
|
- or e.g. "logs/download-counter.json"
|
|
86
98
|
|
|
87
|
-
|
|
88
99
|
```bash
|
|
89
100
|
# normal synchronisation
|
|
90
101
|
sftp-push-sync staging
|
|
@@ -97,6 +108,16 @@ sftp-push-sync prod --download-list --dry-run # view first
|
|
|
97
108
|
sftp-push-sync prod --download-list # then do
|
|
98
109
|
```
|
|
99
110
|
|
|
111
|
+
### Logging Progress
|
|
112
|
+
|
|
113
|
+
Logging can also be configured.
|
|
114
|
+
|
|
115
|
+
- `logLevel` - normal, verbose, laconic.
|
|
116
|
+
- `scanChunk` - After how many elements should a log output be generated during scanning?
|
|
117
|
+
- `analyzeChunk` - After how many elements should a log output be generated during analysis?
|
|
118
|
+
|
|
119
|
+
For >100k files, use analyzeChunk = 10 or 50, otherwise the TTY output itself is a relevant factor.
|
|
120
|
+
|
|
100
121
|
## NPM Scripts
|
|
101
122
|
|
|
102
123
|
- Can be conveniently started via the scripts in `package.json`:
|
|
@@ -146,4 +167,4 @@ The first run always takes a while, especially with lots of images – so be pat
|
|
|
146
167
|
## Links
|
|
147
168
|
|
|
148
169
|
- <https://www.npmjs.com/package/sftp-push-sync>
|
|
149
|
-
- <https://github.com/cnichte/sftp-push-sync>
|
|
170
|
+
- <https://github.com/cnichte/sftp-push-sync>
|
package/bin/sftp-push-sync.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
** sftp-push-sync.mjs - SFTP Syncronisations Tool
|
|
4
4
|
*
|
|
5
|
-
* @author Carsten Nichte, 2025 / https://carsten-nichte.de
|
|
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
|
|
@@ -40,12 +40,21 @@ import { createHash } from "crypto";
|
|
|
40
40
|
import { Writable } from "stream";
|
|
41
41
|
import pc from "picocolors";
|
|
42
42
|
|
|
43
|
+
// get Versionsnummer
|
|
44
|
+
import { createRequire } from "module";
|
|
45
|
+
const require = createRequire(import.meta.url);
|
|
46
|
+
const pkg = require("../package.json");
|
|
47
|
+
|
|
43
48
|
// Colors for the State (works on dark + light background)
|
|
44
49
|
const ADD = pc.green("+"); // Added
|
|
45
50
|
const CHA = pc.yellow("~"); // Changed
|
|
46
51
|
const DEL = pc.red("-"); // Deleted
|
|
47
52
|
const EXC = pc.redBright("-"); // Excluded
|
|
48
53
|
|
|
54
|
+
const hr1 = () => "─".repeat(65); // horizontal line -
|
|
55
|
+
const hr2 = () => "=".repeat(65); // horizontal line =
|
|
56
|
+
const tab_a = () => " ".repeat(3); // indentation for formatting the output.
|
|
57
|
+
|
|
49
58
|
// ---------------------------------------------------------------------------
|
|
50
59
|
// CLI arguments
|
|
51
60
|
// ---------------------------------------------------------------------------
|
|
@@ -53,10 +62,14 @@ const EXC = pc.redBright("-"); // Excluded
|
|
|
53
62
|
const args = process.argv.slice(2);
|
|
54
63
|
const TARGET = args[0];
|
|
55
64
|
const DRY_RUN = args.includes("--dry-run");
|
|
56
|
-
const VERBOSE = args.includes("--verbose") || args.includes("-v");
|
|
57
65
|
const RUN_UPLOAD_LIST = args.includes("--upload-list");
|
|
58
66
|
const RUN_DOWNLOAD_LIST = args.includes("--download-list");
|
|
59
67
|
|
|
68
|
+
// logLevel override via CLI (optional)
|
|
69
|
+
let cliLogLevel = null;
|
|
70
|
+
if (args.includes("--verbose")) cliLogLevel = "verbose";
|
|
71
|
+
if (args.includes("--laconic")) cliLogLevel = "laconic";
|
|
72
|
+
|
|
60
73
|
if (!TARGET) {
|
|
61
74
|
console.error(pc.red("❌ Please specify a connection profile:"));
|
|
62
75
|
console.error(pc.yellow(" sftp-push-sync staging --dry-run"));
|
|
@@ -78,18 +91,20 @@ let CONFIG_RAW;
|
|
|
78
91
|
try {
|
|
79
92
|
CONFIG_RAW = JSON.parse(await fsp.readFile(CONFIG_PATH, "utf8"));
|
|
80
93
|
} catch (err) {
|
|
81
|
-
|
|
94
|
+
console.error(pc.red("❌ Error reading sync.config.json:"), err.message);
|
|
82
95
|
process.exit(1);
|
|
83
96
|
}
|
|
84
97
|
|
|
85
98
|
if (!CONFIG_RAW.connections || typeof CONFIG_RAW.connections !== "object") {
|
|
86
|
-
|
|
99
|
+
console.error(pc.red("❌ sync.config.json must have a 'connections' field."));
|
|
87
100
|
process.exit(1);
|
|
88
101
|
}
|
|
89
102
|
|
|
90
103
|
const TARGET_CONFIG = CONFIG_RAW.connections[TARGET];
|
|
91
104
|
if (!TARGET_CONFIG) {
|
|
92
|
-
|
|
105
|
+
console.error(
|
|
106
|
+
pc.red(`❌ Connection '${TARGET}' not found in sync.config.json.`)
|
|
107
|
+
);
|
|
93
108
|
process.exit(1);
|
|
94
109
|
}
|
|
95
110
|
|
|
@@ -103,18 +118,39 @@ const CONNECTION = {
|
|
|
103
118
|
workers: TARGET_CONFIG.worker ?? 2,
|
|
104
119
|
};
|
|
105
120
|
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
// LogLevel + Progress aus Config
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
// logLevel: "verbose", "normal", "laconic"
|
|
126
|
+
let LOG_LEVEL = (CONFIG_RAW.logLevel ?? "normal").toLowerCase();
|
|
127
|
+
|
|
128
|
+
// Override config with CLI flags
|
|
129
|
+
if (cliLogLevel) {
|
|
130
|
+
LOG_LEVEL = cliLogLevel;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const IS_VERBOSE = LOG_LEVEL === "verbose";
|
|
134
|
+
const IS_LACONIC = LOG_LEVEL === "laconic";
|
|
135
|
+
|
|
136
|
+
const PROGRESS = CONFIG_RAW.progress ?? {};
|
|
137
|
+
const SCAN_CHUNK = PROGRESS.scanChunk ?? (IS_VERBOSE ? 1 : 100);
|
|
138
|
+
const ANALYZE_CHUNK = PROGRESS.analyzeChunk ?? (IS_VERBOSE ? 1 : 10);
|
|
139
|
+
// For >100k files, rather 10–50, for debugging/troubleshooting 1.
|
|
140
|
+
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
106
142
|
// Shared config from JSON
|
|
107
|
-
//
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
|
|
108
145
|
const INCLUDE = CONFIG_RAW.include ?? [];
|
|
109
146
|
const BASE_EXCLUDE = CONFIG_RAW.exclude ?? [];
|
|
110
147
|
|
|
111
|
-
//
|
|
148
|
+
// Special: Lists for targeted uploads/downloads
|
|
112
149
|
function normalizeList(list) {
|
|
113
150
|
if (!Array.isArray(list)) return [];
|
|
114
151
|
return list.flatMap((item) =>
|
|
115
152
|
typeof item === "string"
|
|
116
|
-
?
|
|
117
|
-
item
|
|
153
|
+
? item
|
|
118
154
|
.split(",")
|
|
119
155
|
.map((s) => s.trim())
|
|
120
156
|
.filter(Boolean)
|
|
@@ -128,7 +164,7 @@ const DOWNLOAD_LIST = normalizeList(CONFIG_RAW.downloadList ?? []);
|
|
|
128
164
|
// Effektive Exclude-Liste: explizites exclude + Upload/Download-Listen
|
|
129
165
|
const EXCLUDE = [...BASE_EXCLUDE, ...UPLOAD_LIST, ...DOWNLOAD_LIST];
|
|
130
166
|
|
|
131
|
-
//
|
|
167
|
+
// List of ALL files that were excluded due to uploadList/downloadList
|
|
132
168
|
const AUTO_EXCLUDED = new Set();
|
|
133
169
|
|
|
134
170
|
const TEXT_EXT = CONFIG_RAW.textExtensions ?? [
|
|
@@ -144,6 +180,7 @@ const TEXT_EXT = CONFIG_RAW.textExtensions ?? [
|
|
|
144
180
|
".md",
|
|
145
181
|
".svg",
|
|
146
182
|
];
|
|
183
|
+
|
|
147
184
|
// Cache file name per connection
|
|
148
185
|
const syncCacheName = TARGET_CONFIG.syncCache || `.sync-cache.${TARGET}.json`;
|
|
149
186
|
const CACHE_PATH = path.resolve(syncCacheName);
|
|
@@ -166,7 +203,10 @@ try {
|
|
|
166
203
|
CACHE.remote = raw.remote ?? {};
|
|
167
204
|
}
|
|
168
205
|
} catch (err) {
|
|
169
|
-
|
|
206
|
+
console.warn(
|
|
207
|
+
pc.yellow("⚠️ Could not load cache, starting without:"),
|
|
208
|
+
err.message
|
|
209
|
+
);
|
|
170
210
|
}
|
|
171
211
|
|
|
172
212
|
function cacheKey(relPath) {
|
|
@@ -217,7 +257,7 @@ function log(...msg) {
|
|
|
217
257
|
}
|
|
218
258
|
|
|
219
259
|
function vlog(...msg) {
|
|
220
|
-
if (!
|
|
260
|
+
if (!IS_VERBOSE) return;
|
|
221
261
|
clearProgressLine();
|
|
222
262
|
console.log(...msg);
|
|
223
263
|
}
|
|
@@ -256,27 +296,63 @@ function isTextFile(relPath) {
|
|
|
256
296
|
return TEXT_EXT.includes(ext);
|
|
257
297
|
}
|
|
258
298
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
const
|
|
262
|
-
|
|
299
|
+
function shortenPathForProgress(rel) {
|
|
300
|
+
if (!rel) return "";
|
|
301
|
+
const parts = rel.split("/");
|
|
302
|
+
if (parts.length === 1) {
|
|
303
|
+
return rel; // nur Dateiname
|
|
304
|
+
}
|
|
305
|
+
if (parts.length === 2) {
|
|
306
|
+
return rel; // schon kurz genug
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const last = parts[parts.length - 1];
|
|
310
|
+
const prev = parts[parts.length - 2];
|
|
311
|
+
|
|
312
|
+
// z.B. …/images/foo.jpg
|
|
313
|
+
return `…/${prev}/${last}`;
|
|
314
|
+
}
|
|
263
315
|
|
|
316
|
+
// Two-line progress bar
|
|
317
|
+
function updateProgress2(prefix, current, total, rel = "") {
|
|
264
318
|
if (!process.stdout.isTTY) {
|
|
265
|
-
// Fallback
|
|
266
|
-
|
|
319
|
+
// Fallback für Pipes / Logs
|
|
320
|
+
if (total && total > 0) {
|
|
321
|
+
const percent = ((current / total) * 100).toFixed(1);
|
|
322
|
+
console.log(
|
|
323
|
+
`${tab_a()}${prefix}${current}/${total} Files (${percent}%) – ${rel}`
|
|
324
|
+
);
|
|
325
|
+
} else {
|
|
326
|
+
console.log(`${tab_a()}${prefix}${current} Files – ${rel}`);
|
|
327
|
+
}
|
|
267
328
|
return;
|
|
268
329
|
}
|
|
269
330
|
|
|
270
331
|
const width = process.stdout.columns || 80;
|
|
271
|
-
const line = msg.padEnd(width - 1);
|
|
272
|
-
|
|
273
|
-
progressActive = true;
|
|
274
|
-
process.stdout.write("\r" + line);
|
|
275
332
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
333
|
+
let line1;
|
|
334
|
+
if (total && total > 0) {
|
|
335
|
+
const percent = ((current / total) * 100).toFixed(1);
|
|
336
|
+
line1 = `${tab_a()}${prefix}${current}/${total} Files (${percent}%)`;
|
|
337
|
+
} else {
|
|
338
|
+
// „unknown total“ / Scanner-Modus
|
|
339
|
+
line1 = `${tab_a()}${prefix}${current} Files`;
|
|
279
340
|
}
|
|
341
|
+
|
|
342
|
+
const short = rel ? shortenPathForProgress(rel) : "";
|
|
343
|
+
let line2 = short;
|
|
344
|
+
|
|
345
|
+
if (line1.length > width) line1 = line1.slice(0, width - 1);
|
|
346
|
+
if (line2.length > width) line2 = line2.slice(0, width - 1);
|
|
347
|
+
|
|
348
|
+
// zwei Zeilen überschreiben
|
|
349
|
+
process.stdout.write("\r" + line1.padEnd(width) + "\n");
|
|
350
|
+
process.stdout.write(line2.padEnd(width));
|
|
351
|
+
|
|
352
|
+
// Cursor wieder nach oben (auf die Fortschrittszeile)
|
|
353
|
+
process.stdout.write("\x1b[1A");
|
|
354
|
+
|
|
355
|
+
progressActive = true;
|
|
280
356
|
}
|
|
281
357
|
|
|
282
358
|
// Simple worker pool for parallel tasks
|
|
@@ -297,11 +373,11 @@ async function runTasks(items, workerCount, handler, label = "Tasks") {
|
|
|
297
373
|
try {
|
|
298
374
|
await handler(item);
|
|
299
375
|
} catch (err) {
|
|
300
|
-
elog(pc.red(
|
|
376
|
+
elog(pc.red(`${tab_a()}⚠️ Error in ${label}:`), err.message || err);
|
|
301
377
|
}
|
|
302
378
|
done += 1;
|
|
303
379
|
if (done % 10 === 0 || done === total) {
|
|
304
|
-
|
|
380
|
+
updateProgress2(`${tab_a()}${label}: `, done, total);
|
|
305
381
|
}
|
|
306
382
|
}
|
|
307
383
|
}
|
|
@@ -320,6 +396,7 @@ async function runTasks(items, workerCount, handler, label = "Tasks") {
|
|
|
320
396
|
|
|
321
397
|
async function walkLocal(root) {
|
|
322
398
|
const result = new Map();
|
|
399
|
+
let scanned = 0;
|
|
323
400
|
|
|
324
401
|
async function recurse(current) {
|
|
325
402
|
const entries = await fsp.readdir(current, { withFileTypes: true });
|
|
@@ -329,7 +406,9 @@ async function walkLocal(root) {
|
|
|
329
406
|
await recurse(full);
|
|
330
407
|
} else if (entry.isFile()) {
|
|
331
408
|
const rel = toPosix(path.relative(root, full));
|
|
409
|
+
|
|
332
410
|
if (!isIncluded(rel)) continue;
|
|
411
|
+
|
|
333
412
|
const stat = await fsp.stat(full);
|
|
334
413
|
result.set(rel, {
|
|
335
414
|
rel,
|
|
@@ -338,11 +417,26 @@ async function walkLocal(root) {
|
|
|
338
417
|
mtimeMs: stat.mtimeMs,
|
|
339
418
|
isText: isTextFile(rel),
|
|
340
419
|
});
|
|
420
|
+
|
|
421
|
+
scanned += 1;
|
|
422
|
+
const chunk = IS_VERBOSE ? 1 : SCAN_CHUNK;
|
|
423
|
+
if (scanned === 1 || scanned % chunk === 0) {
|
|
424
|
+
// totally unknown → totally = 0 → no automatic \n
|
|
425
|
+
updateProgress2(" Scan local: ", scanned, 0, rel);
|
|
426
|
+
}
|
|
341
427
|
}
|
|
342
428
|
}
|
|
343
429
|
}
|
|
344
430
|
|
|
345
431
|
await recurse(root);
|
|
432
|
+
|
|
433
|
+
if (scanned > 0) {
|
|
434
|
+
// last line + neat finish
|
|
435
|
+
updateProgress2(" Scan local: ", scanned, 0, "fertig");
|
|
436
|
+
process.stdout.write("\n");
|
|
437
|
+
progressActive = false;
|
|
438
|
+
}
|
|
439
|
+
|
|
346
440
|
return result;
|
|
347
441
|
}
|
|
348
442
|
|
|
@@ -352,6 +446,7 @@ async function walkLocal(root) {
|
|
|
352
446
|
|
|
353
447
|
async function walkRemote(sftp, remoteRoot) {
|
|
354
448
|
const result = new Map();
|
|
449
|
+
let scanned = 0;
|
|
355
450
|
|
|
356
451
|
async function recurse(remoteDir, prefix) {
|
|
357
452
|
const items = await sftp.list(remoteDir);
|
|
@@ -374,11 +469,24 @@ async function walkRemote(sftp, remoteRoot) {
|
|
|
374
469
|
size: Number(item.size),
|
|
375
470
|
modifyTime: item.modifyTime ?? 0,
|
|
376
471
|
});
|
|
472
|
+
|
|
473
|
+
scanned += 1;
|
|
474
|
+
const chunk = IS_VERBOSE ? 1 : SCAN_CHUNK;
|
|
475
|
+
if (scanned === 1 || scanned % chunk === 0) {
|
|
476
|
+
updateProgress2(" Scan remote: ", scanned, 0, rel);
|
|
477
|
+
}
|
|
377
478
|
}
|
|
378
479
|
}
|
|
379
480
|
}
|
|
380
481
|
|
|
381
|
-
await recurse(remoteRoot
|
|
482
|
+
await recurse(remoteRoot);
|
|
483
|
+
|
|
484
|
+
if (scanned > 0) {
|
|
485
|
+
updateProgress2(" Scan remote: ", scanned, 0, "fertig");
|
|
486
|
+
process.stdout.write("\n");
|
|
487
|
+
progressActive = false;
|
|
488
|
+
}
|
|
489
|
+
|
|
382
490
|
return result;
|
|
383
491
|
}
|
|
384
492
|
|
|
@@ -455,6 +563,45 @@ async function getRemoteHash(rel, meta, sftp) {
|
|
|
455
563
|
return hash;
|
|
456
564
|
}
|
|
457
565
|
|
|
566
|
+
// ---------------------------------------------------------------------------
|
|
567
|
+
// SFTP error explanation (for clearer messages)
|
|
568
|
+
// ---------------------------------------------------------------------------
|
|
569
|
+
|
|
570
|
+
function describeSftpError(err) {
|
|
571
|
+
if (!err) return "";
|
|
572
|
+
|
|
573
|
+
const code = err.code || err.errno || "";
|
|
574
|
+
const msg = (err.message || "").toLowerCase();
|
|
575
|
+
|
|
576
|
+
// Netzwerk / DNS
|
|
577
|
+
if (code === "ENOTFOUND") {
|
|
578
|
+
return "Host not found (ENOTFOUND) – Check hostname or DNS entry.";
|
|
579
|
+
}
|
|
580
|
+
if (code === "EHOSTUNREACH") {
|
|
581
|
+
return "Host not reachable (EHOSTUNREACH) – Check network/firewall.";
|
|
582
|
+
}
|
|
583
|
+
if (code === "ECONNREFUSED") {
|
|
584
|
+
return "Connection refused (ECONNREFUSED) – Check the port or SSH service.";
|
|
585
|
+
}
|
|
586
|
+
if (code === "ECONNRESET") {
|
|
587
|
+
return "Connection was reset by the server (ECONNRESET).";
|
|
588
|
+
}
|
|
589
|
+
if (code === "ETIMEDOUT") {
|
|
590
|
+
return "Connection timeout (ETIMEDOUT) – Server is not responding or is blocked.";
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Auth / Authorisations
|
|
594
|
+
if (msg.includes("all configured authentication methods failed")) {
|
|
595
|
+
return "Authentication failed – check your username/password or SSH keys.";
|
|
596
|
+
}
|
|
597
|
+
if (msg.includes("permission denied")) {
|
|
598
|
+
return "Access denied – check permissions on the server.";
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// Fallback
|
|
602
|
+
return "";
|
|
603
|
+
}
|
|
604
|
+
|
|
458
605
|
// ---------------------------------------------------------------------------
|
|
459
606
|
// MAIN
|
|
460
607
|
// ---------------------------------------------------------------------------
|
|
@@ -462,39 +609,46 @@ async function getRemoteHash(rel, meta, sftp) {
|
|
|
462
609
|
async function main() {
|
|
463
610
|
const start = Date.now();
|
|
464
611
|
|
|
465
|
-
log(
|
|
466
|
-
log(
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
612
|
+
log(`\n\n${hr2()}`);
|
|
613
|
+
log(
|
|
614
|
+
pc.bold(
|
|
615
|
+
`🔐 SFTP Push-Synchronisation: sftp-push-sync v${pkg.version} [logLevel=${LOG_LEVEL}]`
|
|
616
|
+
)
|
|
617
|
+
);
|
|
618
|
+
log(`${tab_a()}Connection: ${pc.cyan(TARGET)}`);
|
|
619
|
+
log(`Worker: ${CONNECTION.workers}`);
|
|
620
|
+
log(`${tab_a()}Host: ${pc.green(CONNECTION.host)}:${pc.green(CONNECTION.port)}`);
|
|
621
|
+
log(`${tab_a()}Local: ${pc.green(CONNECTION.localRoot)}`);
|
|
622
|
+
log(`${tab_a()}Remote: ${pc.green(CONNECTION.remoteRoot)}`);
|
|
471
623
|
if (DRY_RUN) log(pc.yellow(" Mode: DRY-RUN (no changes)"));
|
|
472
624
|
if (RUN_UPLOAD_LIST || RUN_DOWNLOAD_LIST) {
|
|
473
625
|
log(
|
|
474
626
|
pc.blue(
|
|
475
|
-
|
|
627
|
+
`${tab_a()}Extra: ${RUN_UPLOAD_LIST ? "uploadList " : ""}${
|
|
476
628
|
RUN_DOWNLOAD_LIST ? "downloadList" : ""
|
|
477
629
|
}`
|
|
478
630
|
)
|
|
479
631
|
);
|
|
480
632
|
}
|
|
481
|
-
log(
|
|
633
|
+
log(`${hr1()}\n`);
|
|
482
634
|
|
|
483
635
|
const sftp = new SftpClient();
|
|
636
|
+
let connected = false;
|
|
484
637
|
|
|
485
638
|
const toAdd = [];
|
|
486
639
|
const toUpdate = [];
|
|
487
640
|
const toDelete = [];
|
|
488
641
|
|
|
489
642
|
try {
|
|
643
|
+
log(pc.cyan("🔌 Connecting to SFTP server …"));
|
|
490
644
|
await sftp.connect({
|
|
491
645
|
host: CONNECTION.host,
|
|
492
646
|
port: CONNECTION.port,
|
|
493
647
|
username: CONNECTION.user,
|
|
494
648
|
password: CONNECTION.password,
|
|
495
649
|
});
|
|
496
|
-
|
|
497
|
-
|
|
650
|
+
connected = true;
|
|
651
|
+
log(pc.green(`${tab_a()}✔ Connected to SFTP.`));
|
|
498
652
|
|
|
499
653
|
if (!fs.existsSync(CONNECTION.localRoot)) {
|
|
500
654
|
console.error(
|
|
@@ -506,20 +660,20 @@ async function main() {
|
|
|
506
660
|
|
|
507
661
|
log(pc.bold(pc.cyan("📥 Phase 1: Scan local files …")));
|
|
508
662
|
const local = await walkLocal(CONNECTION.localRoot);
|
|
509
|
-
log(
|
|
663
|
+
log(`${tab_a()}→ ${local.size} local files`);
|
|
510
664
|
|
|
511
665
|
if (AUTO_EXCLUDED.size > 0) {
|
|
512
666
|
log("");
|
|
513
667
|
log(pc.dim(" Auto-excluded (uploadList/downloadList):"));
|
|
514
668
|
[...AUTO_EXCLUDED].sort().forEach((file) => {
|
|
515
|
-
log(pc.dim(
|
|
669
|
+
log(pc.dim(`${tab_a()} - ${file}`));
|
|
516
670
|
});
|
|
517
671
|
log("");
|
|
518
672
|
}
|
|
519
673
|
|
|
520
674
|
log(pc.bold(pc.cyan("📤 Phase 2: Scan remote files …")));
|
|
521
675
|
const remote = await walkRemote(sftp, CONNECTION.remoteRoot);
|
|
522
|
-
log(
|
|
676
|
+
log(`${tab_a()}→ ${remote.size} remote files\n`);
|
|
523
677
|
|
|
524
678
|
const localKeys = new Set(local.keys());
|
|
525
679
|
const remoteKeys = new Set(remote.keys());
|
|
@@ -531,28 +685,34 @@ async function main() {
|
|
|
531
685
|
// Analysis: just decide, don't upload/delete anything yet
|
|
532
686
|
for (const rel of localKeys) {
|
|
533
687
|
checkedCount += 1;
|
|
688
|
+
|
|
689
|
+
const chunk = IS_VERBOSE ? 1 : ANALYZE_CHUNK;
|
|
534
690
|
if (
|
|
535
|
-
checkedCount === 1 || //
|
|
536
|
-
checkedCount %
|
|
537
|
-
checkedCount === totalToCheck
|
|
691
|
+
checkedCount === 1 || // immediate first issue
|
|
692
|
+
checkedCount % chunk === 0 ||
|
|
693
|
+
checkedCount === totalToCheck
|
|
538
694
|
) {
|
|
539
|
-
|
|
695
|
+
updateProgress2(" Analyse: ", checkedCount, totalToCheck, rel);
|
|
540
696
|
}
|
|
697
|
+
|
|
541
698
|
const l = local.get(rel);
|
|
542
699
|
const r = remote.get(rel);
|
|
543
|
-
|
|
544
700
|
const remotePath = path.posix.join(CONNECTION.remoteRoot, rel);
|
|
545
701
|
|
|
546
702
|
if (!r) {
|
|
547
703
|
toAdd.push({ rel, local: l, remotePath });
|
|
548
|
-
|
|
704
|
+
if (!IS_LACONIC) {
|
|
705
|
+
log(`${ADD} ${pc.green("New:")} ${rel}`);
|
|
706
|
+
}
|
|
549
707
|
continue;
|
|
550
708
|
}
|
|
551
709
|
|
|
552
710
|
// 1. size comparison
|
|
553
711
|
if (l.size !== r.size) {
|
|
554
712
|
toUpdate.push({ rel, local: l, remote: r, remotePath });
|
|
555
|
-
|
|
713
|
+
if (!IS_LACONIC) {
|
|
714
|
+
log(`${CHA} ${pc.yellow("Size changed:")} ${rel}`);
|
|
715
|
+
}
|
|
556
716
|
continue;
|
|
557
717
|
}
|
|
558
718
|
|
|
@@ -570,18 +730,20 @@ async function main() {
|
|
|
570
730
|
).toString("utf8");
|
|
571
731
|
|
|
572
732
|
if (localStr === remoteStr) {
|
|
573
|
-
vlog(
|
|
733
|
+
vlog(`${tab_a()}${pc.dim("✓ Unchanged (Text):")} ${rel}`);
|
|
574
734
|
continue;
|
|
575
735
|
}
|
|
576
736
|
|
|
577
|
-
if (
|
|
737
|
+
if (IS_VERBOSE) {
|
|
578
738
|
const diff = diffWords(remoteStr, localStr);
|
|
579
739
|
const blocks = diff.filter((d) => d.added || d.removed).length;
|
|
580
|
-
vlog(
|
|
740
|
+
vlog(`${tab_a()}${CHA} Text difference (${blocks} blocks) in ${rel}`);
|
|
581
741
|
}
|
|
582
742
|
|
|
583
743
|
toUpdate.push({ rel, local: l, remote: r, remotePath });
|
|
584
|
-
|
|
744
|
+
if (!IS_LACONIC) {
|
|
745
|
+
log(`${tab_a()}${CHA} ${pc.yellow("Content changed (Text):")} ${rel}`);
|
|
746
|
+
}
|
|
585
747
|
} else {
|
|
586
748
|
// Binary: Hash comparison with cache
|
|
587
749
|
const localMeta = l;
|
|
@@ -593,18 +755,20 @@ async function main() {
|
|
|
593
755
|
]);
|
|
594
756
|
|
|
595
757
|
if (localHash === remoteHash) {
|
|
596
|
-
vlog(
|
|
758
|
+
vlog(`${tab_a()}${pc.dim("✓ Unchanged (binary, hash):")} ${rel}`);
|
|
597
759
|
continue;
|
|
598
760
|
}
|
|
599
761
|
|
|
600
|
-
if (
|
|
601
|
-
vlog(
|
|
602
|
-
vlog(
|
|
603
|
-
vlog(
|
|
762
|
+
if (IS_VERBOSE) {
|
|
763
|
+
vlog(`${tab_a()}${CHA} Hash different (binary): ${rel}`);
|
|
764
|
+
vlog(`${tab_a()} local: ${localHash}`);
|
|
765
|
+
vlog(`${tab_a()} remote: ${remoteHash}`);
|
|
604
766
|
}
|
|
605
767
|
|
|
606
768
|
toUpdate.push({ rel, local: l, remote: r, remotePath });
|
|
607
|
-
|
|
769
|
+
if (!IS_LACONIC) {
|
|
770
|
+
log(`${CHA} ${pc.yellow("Content changed (Binary):")} ${rel}`);
|
|
771
|
+
}
|
|
608
772
|
}
|
|
609
773
|
}
|
|
610
774
|
|
|
@@ -615,7 +779,9 @@ async function main() {
|
|
|
615
779
|
if (!localKeys.has(rel)) {
|
|
616
780
|
const r = remote.get(rel);
|
|
617
781
|
toDelete.push({ rel, remotePath: r.remotePath });
|
|
618
|
-
|
|
782
|
+
if (!IS_LACONIC) {
|
|
783
|
+
log(`${tab_a()}${DEL} ${pc.red("Remove:")} ${rel}`);
|
|
784
|
+
}
|
|
619
785
|
}
|
|
620
786
|
}
|
|
621
787
|
|
|
@@ -676,7 +842,11 @@ async function main() {
|
|
|
676
842
|
"Deletes"
|
|
677
843
|
);
|
|
678
844
|
} else {
|
|
679
|
-
log(
|
|
845
|
+
log(
|
|
846
|
+
pc.yellow(
|
|
847
|
+
"\n💡 DRY-RUN: Connection tested, no files transferred or deleted."
|
|
848
|
+
)
|
|
849
|
+
);
|
|
680
850
|
}
|
|
681
851
|
|
|
682
852
|
// -------------------------------------------------------------------
|
|
@@ -686,7 +856,7 @@ async function main() {
|
|
|
686
856
|
if (RUN_UPLOAD_LIST && UPLOAD_LIST.length > 0) {
|
|
687
857
|
log(
|
|
688
858
|
"\n" +
|
|
689
|
-
pc.bold(pc.cyan("⬆️
|
|
859
|
+
pc.bold(pc.cyan("⬆️ Extra Phase: Upload-List (explicit files) …"))
|
|
690
860
|
);
|
|
691
861
|
|
|
692
862
|
const tasks = UPLOAD_LIST.map((rel) => ({
|
|
@@ -697,7 +867,7 @@ async function main() {
|
|
|
697
867
|
|
|
698
868
|
if (DRY_RUN) {
|
|
699
869
|
for (const t of tasks) {
|
|
700
|
-
log(
|
|
870
|
+
log(`${tab_a()}${ADD} would upload (uploadList): ${t.rel}`);
|
|
701
871
|
}
|
|
702
872
|
} else {
|
|
703
873
|
await runTasks(
|
|
@@ -711,7 +881,7 @@ async function main() {
|
|
|
711
881
|
// ignore
|
|
712
882
|
}
|
|
713
883
|
await sftp.put(localPath, remotePath);
|
|
714
|
-
log(
|
|
884
|
+
log(`${tab_a()}${ADD} uploadList: ${rel}`);
|
|
715
885
|
},
|
|
716
886
|
"Upload-List"
|
|
717
887
|
);
|
|
@@ -721,7 +891,7 @@ async function main() {
|
|
|
721
891
|
if (RUN_DOWNLOAD_LIST && DOWNLOAD_LIST.length > 0) {
|
|
722
892
|
log(
|
|
723
893
|
"\n" +
|
|
724
|
-
pc.bold(pc.cyan("⬇️
|
|
894
|
+
pc.bold(pc.cyan("⬇️ Extra Phase: Download-List (explicit files) …"))
|
|
725
895
|
);
|
|
726
896
|
|
|
727
897
|
const tasks = DOWNLOAD_LIST.map((rel) => ({
|
|
@@ -732,7 +902,7 @@ async function main() {
|
|
|
732
902
|
|
|
733
903
|
if (DRY_RUN) {
|
|
734
904
|
for (const t of tasks) {
|
|
735
|
-
log(
|
|
905
|
+
log(`${tab_a()}${ADD} would download (downloadList): ${t.rel}`);
|
|
736
906
|
}
|
|
737
907
|
} else {
|
|
738
908
|
await runTasks(
|
|
@@ -741,7 +911,7 @@ async function main() {
|
|
|
741
911
|
async ({ remotePath, localPath, rel }) => {
|
|
742
912
|
await fsp.mkdir(path.dirname(localPath), { recursive: true });
|
|
743
913
|
await sftp.fastGet(remotePath, localPath);
|
|
744
|
-
log(
|
|
914
|
+
log(`${tab_a()}${ADD} downloadList: ${rel}`);
|
|
745
915
|
},
|
|
746
916
|
"Download-List"
|
|
747
917
|
);
|
|
@@ -755,36 +925,41 @@ async function main() {
|
|
|
755
925
|
|
|
756
926
|
// Summary
|
|
757
927
|
log("\n" + pc.bold(pc.cyan("📊 Summary:")));
|
|
758
|
-
log(
|
|
759
|
-
log(
|
|
760
|
-
log(
|
|
761
|
-
log(
|
|
928
|
+
log(`${tab_a()}Duration: ${pc.green(duration + " s")}`);
|
|
929
|
+
log(`${tab_a()}${ADD} Added : ${toAdd.length}`);
|
|
930
|
+
log(`${tab_a()}${CHA} Changed: ${toUpdate.length}`);
|
|
931
|
+
log(`${tab_a()}${DEL} Deleted: ${toDelete.length}`);
|
|
762
932
|
if (AUTO_EXCLUDED.size > 0) {
|
|
763
933
|
log(
|
|
764
|
-
|
|
934
|
+
`${tab_a()}${EXC} Excluded via uploadList | downloadList: ${AUTO_EXCLUDED.size}`
|
|
765
935
|
);
|
|
766
936
|
}
|
|
767
937
|
if (toAdd.length || toUpdate.length || toDelete.length) {
|
|
768
938
|
log("\n📄 Changes:");
|
|
769
939
|
[...toAdd.map((t) => t.rel)]
|
|
770
940
|
.sort()
|
|
771
|
-
.forEach((f) => console.log(
|
|
941
|
+
.forEach((f) => console.log(`${tab_a()}${ADD} ${f}`));
|
|
772
942
|
[...toUpdate.map((t) => t.rel)]
|
|
773
943
|
.sort()
|
|
774
|
-
.forEach((f) => console.log(
|
|
944
|
+
.forEach((f) => console.log(`${tab_a()}${CHA} ${f}`));
|
|
775
945
|
[...toDelete.map((t) => t.rel)]
|
|
776
946
|
.sort()
|
|
777
|
-
.forEach((f) => console.log(
|
|
947
|
+
.forEach((f) => console.log(`${tab_a()}${DEL} ${f}`));
|
|
778
948
|
} else {
|
|
779
949
|
log("\nNo changes.");
|
|
780
950
|
}
|
|
781
951
|
|
|
782
952
|
log("\n" + pc.bold(pc.green("✅ Sync complete.")));
|
|
783
|
-
log(
|
|
784
|
-
"==================================================================\n\n"
|
|
785
|
-
);
|
|
786
953
|
} catch (err) {
|
|
787
|
-
|
|
954
|
+
const hint = describeSftpError(err);
|
|
955
|
+
elog(pc.red("❌ Synchronisation error:"), err.message || err);
|
|
956
|
+
if (hint) {
|
|
957
|
+
wlog(pc.yellow(`${tab_a()}Mögliche Ursache:`), hint);
|
|
958
|
+
}
|
|
959
|
+
if (IS_VERBOSE) {
|
|
960
|
+
// Vollständiges Error-Objekt nur in verbose anzeigen
|
|
961
|
+
console.error(err);
|
|
962
|
+
}
|
|
788
963
|
process.exitCode = 1;
|
|
789
964
|
try {
|
|
790
965
|
await saveCache(true);
|
|
@@ -793,11 +968,18 @@ async function main() {
|
|
|
793
968
|
}
|
|
794
969
|
} finally {
|
|
795
970
|
try {
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
971
|
+
if (connected) {
|
|
972
|
+
await sftp.end();
|
|
973
|
+
log(pc.green(`${tab_a()}✔ Connection closed.`));
|
|
974
|
+
}
|
|
975
|
+
} catch (e) {
|
|
976
|
+
wlog(
|
|
977
|
+
pc.yellow("⚠️ Could not close SFTP connection cleanly:"),
|
|
978
|
+
e.message || e
|
|
979
|
+
);
|
|
799
980
|
}
|
|
800
981
|
}
|
|
982
|
+
log(`${hr2()}\n\n`);
|
|
801
983
|
}
|
|
802
984
|
|
|
803
985
|
main();
|