sftp-push-sync 2.1.2 → 2.1.4
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 +15 -2
- package/bin/sftp-push-sync.mjs +126 -1363
- package/directory-structure.txt +14 -0
- package/images/example-output-002.jpg +0 -0
- package/package.json +2 -2
- package/src/core/ScanProgressController.mjs +124 -0
- package/src/core/SftpPushSyncApp.mjs +1060 -0
- package/src/core/SyncLogger.mjs +52 -0
- package/src/helpers/compare.mjs +122 -0
- package/src/helpers/directory.mjs +29 -0
- package/src/helpers/hashing.mjs +201 -0
- package/src/helpers/progress-constants.mjs +28 -0
- package/src/helpers/sidecar.mjs +185 -0
- package/src/helpers/walkers.mjs +218 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SyncLogger.mjs
|
|
3
|
+
*
|
|
4
|
+
* @author Carsten Nichte, 2025 / https://carsten-nichte.de/
|
|
5
|
+
*
|
|
6
|
+
*/
|
|
7
|
+
// src/core/SyncLogger.mjs
|
|
8
|
+
import fs from "fs";
|
|
9
|
+
import fsp from "fs/promises";
|
|
10
|
+
import path from "path";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Very small logger: schreibt alles in eine Logdatei
|
|
14
|
+
* und entfernt ANSI-Farbcodes.
|
|
15
|
+
*/
|
|
16
|
+
export class SyncLogger {
|
|
17
|
+
constructor(filePath) {
|
|
18
|
+
this.filePath = filePath;
|
|
19
|
+
this.stream = null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async init() {
|
|
23
|
+
if (!this.filePath || this.stream) return;
|
|
24
|
+
|
|
25
|
+
const dir = path.dirname(this.filePath);
|
|
26
|
+
await fsp.mkdir(dir, { recursive: true });
|
|
27
|
+
|
|
28
|
+
this.stream = fs.createWriteStream(this.filePath, {
|
|
29
|
+
flags: "w",
|
|
30
|
+
encoding: "utf8",
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
writeLine(line) {
|
|
35
|
+
if (!this.stream) return;
|
|
36
|
+
const text = typeof line === "string" ? line : String(line);
|
|
37
|
+
const clean = text.replace(/\x1b\[[0-9;]*m/g, "");
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
this.stream.write(clean + "\n");
|
|
41
|
+
} catch {
|
|
42
|
+
// Stream schon zu → ignorieren
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
close() {
|
|
47
|
+
if (this.stream) {
|
|
48
|
+
this.stream.end();
|
|
49
|
+
this.stream = null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* compare.mjs
|
|
3
|
+
*
|
|
4
|
+
* @author Carsten Nichte, 2025 / https://carsten-nichte.de/
|
|
5
|
+
*
|
|
6
|
+
*/
|
|
7
|
+
// src/helpers/compare.mjs
|
|
8
|
+
import fsp from "fs/promises";
|
|
9
|
+
import path from "path";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Analysiert Unterschiede zwischen local- und remote-Maps.
|
|
13
|
+
*
|
|
14
|
+
* Erwartete Struktur:
|
|
15
|
+
* local: Map<rel, { rel, localPath, size, mtimeMs, isText? }>
|
|
16
|
+
* remote: Map<rel, { rel, remotePath, size, modifyTime }>
|
|
17
|
+
*
|
|
18
|
+
* Optionen:
|
|
19
|
+
* - remoteRoot: Basis-Pfad auf dem Server
|
|
20
|
+
* - sftp: ssh2-sftp-client Instanz
|
|
21
|
+
* - getLocalHash / getRemoteHash: from createHashCache
|
|
22
|
+
* - analyzeChunk: Progress-Schrittgröße
|
|
23
|
+
* - updateProgress(prefix, current, total, rel): optional
|
|
24
|
+
*/
|
|
25
|
+
export async function analyseDifferences({
|
|
26
|
+
local,
|
|
27
|
+
remote,
|
|
28
|
+
remoteRoot,
|
|
29
|
+
sftp,
|
|
30
|
+
getLocalHash,
|
|
31
|
+
getRemoteHash,
|
|
32
|
+
analyzeChunk = 10,
|
|
33
|
+
updateProgress,
|
|
34
|
+
}) {
|
|
35
|
+
const toAdd = [];
|
|
36
|
+
const toUpdate = [];
|
|
37
|
+
|
|
38
|
+
const localKeys = new Set(local.keys());
|
|
39
|
+
const totalToCheck = localKeys.size;
|
|
40
|
+
let checked = 0;
|
|
41
|
+
|
|
42
|
+
for (const rel of localKeys) {
|
|
43
|
+
checked += 1;
|
|
44
|
+
|
|
45
|
+
if (
|
|
46
|
+
updateProgress &&
|
|
47
|
+
(checked === 1 || checked % analyzeChunk === 0 || checked === totalToCheck)
|
|
48
|
+
) {
|
|
49
|
+
updateProgress("Analyse: ", checked, totalToCheck, rel);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const l = local.get(rel);
|
|
53
|
+
const r = remote.get(rel);
|
|
54
|
+
const remotePath = path.posix.join(remoteRoot, rel);
|
|
55
|
+
|
|
56
|
+
// Datei existiert nur lokal → New
|
|
57
|
+
if (!r) {
|
|
58
|
+
toAdd.push({ rel, local: l, remotePath });
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 1. Size-Vergleich
|
|
63
|
+
if (l.size !== r.size) {
|
|
64
|
+
toUpdate.push({ rel, local: l, remote: r, remotePath });
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 2. Content-Vergleich
|
|
69
|
+
if (l.isText) {
|
|
70
|
+
// Text-Datei: vollständiger inhaltlicher Vergleich
|
|
71
|
+
const [localBuf, remoteBuf] = await Promise.all([
|
|
72
|
+
fsp.readFile(l.localPath),
|
|
73
|
+
sftp.get(r.remotePath),
|
|
74
|
+
]);
|
|
75
|
+
|
|
76
|
+
const localStr = localBuf.toString("utf8");
|
|
77
|
+
const remoteStr = (
|
|
78
|
+
Buffer.isBuffer(remoteBuf) ? remoteBuf : Buffer.from(remoteBuf)
|
|
79
|
+
).toString("utf8");
|
|
80
|
+
|
|
81
|
+
if (localStr !== remoteStr) {
|
|
82
|
+
toUpdate.push({ rel, local: l, remote: r, remotePath });
|
|
83
|
+
}
|
|
84
|
+
} else {
|
|
85
|
+
// Binary: Hash-Vergleich mit Cache
|
|
86
|
+
if (!getLocalHash || !getRemoteHash) {
|
|
87
|
+
// Fallback: wenn kein Hash-Cache übergeben wurde, treat as changed
|
|
88
|
+
toUpdate.push({ rel, local: l, remote: r, remotePath });
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const [localHash, remoteHash] = await Promise.all([
|
|
93
|
+
getLocalHash(rel, l),
|
|
94
|
+
getRemoteHash(rel, r, sftp),
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
if (localHash !== remoteHash) {
|
|
98
|
+
toUpdate.push({ rel, local: l, remote: r, remotePath });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return { toAdd, toUpdate };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Ermittelt zu löschende Dateien (remote-only).
|
|
108
|
+
*
|
|
109
|
+
* remote: Map<rel, { rel, remotePath }>
|
|
110
|
+
*/
|
|
111
|
+
export function computeRemoteDeletes({ local, remote }) {
|
|
112
|
+
const toDelete = [];
|
|
113
|
+
const localKeys = new Set(local.keys());
|
|
114
|
+
|
|
115
|
+
for (const [rel, r] of remote.entries()) {
|
|
116
|
+
if (!localKeys.has(rel)) {
|
|
117
|
+
toDelete.push({ rel, remotePath: r.remotePath });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return toDelete;
|
|
122
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* directory.mjs
|
|
3
|
+
*
|
|
4
|
+
* @author Carsten Nichte, 2025 / https://carsten-nichte.de/
|
|
5
|
+
*
|
|
6
|
+
*/
|
|
7
|
+
// src/helpers/directory.mjs
|
|
8
|
+
import path from "path";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Konvertiert einen Pfad in POSIX-Notation (immer /)
|
|
12
|
+
*/
|
|
13
|
+
export function toPosix(p) {
|
|
14
|
+
return p.split(path.sep).join("/");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Kürzt einen Pfad für die Progressanzeige:
|
|
19
|
+
* …/parent/file.ext
|
|
20
|
+
*/
|
|
21
|
+
export function shortenPathForProgress(rel) {
|
|
22
|
+
if (!rel) return "";
|
|
23
|
+
const parts = rel.split("/");
|
|
24
|
+
if (parts.length <= 2) return rel;
|
|
25
|
+
|
|
26
|
+
const last = parts[parts.length - 1];
|
|
27
|
+
const prev = parts[parts.length - 2];
|
|
28
|
+
return `…/${prev}/${last}`;
|
|
29
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hashing.mjs
|
|
3
|
+
*
|
|
4
|
+
* @author Carsten Nichte, 2025 / https://carsten-nichte.de/
|
|
5
|
+
*
|
|
6
|
+
*/
|
|
7
|
+
// src/helpers/hashing.mjs
|
|
8
|
+
import fs from "fs";
|
|
9
|
+
import fsp from "fs/promises";
|
|
10
|
+
import { createHash } from "crypto";
|
|
11
|
+
import { Writable } from "stream";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Streaming-SHA256 für lokale Datei
|
|
15
|
+
*/
|
|
16
|
+
export function hashLocalFile(filePath) {
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
const hash = createHash("sha256");
|
|
19
|
+
const stream = fs.createReadStream(filePath);
|
|
20
|
+
stream.on("error", reject);
|
|
21
|
+
stream.on("data", (chunk) => hash.update(chunk));
|
|
22
|
+
stream.on("end", () => resolve(hash.digest("hex")));
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Streaming-SHA256 für Remote-Datei via ssh2-sftp-client
|
|
28
|
+
*/
|
|
29
|
+
export async function hashRemoteFile(sftp, remotePath) {
|
|
30
|
+
const hash = createHash("sha256");
|
|
31
|
+
|
|
32
|
+
const writable = new Writable({
|
|
33
|
+
write(chunk, enc, cb) {
|
|
34
|
+
hash.update(chunk);
|
|
35
|
+
cb();
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
await sftp.get(remotePath, writable);
|
|
40
|
+
return hash.digest("hex");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Kleiner Helper, der Hash-Cache + Persistenz kapselt.
|
|
45
|
+
*
|
|
46
|
+
* Erwartetes Cache-Format:
|
|
47
|
+
* {
|
|
48
|
+
* version: 1,
|
|
49
|
+
* local: { "<ns>:<rel>": { size, mtimeMs, hash } },
|
|
50
|
+
* remote: { "<ns>:<rel>": { size, modifyTime, hash } }
|
|
51
|
+
* }
|
|
52
|
+
*/
|
|
53
|
+
export function createHashCache({
|
|
54
|
+
cachePath,
|
|
55
|
+
namespace,
|
|
56
|
+
flushInterval = 50,
|
|
57
|
+
}) {
|
|
58
|
+
const ns = namespace || "default";
|
|
59
|
+
|
|
60
|
+
let cache = {
|
|
61
|
+
version: 1,
|
|
62
|
+
local: {},
|
|
63
|
+
remote: {},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// Versuch: bestehenden Cache laden
|
|
67
|
+
(async () => {
|
|
68
|
+
try {
|
|
69
|
+
const raw = await fsp.readFile(cachePath, "utf8");
|
|
70
|
+
const parsed = JSON.parse(raw);
|
|
71
|
+
cache.version = parsed.version ?? 1;
|
|
72
|
+
cache.local = parsed.local ?? {};
|
|
73
|
+
cache.remote = parsed.remote ?? {};
|
|
74
|
+
} catch {
|
|
75
|
+
// kein Cache oder defekt → einfach neu anfangen
|
|
76
|
+
}
|
|
77
|
+
})().catch(() => {});
|
|
78
|
+
|
|
79
|
+
let dirty = false;
|
|
80
|
+
let dirtyCount = 0;
|
|
81
|
+
|
|
82
|
+
function cacheKey(relPath) {
|
|
83
|
+
return `${ns}:${relPath}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function save(force = false) {
|
|
87
|
+
if (!dirty && !force) return;
|
|
88
|
+
const data = JSON.stringify(cache, null, 2);
|
|
89
|
+
await fsp.writeFile(cachePath, data, "utf8");
|
|
90
|
+
dirty = false;
|
|
91
|
+
dirtyCount = 0;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function markDirty() {
|
|
95
|
+
dirty = true;
|
|
96
|
+
dirtyCount += 1;
|
|
97
|
+
if (dirtyCount >= flushInterval) {
|
|
98
|
+
await save();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function getLocalHash(rel, meta) {
|
|
103
|
+
const key = cacheKey(rel);
|
|
104
|
+
const cached = cache.local[key];
|
|
105
|
+
|
|
106
|
+
if (
|
|
107
|
+
cached &&
|
|
108
|
+
cached.size === meta.size &&
|
|
109
|
+
cached.mtimeMs === meta.mtimeMs &&
|
|
110
|
+
cached.hash
|
|
111
|
+
) {
|
|
112
|
+
return cached.hash;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const hash = await hashLocalFile(meta.localPath);
|
|
116
|
+
cache.local[key] = {
|
|
117
|
+
size: meta.size,
|
|
118
|
+
mtimeMs: meta.mtimeMs,
|
|
119
|
+
hash,
|
|
120
|
+
};
|
|
121
|
+
await markDirty();
|
|
122
|
+
return hash;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function getRemoteHash(rel, meta, sftp) {
|
|
126
|
+
const key = cacheKey(rel);
|
|
127
|
+
const cached = cache.remote[key];
|
|
128
|
+
|
|
129
|
+
if (
|
|
130
|
+
cached &&
|
|
131
|
+
cached.size === meta.size &&
|
|
132
|
+
cached.modifyTime === meta.modifyTime &&
|
|
133
|
+
cached.hash
|
|
134
|
+
) {
|
|
135
|
+
return cached.hash;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const hash = await hashRemoteFile(sftp, meta.remotePath);
|
|
139
|
+
cache.remote[key] = {
|
|
140
|
+
size: meta.size,
|
|
141
|
+
modifyTime: meta.modifyTime,
|
|
142
|
+
hash,
|
|
143
|
+
};
|
|
144
|
+
await markDirty();
|
|
145
|
+
return hash;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
cache,
|
|
150
|
+
cacheKey,
|
|
151
|
+
getLocalHash,
|
|
152
|
+
getRemoteHash,
|
|
153
|
+
save,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export async function getLocalHash(rel, meta, cacheLocal, key, markDirty) {
|
|
158
|
+
const cached = cacheLocal[key];
|
|
159
|
+
if (
|
|
160
|
+
cached &&
|
|
161
|
+
cached.size === meta.size &&
|
|
162
|
+
cached.mtimeMs === meta.mtimeMs &&
|
|
163
|
+
cached.hash
|
|
164
|
+
) {
|
|
165
|
+
return cached.hash;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const hash = await hashLocalFile(meta.localPath);
|
|
169
|
+
cacheLocal[key] = {
|
|
170
|
+
size: meta.size,
|
|
171
|
+
mtimeMs: meta.mtimeMs,
|
|
172
|
+
hash,
|
|
173
|
+
};
|
|
174
|
+
if (markDirty) {
|
|
175
|
+
await markDirty();
|
|
176
|
+
}
|
|
177
|
+
return hash;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export async function getRemoteHash(rel, meta, cacheRemote, key, markDirty, sftp) {
|
|
181
|
+
const cached = cacheRemote[key];
|
|
182
|
+
if (
|
|
183
|
+
cached &&
|
|
184
|
+
cached.size === meta.size &&
|
|
185
|
+
cached.modifyTime === meta.modifyTime &&
|
|
186
|
+
cached.hash
|
|
187
|
+
) {
|
|
188
|
+
return cached.hash;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const hash = await hashRemoteFile(sftp, meta.remotePath);
|
|
192
|
+
cacheRemote[key] = {
|
|
193
|
+
size: meta.size,
|
|
194
|
+
modifyTime: meta.modifyTime,
|
|
195
|
+
hash,
|
|
196
|
+
};
|
|
197
|
+
if (markDirty) {
|
|
198
|
+
await markDirty();
|
|
199
|
+
}
|
|
200
|
+
return hash;
|
|
201
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* progress-constants.mjs
|
|
3
|
+
* Central constants for progress and output formatting.
|
|
4
|
+
*
|
|
5
|
+
* @author Carsten Nichte, 2025 / https://carsten-nichte.de/
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
// src/helpers/progress-constants.mjs
|
|
9
|
+
export const hr1 = () => "─".repeat(65); // horizontal line -
|
|
10
|
+
export const hr2 = () => "=".repeat(65); // horizontal line =
|
|
11
|
+
|
|
12
|
+
// Einrückungen (Tabs) für konsistente Ausgabe
|
|
13
|
+
export const TAB_A = " "; // 3 Spaces
|
|
14
|
+
export const TAB_B = " "; // 6 Spaces
|
|
15
|
+
|
|
16
|
+
// Spinner-Frames für Progress-Anzeigen
|
|
17
|
+
export const SPINNER_FRAMES = [
|
|
18
|
+
"⠋",
|
|
19
|
+
"⠙",
|
|
20
|
+
"⠹",
|
|
21
|
+
"⠸",
|
|
22
|
+
"⠼",
|
|
23
|
+
"⠴",
|
|
24
|
+
"⠦",
|
|
25
|
+
"⠧",
|
|
26
|
+
"⠇",
|
|
27
|
+
"⠏",
|
|
28
|
+
];
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sidecar.mjs
|
|
3
|
+
*
|
|
4
|
+
* @author Carsten Nichte, 2025 / https://carsten-nichte.de/
|
|
5
|
+
*
|
|
6
|
+
*/
|
|
7
|
+
// src/helpers/sidecar.mjs
|
|
8
|
+
import fs from "fs";
|
|
9
|
+
import fsp from "fs/promises";
|
|
10
|
+
import path from "path";
|
|
11
|
+
import { walkLocalPlain, walkRemotePlain } from "./walkers.mjs";
|
|
12
|
+
|
|
13
|
+
/** minimales Pattern-Matching à la minimatch (einfach: exakte Strings oder simple "*" am Ende) */
|
|
14
|
+
function matchesAny(patterns, relPath) {
|
|
15
|
+
if (!patterns || patterns.length === 0) return false;
|
|
16
|
+
return patterns.some((pattern) => {
|
|
17
|
+
if (!pattern) return false;
|
|
18
|
+
if (pattern === relPath) return true;
|
|
19
|
+
// primitive *-Unterstützung, falls du willst → sonst weglassen
|
|
20
|
+
if (pattern.endsWith("*")) {
|
|
21
|
+
const prefix = pattern.slice(0, -1);
|
|
22
|
+
return relPath.startsWith(prefix);
|
|
23
|
+
}
|
|
24
|
+
return false;
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Upload-Targets für Sidecar sammeln
|
|
30
|
+
*/
|
|
31
|
+
export async function collectUploadTargets({
|
|
32
|
+
sidecarLocalRoot,
|
|
33
|
+
sidecarRemoteRoot,
|
|
34
|
+
uploadList,
|
|
35
|
+
}) {
|
|
36
|
+
const all = await walkLocalPlain(sidecarLocalRoot);
|
|
37
|
+
const results = [];
|
|
38
|
+
|
|
39
|
+
for (const [rel, meta] of all.entries()) {
|
|
40
|
+
if (matchesAny(uploadList, rel)) {
|
|
41
|
+
const remotePath = path.posix.join(sidecarRemoteRoot, rel);
|
|
42
|
+
results.push({
|
|
43
|
+
rel,
|
|
44
|
+
localPath: meta.localPath,
|
|
45
|
+
remotePath,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return results;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Download-Targets für Sidecar sammeln
|
|
55
|
+
*/
|
|
56
|
+
export async function collectDownloadTargets({
|
|
57
|
+
sftp,
|
|
58
|
+
sidecarLocalRoot,
|
|
59
|
+
sidecarRemoteRoot,
|
|
60
|
+
downloadList,
|
|
61
|
+
}) {
|
|
62
|
+
const all = await walkRemotePlain(sftp, sidecarRemoteRoot);
|
|
63
|
+
const results = [];
|
|
64
|
+
|
|
65
|
+
for (const [rel, meta] of all.entries()) {
|
|
66
|
+
if (matchesAny(downloadList, rel)) {
|
|
67
|
+
const localPath = path.join(sidecarLocalRoot, rel);
|
|
68
|
+
results.push({
|
|
69
|
+
rel,
|
|
70
|
+
remotePath: meta.remotePath,
|
|
71
|
+
localPath,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return results;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Führt den Bypass-Modus aus (sidecar-upload / sidecar-download)
|
|
81
|
+
*
|
|
82
|
+
* Erwartete Parameter:
|
|
83
|
+
* - sftp: ssh2-sftp-client
|
|
84
|
+
* - connection: { sidecarLocalRoot, sidecarRemoteRoot, workers }
|
|
85
|
+
* - uploadList, downloadList: String-Arrays
|
|
86
|
+
* - options: { dryRun, runUploadList, runDownloadList }
|
|
87
|
+
* - runTasks: Workerpool-Funktion (items, workerCount, handler, label)
|
|
88
|
+
* - log, vlog, elog: Logging-Funktionen
|
|
89
|
+
* - symbols: { ADD, CHA, tab_a } → damit du deine bestehenden Symbole weiter nutzen kannst
|
|
90
|
+
*/
|
|
91
|
+
export async function performBypassOnly({
|
|
92
|
+
sftp,
|
|
93
|
+
connection,
|
|
94
|
+
uploadList,
|
|
95
|
+
downloadList,
|
|
96
|
+
options,
|
|
97
|
+
runTasks,
|
|
98
|
+
log,
|
|
99
|
+
vlog,
|
|
100
|
+
elog,
|
|
101
|
+
symbols,
|
|
102
|
+
}) {
|
|
103
|
+
const { dryRun, runUploadList, runDownloadList } = options;
|
|
104
|
+
const { sidecarLocalRoot, sidecarRemoteRoot, workers } = connection;
|
|
105
|
+
const { ADD, CHA, tab_a } = symbols;
|
|
106
|
+
|
|
107
|
+
log("");
|
|
108
|
+
log("🚀 Bypass-Only Mode (skip-sync)");
|
|
109
|
+
log(`${tab_a}Sidecar Local: ${sidecarLocalRoot}`);
|
|
110
|
+
log(`${tab_a}Sidecar Remote: ${sidecarRemoteRoot}`);
|
|
111
|
+
|
|
112
|
+
if (runUploadList && !fs.existsSync(sidecarLocalRoot)) {
|
|
113
|
+
const msg = `Sidecar local root does not exist: ${sidecarLocalRoot}`;
|
|
114
|
+
elog(`❌ ${msg}`);
|
|
115
|
+
throw new Error(msg);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Upload-Bereich
|
|
119
|
+
if (runUploadList) {
|
|
120
|
+
log("");
|
|
121
|
+
log("⬆️ Upload-Bypass (sidecar-upload) …");
|
|
122
|
+
const targets = await collectUploadTargets({
|
|
123
|
+
sidecarLocalRoot,
|
|
124
|
+
sidecarRemoteRoot,
|
|
125
|
+
uploadList,
|
|
126
|
+
});
|
|
127
|
+
log(`${tab_a}→ ${targets.length} files from uploadList`);
|
|
128
|
+
|
|
129
|
+
if (!dryRun) {
|
|
130
|
+
await runTasks(
|
|
131
|
+
targets,
|
|
132
|
+
workers,
|
|
133
|
+
async ({ localPath, remotePath, rel }) => {
|
|
134
|
+
const remoteDir = path.posix.dirname(remotePath);
|
|
135
|
+
try {
|
|
136
|
+
await sftp.mkdir(remoteDir, true);
|
|
137
|
+
} catch {
|
|
138
|
+
// Directory may already exist
|
|
139
|
+
}
|
|
140
|
+
await sftp.put(localPath, remotePath);
|
|
141
|
+
vlog && vlog(`${tab_a}${ADD} Uploaded (bypass): ${rel}`);
|
|
142
|
+
},
|
|
143
|
+
"Bypass Uploads"
|
|
144
|
+
);
|
|
145
|
+
} else {
|
|
146
|
+
for (const t of targets) {
|
|
147
|
+
log(`${tab_a}${ADD} (DRY-RUN) Upload: ${t.rel}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Download-Bereich
|
|
153
|
+
if (runDownloadList) {
|
|
154
|
+
log("");
|
|
155
|
+
log("⬇️ Download-Bypass (sidecar-download) …");
|
|
156
|
+
const targets = await collectDownloadTargets({
|
|
157
|
+
sftp,
|
|
158
|
+
sidecarLocalRoot,
|
|
159
|
+
sidecarRemoteRoot,
|
|
160
|
+
downloadList,
|
|
161
|
+
});
|
|
162
|
+
log(`${tab_a}→ ${targets.length} files from downloadList`);
|
|
163
|
+
|
|
164
|
+
if (!dryRun) {
|
|
165
|
+
await runTasks(
|
|
166
|
+
targets,
|
|
167
|
+
workers,
|
|
168
|
+
async ({ remotePath, localPath, rel }) => {
|
|
169
|
+
const localDir = path.dirname(localPath);
|
|
170
|
+
await fsp.mkdir(localDir, { recursive: true });
|
|
171
|
+
await sftp.get(remotePath, localPath);
|
|
172
|
+
vlog && vlog(`${tab_a}${CHA} Downloaded (bypass): ${rel}`);
|
|
173
|
+
},
|
|
174
|
+
"Bypass Downloads"
|
|
175
|
+
);
|
|
176
|
+
} else {
|
|
177
|
+
for (const t of targets) {
|
|
178
|
+
log(`${tab_a}${CHA} (DRY-RUN) Download: ${t.rel}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
log("");
|
|
184
|
+
log("✅ Bypass-only run finished.");
|
|
185
|
+
}
|