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/LICENSE +190 -0
- package/README.md +85 -117
- package/bin/creepercli.js +12 -24
- package/package.json +48 -48
- package/src/client.js +120 -110
- package/src/commands/auth.js +94 -108
- package/src/commands/exec.js +23 -37
- package/src/commands/fs.js +138 -152
- package/src/commands/index.js +126 -136
- package/src/commands/monitor.js +56 -65
- package/src/commands/search.js +56 -70
- package/src/config.js +47 -61
- package/src/editor.js +84 -98
- package/src/formatters.js +120 -134
- package/src/index.js +160 -171
- package/src/prompts.js +120 -134
- package/src/protocol.js +68 -82
- package/src/repl.js +86 -96
- package/src/session.js +39 -57
- package/src/top.js +39 -53
- package/src/transfer.js +243 -168
- package/src/update.js +71 -85
package/src/session.js
CHANGED
|
@@ -1,58 +1,40 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
const
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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
|
|
35
|
-
if (!
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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
|
|
107
|
+
const part = `${local}.creepercli-part-${process.pid}-${Date.now()}`;
|
|
93
108
|
const hash = crypto.createHash('sha256');
|
|
94
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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)
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
164
|
-
|
|
247
|
+
function replaceFile(part, destination) {
|
|
248
|
+
if (!fs.existsSync(destination)) {
|
|
249
|
+
fs.renameSync(part, destination);
|
|
165
250
|
return;
|
|
166
251
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
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.
|
|
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 = {
|
|
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
|
+
};
|