creepercli 1.0.0 → 1.1.0

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/src/session.js CHANGED
@@ -1,58 +1,40 @@
1
- /*
2
- * Copyright 2026 DemonZDevelopment
3
- *
4
- * Licensed under the Apache License, Version 2.0 (the "License");
5
- * you may not use this file except in compliance with the License.
6
- * You may obtain a copy of the License at
7
- *
8
- * http://www.apache.org/licenses/LICENSE-2.0
9
- *
10
- * Unless required by applicable law or agreed to in writing, software
11
- * distributed under the License is distributed on an "AS IS" BASIS,
12
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
- * See the License for the specific language governing permissions and
14
- * limitations under the License.
15
- */
16
-
17
- 'use strict';
18
-
19
- const fs = require('fs');
20
- const os = require('os');
21
- const path = require('path');
22
-
23
- const DIR = path.join(os.homedir(), '.creepercli');
24
- const CREDS_FILE = path.join(DIR, 'creds');
25
- const HISTORY_FILE = path.join(DIR, 'history');
26
-
27
- function ensureDir() {
28
- fs.mkdirSync(DIR, { recursive: true, mode: 0o700 });
29
- }
30
-
31
- function save(creds) {
32
- ensureDir();
33
- fs.writeFileSync(CREDS_FILE, JSON.stringify(creds, null, 2), { mode: 0o600 });
34
- }
35
-
36
- function load(host, port) {
37
- try {
38
- const raw = fs.readFileSync(CREDS_FILE, 'utf8');
39
- const creds = JSON.parse(raw);
40
- if (creds.host !== host || creds.port !== port) return null;
41
- if (creds.expiresAt && Date.now() > creds.expiresAt) {
42
- clear();
43
- return null;
44
- }
1
+
2
+
3
+ 'use strict';
4
+
5
+ const fs = require('fs');
6
+ const os = require('os');
7
+ const path = require('path');
8
+
9
+ const DIR = path.join(os.homedir(), '.creepercli');
10
+ const CREDS_FILE = path.join(DIR, 'creds');
11
+ const HISTORY_FILE = path.join(DIR, 'history');
12
+
13
+ function ensureDir() {
14
+ fs.mkdirSync(DIR, { recursive: true, mode: 0o700 });
15
+ }
16
+
17
+ function save(creds) {
18
+ ensureDir();
19
+ fs.writeFileSync(CREDS_FILE, JSON.stringify(creds, null, 2), { mode: 0o600 });
20
+ }
21
+
22
+ function load(host, port) {
23
+ try {
24
+ const raw = fs.readFileSync(CREDS_FILE, 'utf8');
25
+ const creds = JSON.parse(raw);
26
+ if (creds.host !== host || creds.port !== port) return null;
45
27
  return creds;
46
- } catch {
47
- return null;
48
- }
49
- }
50
-
51
- function clear() {
52
- try {
53
- fs.unlinkSync(CREDS_FILE);
54
- } catch {
55
- }
56
- }
57
-
58
- module.exports = { DIR, CREDS_FILE, HISTORY_FILE, save, load, clear };
28
+ } catch {
29
+ return null;
30
+ }
31
+ }
32
+
33
+ function clear() {
34
+ try {
35
+ fs.unlinkSync(CREDS_FILE);
36
+ } catch {
37
+ }
38
+ }
39
+
40
+ module.exports = { DIR, CREDS_FILE, HISTORY_FILE, save, load, clear };
package/src/top.js CHANGED
@@ -1,53 +1,39 @@
1
- /*
2
- * Copyright 2026 DemonZDevelopment
3
- *
4
- * Licensed under the Apache License, Version 2.0 (the "License");
5
- * you may not use this file except in compliance with the License.
6
- * You may obtain a copy of the License at
7
- *
8
- * http://www.apache.org/licenses/LICENSE-2.0
9
- *
10
- * Unless required by applicable law or agreed to in writing, software
11
- * distributed under the License is distributed on an "AS IS" BASIS,
12
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
- * See the License for the specific language governing permissions and
14
- * limitations under the License.
15
- */
16
-
17
- 'use strict';
18
-
19
- const readline = require('readline');
20
- const { formatStats, formatTps } = require('./formatters');
21
-
22
- function sleep(ms) {
23
- return new Promise((r) => setTimeout(r, ms));
24
- }
25
-
26
- async function runTop(client, refreshMs) {
27
- if (process.stdin.isTTY) {
28
- readline.emitKeypressEvents(process.stdin);
29
- process.stdin.setRawMode(true);
30
- }
31
- let stopped = false;
32
- const onKey = (str, key) => {
33
- if (key.name === 'q' || key.name === 'escape' || (key.ctrl && key.name === 'c')) stopped = true;
34
- };
35
- process.stdin.on('keypress', onKey);
36
- try {
37
- while (!stopped) {
38
- const res = await client.request('monitor.top', {});
39
- readline.cursorTo(process.stdout, 0, 0);
40
- readline.clearScreenDown(process.stdout);
41
- console.log('CreeperCLI top (q to quit)\n');
42
- console.log(formatStats(res.stats));
43
- console.log();
44
- console.log(formatTps(res.tps));
45
- await sleep(refreshMs);
46
- }
47
- } finally {
48
- process.stdin.removeListener('keypress', onKey);
49
- if (process.stdin.isTTY) process.stdin.setRawMode(false);
50
- }
51
- }
52
-
53
- module.exports = { runTop };
1
+
2
+
3
+ 'use strict';
4
+
5
+ const readline = require('readline');
6
+ const { formatStats, formatTps } = require('./formatters');
7
+
8
+ function sleep(ms) {
9
+ return new Promise((r) => setTimeout(r, ms));
10
+ }
11
+
12
+ async function runTop(client, refreshMs) {
13
+ if (process.stdin.isTTY) {
14
+ readline.emitKeypressEvents(process.stdin);
15
+ process.stdin.setRawMode(true);
16
+ }
17
+ let stopped = false;
18
+ const onKey = (str, key) => {
19
+ if (key.name === 'q' || key.name === 'escape' || (key.ctrl && key.name === 'c')) stopped = true;
20
+ };
21
+ process.stdin.on('keypress', onKey);
22
+ try {
23
+ while (!stopped) {
24
+ const res = await client.request('monitor.top', {});
25
+ readline.cursorTo(process.stdout, 0, 0);
26
+ readline.clearScreenDown(process.stdout);
27
+ console.log('CreeperCLI top (q to quit)\n');
28
+ console.log(formatStats(res.stats));
29
+ console.log();
30
+ console.log(formatTps(res.tps));
31
+ await sleep(refreshMs);
32
+ }
33
+ } finally {
34
+ process.stdin.removeListener('keypress', onKey);
35
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
36
+ }
37
+ }
38
+
39
+ module.exports = { runTop };
package/src/transfer.js CHANGED
@@ -1,217 +1,292 @@
1
- /*
2
- * Copyright 2026 DemonZDevelopment
3
- *
4
- * Licensed under the Apache License, Version 2.0 (the "License");
5
- * you may not use this file except in compliance with the License.
6
- * You may obtain a copy of the License at
7
- *
8
- * http://www.apache.org/licenses/LICENSE-2.0
9
- *
10
- * Unless required by applicable law or agreed to in writing, software
11
- * distributed under the License is distributed on an "AS IS" BASIS,
12
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
- * See the License for the specific language governing permissions and
14
- * limitations under the License.
15
- */
16
-
17
- 'use strict';
18
-
19
- const fs = require('fs');
20
- const path = require('path');
1
+
2
+
3
+ 'use strict';
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
21
7
  const crypto = require('crypto');
