swarmwire-cli 0.1.0 → 0.2.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/README.md +13 -2
- package/bin/swarmwire.js +30 -5
- package/lib/qr.js +25 -0
- package/lib/rendezvous.js +88 -0
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -10,6 +10,12 @@ swarmwire send ./holiday-photos # prints a link, keeps serving until you
|
|
|
10
10
|
swarmwire get "<link>" # on the other machine: downloads it, resumable
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
+
Generate QR codes for easy use with the [swarmwire android app](https://github.com/tibocub/swarmwire-mobile)
|
|
14
|
+
```
|
|
15
|
+
swarmwire send ./holiday-photos --qr # also prints a QR code to flash from the android app
|
|
16
|
+
swarmwire get --qr # displays a QR code that can be flashed to send a file from the android app
|
|
17
|
+
```
|
|
18
|
+
|
|
13
19
|
Anyone who knows what a file contains can find the people sharing it, whatever each calls it: two
|
|
14
20
|
people sending the same file are one swarm. Pass `--private` to `send` for a link that carries a secret, so that only people
|
|
15
21
|
holding the link can find you.
|
|
@@ -18,7 +24,7 @@ Both commands keep a two-line status current. Receiving looks like this:
|
|
|
18
24
|
|
|
19
25
|
```
|
|
20
26
|
[=========> ] 45% [2/3] TOTAL: [154.2/344.0] MB
|
|
21
|
-
UP: 1.1 MB/s
|
|
27
|
+
UP: 1.1 MB/s DOWN: 8.2 MB/s Seeders: 1 Peers: 0
|
|
22
28
|
```
|
|
23
29
|
|
|
24
30
|
When the download is done the bar is replaced by `[Done] you are now seeding.` and `get` keeps
|
|
@@ -35,10 +41,15 @@ are decimal: 1 MB is 1,000,000 bytes.
|
|
|
35
41
|
|
|
36
42
|
| | |
|
|
37
43
|
|---|---|
|
|
38
|
-
| `send <path> [--private]` | share a file or folder; prints a link |
|
|
44
|
+
| `send <path> [--private] [--qr]` | share a file or folder; prints a link (`--qr`: also draws it as a QR code) |
|
|
45
|
+
| `get --qr [output] [--exit]` | draw a QR code and wait: a phone scans it and hands over a link (its own file, or one forwarded from another device) |
|
|
39
46
|
| `get <link> [output] [--exit]` | fetch by link, then keep seeding it (`--exit`: leave instead); the output defaults to the name in the link |
|
|
40
47
|
| `--bootstrap host:port` | use a private DHT instead of the public one (for tests) |
|
|
41
48
|
|
|
49
|
+
`get --qr` listens on a fresh one-shot key on the DHT, shown in the QR code and printed below it as
|
|
50
|
+
`swarmwire-recv://<key>`. It takes the first valid link anyone sends it, then downloads as usual.
|
|
51
|
+
The QR codes go to stderr, so `send`'s stdout is still just the link.
|
|
52
|
+
|
|
42
53
|
Needs Node 20 or later. Files are shared where they lie: nothing is copied or written next to them.
|
|
43
54
|
|
|
44
55
|
## Development
|
package/bin/swarmwire.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* swarmwire send <path> [--private] [--bootstrap host:port]
|
|
3
|
+
* swarmwire send <path> [--private] [--qr] [--bootstrap host:port]
|
|
4
4
|
* swarmwire get <link> [output] [--bootstrap host:port]
|
|
5
|
+
* swarmwire get --qr [output] [--bootstrap host:port]
|
|
5
6
|
*
|
|
6
7
|
* Deliberately two commands. Sending prints a link and keeps serving the file where it is;
|
|
7
8
|
* getting takes that link and nothing else.
|
|
@@ -12,15 +13,19 @@ import { stat } from 'fs/promises'
|
|
|
12
13
|
import Hyperswarm from 'hyperswarm'
|
|
13
14
|
import DHT from 'hyperdht'
|
|
14
15
|
import b4a from 'b4a'
|
|
16
|
+
import { renderQr } from '../lib/qr.js'
|
|
17
|
+
import { receive } from '../lib/rendezvous.js'
|
|
15
18
|
import { Swarmwire, parseLink, groupLength, DEFAULT_GROUP_LOG } from 'swarmwire-lib'
|
|
16
19
|
|
|
17
20
|
const USAGE = `swarmwire - content-addressed file transfer, peer to peer
|
|
18
21
|
|
|
19
22
|
swarmwire send <path> [options] share a file or folder; prints a link
|
|
20
23
|
swarmwire get <link> [output] fetch a file or folder by link
|
|
24
|
+
swarmwire get --qr [output] show a QR code; a phone scans it and hands over a link
|
|
21
25
|
|
|
22
26
|
Options:
|
|
23
27
|
--private only people holding the link can find you (default: anyone who knows the hash can)
|
|
28
|
+
--qr send: also show the link as a QR code. get: wait for a link from a scanner
|
|
24
29
|
--exit get: leave when the download is done instead of seeding it
|
|
25
30
|
--bootstrap <h:p> use a private DHT instead of the public one
|
|
26
31
|
--help
|
|
@@ -32,14 +37,14 @@ let shown = ''
|
|
|
32
37
|
let drawn = false
|
|
33
38
|
let lastPlain = ''
|
|
34
39
|
|
|
35
|
-
const FLAGS = { private: false, exit: false, help: false, bootstrap: true } // true: takes a value
|
|
40
|
+
const FLAGS = { private: false, exit: false, qr: false, help: false, bootstrap: true } // true: takes a value
|
|
36
41
|
|
|
37
42
|
async function main () {
|
|
38
43
|
const { command, args, flags } = parseArgs(process.argv.slice(2))
|
|
39
44
|
|
|
40
45
|
if (flags.help || !command) return print(USAGE)
|
|
41
46
|
if (command === 'send') return send(args[0], flags)
|
|
42
|
-
if (command === 'get') return get(args[0], args[1], flags)
|
|
47
|
+
if (command === 'get') return flags.qr ? get(null, args[0], flags) : get(args[0], args[1], flags)
|
|
43
48
|
|
|
44
49
|
throw new Error(`unknown command: ${command}\n\n${USAGE}`)
|
|
45
50
|
}
|
|
@@ -59,6 +64,7 @@ async function send (path, flags) {
|
|
|
59
64
|
: await node.serve(path, { secret })
|
|
60
65
|
|
|
61
66
|
print(link)
|
|
67
|
+
if (flags.qr) warn(renderQr(link, { color: process.stderr.isTTY }))
|
|
62
68
|
warn(
|
|
63
69
|
folder
|
|
64
70
|
? `serving ${path} (${files} files, ${bytes(size)})${flags.private ? ' privately' : ''}`
|
|
@@ -77,12 +83,18 @@ async function send (path, flags) {
|
|
|
77
83
|
}
|
|
78
84
|
|
|
79
85
|
async function get (link, output, flags) {
|
|
80
|
-
if (!link) throw new Error('get needs a link\n\n' + USAGE)
|
|
86
|
+
if (!link && !flags.qr) throw new Error('get needs a link\n\n' + USAGE)
|
|
87
|
+
|
|
88
|
+
const swarm = createSwarm(flags)
|
|
89
|
+
|
|
90
|
+
if (!link) {
|
|
91
|
+
link = await linkFromScanner(swarm)
|
|
92
|
+
if (!link) return swarm.destroy() // interrupted while waiting
|
|
93
|
+
}
|
|
81
94
|
|
|
82
95
|
const parsed = parseLink(link)
|
|
83
96
|
const out = output ?? safeName(parsed.name) ?? parsed.root.slice(0, 16)
|
|
84
97
|
|
|
85
|
-
const swarm = createSwarm(flags)
|
|
86
98
|
const node = new Swarmwire({ swarm })
|
|
87
99
|
|
|
88
100
|
warn(`fetching ${parsed.root.slice(0, 16)}... into ${out}`)
|
|
@@ -133,6 +145,19 @@ async function get (link, output, flags) {
|
|
|
133
145
|
await swarm.destroy()
|
|
134
146
|
}
|
|
135
147
|
|
|
148
|
+
/** Show a QR code for a one-shot meeting point and wait for a scanner to hand us a link. */
|
|
149
|
+
async function linkFromScanner (swarm) {
|
|
150
|
+
const meeting = await receive(swarm.dht)
|
|
151
|
+
|
|
152
|
+
warn(renderQr(meeting.url, { color: process.stderr.isTTY }))
|
|
153
|
+
warn(`${meeting.url}\nscan this with the swarmwire app and pick something to send; ctrl-c to cancel`)
|
|
154
|
+
|
|
155
|
+
const link = await Promise.race([meeting.link, interrupted().then(() => null)])
|
|
156
|
+
await meeting.close()
|
|
157
|
+
|
|
158
|
+
return link
|
|
159
|
+
}
|
|
160
|
+
|
|
136
161
|
/** Resolves on ctrl-c or a polite kill, so serving can end cleanly. */
|
|
137
162
|
function interrupted () {
|
|
138
163
|
return new Promise((resolve) => {
|
package/lib/qr.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { encode } from 'uqr'
|
|
2
|
+
|
|
3
|
+
const QUIET = 2 // modules of white around the code, as the spec asks
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A QR code as terminal text, two modules per character row. Black on white is set
|
|
7
|
+
* explicitly when `color` is true: on a dark theme the default colors would invert the code.
|
|
8
|
+
*/
|
|
9
|
+
export function renderQr (text, { color = true } = {}) {
|
|
10
|
+
const { data, size } = encode(text, { ecc: 'L', border: 0 })
|
|
11
|
+
const dark = (x, y) => x >= 0 && y >= 0 && x < size && y < size && data[y][x]
|
|
12
|
+
|
|
13
|
+
const rows = []
|
|
14
|
+
for (let y = -QUIET; y < size + QUIET; y += 2) {
|
|
15
|
+
let row = ''
|
|
16
|
+
for (let x = -QUIET; x < size + QUIET; x++) {
|
|
17
|
+
const top = dark(x, y)
|
|
18
|
+
const bottom = dark(x, y + 1)
|
|
19
|
+
row += top && bottom ? '█' : top ? '▀' : bottom ? '▄' : ' '
|
|
20
|
+
}
|
|
21
|
+
rows.push(color ? `\x1b[30;47m${row}\x1b[0m` : row)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return rows.join('\n')
|
|
25
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A one-shot meeting point for handing a link to a machine that cannot be reached by one.
|
|
3
|
+
*
|
|
4
|
+
* `receive` listens on a fresh DHT keypair and shows its public key (as a QR code); whoever
|
|
5
|
+
* scans it calls `deliver` with a link, the listener checks it is one and answers. No topic,
|
|
6
|
+
* no discovery: the key is the address, and it is good for exactly one link.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import b4a from 'b4a'
|
|
10
|
+
import { parseLink } from 'swarmwire-lib'
|
|
11
|
+
|
|
12
|
+
export const SCHEME = 'swarmwire-recv://'
|
|
13
|
+
const MAX_LINK = 4096 // a link is ~150 bytes; this only bounds what a stranger can make us buffer
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {import('hyperdht')} dht
|
|
17
|
+
* @returns {Promise<{url: string, link: Promise<string>, close: () => Promise<void>}>}
|
|
18
|
+
*/
|
|
19
|
+
export async function receive (dht) {
|
|
20
|
+
const keyPair = dht.constructor.keyPair()
|
|
21
|
+
let resolve
|
|
22
|
+
const link = new Promise((r) => { resolve = r })
|
|
23
|
+
let done = false
|
|
24
|
+
|
|
25
|
+
const server = dht.createServer((socket) => {
|
|
26
|
+
let buffer = ''
|
|
27
|
+
socket.setEncoding('utf8')
|
|
28
|
+
socket.on('error', () => {}) // a scanner that walks away is not our problem
|
|
29
|
+
socket.on('data', (chunk) => {
|
|
30
|
+
buffer += chunk
|
|
31
|
+
if (buffer.length > MAX_LINK) return socket.destroy()
|
|
32
|
+
|
|
33
|
+
const end = buffer.indexOf('\n')
|
|
34
|
+
if (end < 0) return
|
|
35
|
+
|
|
36
|
+
const candidate = buffer.slice(0, end).trim()
|
|
37
|
+
buffer = ''
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
if (done) throw new Error('already received')
|
|
41
|
+
parseLink(candidate)
|
|
42
|
+
} catch {
|
|
43
|
+
return socket.end('invalid\n')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
done = true
|
|
47
|
+
socket.end('ok\n')
|
|
48
|
+
resolve(candidate)
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
await server.listen(keyPair)
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
url: SCHEME + b4a.toString(keyPair.publicKey, 'hex'),
|
|
56
|
+
link,
|
|
57
|
+
close: () => server.close()
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Hand `link` to the listener at `url`. Resolves once it has accepted it.
|
|
63
|
+
*
|
|
64
|
+
* @param {import('hyperdht')} dht
|
|
65
|
+
* @param {string} url
|
|
66
|
+
* @param {string} link
|
|
67
|
+
*/
|
|
68
|
+
export async function deliver (dht, url, link) {
|
|
69
|
+
if (!url.startsWith(SCHEME)) throw new Error(`not a swarmwire receive address: ${url}`)
|
|
70
|
+
const publicKey = b4a.from(url.slice(SCHEME.length), 'hex')
|
|
71
|
+
if (publicKey.length !== 32) throw new Error(`not a swarmwire receive address: ${url}`)
|
|
72
|
+
|
|
73
|
+
const socket = dht.connect(publicKey)
|
|
74
|
+
socket.setEncoding('utf8')
|
|
75
|
+
|
|
76
|
+
const answer = new Promise((resolve, reject) => {
|
|
77
|
+
let reply = ''
|
|
78
|
+
socket.on('data', (chunk) => { reply += chunk })
|
|
79
|
+
socket.on('error', reject)
|
|
80
|
+
socket.on('end', () => { socket.end(); resolve(reply.trim()) }) // the listener half-closes after its answer
|
|
81
|
+
socket.on('close', () => resolve(reply.trim()))
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
socket.write(link + '\n')
|
|
85
|
+
const reply = await answer
|
|
86
|
+
|
|
87
|
+
if (reply !== 'ok') throw new Error(`receiver refused the link${reply ? `: ${reply}` : ''}`)
|
|
88
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "swarmwire-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Send a file or folder to someone with a link. Peer to peer, from the terminal.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
"swarmwire": "bin/swarmwire.js"
|
|
24
24
|
},
|
|
25
25
|
"files": [
|
|
26
|
-
"bin/"
|
|
26
|
+
"bin/",
|
|
27
|
+
"lib/"
|
|
27
28
|
],
|
|
28
29
|
"engines": {
|
|
29
30
|
"node": ">=20"
|
|
@@ -35,7 +36,8 @@
|
|
|
35
36
|
"b4a": "^1.9.0",
|
|
36
37
|
"hyperdht": "^6.0.0",
|
|
37
38
|
"hyperswarm": "^4.17.2",
|
|
38
|
-
"swarmwire-lib": "^0.4.
|
|
39
|
+
"swarmwire-lib": "^0.4.1",
|
|
40
|
+
"uqr": "^0.1.3"
|
|
39
41
|
},
|
|
40
42
|
"devDependencies": {
|
|
41
43
|
"@hyperswarm/testnet": "^3.1.4",
|