weed-cli 1.6.2__tar.gz → 1.6.8__tar.gz
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.
- {weed_cli-1.6.2/weed_cli.egg-info → weed_cli-1.6.8}/PKG-INFO +12 -3
- {weed_cli-1.6.2 → weed_cli-1.6.8}/README.md +11 -2
- {weed_cli-1.6.2 → weed_cli-1.6.8}/node.py +22 -0
- {weed_cli-1.6.2 → weed_cli-1.6.8}/pyproject.toml +1 -1
- {weed_cli-1.6.2 → weed_cli-1.6.8}/shell.py +19 -1
- {weed_cli-1.6.2 → weed_cli-1.6.8}/web_ui.py +119 -4
- {weed_cli-1.6.2 → weed_cli-1.6.8}/weed.py +13 -1
- {weed_cli-1.6.2 → weed_cli-1.6.8/weed_cli.egg-info}/PKG-INFO +12 -3
- {weed_cli-1.6.2 → weed_cli-1.6.8}/LICENSE +0 -0
- {weed_cli-1.6.2 → weed_cli-1.6.8}/dht.py +0 -0
- {weed_cli-1.6.2 → weed_cli-1.6.8}/discovery_relay.py +0 -0
- {weed_cli-1.6.2 → weed_cli-1.6.8}/lightning_settle.py +0 -0
- {weed_cli-1.6.2 → weed_cli-1.6.8}/poc_reputation.py +0 -0
- {weed_cli-1.6.2 → weed_cli-1.6.8}/setup.cfg +0 -0
- {weed_cli-1.6.2 → weed_cli-1.6.8}/tunnel_relay.py +0 -0
- {weed_cli-1.6.2 → weed_cli-1.6.8}/weed_cli.egg-info/SOURCES.txt +0 -0
- {weed_cli-1.6.2 → weed_cli-1.6.8}/weed_cli.egg-info/dependency_links.txt +0 -0
- {weed_cli-1.6.2 → weed_cli-1.6.8}/weed_cli.egg-info/entry_points.txt +0 -0
- {weed_cli-1.6.2 → weed_cli-1.6.8}/weed_cli.egg-info/requires.txt +0 -0
- {weed_cli-1.6.2 → weed_cli-1.6.8}/weed_cli.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: weed-cli
|
|
3
|
-
Version: 1.6.
|
|
3
|
+
Version: 1.6.8
|
|
4
4
|
Summary: Censorship-resistant video PoC — discovery/hosting/download over signed relay events, a real Kademlia DHT, or a TLS-capable NAT-traversal tunnel
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: p2p,video,censorship-resistant,discovery,dht,kademlia,nat-traversal
|
|
@@ -174,19 +174,28 @@ blind.
|
|
|
174
174
|
### Docker
|
|
175
175
|
|
|
176
176
|
`Dockerfile.node` + `docker-compose.node.yml` package `web_ui.py` to run
|
|
177
|
-
somewhere other than a laptop
|
|
177
|
+
somewhere other than a laptop — alongside its own local discovery relay
|
|
178
|
+
(`Dockerfile.discovery-relay`), so there's always something real for it
|
|
179
|
+
to announce to and discover from out of the box.
|
|
178
180
|
|
|
179
181
|
```bash
|
|
180
182
|
make node # build + run, http://127.0.0.1:8080
|
|
181
183
|
make node-down # stop it — data persists
|
|
184
|
+
make node-shell # interactive weed shell, same container as the running node
|
|
182
185
|
WEED_SHARE_DIR=~/Movies make node # mount a real directory of .ott archives at /share
|
|
183
|
-
docker compose -f docker-compose.node.yml exec node python3 shell.py # interactive shell, same container
|
|
184
186
|
docker compose -f docker-compose.node.yml exec node python3 weed.py discover # or a one-off CLI command
|
|
185
187
|
```
|
|
186
188
|
|
|
187
189
|
- Identity key, reputation store, and library manifest all persist in a
|
|
188
190
|
named volume (`node-data`, mounted at `$HOME=/data`) — one node keeps
|
|
189
191
|
the same pubkey (and everything vouching for it) across restarts.
|
|
192
|
+
- The `relay` service is `node`'s default `WEED_RELAY` (`http://relay:9101`,
|
|
193
|
+
resolved over Compose's own network) — its events persist in their own
|
|
194
|
+
`relay-data` volume, and port 9101 is published to the host too, for
|
|
195
|
+
curling `/events` directly or pointing a non-Docker `weed`/shell session
|
|
196
|
+
at the exact same relay. Override `WEED_RELAY` (shell env or `.env`,
|
|
197
|
+
see `.env.sample`) to point at public infra instead, e.g. for a real
|
|
198
|
+
end-to-end test.
|
|
190
199
|
- `/share` is a separate *bind* mount, not a named volume, so the actual
|
|
191
200
|
`.ott` archives you're hosting are real files on the host you can see
|
|
192
201
|
and manage directly. Point the web UI's Host form at `/share`.
|
|
@@ -144,19 +144,28 @@ blind.
|
|
|
144
144
|
### Docker
|
|
145
145
|
|
|
146
146
|
`Dockerfile.node` + `docker-compose.node.yml` package `web_ui.py` to run
|
|
147
|
-
somewhere other than a laptop
|
|
147
|
+
somewhere other than a laptop — alongside its own local discovery relay
|
|
148
|
+
(`Dockerfile.discovery-relay`), so there's always something real for it
|
|
149
|
+
to announce to and discover from out of the box.
|
|
148
150
|
|
|
149
151
|
```bash
|
|
150
152
|
make node # build + run, http://127.0.0.1:8080
|
|
151
153
|
make node-down # stop it — data persists
|
|
154
|
+
make node-shell # interactive weed shell, same container as the running node
|
|
152
155
|
WEED_SHARE_DIR=~/Movies make node # mount a real directory of .ott archives at /share
|
|
153
|
-
docker compose -f docker-compose.node.yml exec node python3 shell.py # interactive shell, same container
|
|
154
156
|
docker compose -f docker-compose.node.yml exec node python3 weed.py discover # or a one-off CLI command
|
|
155
157
|
```
|
|
156
158
|
|
|
157
159
|
- Identity key, reputation store, and library manifest all persist in a
|
|
158
160
|
named volume (`node-data`, mounted at `$HOME=/data`) — one node keeps
|
|
159
161
|
the same pubkey (and everything vouching for it) across restarts.
|
|
162
|
+
- The `relay` service is `node`'s default `WEED_RELAY` (`http://relay:9101`,
|
|
163
|
+
resolved over Compose's own network) — its events persist in their own
|
|
164
|
+
`relay-data` volume, and port 9101 is published to the host too, for
|
|
165
|
+
curling `/events` directly or pointing a non-Docker `weed`/shell session
|
|
166
|
+
at the exact same relay. Override `WEED_RELAY` (shell env or `.env`,
|
|
167
|
+
see `.env.sample`) to point at public infra instead, e.g. for a real
|
|
168
|
+
end-to-end test.
|
|
160
169
|
- `/share` is a separate *bind* mount, not a named volume, so the actual
|
|
161
170
|
`.ott` archives you're hosting are real files on the host you can see
|
|
162
171
|
and manage directly. Point the web UI's Host form at `/share`.
|
|
@@ -1034,6 +1034,26 @@ def _relay_url_hint(relay_url):
|
|
|
1034
1034
|
return None
|
|
1035
1035
|
|
|
1036
1036
|
|
|
1037
|
+
def _normalize_relay_url(relay_url):
|
|
1038
|
+
"""A bare host:port with no scheme (e.g. '127.0.0.1:9101', the most
|
|
1039
|
+
natural way to type a local relay) isn't a URL urllib can parse at
|
|
1040
|
+
all -- everything before the first ':' becomes the *scheme*, so this
|
|
1041
|
+
reads as scheme '127.0.0.1' and raises "unknown url type", not a
|
|
1042
|
+
connection error. That already lands on post_event/fetch_events'
|
|
1043
|
+
existing except clauses (an unreachable relay is routine, not
|
|
1044
|
+
exceptional), which is exactly the trap: hosting against a
|
|
1045
|
+
scheme-less relay used to report status: running with zero actual
|
|
1046
|
+
announcements ever reaching it, and discover against one silently
|
|
1047
|
+
came back empty -- nothing hinted that the URL itself, not the
|
|
1048
|
+
relay, was the problem. Every relay this project actually ships or
|
|
1049
|
+
documents is plain HTTP(S), so defaulting a missing scheme to
|
|
1050
|
+
http:// turns the common typo into the thing the user obviously
|
|
1051
|
+
meant instead of a silent no-op."""
|
|
1052
|
+
if '://' not in relay_url:
|
|
1053
|
+
return 'http://' + relay_url
|
|
1054
|
+
return relay_url
|
|
1055
|
+
|
|
1056
|
+
|
|
1037
1057
|
_dns_bound_pool = concurrent.futures.ThreadPoolExecutor(max_workers=8, thread_name_prefix='urlopen')
|
|
1038
1058
|
|
|
1039
1059
|
|
|
@@ -1059,6 +1079,7 @@ def post_event(relay_url, event):
|
|
|
1059
1079
|
hint = _relay_url_hint(relay_url)
|
|
1060
1080
|
if hint:
|
|
1061
1081
|
return {'ok': False, 'error': hint}
|
|
1082
|
+
relay_url = _normalize_relay_url(relay_url)
|
|
1062
1083
|
req = urllib.request.Request(
|
|
1063
1084
|
f'{relay_url}/event', data=json.dumps(event).encode(),
|
|
1064
1085
|
headers={'Content-Type': 'application/json'}, method='POST')
|
|
@@ -1082,6 +1103,7 @@ def fetch_events(relay_url, event_type=None):
|
|
|
1082
1103
|
if hint:
|
|
1083
1104
|
print(f" {relay_url}: {hint}")
|
|
1084
1105
|
return None
|
|
1106
|
+
relay_url = _normalize_relay_url(relay_url)
|
|
1085
1107
|
url = f'{relay_url}/events' + (f'?type={event_type}' if event_type else '')
|
|
1086
1108
|
try:
|
|
1087
1109
|
with _urlopen_bounded(url, timeout=5) as resp:
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "weed-cli"
|
|
7
|
-
version = "1.6.
|
|
7
|
+
version = "1.6.8"
|
|
8
8
|
description = "Censorship-resistant video PoC — discovery/hosting/download over signed relay events, a real Kademlia DHT, or a TLS-capable NAT-traversal tunnel"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -179,6 +179,23 @@ class WeedShell(cmd.Cmd):
|
|
|
179
179
|
# and only fail later, in the background server thread (onecmd's
|
|
180
180
|
# exception net doesn't reach into background threads)
|
|
181
181
|
all_leaves = {e['sha256']: node.load_leaves(archive_dir, e['sha256']) for e in entries}
|
|
182
|
+
# Same reasoning as web_ui.py's _run_host_job: bind the real,
|
|
183
|
+
# permanent listening socket now, before announcing anything to a
|
|
184
|
+
# relay, and hand this exact socket to run_host_server below
|
|
185
|
+
# instead of it binding a second one later. Without this, a port
|
|
186
|
+
# already in use (another `host` still running from earlier in
|
|
187
|
+
# this same session, or a persisted web-UI host on the same
|
|
188
|
+
# default port) only fails once the background thread started
|
|
189
|
+
# below gets around to its own bind — by then `announced ... on
|
|
190
|
+
# relay: {'ok': True, ...}` has already printed, and the relay
|
|
191
|
+
# has a listing for a host that was never actually reachable, with
|
|
192
|
+
# nothing pointing back at why until a downloader's own possession
|
|
193
|
+
# challenge mysteriously fails against it.
|
|
194
|
+
try:
|
|
195
|
+
bound_sock = node.bind_host_port(port)
|
|
196
|
+
except OSError as e:
|
|
197
|
+
print(f' ✗ cannot bind port {port}: {e} — not announcing, nothing started')
|
|
198
|
+
return
|
|
182
199
|
if relay and not no_announce:
|
|
183
200
|
ott_status = node.ott_commit_status(archive_dir)
|
|
184
201
|
for entry in entries:
|
|
@@ -204,7 +221,8 @@ class WeedShell(cmd.Cmd):
|
|
|
204
221
|
self._host_threads.append(tt)
|
|
205
222
|
t = threading.Thread(target=_bg,
|
|
206
223
|
args=(node.run_host_server, archive_dir, file_name, port),
|
|
207
|
-
kwargs={'quiet': True, 'price': price, 'ln_node': ln_node},
|
|
224
|
+
kwargs={'quiet': True, 'price': price, 'ln_node': ln_node, 'sock': bound_sock},
|
|
225
|
+
daemon=True)
|
|
208
226
|
t.start()
|
|
209
227
|
self._host_threads.append(t)
|
|
210
228
|
price_note = f', {price} sat/download' if price else ', free'
|
|
@@ -125,8 +125,14 @@ _lock = threading.Lock()
|
|
|
125
125
|
# reload -- or a server restart -- doesn't forget any of it. Downloads
|
|
126
126
|
# are keyed by content_hash (one record per piece of content, most
|
|
127
127
|
# recent job wins); likes/subscriptions are just lists of hashes/pubkeys.
|
|
128
|
-
#
|
|
129
|
-
|
|
128
|
+
# playlists are purely local organization -- unlike likes/subscriptions
|
|
129
|
+
# they're never signed or gossiped to a relay, nobody else has any
|
|
130
|
+
# business seeing how you've grouped your own downloads -- a list of
|
|
131
|
+
# {id, name, items: [{content_hash, title, signer_pubkey}, ...]}, id
|
|
132
|
+
# stable across renames so the UI can keep pointing at the same playlist
|
|
133
|
+
# while its name changes underneath it. All access goes through _lock,
|
|
134
|
+
# same as _hosts/_jobs.
|
|
135
|
+
_library = {'downloads': {}, 'likes': [], 'subscriptions': [], 'playlists': []}
|
|
130
136
|
|
|
131
137
|
# what to (re-)host on startup -- _hosts above is pure in-memory runtime
|
|
132
138
|
# state, so a restart (a fresh `make node`, a Docker container recreated
|
|
@@ -245,6 +251,7 @@ def _load_library():
|
|
|
245
251
|
'downloads': data.get('downloads') or {},
|
|
246
252
|
'likes': data.get('likes') or [],
|
|
247
253
|
'subscriptions': data.get('subscriptions') or [],
|
|
254
|
+
'playlists': data.get('playlists') or [],
|
|
248
255
|
}
|
|
249
256
|
except (FileNotFoundError, json.JSONDecodeError):
|
|
250
257
|
pass
|
|
@@ -351,8 +358,21 @@ def _run_host_job(host_id, archive_dir, file_name, port, price, relay_urls, adve
|
|
|
351
358
|
for entry in entries:
|
|
352
359
|
for relay_url in relay_urls:
|
|
353
360
|
host_addr = f'{advertise_host}:{port}'
|
|
354
|
-
node.publish(identity, relay_url, entry['sha256'], entry['name'], host_addr,
|
|
355
|
-
|
|
361
|
+
result = node.publish(identity, relay_url, entry['sha256'], entry['name'], host_addr,
|
|
362
|
+
tunnel=tunnel, ott_status=ott_status)
|
|
363
|
+
# publish()/post_event() report a failed announce as a
|
|
364
|
+
# normal {'ok': False, ...} return, not an exception (an
|
|
365
|
+
# unreachable or malformed relay is routine, not
|
|
366
|
+
# exceptional -- see post_event's own docstring) -- which
|
|
367
|
+
# this loop used to just ignore entirely, so a host could
|
|
368
|
+
# sit at status: running, "announced" on a relay it never
|
|
369
|
+
# actually reached, with nothing in the UI to say so.
|
|
370
|
+
# do_host (shell.py) already prints every result
|
|
371
|
+
# unconditionally; this only prints the failures; a
|
|
372
|
+
# working host's log staying free of routine "ok: True"
|
|
373
|
+
# noise matters more here than in a CLI's own scrollback.
|
|
374
|
+
if isinstance(result, dict) and not result.get('ok', True):
|
|
375
|
+
print(f' ✗ announce to {relay_url} failed: {result.get("error")}')
|
|
356
376
|
announced = relay_urls
|
|
357
377
|
files = [{'name': e['name'], 'content_hash': e['sha256']} for e in entries]
|
|
358
378
|
with _lock:
|
|
@@ -479,6 +499,7 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
479
499
|
'downloads': list(_library['downloads'].values()),
|
|
480
500
|
'likes': list(_library['likes']),
|
|
481
501
|
'subscriptions': list(_library['subscriptions']),
|
|
502
|
+
'playlists': list(_library['playlists']),
|
|
482
503
|
})
|
|
483
504
|
if path.startswith('/api/download/'):
|
|
484
505
|
job_id = path[len('/api/download/'):]
|
|
@@ -539,6 +560,12 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
539
560
|
'/api/host': self._handle_host, '/api/download': self._handle_download,
|
|
540
561
|
'/api/like': self._handle_like, '/api/subscribe': self._handle_subscribe,
|
|
541
562
|
'/api/verify': self._handle_verify,
|
|
563
|
+
'/api/playlists/create': self._handle_playlist_create,
|
|
564
|
+
'/api/playlists/rename': self._handle_playlist_rename,
|
|
565
|
+
'/api/playlists/delete': self._handle_playlist_delete,
|
|
566
|
+
'/api/playlists/add': self._handle_playlist_add,
|
|
567
|
+
'/api/playlists/remove': self._handle_playlist_remove,
|
|
568
|
+
'/api/playlists/move': self._handle_playlist_move,
|
|
542
569
|
}
|
|
543
570
|
handler = handlers.get(path)
|
|
544
571
|
if not handler:
|
|
@@ -617,6 +644,94 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
617
644
|
_save_library()
|
|
618
645
|
self._json({'result': result})
|
|
619
646
|
|
|
647
|
+
def _handle_playlist_create(self, body):
|
|
648
|
+
name = (body.get('name') or '').strip()
|
|
649
|
+
if not name:
|
|
650
|
+
return self._json({'error': 'name required'}, status=400)
|
|
651
|
+
playlist = {'id': uuid.uuid4().hex[:12], 'name': name, 'items': []}
|
|
652
|
+
with _lock:
|
|
653
|
+
_library['playlists'].append(playlist)
|
|
654
|
+
_save_library()
|
|
655
|
+
self._json({'playlist': playlist})
|
|
656
|
+
|
|
657
|
+
def _handle_playlist_rename(self, body):
|
|
658
|
+
playlist_id = body.get('playlist_id')
|
|
659
|
+
name = (body.get('name') or '').strip()
|
|
660
|
+
if not playlist_id or not name:
|
|
661
|
+
return self._json({'error': 'playlist_id and name required'}, status=400)
|
|
662
|
+
with _lock:
|
|
663
|
+
playlist = next((p for p in _library['playlists'] if p['id'] == playlist_id), None)
|
|
664
|
+
if not playlist:
|
|
665
|
+
return self._json({'error': 'no such playlist'}, status=404)
|
|
666
|
+
playlist['name'] = name
|
|
667
|
+
_save_library()
|
|
668
|
+
self._json({'playlist': playlist})
|
|
669
|
+
|
|
670
|
+
def _handle_playlist_delete(self, body):
|
|
671
|
+
playlist_id = body.get('playlist_id')
|
|
672
|
+
if not playlist_id:
|
|
673
|
+
return self._json({'error': 'playlist_id required'}, status=400)
|
|
674
|
+
with _lock:
|
|
675
|
+
_library['playlists'] = [p for p in _library['playlists'] if p['id'] != playlist_id]
|
|
676
|
+
_save_library()
|
|
677
|
+
self._json({'ok': True})
|
|
678
|
+
|
|
679
|
+
def _handle_playlist_add(self, body):
|
|
680
|
+
playlist_id = body.get('playlist_id')
|
|
681
|
+
content_hash = body.get('content_hash')
|
|
682
|
+
if not playlist_id or not content_hash:
|
|
683
|
+
return self._json({'error': 'playlist_id and content_hash required'}, status=400)
|
|
684
|
+
with _lock:
|
|
685
|
+
playlist = next((p for p in _library['playlists'] if p['id'] == playlist_id), None)
|
|
686
|
+
if not playlist:
|
|
687
|
+
return self._json({'error': 'no such playlist'}, status=404)
|
|
688
|
+
# re-adding something already in the playlist just moves it to
|
|
689
|
+
# this spot instead of piling up a duplicate entry -- same
|
|
690
|
+
# dedup-by-content_hash instinct _library['downloads'] already
|
|
691
|
+
# has, just for a list instead of a dict
|
|
692
|
+
playlist['items'] = [it for it in playlist['items'] if it['content_hash'] != content_hash]
|
|
693
|
+
playlist['items'].append({
|
|
694
|
+
'content_hash': content_hash,
|
|
695
|
+
'title': body.get('title') or None,
|
|
696
|
+
'signer_pubkey': body.get('signer_pubkey') or None,
|
|
697
|
+
})
|
|
698
|
+
_save_library()
|
|
699
|
+
self._json({'playlist': playlist})
|
|
700
|
+
|
|
701
|
+
def _handle_playlist_remove(self, body):
|
|
702
|
+
playlist_id = body.get('playlist_id')
|
|
703
|
+
content_hash = body.get('content_hash')
|
|
704
|
+
if not playlist_id or not content_hash:
|
|
705
|
+
return self._json({'error': 'playlist_id and content_hash required'}, status=400)
|
|
706
|
+
with _lock:
|
|
707
|
+
playlist = next((p for p in _library['playlists'] if p['id'] == playlist_id), None)
|
|
708
|
+
if not playlist:
|
|
709
|
+
return self._json({'error': 'no such playlist'}, status=404)
|
|
710
|
+
playlist['items'] = [it for it in playlist['items'] if it['content_hash'] != content_hash]
|
|
711
|
+
_save_library()
|
|
712
|
+
self._json({'playlist': playlist})
|
|
713
|
+
|
|
714
|
+
def _handle_playlist_move(self, body):
|
|
715
|
+
playlist_id = body.get('playlist_id')
|
|
716
|
+
content_hash = body.get('content_hash')
|
|
717
|
+
direction = body.get('direction')
|
|
718
|
+
if not playlist_id or not content_hash or direction not in ('up', 'down'):
|
|
719
|
+
return self._json({'error': "playlist_id, content_hash, and direction ('up'|'down') required"},
|
|
720
|
+
status=400)
|
|
721
|
+
with _lock:
|
|
722
|
+
playlist = next((p for p in _library['playlists'] if p['id'] == playlist_id), None)
|
|
723
|
+
if not playlist:
|
|
724
|
+
return self._json({'error': 'no such playlist'}, status=404)
|
|
725
|
+
items = playlist['items']
|
|
726
|
+
idx = next((i for i, it in enumerate(items) if it['content_hash'] == content_hash), None)
|
|
727
|
+
if idx is None:
|
|
728
|
+
return self._json({'error': 'content_hash not in this playlist'}, status=404)
|
|
729
|
+
swap_with = idx - 1 if direction == 'up' else idx + 1
|
|
730
|
+
if 0 <= swap_with < len(items):
|
|
731
|
+
items[idx], items[swap_with] = items[swap_with], items[idx]
|
|
732
|
+
_save_library()
|
|
733
|
+
self._json({'playlist': playlist})
|
|
734
|
+
|
|
620
735
|
def _handle_verify(self, body):
|
|
621
736
|
"""Re-checks an already-downloaded file against its own
|
|
622
737
|
Merkle-committed chunk hashes without re-fetching it -- see
|
|
@@ -188,6 +188,18 @@ def cmd_host(args):
|
|
|
188
188
|
# matching chunk data would otherwise get announced to the relay and
|
|
189
189
|
# only fail later, in the background server thread
|
|
190
190
|
all_leaves = {e['sha256']: node.load_leaves(args.archive_dir, e['sha256']) for e in entries}
|
|
191
|
+
# Bind the real listening socket now, before announcing anything --
|
|
192
|
+
# same reasoning as web_ui.py's _run_host_job and shell.py's do_host:
|
|
193
|
+
# a port already in use only used to surface once run_host_server got
|
|
194
|
+
# around to its own (later) bind, by which point every relay below
|
|
195
|
+
# had already been told this host is reachable at host_addr. Binding
|
|
196
|
+
# first and reusing the same socket in run_host_server (see sock=
|
|
197
|
+
# below) means a taken port fails loudly here instead, before a
|
|
198
|
+
# single relay ever hears about it.
|
|
199
|
+
try:
|
|
200
|
+
bound_sock = node.bind_host_port(args.port)
|
|
201
|
+
except OSError as e:
|
|
202
|
+
sys.exit(f'cannot bind port {args.port}: {e} — not announcing, nothing started')
|
|
191
203
|
# one archive_dir, one merkle root, one commit -- computed once outside
|
|
192
204
|
# the per-relay/per-entry loops below, not per file
|
|
193
205
|
ott_status = node.ott_commit_status(args.archive_dir)
|
|
@@ -213,7 +225,7 @@ def cmd_host(args):
|
|
|
213
225
|
kwargs={'use_tls': use_tls, 'ln_node': args.lightning_node},
|
|
214
226
|
daemon=True).start()
|
|
215
227
|
node.run_host_server(args.archive_dir, args.file, args.port, price=args.price,
|
|
216
|
-
ln_node=args.lightning_node)
|
|
228
|
+
ln_node=args.lightning_node, sock=bound_sock)
|
|
217
229
|
|
|
218
230
|
|
|
219
231
|
def cmd_discover(args):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: weed-cli
|
|
3
|
-
Version: 1.6.
|
|
3
|
+
Version: 1.6.8
|
|
4
4
|
Summary: Censorship-resistant video PoC — discovery/hosting/download over signed relay events, a real Kademlia DHT, or a TLS-capable NAT-traversal tunnel
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: p2p,video,censorship-resistant,discovery,dht,kademlia,nat-traversal
|
|
@@ -174,19 +174,28 @@ blind.
|
|
|
174
174
|
### Docker
|
|
175
175
|
|
|
176
176
|
`Dockerfile.node` + `docker-compose.node.yml` package `web_ui.py` to run
|
|
177
|
-
somewhere other than a laptop
|
|
177
|
+
somewhere other than a laptop — alongside its own local discovery relay
|
|
178
|
+
(`Dockerfile.discovery-relay`), so there's always something real for it
|
|
179
|
+
to announce to and discover from out of the box.
|
|
178
180
|
|
|
179
181
|
```bash
|
|
180
182
|
make node # build + run, http://127.0.0.1:8080
|
|
181
183
|
make node-down # stop it — data persists
|
|
184
|
+
make node-shell # interactive weed shell, same container as the running node
|
|
182
185
|
WEED_SHARE_DIR=~/Movies make node # mount a real directory of .ott archives at /share
|
|
183
|
-
docker compose -f docker-compose.node.yml exec node python3 shell.py # interactive shell, same container
|
|
184
186
|
docker compose -f docker-compose.node.yml exec node python3 weed.py discover # or a one-off CLI command
|
|
185
187
|
```
|
|
186
188
|
|
|
187
189
|
- Identity key, reputation store, and library manifest all persist in a
|
|
188
190
|
named volume (`node-data`, mounted at `$HOME=/data`) — one node keeps
|
|
189
191
|
the same pubkey (and everything vouching for it) across restarts.
|
|
192
|
+
- The `relay` service is `node`'s default `WEED_RELAY` (`http://relay:9101`,
|
|
193
|
+
resolved over Compose's own network) — its events persist in their own
|
|
194
|
+
`relay-data` volume, and port 9101 is published to the host too, for
|
|
195
|
+
curling `/events` directly or pointing a non-Docker `weed`/shell session
|
|
196
|
+
at the exact same relay. Override `WEED_RELAY` (shell env or `.env`,
|
|
197
|
+
see `.env.sample`) to point at public infra instead, e.g. for a real
|
|
198
|
+
end-to-end test.
|
|
190
199
|
- `/share` is a separate *bind* mount, not a named volume, so the actual
|
|
191
200
|
`.ott` archives you're hosting are real files on the host you can see
|
|
192
201
|
and manage directly. Point the web UI's Host form at `/share`.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|