sftp-push-sync 1.0.15 → 1.0.17
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 +85 -40
- package/bin/sftp-push-sync.mjs +403 -154
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -65,26 +65,66 @@ Create a `sync.config.json` in the root folder of your project:
|
|
|
65
65
|
},
|
|
66
66
|
"include": [],
|
|
67
67
|
"exclude": ["**/.DS_Store", "**/.git/**", "**/node_modules/**"],
|
|
68
|
-
"textExtensions": [
|
|
69
|
-
|
|
70
|
-
".xml",
|
|
71
|
-
".txt",
|
|
72
|
-
".json",
|
|
73
|
-
".js",
|
|
74
|
-
".css",
|
|
75
|
-
".md",
|
|
76
|
-
".svg"
|
|
77
|
-
],
|
|
68
|
+
"textExtensions": [".html",".xml",".txt",".json",".js",".css",".md",".svg"],
|
|
69
|
+
"mediaExtensions": [".jpg",".jpeg",".png",".webp",".gif",".avif",".tif",".tiff",".mp4",".mov",".m4v","mp3",".wav",".flac"],
|
|
78
70
|
"progress": {
|
|
79
71
|
"scanChunk": 10,
|
|
80
72
|
"analyzeChunk": 1
|
|
81
73
|
},
|
|
82
74
|
"logLevel": "normal",
|
|
75
|
+
"logFile": ".sftp-push-sync.{target}.log",
|
|
83
76
|
"uploadList": [],
|
|
84
77
|
"downloadList": ["download-counter.json"]
|
|
85
78
|
}
|
|
86
79
|
```
|
|
87
80
|
|
|
81
|
+
### CLI Usage
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
# Normal synchronisation
|
|
85
|
+
node bin/sftp-push-sync.mjs staging
|
|
86
|
+
|
|
87
|
+
# Consider normal synchronisation + upload list
|
|
88
|
+
node bin/sftp-push-sync.mjs staging --upload-list
|
|
89
|
+
|
|
90
|
+
# Only lists, no standard synchronisation
|
|
91
|
+
node bin/sftp-push-sync.mjs staging --skip-sync --upload-list
|
|
92
|
+
node bin/sftp-push-sync.mjs staging --skip-sync --download-list
|
|
93
|
+
node bin/sftp-push-sync.mjs staging --skip-sync --upload-list --download-list
|
|
94
|
+
|
|
95
|
+
# (optional) only run lists dry
|
|
96
|
+
node bin/sftp-push-sync.mjs staging --skip-sync --upload-list --dry-run
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
- Can be conveniently started via the scripts in `package.json`:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
# For example
|
|
103
|
+
npm run sync:staging
|
|
104
|
+
# or short
|
|
105
|
+
npm run ss
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
If you have stored the scripts in `package.json` as follows:
|
|
109
|
+
|
|
110
|
+
```json
|
|
111
|
+
|
|
112
|
+
"scripts": {
|
|
113
|
+
"sync:staging": "sftp-push-sync staging",
|
|
114
|
+
"sync:staging:dry": "sftp-push-sync staging --dry-run",
|
|
115
|
+
"ss": "npm run sync:staging",
|
|
116
|
+
"ssd": "npm run sync:staging:dry",
|
|
117
|
+
|
|
118
|
+
"sync:prod": "sftp-push-sync prod",
|
|
119
|
+
"sync:prod:dry": "sftp-push-sync prod --dry-run",
|
|
120
|
+
"sp": "npm run sync:prod",
|
|
121
|
+
"spd": "npm run sync:prod:dry",
|
|
122
|
+
},
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The dry run is a great way to compare files and fill the cache.
|
|
126
|
+
|
|
127
|
+
|
|
88
128
|
### special uploads / downloads
|
|
89
129
|
|
|
90
130
|
A list of files that are excluded from the sync comparison and can be downloaded or uploaded separately.
|
|
@@ -113,41 +153,45 @@ sftp-push-sync prod --download-list # then do
|
|
|
113
153
|
Logging can also be configured.
|
|
114
154
|
|
|
115
155
|
- `logLevel` - normal, verbose, laconic.
|
|
156
|
+
- `logFile` - an optional logFile.
|
|
116
157
|
- `scanChunk` - After how many elements should a log output be generated during scanning?
|
|
117
158
|
- `analyzeChunk` - After how many elements should a log output be generated during analysis?
|
|
118
159
|
|
|
119
160
|
For >100k files, use analyzeChunk = 10 or 50, otherwise the TTY output itself is a relevant factor.
|
|
120
161
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
162
|
+
### Wildcards
|
|
163
|
+
|
|
164
|
+
Examples for Wirdcards for `include`, `exclude`, `uploadList` and `downloadList`:
|
|
165
|
+
|
|
166
|
+
- `"content/**"` - ALLES unterhalb von `content/`
|
|
167
|
+
- `".html", ".htm", ".md", ".txt", ".json"`- Nur bestimmte Dateiendungen
|
|
168
|
+
- `"**/*.html"` - alle HTML-Dateien
|
|
169
|
+
- `"**/*.md"`- alle Markdown-Dateien
|
|
170
|
+
- `"content/**/*.md"` - nur Markdown in `content/`
|
|
171
|
+
- `"static/images/**/*.jpg"`
|
|
172
|
+
- `"**/thumb-*.*"` - thumb-Bilder überall
|
|
173
|
+
- `"**/*-draft.*"` - Dateien mit -draft vor der Extension
|
|
174
|
+
- `"content/**/*.md"` - alle Markdown-Dateien
|
|
175
|
+
- `"config/**"` - komplette Konfiguration
|
|
176
|
+
- `"static/images/covers/**"`- nur Cover-Bilder
|
|
177
|
+
- `"logs/**/*.log"` - alle Logs aus logs/
|
|
178
|
+
- `"reports/**/*.xlsx"`
|
|
179
|
+
|
|
180
|
+
practical excludes:
|
|
181
|
+
|
|
182
|
+
```txt
|
|
183
|
+
"exclude": [
|
|
184
|
+
".git/**", // kompletter .git Ordner
|
|
185
|
+
".idea/**", // JetBrains
|
|
186
|
+
"node_modules/**", // Node dependencies
|
|
187
|
+
"dist/**", // Build Output
|
|
188
|
+
"**/*.map", // Source Maps
|
|
189
|
+
"**/~*", // Emacs/Editor-Backups (~Dateien)
|
|
190
|
+
"**/#*#", // weitere Editor-Backups
|
|
191
|
+
"**/.DS_Store" // macOS Trash
|
|
192
|
+
]
|
|
130
193
|
```
|
|
131
194
|
|
|
132
|
-
If you have stored the scripts in `package.json` as follows:
|
|
133
|
-
|
|
134
|
-
```json
|
|
135
|
-
|
|
136
|
-
"scripts": {
|
|
137
|
-
"sync:staging": "sftp-push-sync staging",
|
|
138
|
-
"sync:staging:dry": "sftp-push-sync staging --dry-run",
|
|
139
|
-
"ss": "npm run sync:staging",
|
|
140
|
-
"ssd": "npm run sync:staging:dry",
|
|
141
|
-
|
|
142
|
-
"sync:prod": "sftp-push-sync prod",
|
|
143
|
-
"sync:prod:dry": "sftp-push-sync prod --dry-run",
|
|
144
|
-
"sp": "npm run sync:prod",
|
|
145
|
-
"spd": "npm run sync:prod:dry",
|
|
146
|
-
},
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
The dry run is a great way to compare files and fill the cache.
|
|
150
|
-
|
|
151
195
|
## Which files are needed?
|
|
152
196
|
|
|
153
197
|
- `sync.config.json` - The configuration file (with passwords in plain text, so please leave it out of the git repository)
|
|
@@ -155,10 +199,11 @@ The dry run is a great way to compare files and fill the cache.
|
|
|
155
199
|
## Which files are created?
|
|
156
200
|
|
|
157
201
|
- The cache files: `.sync-cache.*.json`
|
|
202
|
+
- The log file: `.sftp-push-sync.{target}.log` (Optional, overwritten with each run)
|
|
158
203
|
|
|
159
|
-
You can safely delete the local cache at any time. The first analysis will then take longer again
|
|
204
|
+
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.
|
|
160
205
|
|
|
161
|
-
The first run always takes a while, especially with lots of images – so be patient! Once the cache is full, it will be faster.
|
|
206
|
+
Note: The first run always takes a while, especially with lots of images – so be patient! Once the cache is full, it will be faster.
|
|
162
207
|
|
|
163
208
|
## Example Output
|
|
164
209
|
|
package/bin/sftp-push-sync.mjs
CHANGED
|
@@ -55,9 +55,6 @@ const hr1 = () => "─".repeat(65); // horizontal line -
|
|
|
55
55
|
const hr2 = () => "=".repeat(65); // horizontal line =
|
|
56
56
|
const tab_a = () => " ".repeat(3); // indentation for formatting the terminal output.
|
|
57
57
|
const tab_b = () => " ".repeat(6);
|
|
58
|
-
const cr_a = () => "\n".repeat(1);
|
|
59
|
-
const cr_b = () => "\n".repeat(2);
|
|
60
|
-
|
|
61
58
|
|
|
62
59
|
// ---------------------------------------------------------------------------
|
|
63
60
|
// CLI arguments
|
|
@@ -68,6 +65,7 @@ const TARGET = args[0];
|
|
|
68
65
|
const DRY_RUN = args.includes("--dry-run");
|
|
69
66
|
const RUN_UPLOAD_LIST = args.includes("--upload-list");
|
|
70
67
|
const RUN_DOWNLOAD_LIST = args.includes("--download-list");
|
|
68
|
+
const SKIP_SYNC = args.includes("--skip-sync");
|
|
71
69
|
|
|
72
70
|
// logLevel override via CLI (optional)
|
|
73
71
|
let cliLogLevel = null;
|
|
@@ -80,6 +78,14 @@ if (!TARGET) {
|
|
|
80
78
|
process.exit(1);
|
|
81
79
|
}
|
|
82
80
|
|
|
81
|
+
// Wenn jemand --skip-sync ohne Listen benutzt → sinnlos, also abbrechen
|
|
82
|
+
if (SKIP_SYNC && !RUN_UPLOAD_LIST && !RUN_DOWNLOAD_LIST) {
|
|
83
|
+
console.error(
|
|
84
|
+
pc.red("❌ --skip-sync requires at least --upload-list or --download-list.")
|
|
85
|
+
);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
|
|
83
89
|
// ---------------------------------------------------------------------------
|
|
84
90
|
// Load config file
|
|
85
91
|
// ---------------------------------------------------------------------------
|
|
@@ -104,6 +110,93 @@ if (!CONFIG_RAW.connections || typeof CONFIG_RAW.connections !== "object") {
|
|
|
104
110
|
process.exit(1);
|
|
105
111
|
}
|
|
106
112
|
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
// Logging helpers (Terminal + optional Logfile)
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// Default: .sync.{TARGET}.log, kann via config.logFile überschrieben werden
|
|
117
|
+
const DEFAULT_LOG_FILE = `.sync.${TARGET}.log`;
|
|
118
|
+
const rawLogFilePattern = CONFIG_RAW.logFile || DEFAULT_LOG_FILE;
|
|
119
|
+
const LOG_FILE = path.resolve(
|
|
120
|
+
rawLogFilePattern.replace("{target}", TARGET)
|
|
121
|
+
);
|
|
122
|
+
let LOG_STREAM = null;
|
|
123
|
+
|
|
124
|
+
/** einmalig Logfile-Stream öffnen */
|
|
125
|
+
function openLogFile() {
|
|
126
|
+
if (!LOG_FILE) return;
|
|
127
|
+
if (!LOG_STREAM) {
|
|
128
|
+
LOG_STREAM = fs.createWriteStream(LOG_FILE, {
|
|
129
|
+
flags: "w", // pro Lauf überschreiben
|
|
130
|
+
encoding: "utf8",
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** eine fertige Zeile ins Logfile schreiben (ohne Einfluss auf Terminal) */
|
|
136
|
+
function writeLogLine(line) {
|
|
137
|
+
if (!LOG_STREAM) return;
|
|
138
|
+
// ANSI-Farbsequenzen aus der Log-Zeile entfernen
|
|
139
|
+
const clean =
|
|
140
|
+
typeof line === "string"
|
|
141
|
+
? line.replace(/\x1b\[[0-9;]*m/g, "")
|
|
142
|
+
: String(line).replace(/\x1b\[[0-9;]*m/g, "");
|
|
143
|
+
try {
|
|
144
|
+
LOG_STREAM.write(clean + "\n");
|
|
145
|
+
} catch {
|
|
146
|
+
// falls Stream schon zu ist, einfach ignorieren – verhindert ERR_STREAM_WRITE_AFTER_END
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Konsole + Logfile (normal) */
|
|
151
|
+
function rawConsoleLog(...msg) {
|
|
152
|
+
clearProgressLine();
|
|
153
|
+
console.log(...msg);
|
|
154
|
+
const line = msg
|
|
155
|
+
.map((m) => (typeof m === "string" ? m : String(m)))
|
|
156
|
+
.join(" ");
|
|
157
|
+
writeLogLine(line);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function rawConsoleError(...msg) {
|
|
161
|
+
clearProgressLine();
|
|
162
|
+
console.error(...msg);
|
|
163
|
+
const line = msg
|
|
164
|
+
.map((m) => (typeof m === "string" ? m : String(m)))
|
|
165
|
+
.join(" ");
|
|
166
|
+
writeLogLine("[ERROR] " + line);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function rawConsoleWarn(...msg) {
|
|
170
|
+
clearProgressLine();
|
|
171
|
+
console.warn(...msg);
|
|
172
|
+
const line = msg
|
|
173
|
+
.map((m) => (typeof m === "string" ? m : String(m)))
|
|
174
|
+
.join(" ");
|
|
175
|
+
writeLogLine("[WARN] " + line);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// High-level Helfer, die du überall im Script schon verwendest:
|
|
179
|
+
function log(...msg) {
|
|
180
|
+
rawConsoleLog(...msg);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function vlog(...msg) {
|
|
184
|
+
if (!IS_VERBOSE) return;
|
|
185
|
+
rawConsoleLog(...msg);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function elog(...msg) {
|
|
189
|
+
rawConsoleError(...msg);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function wlog(...msg) {
|
|
193
|
+
rawConsoleWarn(...msg);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
// Connection
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
|
|
107
200
|
const TARGET_CONFIG = CONFIG_RAW.connections[TARGET];
|
|
108
201
|
if (!TARGET_CONFIG) {
|
|
109
202
|
console.error(
|
|
@@ -149,6 +242,38 @@ const ANALYZE_CHUNK = PROGRESS.analyzeChunk ?? (IS_VERBOSE ? 1 : 10);
|
|
|
149
242
|
const INCLUDE = CONFIG_RAW.include ?? [];
|
|
150
243
|
const BASE_EXCLUDE = CONFIG_RAW.exclude ?? [];
|
|
151
244
|
|
|
245
|
+
// textExtensions
|
|
246
|
+
const TEXT_EXT = CONFIG_RAW.textExtensions ?? [
|
|
247
|
+
".html",
|
|
248
|
+
".htm",
|
|
249
|
+
".xml",
|
|
250
|
+
".txt",
|
|
251
|
+
".json",
|
|
252
|
+
".js",
|
|
253
|
+
".mjs",
|
|
254
|
+
".cjs",
|
|
255
|
+
".css",
|
|
256
|
+
".md",
|
|
257
|
+
".svg",
|
|
258
|
+
];
|
|
259
|
+
|
|
260
|
+
// mediaExtensions – aktuell nur Meta, aber schon konfigurierbar
|
|
261
|
+
const MEDIA_EXT = CONFIG_RAW.mediaExtensions ?? [
|
|
262
|
+
".jpg",
|
|
263
|
+
".jpeg",
|
|
264
|
+
".png",
|
|
265
|
+
".gif",
|
|
266
|
+
".webp",
|
|
267
|
+
".avif",
|
|
268
|
+
".mp4",
|
|
269
|
+
".mov",
|
|
270
|
+
".mp3",
|
|
271
|
+
".wav",
|
|
272
|
+
".ogg",
|
|
273
|
+
".flac",
|
|
274
|
+
".pdf",
|
|
275
|
+
];
|
|
276
|
+
|
|
152
277
|
// Special: Lists for targeted uploads/downloads
|
|
153
278
|
function normalizeList(list) {
|
|
154
279
|
if (!Array.isArray(list)) return [];
|
|
@@ -166,25 +291,13 @@ const UPLOAD_LIST = normalizeList(CONFIG_RAW.uploadList ?? []);
|
|
|
166
291
|
const DOWNLOAD_LIST = normalizeList(CONFIG_RAW.downloadList ?? []);
|
|
167
292
|
|
|
168
293
|
// Effektive Exclude-Liste: explizites exclude + Upload/Download-Listen
|
|
294
|
+
// → diese Dateien werden im „normalen“ Sync nicht angerührt,
|
|
295
|
+
// sondern nur über die Bypass-Mechanik behandelt.
|
|
169
296
|
const EXCLUDE = [...BASE_EXCLUDE, ...UPLOAD_LIST, ...DOWNLOAD_LIST];
|
|
170
297
|
|
|
171
298
|
// List of ALL files that were excluded due to uploadList/downloadList
|
|
172
299
|
const AUTO_EXCLUDED = new Set();
|
|
173
300
|
|
|
174
|
-
const TEXT_EXT = CONFIG_RAW.textExtensions ?? [
|
|
175
|
-
".html",
|
|
176
|
-
".htm",
|
|
177
|
-
".xml",
|
|
178
|
-
".txt",
|
|
179
|
-
".json",
|
|
180
|
-
".js",
|
|
181
|
-
".mjs",
|
|
182
|
-
".cjs",
|
|
183
|
-
".css",
|
|
184
|
-
".md",
|
|
185
|
-
".svg",
|
|
186
|
-
];
|
|
187
|
-
|
|
188
301
|
// Cache file name per connection
|
|
189
302
|
const syncCacheName = TARGET_CONFIG.syncCache || `.sync-cache.${TARGET}.json`;
|
|
190
303
|
const CACHE_PATH = path.resolve(syncCacheName);
|
|
@@ -245,11 +358,14 @@ let progressActive = false;
|
|
|
245
358
|
|
|
246
359
|
function clearProgressLine() {
|
|
247
360
|
if (!process.stdout.isTTY || !progressActive) return;
|
|
248
|
-
const width = process.stdout.columns || 80;
|
|
249
|
-
const blank = " ".repeat(width);
|
|
250
361
|
|
|
251
|
-
//
|
|
252
|
-
|
|
362
|
+
// Zwei Progress-Zeilen ohne zusätzliche Newlines leeren:
|
|
363
|
+
// Cursor steht nach updateProgress2() auf der ersten Zeile.
|
|
364
|
+
process.stdout.write("\r"); // an Zeilenanfang
|
|
365
|
+
process.stdout.write("\x1b[2K"); // erste Zeile löschen
|
|
366
|
+
process.stdout.write("\x1b[1B"); // eine Zeile nach unten
|
|
367
|
+
process.stdout.write("\x1b[2K"); // zweite Zeile löschen
|
|
368
|
+
process.stdout.write("\x1b[1A"); // wieder nach oben
|
|
253
369
|
|
|
254
370
|
progressActive = false;
|
|
255
371
|
}
|
|
@@ -258,27 +374,6 @@ function toPosix(p) {
|
|
|
258
374
|
return p.split(path.sep).join("/");
|
|
259
375
|
}
|
|
260
376
|
|
|
261
|
-
function log(...msg) {
|
|
262
|
-
clearProgressLine();
|
|
263
|
-
console.log(...msg);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
function vlog(...msg) {
|
|
267
|
-
if (!IS_VERBOSE) return;
|
|
268
|
-
clearProgressLine();
|
|
269
|
-
console.log(...msg);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function elog(...msg) {
|
|
273
|
-
clearProgressLine();
|
|
274
|
-
console.error(...msg);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
function wlog(...msg) {
|
|
278
|
-
clearProgressLine();
|
|
279
|
-
console.warn(...msg);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
377
|
function matchesAny(patterns, relPath) {
|
|
283
378
|
if (!patterns || patterns.length === 0) return false;
|
|
284
379
|
return patterns.some((pattern) => minimatch(relPath, pattern, { dot: true }));
|
|
@@ -303,6 +398,11 @@ function isTextFile(relPath) {
|
|
|
303
398
|
return TEXT_EXT.includes(ext);
|
|
304
399
|
}
|
|
305
400
|
|
|
401
|
+
function isMediaFile(relPath) {
|
|
402
|
+
const ext = path.extname(relPath).toLowerCase();
|
|
403
|
+
return MEDIA_EXT.includes(ext);
|
|
404
|
+
}
|
|
405
|
+
|
|
306
406
|
function shortenPathForProgress(rel) {
|
|
307
407
|
if (!rel) return "";
|
|
308
408
|
const parts = rel.split("/");
|
|
@@ -320,17 +420,28 @@ function shortenPathForProgress(rel) {
|
|
|
320
420
|
return `…/${prev}/${last}`;
|
|
321
421
|
}
|
|
322
422
|
|
|
323
|
-
// Two-line progress bar
|
|
423
|
+
// Two-line progress bar (for terminal) + 1-line log entry
|
|
324
424
|
function updateProgress2(prefix, current, total, rel = "") {
|
|
425
|
+
const short = rel ? shortenPathForProgress(rel) : "";
|
|
426
|
+
|
|
427
|
+
//Log file: always as a single line with **full** rel path
|
|
428
|
+
const base =
|
|
429
|
+
total && total > 0
|
|
430
|
+
? `${prefix}${current}/${total} Files`
|
|
431
|
+
: `${prefix}${current} Files`;
|
|
432
|
+
writeLogLine(
|
|
433
|
+
`[progress] ${base}${rel ? " – " + rel : ""}`
|
|
434
|
+
);
|
|
435
|
+
|
|
325
436
|
if (!process.stdout.isTTY) {
|
|
326
|
-
// Fallback
|
|
437
|
+
// Fallback-Terminal
|
|
327
438
|
if (total && total > 0) {
|
|
328
439
|
const percent = ((current / total) * 100).toFixed(1);
|
|
329
440
|
console.log(
|
|
330
|
-
`${tab_a()}${prefix}${current}/${total} Files (${percent}%) – ${
|
|
441
|
+
`${tab_a()}${prefix}${current}/${total} Files (${percent}%) – ${short}`
|
|
331
442
|
);
|
|
332
443
|
} else {
|
|
333
|
-
console.log(`${tab_a()}${prefix}${current} Files – ${
|
|
444
|
+
console.log(`${tab_a()}${prefix}${current} Files – ${short}`);
|
|
334
445
|
}
|
|
335
446
|
return;
|
|
336
447
|
}
|
|
@@ -346,14 +457,13 @@ function updateProgress2(prefix, current, total, rel = "") {
|
|
|
346
457
|
line1 = `${tab_a()}${prefix}${current} Files`;
|
|
347
458
|
}
|
|
348
459
|
|
|
349
|
-
const short = rel ? shortenPathForProgress(rel) : "";
|
|
350
460
|
let line2 = short;
|
|
351
461
|
|
|
352
462
|
if (line1.length > width) line1 = line1.slice(0, width - 1);
|
|
353
463
|
if (line2.length > width) line2 = line2.slice(0, width - 1);
|
|
354
464
|
|
|
355
465
|
// zwei Zeilen überschreiben
|
|
356
|
-
process.stdout.write("\r" + line1.padEnd(width) +
|
|
466
|
+
process.stdout.write("\r" + line1.padEnd(width) + "\n");
|
|
357
467
|
process.stdout.write(line2.padEnd(width));
|
|
358
468
|
|
|
359
469
|
// Cursor wieder nach oben (auf die Fortschrittszeile)
|
|
@@ -383,8 +493,8 @@ async function runTasks(items, workerCount, handler, label = "Tasks") {
|
|
|
383
493
|
elog(pc.red(`${tab_a()}⚠️ Error in ${label}:`), err.message || err);
|
|
384
494
|
}
|
|
385
495
|
done += 1;
|
|
386
|
-
if (done % 10 === 0 || done === total) {
|
|
387
|
-
updateProgress2(`${
|
|
496
|
+
if (done === 1 || done % 10 === 0 || done === total) {
|
|
497
|
+
updateProgress2(`${label}: `, done, total, item.rel ?? "");
|
|
388
498
|
}
|
|
389
499
|
}
|
|
390
500
|
}
|
|
@@ -423,13 +533,13 @@ async function walkLocal(root) {
|
|
|
423
533
|
size: stat.size,
|
|
424
534
|
mtimeMs: stat.mtimeMs,
|
|
425
535
|
isText: isTextFile(rel),
|
|
536
|
+
isMedia: isMediaFile(rel),
|
|
426
537
|
});
|
|
427
538
|
|
|
428
539
|
scanned += 1;
|
|
429
540
|
const chunk = IS_VERBOSE ? 1 : SCAN_CHUNK;
|
|
430
541
|
if (scanned === 1 || scanned % chunk === 0) {
|
|
431
|
-
|
|
432
|
-
updateProgress2(`${tab_a()}Scan local: `, scanned, 0, rel);
|
|
542
|
+
updateProgress2("Scan local: ", scanned, 0, rel);
|
|
433
543
|
}
|
|
434
544
|
}
|
|
435
545
|
}
|
|
@@ -438,15 +548,38 @@ async function walkLocal(root) {
|
|
|
438
548
|
await recurse(root);
|
|
439
549
|
|
|
440
550
|
if (scanned > 0) {
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
process.stdout.write(cr_a());
|
|
551
|
+
updateProgress2("Scan local: ", scanned, 0, "fertig");
|
|
552
|
+
process.stdout.write("\n");
|
|
444
553
|
progressActive = false;
|
|
445
554
|
}
|
|
446
555
|
|
|
447
556
|
return result;
|
|
448
557
|
}
|
|
449
558
|
|
|
559
|
+
// Plain walker für Bypass (ignoriert INCLUDE/EXCLUDE)
|
|
560
|
+
async function walkLocalPlain(root) {
|
|
561
|
+
const result = new Map();
|
|
562
|
+
|
|
563
|
+
async function recurse(current) {
|
|
564
|
+
const entries = await fsp.readdir(current, { withFileTypes: true });
|
|
565
|
+
for (const entry of entries) {
|
|
566
|
+
const full = path.join(current, entry.name);
|
|
567
|
+
if (entry.isDirectory()) {
|
|
568
|
+
await recurse(full);
|
|
569
|
+
} else if (entry.isFile()) {
|
|
570
|
+
const rel = toPosix(path.relative(root, full));
|
|
571
|
+
result.set(rel, {
|
|
572
|
+
rel,
|
|
573
|
+
localPath: full,
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
await recurse(root);
|
|
580
|
+
return result;
|
|
581
|
+
}
|
|
582
|
+
|
|
450
583
|
// ---------------------------------------------------------------------------
|
|
451
584
|
// Remote walker (recursive, all subdirectories) – respects INCLUDE/EXCLUDE
|
|
452
585
|
// ---------------------------------------------------------------------------
|
|
@@ -480,23 +613,51 @@ async function walkRemote(sftp, remoteRoot) {
|
|
|
480
613
|
scanned += 1;
|
|
481
614
|
const chunk = IS_VERBOSE ? 1 : SCAN_CHUNK;
|
|
482
615
|
if (scanned === 1 || scanned % chunk === 0) {
|
|
483
|
-
updateProgress2(
|
|
616
|
+
updateProgress2("Scan remote: ", scanned, 0, rel);
|
|
484
617
|
}
|
|
485
618
|
}
|
|
486
619
|
}
|
|
487
620
|
}
|
|
488
621
|
|
|
489
|
-
await recurse(remoteRoot);
|
|
622
|
+
await recurse(remoteRoot, "");
|
|
490
623
|
|
|
491
624
|
if (scanned > 0) {
|
|
492
|
-
updateProgress2(
|
|
493
|
-
process.stdout.write(
|
|
625
|
+
updateProgress2("Scan remote: ", scanned, 0, "fertig");
|
|
626
|
+
process.stdout.write("\n");
|
|
494
627
|
progressActive = false;
|
|
495
628
|
}
|
|
496
629
|
|
|
497
630
|
return result;
|
|
498
631
|
}
|
|
499
632
|
|
|
633
|
+
// Plain walker für Bypass (ignoriert INCLUDE/EXCLUDE)
|
|
634
|
+
async function walkRemotePlain(sftp, remoteRoot) {
|
|
635
|
+
const result = new Map();
|
|
636
|
+
|
|
637
|
+
async function recurse(remoteDir, prefix) {
|
|
638
|
+
const items = await sftp.list(remoteDir);
|
|
639
|
+
|
|
640
|
+
for (const item of items) {
|
|
641
|
+
if (!item.name || item.name === "." || item.name === "..") continue;
|
|
642
|
+
|
|
643
|
+
const full = path.posix.join(remoteDir, item.name);
|
|
644
|
+
const rel = prefix ? `${prefix}/${item.name}` : item.name;
|
|
645
|
+
|
|
646
|
+
if (item.type === "d") {
|
|
647
|
+
await recurse(full, rel);
|
|
648
|
+
} else {
|
|
649
|
+
result.set(rel, {
|
|
650
|
+
rel,
|
|
651
|
+
remotePath: full,
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
await recurse(remoteRoot, "");
|
|
658
|
+
return result;
|
|
659
|
+
}
|
|
660
|
+
|
|
500
661
|
// ---------------------------------------------------------------------------
|
|
501
662
|
// Hash helper for binaries (streaming, memory-efficient)
|
|
502
663
|
// ---------------------------------------------------------------------------
|
|
@@ -609,25 +770,140 @@ function describeSftpError(err) {
|
|
|
609
770
|
return "";
|
|
610
771
|
}
|
|
611
772
|
|
|
773
|
+
// ---------------------------------------------------------------------------
|
|
774
|
+
// Bypass-only Mode (uploadList / downloadList ohne normalen Sync)
|
|
775
|
+
// ---------------------------------------------------------------------------
|
|
776
|
+
|
|
777
|
+
async function collectUploadTargets() {
|
|
778
|
+
const all = await walkLocalPlain(CONNECTION.localRoot);
|
|
779
|
+
const results = [];
|
|
780
|
+
|
|
781
|
+
for (const [rel, meta] of all.entries()) {
|
|
782
|
+
if (matchesAny(UPLOAD_LIST, rel)) {
|
|
783
|
+
const remotePath = path.posix.join(CONNECTION.remoteRoot, rel);
|
|
784
|
+
results.push({
|
|
785
|
+
rel,
|
|
786
|
+
localPath: meta.localPath,
|
|
787
|
+
remotePath,
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
return results;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
async function collectDownloadTargets(sftp) {
|
|
796
|
+
const all = await walkRemotePlain(sftp, CONNECTION.remoteRoot);
|
|
797
|
+
const results = [];
|
|
798
|
+
|
|
799
|
+
for (const [rel, meta] of all.entries()) {
|
|
800
|
+
if (matchesAny(DOWNLOAD_LIST, rel)) {
|
|
801
|
+
const localPath = path.join(CONNECTION.localRoot, rel);
|
|
802
|
+
results.push({
|
|
803
|
+
rel,
|
|
804
|
+
remotePath: meta.remotePath,
|
|
805
|
+
localPath,
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
return results;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
async function performBypassOnly(sftp) {
|
|
814
|
+
log("");
|
|
815
|
+
log(pc.bold(pc.cyan("🚀 Bypass-Only Mode (skip-sync)")));
|
|
816
|
+
|
|
817
|
+
if (RUN_UPLOAD_LIST) {
|
|
818
|
+
log("");
|
|
819
|
+
log(pc.bold(pc.cyan("⬆️ Upload-Bypass (uploadList) …")));
|
|
820
|
+
const targets = await collectUploadTargets();
|
|
821
|
+
log(`${tab_a()}→ ${targets.length} files from uploadList`);
|
|
822
|
+
|
|
823
|
+
if (!DRY_RUN) {
|
|
824
|
+
await runTasks(
|
|
825
|
+
targets,
|
|
826
|
+
CONNECTION.workers,
|
|
827
|
+
async ({ localPath, remotePath, rel }) => {
|
|
828
|
+
const remoteDir = path.posix.dirname(remotePath);
|
|
829
|
+
try {
|
|
830
|
+
await sftp.mkdir(remoteDir, true);
|
|
831
|
+
} catch {
|
|
832
|
+
// Directory may already exist
|
|
833
|
+
}
|
|
834
|
+
await sftp.put(localPath, remotePath);
|
|
835
|
+
vlog(`${tab_a()}${ADD} Uploaded (bypass): ${rel}`);
|
|
836
|
+
},
|
|
837
|
+
"Bypass Uploads"
|
|
838
|
+
);
|
|
839
|
+
} else {
|
|
840
|
+
for (const t of targets) {
|
|
841
|
+
log(`${tab_a()}${ADD} (DRY-RUN) Upload: ${t.rel}`);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
if (RUN_DOWNLOAD_LIST) {
|
|
847
|
+
log("");
|
|
848
|
+
log(pc.bold(pc.cyan("⬇️ Download-Bypass (downloadList) …")));
|
|
849
|
+
const targets = await collectDownloadTargets(sftp);
|
|
850
|
+
log(`${tab_a()}→ ${targets.length} files from downloadList`);
|
|
851
|
+
|
|
852
|
+
if (!DRY_RUN) {
|
|
853
|
+
await runTasks(
|
|
854
|
+
targets,
|
|
855
|
+
CONNECTION.workers,
|
|
856
|
+
async ({ remotePath, localPath, rel }) => {
|
|
857
|
+
const localDir = path.dirname(localPath);
|
|
858
|
+
await fsp.mkdir(localDir, { recursive: true });
|
|
859
|
+
await sftp.get(remotePath, localPath);
|
|
860
|
+
vlog(`${tab_a()}${CHA} Downloaded (bypass): ${rel}`);
|
|
861
|
+
},
|
|
862
|
+
"Bypass Downloads"
|
|
863
|
+
);
|
|
864
|
+
} else {
|
|
865
|
+
for (const t of targets) {
|
|
866
|
+
log(`${tab_a()}${CHA} (DRY-RUN) Download: ${t.rel}`);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
log("");
|
|
872
|
+
log(pc.bold(pc.green("✅ Bypass-only run finished.")));
|
|
873
|
+
}
|
|
874
|
+
|
|
612
875
|
// ---------------------------------------------------------------------------
|
|
613
876
|
// MAIN
|
|
614
877
|
// ---------------------------------------------------------------------------
|
|
615
878
|
|
|
879
|
+
async function initLogFile() {
|
|
880
|
+
if (!LOG_FILE) return;
|
|
881
|
+
const dir = path.dirname(LOG_FILE);
|
|
882
|
+
await fsp.mkdir(dir, { recursive: true });
|
|
883
|
+
LOG_STREAM = fs.createWriteStream(LOG_FILE, {
|
|
884
|
+
flags: "w",
|
|
885
|
+
encoding: "utf8",
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
|
|
616
889
|
async function main() {
|
|
617
890
|
const start = Date.now();
|
|
618
891
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
);
|
|
892
|
+
await initLogFile();
|
|
893
|
+
|
|
894
|
+
// Header-Abstand wie gehabt: zwei Leerzeilen davor
|
|
895
|
+
log("\n" + hr2());
|
|
896
|
+
log(pc.bold(`🔐 SFTP Push-Synchronisation: sftp-push-sync v${pkg.version}`));
|
|
897
|
+
log(`${tab_a()}LogLevel: ${LOG_LEVEL}`);
|
|
625
898
|
log(`${tab_a()}Connection: ${pc.cyan(TARGET)}`);
|
|
626
899
|
log(`${tab_a()}Worker: ${CONNECTION.workers}`);
|
|
627
|
-
log(
|
|
900
|
+
log(
|
|
901
|
+
`${tab_a()}Host: ${pc.green(CONNECTION.host)}:${pc.green(CONNECTION.port)}`
|
|
902
|
+
);
|
|
628
903
|
log(`${tab_a()}Local: ${pc.green(CONNECTION.localRoot)}`);
|
|
629
904
|
log(`${tab_a()}Remote: ${pc.green(CONNECTION.remoteRoot)}`);
|
|
630
905
|
if (DRY_RUN) log(pc.yellow(`${tab_a()}Mode: DRY-RUN (no changes)`));
|
|
906
|
+
if (SKIP_SYNC) log(pc.yellow(`${tab_a()}Mode: SKIP-SYNC (bypass only)`));
|
|
631
907
|
if (RUN_UPLOAD_LIST || RUN_DOWNLOAD_LIST) {
|
|
632
908
|
log(
|
|
633
909
|
pc.blue(
|
|
@@ -637,7 +913,10 @@ async function main() {
|
|
|
637
913
|
)
|
|
638
914
|
);
|
|
639
915
|
}
|
|
640
|
-
|
|
916
|
+
if (LOG_FILE) {
|
|
917
|
+
log(`${tab_a()}LogFile: ${pc.cyan(LOG_FILE)}`);
|
|
918
|
+
}
|
|
919
|
+
log(hr1());
|
|
641
920
|
|
|
642
921
|
const sftp = new SftpClient();
|
|
643
922
|
let connected = false;
|
|
@@ -647,6 +926,7 @@ async function main() {
|
|
|
647
926
|
const toDelete = [];
|
|
648
927
|
|
|
649
928
|
try {
|
|
929
|
+
log("");
|
|
650
930
|
log(pc.cyan("🔌 Connecting to SFTP server …"));
|
|
651
931
|
await sftp.connect({
|
|
652
932
|
host: CONNECTION.host,
|
|
@@ -665,7 +945,25 @@ async function main() {
|
|
|
665
945
|
process.exit(1);
|
|
666
946
|
}
|
|
667
947
|
|
|
668
|
-
|
|
948
|
+
// -------------------------------------------------------------
|
|
949
|
+
// SKIP-SYNC-Modus → nur Bypass mit Listen
|
|
950
|
+
// -------------------------------------------------------------
|
|
951
|
+
if (SKIP_SYNC) {
|
|
952
|
+
await performBypassOnly(sftp);
|
|
953
|
+
const duration = ((Date.now() - start) / 1000).toFixed(2);
|
|
954
|
+
log("");
|
|
955
|
+
log(pc.bold(pc.cyan("📊 Summary (bypass only):")));
|
|
956
|
+
log(`${tab_a()}Duration: ${pc.green(duration + " s")}`);
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// -------------------------------------------------------------
|
|
961
|
+
// Normaler Sync (inkl. evtl. paralleler Listen-Excludes)
|
|
962
|
+
// -------------------------------------------------------------
|
|
963
|
+
|
|
964
|
+
// Phase 1 – mit exakt einer Leerzeile davor
|
|
965
|
+
log("");
|
|
966
|
+
log(pc.bold(pc.cyan("📥 Phase 1: Scan local files …")));
|
|
669
967
|
const local = await walkLocal(CONNECTION.localRoot);
|
|
670
968
|
log(`${tab_a()}→ ${local.size} local files`);
|
|
671
969
|
|
|
@@ -678,14 +976,17 @@ async function main() {
|
|
|
678
976
|
log("");
|
|
679
977
|
}
|
|
680
978
|
|
|
681
|
-
|
|
979
|
+
// Phase 2 – auch mit einer Leerzeile davor
|
|
980
|
+
log("");
|
|
981
|
+
log(pc.bold(pc.cyan("📤 Phase 2: Scan remote files …")));
|
|
682
982
|
const remote = await walkRemote(sftp, CONNECTION.remoteRoot);
|
|
683
|
-
log(`${tab_a()}→ ${remote.size} remote files
|
|
983
|
+
log(`${tab_a()}→ ${remote.size} remote files`);
|
|
984
|
+
log("");
|
|
684
985
|
|
|
685
986
|
const localKeys = new Set(local.keys());
|
|
686
987
|
const remoteKeys = new Set(remote.keys());
|
|
687
988
|
|
|
688
|
-
log(
|
|
989
|
+
log(pc.bold(pc.cyan("🔎 Phase 3: Compare & decide …")));
|
|
689
990
|
const totalToCheck = localKeys.size;
|
|
690
991
|
let checkedCount = 0;
|
|
691
992
|
|
|
@@ -699,7 +1000,7 @@ async function main() {
|
|
|
699
1000
|
checkedCount % chunk === 0 ||
|
|
700
1001
|
checkedCount === totalToCheck
|
|
701
1002
|
) {
|
|
702
|
-
updateProgress2("
|
|
1003
|
+
updateProgress2("Analyse: ", checkedCount, totalToCheck, rel);
|
|
703
1004
|
}
|
|
704
1005
|
|
|
705
1006
|
const l = local.get(rel);
|
|
@@ -749,7 +1050,9 @@ async function main() {
|
|
|
749
1050
|
|
|
750
1051
|
toUpdate.push({ rel, local: l, remote: r, remotePath });
|
|
751
1052
|
if (!IS_LACONIC) {
|
|
752
|
-
log(
|
|
1053
|
+
log(
|
|
1054
|
+
`${tab_a()}${CHA} ${pc.yellow("Content changed (Text):")} ${rel}`
|
|
1055
|
+
);
|
|
753
1056
|
}
|
|
754
1057
|
} else {
|
|
755
1058
|
// Binary: Hash comparison with cache
|
|
@@ -781,12 +1084,12 @@ async function main() {
|
|
|
781
1084
|
|
|
782
1085
|
// Wenn Phase 3 nichts gefunden hat, explizit sagen
|
|
783
1086
|
if (toAdd.length === 0 && toUpdate.length === 0) {
|
|
1087
|
+
log("");
|
|
784
1088
|
log(`${tab_a()}No differences found. Everything is up to date.`);
|
|
785
1089
|
}
|
|
786
1090
|
|
|
787
|
-
log(
|
|
788
|
-
|
|
789
|
-
);
|
|
1091
|
+
log("");
|
|
1092
|
+
log(pc.bold(pc.cyan("🧹 Phase 4: Removing orphaned remote files …")));
|
|
790
1093
|
for (const rel of remoteKeys) {
|
|
791
1094
|
if (!localKeys.has(rel)) {
|
|
792
1095
|
const r = remote.get(rel);
|
|
@@ -807,7 +1110,8 @@ async function main() {
|
|
|
807
1110
|
// -------------------------------------------------------------------
|
|
808
1111
|
|
|
809
1112
|
if (!DRY_RUN) {
|
|
810
|
-
log(
|
|
1113
|
+
log("");
|
|
1114
|
+
log(pc.bold(pc.cyan("🚚 Phase 5: Apply changes …")));
|
|
811
1115
|
|
|
812
1116
|
// Upload new files
|
|
813
1117
|
await runTasks(
|
|
@@ -859,82 +1163,14 @@ async function main() {
|
|
|
859
1163
|
"Deletes"
|
|
860
1164
|
);
|
|
861
1165
|
} else {
|
|
1166
|
+
log("");
|
|
862
1167
|
log(
|
|
863
1168
|
pc.yellow(
|
|
864
|
-
|
|
1169
|
+
"💡 DRY-RUN: Connection tested, no files transferred or deleted."
|
|
865
1170
|
)
|
|
866
1171
|
);
|
|
867
1172
|
}
|
|
868
1173
|
|
|
869
|
-
// -------------------------------------------------------------------
|
|
870
|
-
// Phase 6: optional uploadList / downloadList
|
|
871
|
-
// -------------------------------------------------------------------
|
|
872
|
-
|
|
873
|
-
if (RUN_UPLOAD_LIST && UPLOAD_LIST.length > 0) {
|
|
874
|
-
log(
|
|
875
|
-
cr_a() +
|
|
876
|
-
pc.bold(pc.cyan("⬆️ Extra Phase: Upload-List (explicit files) …"))
|
|
877
|
-
);
|
|
878
|
-
|
|
879
|
-
const tasks = UPLOAD_LIST.map((rel) => ({
|
|
880
|
-
rel,
|
|
881
|
-
localPath: path.join(CONNECTION.localRoot, rel),
|
|
882
|
-
remotePath: path.posix.join(CONNECTION.remoteRoot, toPosix(rel)),
|
|
883
|
-
}));
|
|
884
|
-
|
|
885
|
-
if (DRY_RUN) {
|
|
886
|
-
for (const t of tasks) {
|
|
887
|
-
log(`${tab_a()}${ADD} would upload (uploadList): ${t.rel}`);
|
|
888
|
-
}
|
|
889
|
-
} else {
|
|
890
|
-
await runTasks(
|
|
891
|
-
tasks,
|
|
892
|
-
CONNECTION.workers,
|
|
893
|
-
async ({ localPath, remotePath, rel }) => {
|
|
894
|
-
const remoteDir = path.posix.dirname(remotePath);
|
|
895
|
-
try {
|
|
896
|
-
await sftp.mkdir(remoteDir, true);
|
|
897
|
-
} catch {
|
|
898
|
-
// ignore
|
|
899
|
-
}
|
|
900
|
-
await sftp.put(localPath, remotePath);
|
|
901
|
-
log(`${tab_a()}${ADD} uploadList: ${rel}`);
|
|
902
|
-
},
|
|
903
|
-
"Upload-List"
|
|
904
|
-
);
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
if (RUN_DOWNLOAD_LIST && DOWNLOAD_LIST.length > 0) {
|
|
909
|
-
log(
|
|
910
|
-
cr_a() +
|
|
911
|
-
pc.bold(pc.cyan("⬇️ Extra Phase: Download-List (explicit files) …"))
|
|
912
|
-
);
|
|
913
|
-
|
|
914
|
-
const tasks = DOWNLOAD_LIST.map((rel) => ({
|
|
915
|
-
rel,
|
|
916
|
-
remotePath: path.posix.join(CONNECTION.remoteRoot, toPosix(rel)),
|
|
917
|
-
localPath: path.join(CONNECTION.localRoot, rel),
|
|
918
|
-
}));
|
|
919
|
-
|
|
920
|
-
if (DRY_RUN) {
|
|
921
|
-
for (const t of tasks) {
|
|
922
|
-
log(`${tab_a()}${ADD} would download (downloadList): ${t.rel}`);
|
|
923
|
-
}
|
|
924
|
-
} else {
|
|
925
|
-
await runTasks(
|
|
926
|
-
tasks,
|
|
927
|
-
CONNECTION.workers,
|
|
928
|
-
async ({ remotePath, localPath, rel }) => {
|
|
929
|
-
await fsp.mkdir(path.dirname(localPath), { recursive: true });
|
|
930
|
-
await sftp.fastGet(remotePath, localPath);
|
|
931
|
-
log(`${tab_a()}${ADD} downloadList: ${rel}`);
|
|
932
|
-
},
|
|
933
|
-
"Download-List"
|
|
934
|
-
);
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
|
|
938
1174
|
const duration = ((Date.now() - start) / 1000).toFixed(2);
|
|
939
1175
|
|
|
940
1176
|
// Write cache safely at the end
|
|
@@ -942,18 +1178,22 @@ async function main() {
|
|
|
942
1178
|
|
|
943
1179
|
// Summary
|
|
944
1180
|
log(hr1());
|
|
945
|
-
log(
|
|
1181
|
+
log("");
|
|
1182
|
+
log(pc.bold(pc.cyan("📊 Summary:")));
|
|
946
1183
|
log(`${tab_a()}Duration: ${pc.green(duration + " s")}`);
|
|
947
1184
|
log(`${tab_a()}${ADD} Added : ${toAdd.length}`);
|
|
948
1185
|
log(`${tab_a()}${CHA} Changed: ${toUpdate.length}`);
|
|
949
1186
|
log(`${tab_a()}${DEL} Deleted: ${toDelete.length}`);
|
|
950
1187
|
if (AUTO_EXCLUDED.size > 0) {
|
|
951
1188
|
log(
|
|
952
|
-
`${tab_a()}${EXC} Excluded via uploadList | downloadList: ${
|
|
1189
|
+
`${tab_a()}${EXC} Excluded via uploadList | downloadList: ${
|
|
1190
|
+
AUTO_EXCLUDED.size
|
|
1191
|
+
}`
|
|
953
1192
|
);
|
|
954
1193
|
}
|
|
955
1194
|
if (toAdd.length || toUpdate.length || toDelete.length) {
|
|
956
|
-
log(
|
|
1195
|
+
log("");
|
|
1196
|
+
log("📄 Changes:");
|
|
957
1197
|
[...toAdd.map((t) => t.rel)]
|
|
958
1198
|
.sort()
|
|
959
1199
|
.forEach((f) => console.log(`${tab_a()}${ADD} ${f}`));
|
|
@@ -964,10 +1204,12 @@ async function main() {
|
|
|
964
1204
|
.sort()
|
|
965
1205
|
.forEach((f) => console.log(`${tab_a()}${DEL} ${f}`));
|
|
966
1206
|
} else {
|
|
967
|
-
log(
|
|
1207
|
+
log("");
|
|
1208
|
+
log("No changes.");
|
|
968
1209
|
}
|
|
969
1210
|
|
|
970
|
-
log(
|
|
1211
|
+
log("");
|
|
1212
|
+
log(pc.bold(pc.green("✅ Sync complete.")));
|
|
971
1213
|
} catch (err) {
|
|
972
1214
|
const hint = describeSftpError(err);
|
|
973
1215
|
elog(pc.red("❌ Synchronisation error:"), err.message || err);
|
|
@@ -996,8 +1238,15 @@ async function main() {
|
|
|
996
1238
|
e.message || e
|
|
997
1239
|
);
|
|
998
1240
|
}
|
|
1241
|
+
|
|
1242
|
+
// Abschlusslinie + Leerzeile **vor** dem Schließen des Logfiles
|
|
1243
|
+
log(hr2());
|
|
1244
|
+
log("");
|
|
1245
|
+
|
|
1246
|
+
if (LOG_STREAM) {
|
|
1247
|
+
LOG_STREAM.end();
|
|
1248
|
+
}
|
|
999
1249
|
}
|
|
1000
|
-
log(`${hr2()}${cr_b()}`);
|
|
1001
1250
|
}
|
|
1002
1251
|
|
|
1003
|
-
main();
|
|
1252
|
+
main();
|