From 36c41f2282fe5b83ade92c959f9a875249c520d4 Mon Sep 17 00:00:00 2001 From: Vitaliy Filippov Date: Tue, 7 May 2024 14:58:26 +0300 Subject: [PATCH] Implement client --- anticli.js | 224 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 224 insertions(+) create mode 100644 anticli.js diff --git a/anticli.js b/anticli.js new file mode 100644 index 0000000..12529cb --- /dev/null +++ b/anticli.js @@ -0,0 +1,224 @@ +#!/usr/bin/node + +const fsp = require('fs').promises; +const http = require('http'); +const https = require('https'); + +class AntiEtcdCli +{ + static parse(args) + { + const cmd = []; + const options = {}; + for (let i = 2; i < args.length; i++) + { + const arg = args[i].toLowerCase().replace(/^--(.+)$/, (m, m1) => '--'+m1.replace(/-/g, '_')); + if (arg === '-h' || arg === '--help') + { + console.error( + 'USAGE:\n'+ + ' anticli.js [OPTIONS] put []\n'+ + ' anticli.js [OPTIONS] get [-p|--prefix] [-v|--print-value-only] [-k|--keys-only]\n'+ + ' anticli.js [OPTIONS] del [-p|--prefix]\n'+ + 'OPTIONS:\n'+ + ' [--endpoints|-e http://node1:2379,http://node2:2379,http://node3:2379]\n'+ + ' [--timeout 1000]' + ); + process.exit(); + } + else if (arg == '-e' || arg == '--endpoints') + { + options['endpoints'] = args[++i].split(/\s*[,\s]+\s*/); + } + else if (arg == '-p' || arg == '--prefix') + { + options['prefix'] = true; + } + else if (arg == '-v' || arg == '--print_value_only') + { + options['print_value_only'] = true; + } + else if (arg == '-k' || arg == '--keys_only') + { + options['keys_only'] = true; + } + else if (arg[0] == '-' && arg[1] !== '-') + { + console.error('Unknown option '+arg); + process.exit(1); + } + else if (arg.substr(0, 2) == '--') + { + options[arg.substr(2)] = args[++i]; + } + else + { + cmd.push(arg); + } + } + if (!cmd.length || cmd[0] != 'get' && cmd[0] != 'put' && cmd[0] != 'del') + { + console.log('Supported commands: get, put, del. Use --help to see details'); + process.exit(1); + } + return [ cmd, options ]; + } + + async run(cmd, options) + { + this.options = options; + if (!this.options.endpoints) + { + this.options.endpoints = [ 'http://localhost:2379' ]; + } + if (cmd[0] == 'get') + { + await this.get(cmd.slice(1)); + } + else if (cmd[0] == 'put') + { + await this.put(cmd[1], cmd.length > 2 ? cmd[2] : undefined); + } + else if (cmd[0] == 'del') + { + await this.del(cmd.slice(1)); + } + } + + async get(keys) + { + if (this.options.prefix) + { + keys = keys.map(k => k.replace(/\/+$/, '')); + } + const txn = { success: keys.map(key => ({ request_range: this.options.prefix ? { key: b64(key+'/'), range_end: b64(key+'0') } : { key: b64(key) } })) }; + const res = await this.request('/v3/kv/txn', txn); + for (const r of (res||{}).responses||[]) + { + if (r.response_range) + { + for (const kv of r.response_range.kvs) + { + if (!this.options.print_value_only) + { + console.log(de64(kv.key)); + } + if (!this.options.keys_only) + { + console.log(de64(kv.value)); + } + } + } + } + } + + async put(key, value) + { + if (value === undefined) + { + value = await fsp.readFile(0, { encoding: 'utf-8' }); + } + const res = await this.request('/v3/kv/put', { key: b64(key), value: b64(value) }); + if (res && res.header) + { + console.log('OK'); + } + } + + async del(keys) + { + if (this.options.prefix) + { + keys = keys.map(k => k.replace(/\/+$/, '')); + } + const txn = { success: keys.map(k => ({ request_delete_range: this.options.prefix ? { key: b64(key+'/'), range_end: b64(key+'0') } : { key: b64(key) } })) }; + const res = await this.request('/v3/kv/txn', txn); + for (const r of (res||{}).responses||[]) + { + if (r.response_delete_range) + { + console.log(r.response_delete_range.deleted); + } + } + } + + async request(path, body) + { + for (const url of this.options.endpoints) + { + const cur_url = url.replace(/\/+$/, '')+path; + try + { + return (await POST(cur_url, body, this.options.timeout||1000)).json; + } + catch (e) + { + console.error(cur_url+': '+e.message); + } + } + return null; + } +} + +function POST(url, body, timeout) +{ + return new Promise(ok => + { + const body_text = Buffer.from(JSON.stringify(body)); + let timer_id = timeout > 0 ? setTimeout(() => + { + if (req) + req.abort(); + req = null; + ok({ error: 'timeout' }); + }, timeout) : null; + let req = (url.substr(0, 6).toLowerCase() == 'https://' ? https : http).request(url, { method: 'POST', headers: { + 'Content-Type': 'application/json', + 'Content-Length': body_text.length, + } }, (res) => + { + if (!req) + { + return; + } + clearTimeout(timer_id); + let res_body = ''; + res.setEncoding('utf8'); + res.on('error', (error) => ok({ error })); + res.on('data', chunk => { res_body += chunk; }); + res.on('end', () => + { + if (res.statusCode != 200) + { + ok({ error: res_body, code: res.statusCode }); + return; + } + try + { + res_body = JSON.parse(res_body); + ok({ response: res, json: res_body }); + } + catch (e) + { + ok({ error: e, response: res, body: res_body }); + } + }); + }); + req.on('error', (error) => ok({ error })); + req.on('close', () => ok({ error: new Error('Connection closed prematurely') })); + req.write(body_text); + req.end(); + }); +} + +function b64(str) +{ + return Buffer.from(str).toString('base64'); +} + +function de64(str) +{ + return Buffer.from(str, 'base64').toString(); +} + +new AntiEtcdCli().run(...AntiEtcdCli.parse(process.argv)).catch(console.error);