creepercli 1.0.0 → 1.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/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');
21
- const crypto = require('crypto');
22
- const { confirm } = require('./prompts');
23
-
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
- }
33
-
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');
44
- }
45
-
46
- 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);
54
- const start = await ctx.client.request('xfer.push.start', {
55
- path: remote,
56
- size: stat.size,
57
- sha256: sha,
58
- force: args.includes('--force'),
59
- }, { timeoutMs: 60000 });
60
- const { transferId, chunkSize } = start;
61
- const fd = fs.openSync(local, 'r');
62
- let transferred = 0;
63
- let index = 0;
64
- try {
65
- 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++;
76
- showProgress(transferred, stat.size);
77
- }
78
- } finally {
79
- fs.closeSync(fd);
80
- }
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
- }
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>');
89
- remote = remote.replace(/\\/g, '/');
90
- const start = await ctx.client.request('xfer.pull.start', { path: remote });
91
- const { transferId, size, sha256: expectedSha, chunkSize } = start;
92
- const part = local + '.creepercli-part';
93
- const hash = crypto.createHash('sha256');
94
- const fd = fs.openSync(part, 'w');
95
- let transferred = 0;
96
- let index = 0;
97
- try {
98
- while (true) {
99
- const res = await ctx.client.request('xfer.pull.chunk', { transferId, index }, { timeoutMs: 120000 });
100
- const data = Buffer.from(res.data || '', 'base64');
101
- if (data.length === 0) break;
102
- fs.writeSync(fd, data, 0, data.length);
103
- hash.update(data);
104
- transferred += data.length;
105
- index++;
106
- showProgress(transferred, size);
107
- }
108
- } finally {
109
- fs.closeSync(fd);
110
- }
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
- }
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, '/');
125
- const yes = args.includes('--yes') || args.includes('-y') || !!(ctx.flags && ctx.flags.yes);
126
- await ctx.client.request('fs.mkdir', { path: remoteDir, parents: true }).catch(() => {});
127
-
128
- const remote = await ctx.client.request('xfer.list', { path: remoteDir }, { timeoutMs: 600000 });
129
- const remoteMap = new Map();
130
- 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;
144
- }
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);
161
- }
162
-
163
- if (!toDownload.length && !toUpload.length) {
164
- console.log('Directories are in sync.');
165
- return;
166
- }
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)]);
181
- }
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;
189
- }
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, '/');
209
- 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 };
1
+
2
+
3
+ 'use strict';
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const crypto = require('crypto');
8
+ const { confirm } = require('./prompts');
9
+
10
+ const MAX_TRANSFER_CHUNK_BYTES = 1024 * 1024;
11
+
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
+ }
28
+ }
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
+
52
+ async function cpush(ctx, args) {
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);
60
+ const start = await ctx.client.request('xfer.push.start', {
61
+ path: remote,
62
+ size: stat.size,
63
+ sha256: sha,
64
+ force: args.includes('--force'),
65
+ }, { timeoutMs: 60000 });
66
+ const { transferId, chunkSize } = start;
67
+ let fd = null;
68
+ let transferred = 0;
69
+ let index = 0;
70
+ try {
71
+ validateTransferStart(start);
72
+ fd = fs.openSync(local, 'r');
73
+ const buf = Buffer.alloc(chunkSize);
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++;
84
+ showProgress(transferred, stat.size);
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;
96
+ } finally {
97
+ if (fd !== null) fs.closeSync(fd);
98
+ }
99
+ }
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>');
104
+ remote = remote.replace(/\\/g, '/');
105
+ const start = await ctx.client.request('xfer.pull.start', { path: remote });
106
+ const { transferId, size, sha256: expectedSha, chunkSize } = start;
107
+ const part = `${local}.creepercli-part-${process.pid}-${Date.now()}`;
108
+ const hash = crypto.createHash('sha256');
109
+ let fd = null;
110
+ let transferred = 0;
111
+ let index = 0;
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');
117
+ while (true) {
118
+ const res = await ctx.client.request('xfer.pull.chunk', { transferId, index }, { timeoutMs: 120000 });
119
+ const data = Buffer.from(res.data || '', 'base64');
120
+ if (data.length === 0) break;
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;
127
+ index++;
128
+ showProgress(transferred, size);
129
+ }
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);
154
+ }
155
+ }
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, '/');
161
+ const yes = args.includes('--yes') || args.includes('-y') || !!(ctx.flags && ctx.flags.yes);
162
+ await ctx.client.request('fs.mkdir', { path: remoteDir, parents: true }).catch(() => {});
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
+
170
+ const remote = await ctx.client.request('xfer.list', { path: remoteDir }, { timeoutMs: 600000 });
171
+ const remoteMap = new Map();
172
+ for (const e of remote.entries) {
173
+ if (!e.isDir) {
174
+ const rel = relativeRemotePath(remoteDir, e.path);
175
+ if (rel) remoteMap.set(rel, e);
176
+ }
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}`);
243
+ }
244
+ return cleanPath.slice(cleanBase.length + 1);
245
+ }
246
+
247
+ function replaceFile(part, destination) {
248
+ if (!fs.existsSync(destination)) {
249
+ fs.renameSync(part, destination);
250
+ return;
251
+ }
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;
259
+ }
260
+ try { fs.rmSync(backup, { force: true }); } catch {}
261
+ }
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;
282
+ if (st.isDirectory()) walk(p, rel);
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
+ };