sftp-push-sync 2.0.0 → 2.1.1
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/CHANGELOG.md +11 -0
- package/README.md +14 -3
- package/bin/sftp-push-sync.mjs +183 -40
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.1.0] - 2025-11-19
|
|
4
|
+
|
|
5
|
+
Sync only handles files and creates missing directories during upload.
|
|
6
|
+
However, it should also manage directories:
|
|
7
|
+
|
|
8
|
+
- They should (optionally) be removed if:
|
|
9
|
+
- for example, a directory is empty because all files have been deleted from it.
|
|
10
|
+
- or if a directory no longer exists locally.
|
|
11
|
+
|
|
12
|
+
This is now taken into account with the option: `cleanupEmptyDirs`.
|
|
13
|
+
|
|
3
14
|
## [2.0.0] - 2025-11-18
|
|
4
15
|
|
|
5
16
|
### Breaking
|
package/README.md
CHANGED
|
@@ -85,6 +85,7 @@ Create a `sync.config.json` in the root folder of your project:
|
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
87
|
},
|
|
88
|
+
"cleanupEmptyDirs": true,
|
|
88
89
|
"include": [],
|
|
89
90
|
"exclude": ["**/.DS_Store", "**/.git/**", "**/node_modules/**"],
|
|
90
91
|
"textExtensions": [".html",".xml",".txt",".json",".js",".css",".md",".svg"],
|
|
@@ -162,7 +163,8 @@ sftp-push-sync staging
|
|
|
162
163
|
# Normal synchronisation + explicitly transfer sidecar upload list
|
|
163
164
|
sftp-push-sync staging --sidecar-upload
|
|
164
165
|
|
|
165
|
-
# just fetch the sidecar download list from the server
|
|
166
|
+
# just fetch the sidecar download list from the server
|
|
167
|
+
# combined with normal synchronisation
|
|
166
168
|
sftp-push-sync prod --sidecar-download --dry-run # view first
|
|
167
169
|
sftp-push-sync prod --sidecar-download # then do
|
|
168
170
|
```
|
|
@@ -218,6 +220,15 @@ practical excludes:
|
|
|
218
220
|
]
|
|
219
221
|
```
|
|
220
222
|
|
|
223
|
+
### Folder handling
|
|
224
|
+
|
|
225
|
+
Sync only handles files and creates missing directories during upload.
|
|
226
|
+
However, it should also manage directories:
|
|
227
|
+
|
|
228
|
+
- They should (optionally) be removed if:
|
|
229
|
+
- for example, a directory is empty because all files have been deleted from it.
|
|
230
|
+
- or if a directory no longer exists locally.
|
|
231
|
+
|
|
221
232
|
## Which files are needed?
|
|
222
233
|
|
|
223
234
|
- `sync.config.json` - The configuration file (with passwords in plain text, so please leave it out of the git repository)
|
|
@@ -227,9 +238,9 @@ practical excludes:
|
|
|
227
238
|
- The cache files: `.sync-cache.*.json`
|
|
228
239
|
- The log file: `.sftp-push-sync.{target}.log` (Optional, overwritten with each run)
|
|
229
240
|
|
|
230
|
-
You can safely delete the local cache at any time. The first analysis will then take longer
|
|
241
|
+
You can safely delete the local cache at any time. The first analysis will then take longer, because remote hashes will be streamed again. After that, everything will run fast.
|
|
231
242
|
|
|
232
|
-
Note: The first run always takes a while, especially with lots of
|
|
243
|
+
Note: The first run always takes a while, especially with lots of media – so be patient! Once the cache is full, it will be faster.
|
|
233
244
|
|
|
234
245
|
## Example Output
|
|
235
246
|
|
package/bin/sftp-push-sync.mjs
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
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
|
+
* 4. Supports separate sidecar upload/download lists for special files
|
|
11
12
|
*
|
|
12
13
|
* Features:
|
|
13
14
|
* - multiple connections in sync.config.json
|
|
@@ -25,6 +26,11 @@
|
|
|
25
26
|
* - For example, log files or other special files.
|
|
26
27
|
* - These files can be downloaded or uploaded separately.
|
|
27
28
|
*
|
|
29
|
+
* Folder handling:
|
|
30
|
+
* Delete Folders if
|
|
31
|
+
* - If, for example, a directory is empty because all files have been deleted from it.
|
|
32
|
+
* - Or if a directory no longer exists locally.
|
|
33
|
+
*
|
|
28
34
|
* The file sftp-push-sync.mjs is pure JavaScript (ESM), not TypeScript.
|
|
29
35
|
* Node.js can execute it directly as long as "type": "module" is specified in package.json
|
|
30
36
|
* or the file has the extension .mjs.
|
|
@@ -81,7 +87,9 @@ if (!TARGET) {
|
|
|
81
87
|
// Wenn jemand --skip-sync ohne Listen benutzt → sinnlos, also abbrechen
|
|
82
88
|
if (SKIP_SYNC && !RUN_UPLOAD_LIST && !RUN_DOWNLOAD_LIST) {
|
|
83
89
|
console.error(
|
|
84
|
-
pc.red(
|
|
90
|
+
pc.red(
|
|
91
|
+
"❌ --skip-sync requires at least --sidecar-upload or --sidecar-download."
|
|
92
|
+
)
|
|
85
93
|
);
|
|
86
94
|
process.exit(1);
|
|
87
95
|
}
|
|
@@ -113,12 +121,11 @@ if (!CONFIG_RAW.connections || typeof CONFIG_RAW.connections !== "object") {
|
|
|
113
121
|
// ---------------------------------------------------------------------------
|
|
114
122
|
// Logging helpers (Terminal + optional Logfile)
|
|
115
123
|
// ---------------------------------------------------------------------------
|
|
124
|
+
|
|
116
125
|
// Default: .sync.{TARGET}.log, kann via config.logFile überschrieben werden
|
|
117
126
|
const DEFAULT_LOG_FILE = `.sync.${TARGET}.log`;
|
|
118
127
|
const rawLogFilePattern = CONFIG_RAW.logFile || DEFAULT_LOG_FILE;
|
|
119
|
-
const LOG_FILE = path.resolve(
|
|
120
|
-
rawLogFilePattern.replace("{target}", TARGET)
|
|
121
|
-
);
|
|
128
|
+
const LOG_FILE = path.resolve(rawLogFilePattern.replace("{target}", TARGET));
|
|
122
129
|
let LOG_STREAM = null;
|
|
123
130
|
|
|
124
131
|
/** einmalig Logfile-Stream öffnen */
|
|
@@ -175,7 +182,7 @@ function rawConsoleWarn(...msg) {
|
|
|
175
182
|
writeLogLine("[WARN] " + line);
|
|
176
183
|
}
|
|
177
184
|
|
|
178
|
-
// High-level Helfer
|
|
185
|
+
// High-level Helfer
|
|
179
186
|
function log(...msg) {
|
|
180
187
|
rawConsoleLog(...msg);
|
|
181
188
|
}
|
|
@@ -205,6 +212,7 @@ if (!TARGET_CONFIG) {
|
|
|
205
212
|
process.exit(1);
|
|
206
213
|
}
|
|
207
214
|
|
|
215
|
+
// Haupt-Sync-Config + Sidecar
|
|
208
216
|
const SYNC_CFG = TARGET_CONFIG.sync ?? TARGET_CONFIG;
|
|
209
217
|
const SIDECAR_CFG = TARGET_CONFIG.sidecar ?? {};
|
|
210
218
|
|
|
@@ -225,10 +233,8 @@ const CONNECTION = {
|
|
|
225
233
|
// Main sync roots
|
|
226
234
|
localRoot: path.resolve(SYNC_CFG.localRoot),
|
|
227
235
|
remoteRoot: SYNC_CFG.remoteRoot,
|
|
228
|
-
// Sidecar roots (
|
|
229
|
-
sidecarLocalRoot: path.resolve(
|
|
230
|
-
SIDECAR_CFG.localRoot ?? SYNC_CFG.localRoot
|
|
231
|
-
),
|
|
236
|
+
// Sidecar roots (für sidecar-upload / sidecar-download)
|
|
237
|
+
sidecarLocalRoot: path.resolve(SIDECAR_CFG.localRoot ?? SYNC_CFG.localRoot),
|
|
232
238
|
sidecarRemoteRoot: SIDECAR_CFG.remoteRoot ?? SYNC_CFG.remoteRoot,
|
|
233
239
|
workers: TARGET_CONFIG.worker ?? 2,
|
|
234
240
|
};
|
|
@@ -253,6 +259,10 @@ const SCAN_CHUNK = PROGRESS.scanChunk ?? (IS_VERBOSE ? 1 : 100);
|
|
|
253
259
|
const ANALYZE_CHUNK = PROGRESS.analyzeChunk ?? (IS_VERBOSE ? 1 : 10);
|
|
254
260
|
// For >100k files, rather 10–50, for debugging/troubleshooting 1.
|
|
255
261
|
|
|
262
|
+
// Leere Verzeichnisse nach dem Sync entfernen?
|
|
263
|
+
const CLEANUP_EMPTY_DIRS = CONFIG_RAW.cleanupEmptyDirs ?? true;
|
|
264
|
+
const CLEANUP_EMPTY_ROOTS = CONFIG_RAW.cleanupEmptyRoots ?? false;
|
|
265
|
+
|
|
256
266
|
// ---------------------------------------------------------------------------
|
|
257
267
|
// Shared config from JSON
|
|
258
268
|
// ---------------------------------------------------------------------------
|
|
@@ -311,10 +321,10 @@ const DOWNLOAD_LIST = normalizeList(SIDECAR_CFG.downloadList ?? []);
|
|
|
311
321
|
|
|
312
322
|
// Effektive Exclude-Liste: explizites exclude + Upload/Download-Listen
|
|
313
323
|
// → diese Dateien werden im „normalen“ Sync nicht angerührt,
|
|
314
|
-
// sondern nur über die
|
|
324
|
+
// sondern nur über die Sidecar-Mechanik behandelt.
|
|
315
325
|
const EXCLUDE = [...BASE_EXCLUDE, ...UPLOAD_LIST, ...DOWNLOAD_LIST];
|
|
316
326
|
|
|
317
|
-
// List of ALL files that were
|
|
327
|
+
// List of ALL files that were ausgeschlossen durch uploadList/downloadList
|
|
318
328
|
const AUTO_EXCLUDED = new Set();
|
|
319
329
|
|
|
320
330
|
// Cache file name per connection
|
|
@@ -403,7 +413,7 @@ function isIncluded(relPath) {
|
|
|
403
413
|
if (INCLUDE.length > 0 && !matchesAny(INCLUDE, relPath)) return false;
|
|
404
414
|
// Exclude-Regeln
|
|
405
415
|
if (EXCLUDE.length > 0 && matchesAny(EXCLUDE, relPath)) {
|
|
406
|
-
// Falls durch
|
|
416
|
+
// Falls durch Sidecar-Listen → merken
|
|
407
417
|
if (UPLOAD_LIST.includes(relPath) || DOWNLOAD_LIST.includes(relPath)) {
|
|
408
418
|
AUTO_EXCLUDED.add(relPath);
|
|
409
419
|
}
|
|
@@ -443,14 +453,12 @@ function shortenPathForProgress(rel) {
|
|
|
443
453
|
function updateProgress2(prefix, current, total, rel = "") {
|
|
444
454
|
const short = rel ? shortenPathForProgress(rel) : "";
|
|
445
455
|
|
|
446
|
-
//Log file: always as a single line with **full** rel path
|
|
456
|
+
// Log file: always as a single line with **full** rel path
|
|
447
457
|
const base =
|
|
448
458
|
total && total > 0
|
|
449
459
|
? `${prefix}${current}/${total} Files`
|
|
450
460
|
: `${prefix}${current} Files`;
|
|
451
|
-
writeLogLine(
|
|
452
|
-
`[progress] ${base}${rel ? " – " + rel : ""}`
|
|
453
|
-
);
|
|
461
|
+
writeLogLine(`[progress] ${base}${rel ? " – " + rel : ""}`);
|
|
454
462
|
|
|
455
463
|
if (!process.stdout.isTTY) {
|
|
456
464
|
// Fallback-Terminal
|
|
@@ -526,6 +534,124 @@ async function runTasks(items, workerCount, handler, label = "Tasks") {
|
|
|
526
534
|
await Promise.all(workers);
|
|
527
535
|
}
|
|
528
536
|
|
|
537
|
+
// ---------------------------------------------------------------------------
|
|
538
|
+
// Neue Helper: Verzeichnisse für Uploads/Updates vorbereiten
|
|
539
|
+
// ---------------------------------------------------------------------------
|
|
540
|
+
|
|
541
|
+
function collectDirsFromChanges(changes) {
|
|
542
|
+
const dirs = new Set();
|
|
543
|
+
|
|
544
|
+
for (const item of changes) {
|
|
545
|
+
const rel = item.rel;
|
|
546
|
+
if (!rel) continue;
|
|
547
|
+
|
|
548
|
+
const parts = rel.split("/");
|
|
549
|
+
if (parts.length <= 1) continue; // Dateien im Root
|
|
550
|
+
|
|
551
|
+
let acc = "";
|
|
552
|
+
for (let i = 0; i < parts.length - 1; i += 1) {
|
|
553
|
+
acc = acc ? `${acc}/${parts[i]}` : parts[i];
|
|
554
|
+
dirs.add(acc);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// flachere Pfade zuerst, damit Eltern vor Kindern angelegt werden
|
|
559
|
+
return [...dirs].sort(
|
|
560
|
+
(a, b) => a.split("/").length - b.split("/").length
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
async function ensureAllRemoteDirsExist(sftp, remoteRoot, toAdd, toUpdate) {
|
|
565
|
+
const dirs = collectDirsFromChanges([...toAdd, ...toUpdate]);
|
|
566
|
+
|
|
567
|
+
for (const relDir of dirs) {
|
|
568
|
+
const remoteDir = path.posix.join(remoteRoot, relDir);
|
|
569
|
+
try {
|
|
570
|
+
await sftp.mkdir(remoteDir, true);
|
|
571
|
+
vlog(`${tab_a()}${pc.dim("dir ok:")} ${remoteDir}`);
|
|
572
|
+
} catch {
|
|
573
|
+
// Directory may already exist / keine Rechte – ignorieren
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// -----------------------------------------------------------
|
|
579
|
+
// Cleanup: remove *only truly empty* directories on remote
|
|
580
|
+
// -----------------------------------------------------------
|
|
581
|
+
|
|
582
|
+
async function cleanupEmptyDirs(sftp, rootDir) {
|
|
583
|
+
// Rekursiv prüfen, ob ein Verzeichnis und seine Unterverzeichnisse
|
|
584
|
+
// KEINE Dateien enthalten. Nur dann löschen wir es.
|
|
585
|
+
async function recurse(dir, depth = 0) {
|
|
586
|
+
let hasFile = false;
|
|
587
|
+
const subdirs = [];
|
|
588
|
+
|
|
589
|
+
let items;
|
|
590
|
+
try {
|
|
591
|
+
items = await sftp.list(dir);
|
|
592
|
+
} catch (e) {
|
|
593
|
+
// Falls das Verzeichnis inzwischen weg ist o.ä., brechen wir hier ab.
|
|
594
|
+
wlog(
|
|
595
|
+
pc.yellow("⚠️ Could not list directory during cleanup:"),
|
|
596
|
+
dir,
|
|
597
|
+
e.message || e
|
|
598
|
+
);
|
|
599
|
+
return false;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
for (const item of items) {
|
|
603
|
+
if (!item.name || item.name === "." || item.name === "..") continue;
|
|
604
|
+
|
|
605
|
+
if (item.type === "d") {
|
|
606
|
+
subdirs.push(item);
|
|
607
|
+
} else {
|
|
608
|
+
// Jede Datei (egal ob sie nach INCLUDE/EXCLUDE
|
|
609
|
+
// sonst ignoriert würde) verhindert das Löschen.
|
|
610
|
+
hasFile = true;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Erst alle Unterverzeichnisse aufräumen (post-order)
|
|
615
|
+
let allSubdirsEmpty = true;
|
|
616
|
+
for (const sub of subdirs) {
|
|
617
|
+
const full = path.posix.join(dir, sub.name);
|
|
618
|
+
const subEmpty = await recurse(full, depth + 1);
|
|
619
|
+
if (!subEmpty) {
|
|
620
|
+
allSubdirsEmpty = false;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const isRoot = dir === rootDir;
|
|
625
|
+
const isEmpty = !hasFile && allSubdirsEmpty;
|
|
626
|
+
|
|
627
|
+
// Root nur löschen, wenn explizit erlaubt
|
|
628
|
+
if (isEmpty && (!isRoot || CLEANUP_EMPTY_ROOTS)) {
|
|
629
|
+
const rel = toPosix(path.relative(rootDir, dir)) || ".";
|
|
630
|
+
if (DRY_RUN) {
|
|
631
|
+
log(`${tab_a()}${DEL} (DRY-RUN) Remove empty directory: ${rel}`);
|
|
632
|
+
} else {
|
|
633
|
+
try {
|
|
634
|
+
// Nicht rekursiv: wir löschen nur, wenn unser eigener Check "leer" sagt.
|
|
635
|
+
await sftp.rmdir(dir, false);
|
|
636
|
+
log(`${tab_a()}${DEL} Removed empty directory: ${rel}`);
|
|
637
|
+
} catch (e) {
|
|
638
|
+
wlog(
|
|
639
|
+
pc.yellow("⚠️ Could not remove directory:"),
|
|
640
|
+
dir,
|
|
641
|
+
e.message || e
|
|
642
|
+
);
|
|
643
|
+
// Falls rmdir scheitert, betrachten wir das Verzeichnis als "nicht leer"
|
|
644
|
+
return false;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
return isEmpty;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
await recurse(rootDir, 0);
|
|
653
|
+
}
|
|
654
|
+
|
|
529
655
|
// ---------------------------------------------------------------------------
|
|
530
656
|
// Local file walker (recursive, all subdirectories)
|
|
531
657
|
// ---------------------------------------------------------------------------
|
|
@@ -616,7 +742,7 @@ async function walkRemote(sftp, remoteRoot) {
|
|
|
616
742
|
const full = path.posix.join(remoteDir, item.name);
|
|
617
743
|
const rel = prefix ? `${prefix}/${item.name}` : item.name;
|
|
618
744
|
|
|
619
|
-
//
|
|
745
|
+
// Include/Exclude-Regeln auch auf Remote anwenden
|
|
620
746
|
if (!isIncluded(rel)) continue;
|
|
621
747
|
|
|
622
748
|
if (item.type === "d") {
|
|
@@ -790,7 +916,7 @@ function describeSftpError(err) {
|
|
|
790
916
|
}
|
|
791
917
|
|
|
792
918
|
// ---------------------------------------------------------------------------
|
|
793
|
-
// Bypass-only Mode (
|
|
919
|
+
// Bypass-only Mode (sidecar-upload / sidecar-download ohne normalen Sync)
|
|
794
920
|
// ---------------------------------------------------------------------------
|
|
795
921
|
|
|
796
922
|
async function collectUploadTargets() {
|
|
@@ -832,12 +958,8 @@ async function collectDownloadTargets(sftp) {
|
|
|
832
958
|
async function performBypassOnly(sftp) {
|
|
833
959
|
log("");
|
|
834
960
|
log(pc.bold(pc.cyan("🚀 Bypass-Only Mode (skip-sync)")));
|
|
835
|
-
log(
|
|
836
|
-
|
|
837
|
-
);
|
|
838
|
-
log(
|
|
839
|
-
`${tab_a()}Sidecar Remote: ${pc.green(CONNECTION.sidecarRemoteRoot)}`
|
|
840
|
-
);
|
|
961
|
+
log(`${tab_a()}Sidecar Local: ${pc.green(CONNECTION.sidecarLocalRoot)}`);
|
|
962
|
+
log(`${tab_a()}Sidecar Remote: ${pc.green(CONNECTION.sidecarRemoteRoot)}`);
|
|
841
963
|
|
|
842
964
|
if (RUN_UPLOAD_LIST && !fs.existsSync(CONNECTION.sidecarLocalRoot)) {
|
|
843
965
|
elog(
|
|
@@ -849,7 +971,7 @@ async function performBypassOnly(sftp) {
|
|
|
849
971
|
|
|
850
972
|
if (RUN_UPLOAD_LIST) {
|
|
851
973
|
log("");
|
|
852
|
-
log(pc.bold(pc.cyan("⬆️ Upload-Bypass (
|
|
974
|
+
log(pc.bold(pc.cyan("⬆️ Upload-Bypass (sidecar-upload) …")));
|
|
853
975
|
const targets = await collectUploadTargets();
|
|
854
976
|
log(`${tab_a()}→ ${targets.length} files from uploadList`);
|
|
855
977
|
|
|
@@ -878,7 +1000,7 @@ async function performBypassOnly(sftp) {
|
|
|
878
1000
|
|
|
879
1001
|
if (RUN_DOWNLOAD_LIST) {
|
|
880
1002
|
log("");
|
|
881
|
-
log(pc.bold(pc.cyan("⬇️ Download-Bypass (
|
|
1003
|
+
log(pc.bold(pc.cyan("⬇️ Download-Bypass (sidecar-download) …")));
|
|
882
1004
|
const targets = await collectDownloadTargets(sftp);
|
|
883
1005
|
log(`${tab_a()}→ ${targets.length} files from downloadList`);
|
|
884
1006
|
|
|
@@ -936,24 +1058,23 @@ async function main() {
|
|
|
936
1058
|
log(`${tab_a()}Local: ${pc.green(CONNECTION.localRoot)}`);
|
|
937
1059
|
log(`${tab_a()}Remote: ${pc.green(CONNECTION.remoteRoot)}`);
|
|
938
1060
|
if (RUN_UPLOAD_LIST || RUN_DOWNLOAD_LIST || SKIP_SYNC) {
|
|
939
|
-
log(
|
|
940
|
-
|
|
941
|
-
);
|
|
942
|
-
log(
|
|
943
|
-
`${tab_a()}Sidecar Remote: ${pc.green(CONNECTION.sidecarRemoteRoot)}`
|
|
944
|
-
);
|
|
1061
|
+
log(`${tab_a()}Sidecar Local: ${pc.green(CONNECTION.sidecarLocalRoot)}`);
|
|
1062
|
+
log(`${tab_a()}Sidecar Remote: ${pc.green(CONNECTION.sidecarRemoteRoot)}`);
|
|
945
1063
|
}
|
|
946
1064
|
if (DRY_RUN) log(pc.yellow(`${tab_a()}Mode: DRY-RUN (no changes)`));
|
|
947
1065
|
if (SKIP_SYNC) log(pc.yellow(`${tab_a()}Mode: SKIP-SYNC (bypass only)`));
|
|
948
1066
|
if (RUN_UPLOAD_LIST || RUN_DOWNLOAD_LIST) {
|
|
949
1067
|
log(
|
|
950
1068
|
pc.blue(
|
|
951
|
-
`${tab_a()}Extra: ${
|
|
952
|
-
|
|
953
|
-
}`
|
|
1069
|
+
`${tab_a()}Extra: ${
|
|
1070
|
+
RUN_UPLOAD_LIST ? "sidecar-upload " : ""
|
|
1071
|
+
}${RUN_DOWNLOAD_LIST ? "sidecar-download" : ""}`
|
|
954
1072
|
)
|
|
955
1073
|
);
|
|
956
1074
|
}
|
|
1075
|
+
if (CLEANUP_EMPTY_DIRS) {
|
|
1076
|
+
log(`${tab_a()}Cleanup empty dirs: ${pc.green("enabled")}`);
|
|
1077
|
+
}
|
|
957
1078
|
if (LOG_FILE) {
|
|
958
1079
|
log(`${tab_a()}LogFile: ${pc.cyan(LOG_FILE)}`);
|
|
959
1080
|
}
|
|
@@ -987,7 +1108,7 @@ async function main() {
|
|
|
987
1108
|
}
|
|
988
1109
|
|
|
989
1110
|
// -------------------------------------------------------------
|
|
990
|
-
// SKIP-SYNC-Modus → nur
|
|
1111
|
+
// SKIP-SYNC-Modus → nur Sidecar-Listen
|
|
991
1112
|
// -------------------------------------------------------------
|
|
992
1113
|
if (SKIP_SYNC) {
|
|
993
1114
|
await performBypassOnly(sftp);
|
|
@@ -999,7 +1120,7 @@ async function main() {
|
|
|
999
1120
|
}
|
|
1000
1121
|
|
|
1001
1122
|
// -------------------------------------------------------------
|
|
1002
|
-
// Normaler Sync (inkl. evtl. paralleler
|
|
1123
|
+
// Normaler Sync (inkl. evtl. paralleler Sidecar-Excludes)
|
|
1003
1124
|
// -------------------------------------------------------------
|
|
1004
1125
|
|
|
1005
1126
|
// Phase 1 – mit exakt einer Leerzeile davor
|
|
@@ -1010,7 +1131,7 @@ async function main() {
|
|
|
1010
1131
|
|
|
1011
1132
|
if (AUTO_EXCLUDED.size > 0) {
|
|
1012
1133
|
log("");
|
|
1013
|
-
log(pc.dim(" Auto-excluded (
|
|
1134
|
+
log(pc.dim(" Auto-excluded (sidecar upload/download):"));
|
|
1014
1135
|
[...AUTO_EXCLUDED].sort().forEach((file) => {
|
|
1015
1136
|
log(pc.dim(`${tab_a()} - ${file}`));
|
|
1016
1137
|
});
|
|
@@ -1146,6 +1267,20 @@ async function main() {
|
|
|
1146
1267
|
log(`${tab_a()}No orphaned remote files found.`);
|
|
1147
1268
|
}
|
|
1148
1269
|
|
|
1270
|
+
// -------------------------------------------------------------------
|
|
1271
|
+
// Verzeichnisse vorab anlegen (damit Worker sich nicht ins Gehege kommen)
|
|
1272
|
+
// -------------------------------------------------------------------
|
|
1273
|
+
if (!DRY_RUN && (toAdd.length || toUpdate.length)) {
|
|
1274
|
+
log("");
|
|
1275
|
+
log(pc.bold(pc.cyan("📁 Preparing remote directories …")));
|
|
1276
|
+
await ensureAllRemoteDirsExist(
|
|
1277
|
+
sftp,
|
|
1278
|
+
CONNECTION.remoteRoot,
|
|
1279
|
+
toAdd,
|
|
1280
|
+
toUpdate
|
|
1281
|
+
);
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1149
1284
|
// -------------------------------------------------------------------
|
|
1150
1285
|
// Phase 5: Execute changes (parallel, worker-based)
|
|
1151
1286
|
// -------------------------------------------------------------------
|
|
@@ -1159,6 +1294,7 @@ async function main() {
|
|
|
1159
1294
|
toAdd,
|
|
1160
1295
|
CONNECTION.workers,
|
|
1161
1296
|
async ({ local: l, remotePath }) => {
|
|
1297
|
+
// Verzeichnisse sollten bereits existieren – mkdir hier nur als Fallback
|
|
1162
1298
|
const remoteDir = path.posix.dirname(remotePath);
|
|
1163
1299
|
try {
|
|
1164
1300
|
await sftp.mkdir(remoteDir, true);
|
|
@@ -1212,6 +1348,13 @@ async function main() {
|
|
|
1212
1348
|
);
|
|
1213
1349
|
}
|
|
1214
1350
|
|
|
1351
|
+
// Optional: leere Verzeichnisse aufräumen
|
|
1352
|
+
if (!DRY_RUN && CLEANUP_EMPTY_DIRS) {
|
|
1353
|
+
log("");
|
|
1354
|
+
log(pc.bold(pc.cyan("🧹 Cleaning up empty remote directories …")));
|
|
1355
|
+
await cleanupEmptyDirs(sftp, CONNECTION.remoteRoot);
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1215
1358
|
const duration = ((Date.now() - start) / 1000).toFixed(2);
|
|
1216
1359
|
|
|
1217
1360
|
// Write cache safely at the end
|
|
@@ -1227,7 +1370,7 @@ async function main() {
|
|
|
1227
1370
|
log(`${tab_a()}${DEL} Deleted: ${toDelete.length}`);
|
|
1228
1371
|
if (AUTO_EXCLUDED.size > 0) {
|
|
1229
1372
|
log(
|
|
1230
|
-
`${tab_a()}${EXC} Excluded via
|
|
1373
|
+
`${tab_a()}${EXC} Excluded via sidecar upload/download: ${
|
|
1231
1374
|
AUTO_EXCLUDED.size
|
|
1232
1375
|
}`
|
|
1233
1376
|
);
|
|
@@ -1290,4 +1433,4 @@ async function main() {
|
|
|
1290
1433
|
}
|
|
1291
1434
|
}
|
|
1292
1435
|
|
|
1293
|
-
main();
|
|
1436
|
+
main();
|