22
8
  const { confirm } = require('./prompts');
23
9
 
24
- function sha256File(file) {
25
- return new Promise((resolve, reject) => {
26
- const hash = crypto.createHash('sha256');
27
- const stream = fs.createReadStream(file);
28
- stream.on('data', (d) => hash.update(d));
29
- stream.on('end', () => resolve(hash.digest('hex')));
30
- stream.on('error', reject);
31
- });
32
- }
10
+ const MAX_TRANSFER_CHUNK_BYTES = 1024 * 1024;
33
11
 
34
- function showProgress(transferred, total) {
35
- if (!total) return;
36
- const pct = Math.floor((transferred / total) * 100);
37
- const kb = Math.floor(transferred / 1024);
38
- const totalKb = Math.floor(total / 1024);
39
- process.stdout.write(`\r${pct}% (${kb}KB / ${totalKb}KB) `);
40
- }
41
-
42
- function clearProgress() {
43
- process.stdout.write('\r'.padEnd(70) + '\r');
12
+ function validateTransferStart(start, expectFileMetadata = false) {
13
+ if (!start || typeof start.transferId !== 'string' || !start.transferId) {
14
+ throw new Error('Server returned an invalid transfer id');
15
+ }
16
+ if (!Number.isInteger(start.chunkSize) || start.chunkSize < 1024
17
+ || start.chunkSize > MAX_TRANSFER_CHUNK_BYTES) {
18
+ throw new Error('Server returned an invalid transfer chunk size');
19
+ }
20
+ if (expectFileMetadata) {
21
+ if (!Number.isSafeInteger(start.size) || start.size < 0) {
22
+ throw new Error('Server returned an invalid file size');
23
+ }
24
+ if (typeof start.sha256 !== 'string' || !/^[0-9a-f]{64}$/i.test(start.sha256)) {
25
+ throw new Error('Server returned an invalid file checksum');
26
+ }
27
+ }
44
28
  }
45
-
29
+
30
+ function sha256File(file) {
31
+ return new Promise((resolve, reject) => {
32
+ const hash = crypto.createHash('sha256');
33
+ const stream = fs.createReadStream(file);
34
+ stream.on('data', (d) => hash.update(d));
35
+ stream.on('end', () => resolve(hash.digest('hex')));
36
+ stream.on('error', reject);
37
+ });
38
+ }
39
+
40
+ function showProgress(transferred, total) {
41
+ if (!total) return;
42
+ const pct = Math.floor((transferred / total) * 100);
43
+ const kb = Math.floor(transferred / 1024);
44
+ const totalKb = Math.floor(total / 1024);
45
+ process.stdout.write(`\r${pct}% (${kb}KB / ${totalKb}KB) `);
46
+ }
47
+
48
+ function clearProgress() {
49
+ process.stdout.write('\r'.padEnd(70) + '\r');
50
+ }
51
+
46
52
  async function cpush(ctx, args) {
47
- const rest = args.filter((a) => !a.startsWith('--'));
48
- let [local, remote] = rest;
49
- if (!local || !remote) throw new Error('Usage: cpush <local-file> <remote-path>');
50
- remote = remote.replace(/\\/g, '/');
51
- const stat = fs.statSync(local);
52
- if (!stat.isFile()) throw new Error('Local path must be a file');
53
- const sha = await sha256File(local);
53
+ const rest = args.filter((a) => !a.startsWith('--'));
54
+ let [local, remote] = rest;
55
+ if (!local || !remote) throw new Error('Usage: cpush <local-file> <remote-path>');
56
+ remote = remote.replace(/\\/g, '/');
57
+ const stat = fs.statSync(local);
58
+ if (!stat.isFile()) throw new Error('Local path must be a file');
59
+ const sha = await sha256File(local);
54
60
  const start = await ctx.client.request('xfer.push.start', {
55
- path: remote,
56
- size: stat.size,
57
- sha256: sha,
61
+ path: remote,
62
+ size: stat.size,
63
+ sha256: sha,
58
64
  force: args.includes('--force'),
59
65
  }, { timeoutMs: 60000 });
60
66
  const { transferId, chunkSize } = start;
61
- const fd = fs.openSync(local, 'r');
67
+ let fd = null;
62
68
  let transferred = 0;
63
69
  let index = 0;
64
70
  try {
71
+ validateTransferStart(start);
72
+ fd = fs.openSync(local, 'r');
65
73
  const buf = Buffer.alloc(chunkSize);
66
- while (true) {
67
- const n = fs.readSync(fd, buf, 0, chunkSize, transferred);
68
- if (n === 0) break;
69
- await ctx.client.request('xfer.push.chunk', {
70
- transferId,
71
- index,
72
- data: buf.subarray(0, n).toString('base64'),
73
- }, { timeoutMs: 120000 });
74
- transferred += n;
75
- index++;
74
+ while (true) {
75
+ const n = fs.readSync(fd, buf, 0, chunkSize, transferred);
76
+ if (n === 0) break;
77
+ await ctx.client.request('xfer.push.chunk', {
78
+ transferId,
79
+ index,
80
+ data: buf.subarray(0, n).toString('base64'),
81
+ }, { timeoutMs: 120000 });
82
+ transferred += n;
83
+ index++;
76
84
  showProgress(transferred, stat.size);
77
85
  }
86
+ const finish = await ctx.client.request('xfer.push.finish', { transferId, sha256: sha });
87
+ if (!finish || typeof finish.sha256 !== 'string' || finish.sha256.toLowerCase() !== sha) {
88
+ throw new Error('Server reported an unexpected upload checksum');
89
+ }
90
+ clearProgress();
91
+ console.log(`Pushed ${local} -> ${finish.target} (${finish.size} bytes, sha256 ${finish.sha256})`);
92
+ } catch (err) {
93
+ await ctx.client.request('xfer.abort', { transferId }).catch(() => {});
94
+ clearProgress();
95
+ throw err;
78
96
  } finally {
79
- fs.closeSync(fd);
97
+ if (fd !== null) fs.closeSync(fd);
80
98
  }
81
- clearProgress();
82
- const finish = await ctx.client.request('xfer.push.finish', { transferId, sha256: sha });
83
- console.log(`Pushed ${local} -> ${finish.target} (${finish.size} bytes, sha256 ${finish.sha256})`);
84
99
  }
85
-
86
- async function cpull(ctx, args) {
87
- let [remote, local] = args.filter((a) => !a.startsWith('--'));
88
- if (!remote || !local) throw new Error('Usage: cpull <remote-path> <local-file>');
100
+
101
+ async function cpull(ctx, args) {
102
+ let [remote, local] = args.filter((a) => !a.startsWith('--'));
103
+ if (!remote || !local) throw new Error('Usage: cpull <remote-path> <local-file>');
89
104
  remote = remote.replace(/\\/g, '/');
90
105
  const start = await ctx.client.request('xfer.pull.start', { path: remote });
91
106
  const { transferId, size, sha256: expectedSha, chunkSize } = start;
92
- const part = local + '.creepercli-part';
107
+ const part = `${local}.creepercli-part-${process.pid}-${Date.now()}`;
93
108
  const hash = crypto.createHash('sha256');
94
- const fd = fs.openSync(part, 'w');
109
+ let fd = null;
95
110
  let transferred = 0;
96
111
  let index = 0;
97
112
  try {
113
+ validateTransferStart(start, true);
114
+ const parent = path.dirname(path.resolve(local));
115
+ fs.mkdirSync(parent, { recursive: true, mode: 0o700 });
116
+ fd = fs.openSync(part, 'w');
98
117
  while (true) {
99
- const res = await ctx.client.request('xfer.pull.chunk', { transferId, index }, { timeoutMs: 120000 });
118
+ const res = await ctx.client.request('xfer.pull.chunk', { transferId, index }, { timeoutMs: 120000 });
100
119
  const data = Buffer.from(res.data || '', 'base64');
101
120
  if (data.length === 0) break;
102
- fs.writeSync(fd, data, 0, data.length);
103
- hash.update(data);
104
- transferred += data.length;
121
+ if (data.length > chunkSize || transferred + data.length > size) {
122
+ throw new Error('Server sent an invalid transfer chunk');
123
+ }
124
+ fs.writeSync(fd, data, 0, data.length);
125
+ hash.update(data);
126
+ transferred += data.length;
105
127
  index++;
106
128
  showProgress(transferred, size);
107
129
  }
108
- } finally {
109
130
  fs.closeSync(fd);
131
+ fd = null;
132
+ if (transferred !== size) {
133
+ throw new Error(`Transfer ended early: expected ${size} bytes, received ${transferred}`);
134
+ }
135
+ const actual = await ctx.client.request('xfer.pull.finish', { transferId, sha256: hash.digest('hex') });
136
+ clearProgress();
137
+ if (!actual || typeof actual.sha256 !== 'string'
138
+ || actual.sha256.toLowerCase() !== expectedSha.toLowerCase()) {
139
+ throw new Error('Checksum mismatch: transfer aborted');
140
+ }
141
+ replaceFile(part, local);
142
+ console.log(`Pulled ${remote} -> ${local} (${transferred} bytes, sha256 ${actual.sha256})`);
143
+ } catch (err) {
144
+ if (fd !== null) {
145
+ fs.closeSync(fd);
146
+ fd = null;
147
+ }
148
+ await ctx.client.request('xfer.abort', { transferId }).catch(() => {});
149
+ try { fs.unlinkSync(part); } catch {}
150
+ clearProgress();
151
+ throw err;
152
+ } finally {
153
+ if (fd !== null) fs.closeSync(fd);
110
154
  }
111
- const actual = await ctx.client.request('xfer.pull.finish', { transferId, sha256: hash.digest('hex') });
112
- clearProgress();
113
- if (actual.sha256 !== expectedSha) {
114
- fs.unlinkSync(part);
115
- throw new Error('Checksum mismatch: partial file removed, transfer aborted');
116
- }
117
- fs.renameSync(part, local);
118
- console.log(`Pulled ${remote} -> ${local} (${transferred} bytes, sha256 ${actual.sha256})`);
119
155
  }
120
-
121
- async function csync(ctx, args) {
122
- let [remoteDir, localDir] = args.filter((a) => a !== '-y' && !a.startsWith('--'));
123
- if (!remoteDir || !localDir) throw new Error('Usage: csync <remote-dir> <local-dir> [--yes]');
124
- remoteDir = remoteDir.replace(/\\/g, '/');
156
+
157
+ async function csync(ctx, args) {
158
+ let [remoteDir, localDir] = args.filter((a) => a !== '-y' && !a.startsWith('--'));
159
+ if (!remoteDir || !localDir) throw new Error('Usage: csync <remote-dir> <local-dir> [--yes]');
160
+ remoteDir = remoteDir.replace(/\\/g, '/');
125
161
  const yes = args.includes('--yes') || args.includes('-y') || !!(ctx.flags && ctx.flags.yes);
126
162
  await ctx.client.request('fs.mkdir', { path: remoteDir, parents: true }).catch(() => {});
127
163
 
164
+ const remoteInfo = await ctx.client.request('fs.info', { path: remoteDir });
165
+ if (!remoteInfo.isDir || typeof remoteInfo.path !== 'string' || !remoteInfo.path.startsWith('/')) {
166
+ throw new Error('Server returned an invalid sync directory');
167
+ }
168
+ remoteDir = remoteInfo.path;
169
+
128
170
  const remote = await ctx.client.request('xfer.list', { path: remoteDir }, { timeoutMs: 600000 });
129
171
  const remoteMap = new Map();
130
172
  for (const e of remote.entries) {
131
- if (!e.isDir) remoteMap.set(e.path.replace(/^\//, ''), e);
132
- }
133
- if (remote.truncated) console.error('(remote listing truncated)');
134
-
135
- const localMap = walkLocal(localDir);
136
-
137
- const toDownload = [];
138
- const toUpload = [];
139
- for (const [rel, e] of remoteMap) {
140
- const local = localMap.get(rel);
141
- if (!local) {
142
- toDownload.push(rel);
143
- continue;
173
+ if (!e.isDir) {
174
+ const rel = relativeRemotePath(remoteDir, e.path);
175
+ if (rel) remoteMap.set(rel, e);
144
176
  }
145
- if (local.size === e.size && local.mtime === e.mtime) continue;
146
- if (local.size === e.size) {
147
- let localSha;
148
- try {
149
- localSha = await sha256File(path.join(localDir, rel));
150
- } catch {
151
- toUpload.push(rel);
152
- continue;
153
- }
154
- if (localSha === e.sha256) continue;
155
- }
156
- if (e.mtime > local.mtime) toDownload.push(rel);
157
- else toUpload.push(rel);
158
- }
159
- for (const rel of localMap.keys()) {
160
- if (!remoteMap.has(rel)) toUpload.push(rel);
177
+ }
178
+ if (remote.truncated) console.error('(remote listing truncated)');
179
+
180
+ const localMap = walkLocal(localDir);
181
+
182
+ const toDownload = [];
183
+ const toUpload = [];
184
+ for (const [rel, e] of remoteMap) {
185
+ const local = localMap.get(rel);
186
+ if (!local) {
187
+ toDownload.push(rel);
188
+ continue;
189
+ }
190
+ if (local.size === e.size && local.mtime === e.mtime) continue;
191
+ if (local.size === e.size) {
192
+ let localSha;
193
+ try {
194
+ localSha = await sha256File(path.join(localDir, rel));
195
+ } catch {
196
+ toUpload.push(rel);
197
+ continue;
198
+ }
199
+ if (localSha === e.sha256) continue;
200
+ }
201
+ if (e.mtime > local.mtime) toDownload.push(rel);
202
+ else toUpload.push(rel);
203
+ }
204
+ for (const rel of localMap.keys()) {
205
+ if (!remoteMap.has(rel)) toUpload.push(rel);
206
+ }
207
+
208
+ if (!toDownload.length && !toUpload.length) {
209
+ console.log('Directories are in sync.');
210
+ return;
211
+ }
212
+ console.log(`Download: ${toDownload.length} file(s), Upload: ${toUpload.length} file(s)`);
213
+ if (!yes && toUpload.length) {
214
+ const ok = await confirm(`Upload ${toUpload.length} local file(s)?`, false);
215
+ if (!ok) toUpload.length = 0;
216
+ }
217
+ if (!yes && toDownload.length) {
218
+ const ok = await confirm(`Download ${toDownload.length} remote file(s)?`, false);
219
+ if (!ok) toDownload.length = 0;
220
+ }
221
+ for (const rel of toDownload) {
222
+ await cpull(ctx, [joinRemote(remoteDir, rel), path.join(localDir, rel)]);
223
+ }
224
+ for (const rel of toUpload) {
225
+ await cpush(ctx, [path.join(localDir, rel), joinRemote(remoteDir, rel)]);
226
+ }
227
+ console.log('Sync complete.');
228
+ }
229
+
230
+ function joinRemote(base, rel) {
231
+ const cleanBase = base.replace(/\\/g, '/');
232
+ const cleanRel = rel.replace(/\\/g, '/');
233
+ return (cleanBase.endsWith('/') ? cleanBase : cleanBase + '/') + cleanRel;
234
+ }
235
+
236
+ function relativeRemotePath(base, absolutePath) {
237
+ const cleanBase = ('/' + base.replace(/\\/g, '/').replace(/^\/+|\/+$/g, '')).replace(/^\/$/, '');
238
+ const cleanPath = '/' + absolutePath.replace(/\\/g, '/').replace(/^\/+/, '');
239
+ if (!cleanBase) return cleanPath.replace(/^\//, '');
240
+ if (cleanPath === cleanBase) return '';
241
+ if (!cleanPath.startsWith(cleanBase + '/')) {
242
+ throw new Error(`Server returned path outside sync root: ${absolutePath}`);
161
243
  }
244
+ return cleanPath.slice(cleanBase.length + 1);
245
+ }
162
246
 
163
- if (!toDownload.length && !toUpload.length) {
164
- console.log('Directories are in sync.');
247
+ function replaceFile(part, destination) {
248
+ if (!fs.existsSync(destination)) {
249
+ fs.renameSync(part, destination);
165
250
  return;
166
251
  }
167
- console.log(`Download: ${toDownload.length} file(s), Upload: ${toUpload.length} file(s)`);
168
- if (!yes && toUpload.length) {
169
- const ok = await confirm(`Upload ${toUpload.length} local file(s)?`, false);
170
- if (!ok) toUpload.length = 0;
171
- }
172
- if (!yes && toDownload.length) {
173
- const ok = await confirm(`Download ${toDownload.length} remote file(s)?`, false);
174
- if (!ok) toDownload.length = 0;
175
- }
176
- for (const rel of toDownload) {
177
- await cpull(ctx, [joinRemote(remoteDir, rel), path.join(localDir, rel)]);
178
- }
179
- for (const rel of toUpload) {
180
- await cpush(ctx, [path.join(localDir, rel), joinRemote(remoteDir, rel)]);
252
+ const backup = `${destination}.creepercli-backup-${process.pid}-${Date.now()}`;
253
+ fs.renameSync(destination, backup);
254
+ try {
255
+ fs.renameSync(part, destination);
256
+ } catch (err) {
257
+ try { fs.renameSync(backup, destination); } catch {}
258
+ throw err;
181
259
  }
182
- console.log('Sync complete.');
183
- }
184
-
185
- function joinRemote(base, rel) {
186
- const cleanBase = base.replace(/\\/g, '/');
187
- const cleanRel = rel.replace(/\\/g, '/');
188
- return (cleanBase.endsWith('/') ? cleanBase : cleanBase + '/') + cleanRel;
260
+ try { fs.rmSync(backup, { force: true }); } catch {}
189
261
  }
190
-
191
- function walkLocal(dir) {
192
- const map = new Map();
193
- function walk(d, prefix) {
194
- let names;
195
- try {
196
- names = fs.readdirSync(d);
197
- } catch {
198
- return;
199
- }
200
- for (const name of names) {
201
- const p = path.join(d, name);
202
- let st;
203
- try {
204
- st = fs.statSync(p);
205
- } catch {
206
- continue;
207
- }
208
- const rel = ((prefix ? prefix + '/' : '') + name).replace(/\\/g, '/');
262
+
263
+ function walkLocal(dir) {
264
+ const map = new Map();
265
+ function walk(d, prefix) {
266
+ let names;
267
+ try {
268
+ names = fs.readdirSync(d);
269
+ } catch {
270
+ return;
271
+ }
272
+ for (const name of names) {
273
+ const p = path.join(d, name);
274
+ let st;
275
+ try {
276
+ st = fs.lstatSync(p);
277
+ } catch {
278
+ continue;
279
+ }
280
+ const rel = ((prefix ? prefix + '/' : '') + name).replace(/\\/g, '/');
281
+ if (st.isSymbolicLink()) continue;
209
282
  if (st.isDirectory()) walk(p, rel);
210
- else if (st.isFile()) map.set(rel, { size: st.size, mtime: Math.floor(st.mtimeMs) });
211
- }
212
- }
213
- walk(dir, '');
214
- return map;
215
- }
216
-
217
- module.exports = { cpush, cpull, csync, joinRemote, walkLocal };
283
+ else if (st.isFile()) map.set(rel, { size: st.size, mtime: Math.floor(st.mtimeMs) });
284
+ }
285
+ }
286
+ walk(dir, '');
287
+ return map;
288
+ }
289
+
290
+ module.exports = {
291
+ cpush, cpull, csync, joinRemote, relativeRemotePath, replaceFile, walkLocal, validateTransferStart,
292
+ };