weed-cli 1.6.0__tar.gz → 1.6.2__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.0/weed_cli.egg-info → weed_cli-1.6.2}/PKG-INFO +3 -1
- {weed_cli-1.6.0 → weed_cli-1.6.2}/README.md +2 -0
- {weed_cli-1.6.0 → weed_cli-1.6.2}/node.py +87 -11
- {weed_cli-1.6.0 → weed_cli-1.6.2}/pyproject.toml +1 -1
- {weed_cli-1.6.0 → weed_cli-1.6.2}/web_ui.py +69 -32
- {weed_cli-1.6.0 → weed_cli-1.6.2/weed_cli.egg-info}/PKG-INFO +3 -1
- {weed_cli-1.6.0 → weed_cli-1.6.2}/LICENSE +0 -0
- {weed_cli-1.6.0 → weed_cli-1.6.2}/dht.py +0 -0
- {weed_cli-1.6.0 → weed_cli-1.6.2}/discovery_relay.py +0 -0
- {weed_cli-1.6.0 → weed_cli-1.6.2}/lightning_settle.py +0 -0
- {weed_cli-1.6.0 → weed_cli-1.6.2}/poc_reputation.py +0 -0
- {weed_cli-1.6.0 → weed_cli-1.6.2}/setup.cfg +0 -0
- {weed_cli-1.6.0 → weed_cli-1.6.2}/shell.py +0 -0
- {weed_cli-1.6.0 → weed_cli-1.6.2}/tunnel_relay.py +0 -0
- {weed_cli-1.6.0 → weed_cli-1.6.2}/weed.py +0 -0
- {weed_cli-1.6.0 → weed_cli-1.6.2}/weed_cli.egg-info/SOURCES.txt +0 -0
- {weed_cli-1.6.0 → weed_cli-1.6.2}/weed_cli.egg-info/dependency_links.txt +0 -0
- {weed_cli-1.6.0 → weed_cli-1.6.2}/weed_cli.egg-info/entry_points.txt +0 -0
- {weed_cli-1.6.0 → weed_cli-1.6.2}/weed_cli.egg-info/requires.txt +0 -0
- {weed_cli-1.6.0 → weed_cli-1.6.2}/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.2
|
|
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
|
|
@@ -180,6 +180,8 @@ somewhere other than a laptop.
|
|
|
180
180
|
make node # build + run, http://127.0.0.1:8080
|
|
181
181
|
make node-down # stop it — data persists
|
|
182
182
|
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
|
+
docker compose -f docker-compose.node.yml exec node python3 weed.py discover # or a one-off CLI command
|
|
183
185
|
```
|
|
184
186
|
|
|
185
187
|
- Identity key, reputation store, and library manifest all persist in a
|
|
@@ -150,6 +150,8 @@ somewhere other than a laptop.
|
|
|
150
150
|
make node # build + run, http://127.0.0.1:8080
|
|
151
151
|
make node-down # stop it — data persists
|
|
152
152
|
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
|
+
docker compose -f docker-compose.node.yml exec node python3 weed.py discover # or a one-off CLI command
|
|
153
155
|
```
|
|
154
156
|
|
|
155
157
|
- Identity key, reputation store, and library manifest all persist in a
|
|
@@ -19,6 +19,7 @@ every previous script verified chunks locally or over a network, none of
|
|
|
19
19
|
them reassembled a full file from a remote peer onto disk before this.
|
|
20
20
|
"""
|
|
21
21
|
import base64
|
|
22
|
+
import concurrent.futures
|
|
22
23
|
import hashlib
|
|
23
24
|
import json
|
|
24
25
|
import os
|
|
@@ -44,8 +45,12 @@ TAGLINE = 'we do in 4 what others do in 5'
|
|
|
44
45
|
|
|
45
46
|
|
|
46
47
|
def weed_version():
|
|
47
|
-
"""Installed package version (pyproject.toml's source of truth), with
|
|
48
|
-
|
|
48
|
+
"""Installed package version (pyproject.toml's source of truth), with
|
|
49
|
+
fallbacks for the two other real ways this runs: straight from a git
|
|
50
|
+
checkout without `pip install -e .` (read pyproject.toml directly —
|
|
51
|
+
Dockerfile.node copies it in for exactly this, since it copies loose
|
|
52
|
+
.py files rather than pip-installing the package), or neither file
|
|
53
|
+
present at all (genuinely no version info available)."""
|
|
49
54
|
try:
|
|
50
55
|
from importlib.metadata import version, PackageNotFoundError
|
|
51
56
|
try:
|
|
@@ -54,6 +59,15 @@ def weed_version():
|
|
|
54
59
|
pass
|
|
55
60
|
except ImportError:
|
|
56
61
|
pass
|
|
62
|
+
pyproject_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'pyproject.toml')
|
|
63
|
+
try:
|
|
64
|
+
with open(pyproject_path) as f:
|
|
65
|
+
for line in f:
|
|
66
|
+
line = line.strip()
|
|
67
|
+
if line.startswith('version'):
|
|
68
|
+
return line.split('=', 1)[1].strip().strip('"\'')
|
|
69
|
+
except (OSError, IndexError):
|
|
70
|
+
pass
|
|
57
71
|
return '0.0.0-dev'
|
|
58
72
|
|
|
59
73
|
|
|
@@ -65,9 +79,36 @@ def _git_commit_hash():
|
|
|
65
79
|
behind a real debugging session: two checkouts reporting the same
|
|
66
80
|
version, one of them missing a fix the other had. None for anything
|
|
67
81
|
that isn't a git checkout at all (installed from a built wheel/sdist,
|
|
68
|
-
no .git present) — a version number is all there is to go on there.
|
|
82
|
+
no .git present) — a version number is all there is to go on there.
|
|
83
|
+
|
|
84
|
+
$WEED_GIT_COMMIT checked first — Dockerfile.node has no .git directory
|
|
85
|
+
at all (never copied in, on purpose: bloats the image and ships the
|
|
86
|
+
full history for no reason), so a container build has no way to
|
|
87
|
+
answer this live. docker-compose.node.yml passes the *host's* commit
|
|
88
|
+
hash in as a build arg at image-build time instead, baked in as this
|
|
89
|
+
env var, same intent as the live git lookup below just computed once,
|
|
90
|
+
earlier, somewhere that actually has the repo.
|
|
91
|
+
|
|
92
|
+
`git rev-parse` walks *up* parent directories looking for a .git —
|
|
93
|
+
a real `pip install .` (not `-e`) copies this file into site-packages,
|
|
94
|
+
which usually isn't a checkout at all, but if it happens to be nested
|
|
95
|
+
anywhere under some unrelated git-tracked ancestor directory (a
|
|
96
|
+
dotfiles repo, a pyenv install tracked in git, anything above it),
|
|
97
|
+
this would otherwise silently report *that* repo's own unrelated
|
|
98
|
+
commit — worse than showing nothing, since it looks plausible. Only
|
|
99
|
+
trust the hash once --show-toplevel confirms this file's own
|
|
100
|
+
directory really is that repo's root."""
|
|
101
|
+
env_commit = os.environ.get('WEED_GIT_COMMIT')
|
|
102
|
+
if env_commit:
|
|
103
|
+
return env_commit
|
|
69
104
|
repo_dir = os.path.dirname(os.path.abspath(__file__))
|
|
70
105
|
try:
|
|
106
|
+
toplevel = subprocess.run(['git', 'rev-parse', '--show-toplevel'], cwd=repo_dir,
|
|
107
|
+
capture_output=True, text=True, timeout=2)
|
|
108
|
+
if toplevel.returncode != 0:
|
|
109
|
+
return None
|
|
110
|
+
if os.path.realpath(toplevel.stdout.strip()) != os.path.realpath(repo_dir):
|
|
111
|
+
return None
|
|
71
112
|
result = subprocess.run(['git', 'rev-parse', '--short', 'HEAD'], cwd=repo_dir,
|
|
72
113
|
capture_output=True, text=True, timeout=2)
|
|
73
114
|
if result.returncode == 0:
|
|
@@ -337,8 +378,24 @@ def serve_session(conn, entries_by_hash, default_hash, price, ln_node=None):
|
|
|
337
378
|
_graceful_close(conn)
|
|
338
379
|
|
|
339
380
|
|
|
381
|
+
def bind_host_port(port, bind_host='0.0.0.0'):
|
|
382
|
+
"""Split out of run_host_server so a caller (see web_ui.py's
|
|
383
|
+
_run_host_job) can bind the real, permanent listening socket early —
|
|
384
|
+
right where a port-collision needs to fail fast, before announcing
|
|
385
|
+
anything — and hand that exact socket to run_host_server later
|
|
386
|
+
instead of it binding a second, separate one. A bind-then-close probe
|
|
387
|
+
followed by a *second*, later bind of the same port isn't atomic
|
|
388
|
+
across two hosts starting concurrently: both can pass the probe
|
|
389
|
+
before either does the real bind. Binding once, early, and reusing
|
|
390
|
+
the same socket removes that race entirely instead of narrowing it."""
|
|
391
|
+
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
392
|
+
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
393
|
+
srv.bind((bind_host, port))
|
|
394
|
+
return srv
|
|
395
|
+
|
|
396
|
+
|
|
340
397
|
def run_host_server(archive_dir, file_name, port, bind_host='0.0.0.0', quiet=False, price=0,
|
|
341
|
-
ln_node=None):
|
|
398
|
+
ln_node=None, sock=None):
|
|
342
399
|
archive_dir = os.path.expanduser(archive_dir)
|
|
343
400
|
entries = load_manifest_entries(archive_dir, file_name)
|
|
344
401
|
entries_by_hash = {}
|
|
@@ -350,9 +407,7 @@ def run_host_server(archive_dir, file_name, port, bind_host='0.0.0.0', quiet=Fal
|
|
|
350
407
|
entries_by_hash[entry['sha256']] = (entry, leaves, file_path)
|
|
351
408
|
default_hash = next(iter(entries_by_hash)) if len(entries_by_hash) == 1 else None
|
|
352
409
|
|
|
353
|
-
srv =
|
|
354
|
-
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
355
|
-
srv.bind((bind_host, port))
|
|
410
|
+
srv = sock if sock is not None else bind_host_port(port, bind_host)
|
|
356
411
|
srv.listen(8)
|
|
357
412
|
if not quiet:
|
|
358
413
|
# a background thread's print() races with cmd.Cmd's input()-driven
|
|
@@ -979,6 +1034,27 @@ def _relay_url_hint(relay_url):
|
|
|
979
1034
|
return None
|
|
980
1035
|
|
|
981
1036
|
|
|
1037
|
+
_dns_bound_pool = concurrent.futures.ThreadPoolExecutor(max_workers=8, thread_name_prefix='urlopen')
|
|
1038
|
+
|
|
1039
|
+
|
|
1040
|
+
def _urlopen_bounded(req_or_url, timeout=5):
|
|
1041
|
+
"""urlopen's own timeout= only bounds the connect/read phases — the
|
|
1042
|
+
getaddrinfo() call underneath (DNS resolution) is a blocking libc call
|
|
1043
|
+
with no timeout of its own, so a relay hostname that's slow or
|
|
1044
|
+
unreachable to resolve can hang here indefinitely no matter what
|
|
1045
|
+
timeout= says. That's exactly the situation right after a container
|
|
1046
|
+
boots and its network/DNS isn't fully settled yet: _resume_persisted_hosts
|
|
1047
|
+
(web_ui.py) resuming a host at that moment used to get stuck at
|
|
1048
|
+
"(starting…)" forever, since nothing downstream ever times out either.
|
|
1049
|
+
Running the call in a worker thread and bounding *that* with
|
|
1050
|
+
.result(timeout=) catches DNS hangs the same as connect/read hangs —
|
|
1051
|
+
the stuck worker thread leaks (a blocking C call can't be cancelled),
|
|
1052
|
+
but it's a daemon-pool thread and this process was going to give up and
|
|
1053
|
+
move on regardless."""
|
|
1054
|
+
future = _dns_bound_pool.submit(urllib.request.urlopen, req_or_url, timeout=timeout)
|
|
1055
|
+
return future.result(timeout=timeout + 1)
|
|
1056
|
+
|
|
1057
|
+
|
|
982
1058
|
def post_event(relay_url, event):
|
|
983
1059
|
hint = _relay_url_hint(relay_url)
|
|
984
1060
|
if hint:
|
|
@@ -987,11 +1063,11 @@ def post_event(relay_url, event):
|
|
|
987
1063
|
f'{relay_url}/event', data=json.dumps(event).encode(),
|
|
988
1064
|
headers={'Content-Type': 'application/json'}, method='POST')
|
|
989
1065
|
try:
|
|
990
|
-
with
|
|
1066
|
+
with _urlopen_bounded(req, timeout=5) as resp:
|
|
991
1067
|
return json.loads(resp.read())
|
|
992
1068
|
except urllib.error.HTTPError as e:
|
|
993
1069
|
return json.loads(e.read())
|
|
994
|
-
except (urllib.error.URLError, ConnectionRefusedError) as e:
|
|
1070
|
+
except (urllib.error.URLError, ConnectionRefusedError, concurrent.futures.TimeoutError) as e:
|
|
995
1071
|
# download_with_auction posts the post-download attestation to every
|
|
996
1072
|
# relay in relay_urls, which by default includes 127.0.0.1:9101
|
|
997
1073
|
# whether or not anything is actually listening there (same default
|
|
@@ -1008,9 +1084,9 @@ def fetch_events(relay_url, event_type=None):
|
|
|
1008
1084
|
return None
|
|
1009
1085
|
url = f'{relay_url}/events' + (f'?type={event_type}' if event_type else '')
|
|
1010
1086
|
try:
|
|
1011
|
-
with
|
|
1087
|
+
with _urlopen_bounded(url, timeout=5) as resp:
|
|
1012
1088
|
return json.loads(resp.read())
|
|
1013
|
-
except (urllib.error.URLError, ConnectionRefusedError):
|
|
1089
|
+
except (urllib.error.URLError, ConnectionRefusedError, concurrent.futures.TimeoutError):
|
|
1014
1090
|
return None
|
|
1015
1091
|
|
|
1016
1092
|
|
|
@@ -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.2"
|
|
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" }
|
|
@@ -187,9 +187,35 @@ def _ott_status(archive_dir):
|
|
|
187
187
|
|
|
188
188
|
|
|
189
189
|
def _resume_persisted_hosts():
|
|
190
|
+
"""Sequential on purpose, not fire-and-forget-them-all-at-once: two
|
|
191
|
+
persisted configs sharing a port (the default in the Host form is
|
|
192
|
+
always 9201, so this is the common case, not an edge case) could
|
|
193
|
+
both bind successfully if started concurrently, since node.bind_host_port
|
|
194
|
+
for the second one wouldn't even run until the first's already had time
|
|
195
|
+
to occupy the port — but nothing serializes *when* each thread gets
|
|
196
|
+
there. Waiting for each host to reach 'bound' (or 'error', if the bind
|
|
197
|
+
itself failed) before starting the next one closes that window: by the
|
|
198
|
+
time the next one binds, the previous one already has the port or
|
|
199
|
+
already knows it can't.
|
|
200
|
+
|
|
201
|
+
Only waits for the bind, not all the way to 'running' — 'running'
|
|
202
|
+
doesn't happen until after each host's full publish loop (real network
|
|
203
|
+
I/O, up to 5s timeout per relay), which several persisted hosts
|
|
204
|
+
resuming in sequence have no reason to serialize on. That was a real
|
|
205
|
+
regression the first version of this function had: booting with a
|
|
206
|
+
handful of persisted hosts got noticeably slower, for zero extra
|
|
207
|
+
correctness, since the publish delay doesn't affect port ownership at
|
|
208
|
+
all."""
|
|
190
209
|
for cfg in _persisted_hosts.values():
|
|
191
|
-
_start_host_job(cfg['archive_dir'], cfg['file_name'], cfg['port'], cfg['price'],
|
|
192
|
-
|
|
210
|
+
host_id = _start_host_job(cfg['archive_dir'], cfg['file_name'], cfg['port'], cfg['price'],
|
|
211
|
+
cfg['relay'], cfg['advertise_host'], cfg['tunnel'],
|
|
212
|
+
cfg.get('lightning_node'))
|
|
213
|
+
for _ in range(100): # up to ~10s per host before moving on regardless
|
|
214
|
+
with _lock:
|
|
215
|
+
status = _hosts[host_id]['status']
|
|
216
|
+
if status in ('bound', 'running', 'error'):
|
|
217
|
+
break
|
|
218
|
+
time.sleep(0.1)
|
|
193
219
|
|
|
194
220
|
|
|
195
221
|
def _unpublish_all_hosts():
|
|
@@ -284,29 +310,6 @@ def _detect_lan_ip():
|
|
|
284
310
|
s.close()
|
|
285
311
|
|
|
286
312
|
|
|
287
|
-
def _probe_port_free(port, bind_host='0.0.0.0'):
|
|
288
|
-
"""Bind-and-immediately-release, purely to fail fast on a port
|
|
289
|
-
collision *before* announcing anything — see _run_host_job's real
|
|
290
|
-
incident: the web UI's Host form always defaults to port 9201, so
|
|
291
|
-
hosting more than one file (including multiple entries auto-resumed
|
|
292
|
-
at once on startup, see _resume_persisted_hosts) without picking a
|
|
293
|
-
distinct port each time meant every loser published a real, signed
|
|
294
|
-
announcement and only discovered it couldn't actually bind *after*
|
|
295
|
-
— a permanent, unreachable zombie listing nothing could ever clean
|
|
296
|
-
up, since a host that never reaches 'running' never gets files/
|
|
297
|
-
announced_on populated for the graceful-shutdown unpublish to use
|
|
298
|
-
either. Still has a small window (this socket closes before
|
|
299
|
-
run_host_server's own real bind), not a hard guarantee — but it
|
|
300
|
-
catches the common case (concurrent startup, the actual incident)
|
|
301
|
-
instead of the current zero attempt to catch it at all."""
|
|
302
|
-
probe = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
303
|
-
probe.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
304
|
-
try:
|
|
305
|
-
probe.bind((bind_host, port))
|
|
306
|
-
finally:
|
|
307
|
-
probe.close()
|
|
308
|
-
|
|
309
|
-
|
|
310
313
|
def _run_host_job(host_id, archive_dir, file_name, port, price, relay_urls, advertise_host, tunnel,
|
|
311
314
|
ln_node=None):
|
|
312
315
|
captured = io.StringIO()
|
|
@@ -325,10 +328,24 @@ def _run_host_job(host_id, archive_dir, file_name, port, price, relay_urls, adve
|
|
|
325
328
|
# and only fail later, deep in a background thread with no way
|
|
326
329
|
# for the UI to ever find out
|
|
327
330
|
all_leaves = {e['sha256']: node.load_leaves(archive_dir, e['sha256']) for e in entries}
|
|
328
|
-
#
|
|
329
|
-
#
|
|
330
|
-
#
|
|
331
|
-
|
|
331
|
+
# bind the real, permanent listening socket now — before
|
|
332
|
+
# announcing anything — and hand it to run_host_server below
|
|
333
|
+
# instead of it binding a second one later. A separate
|
|
334
|
+
# bind-then-close probe here, followed by a *later* real bind,
|
|
335
|
+
# isn't atomic across two hosts starting concurrently (see
|
|
336
|
+
# _resume_persisted_hosts): both could pass the probe before
|
|
337
|
+
# either did the real bind. One bind, kept alive and reused,
|
|
338
|
+
# has no such window — the port either really is free right
|
|
339
|
+
# now or this raises immediately, before any publish.
|
|
340
|
+
bound_sock = node.bind_host_port(port)
|
|
341
|
+
# a distinct status the instant the bind (the only part
|
|
342
|
+
# _resume_persisted_hosts actually needs to wait for) is done
|
|
343
|
+
# — 'running' doesn't happen until after the publish loop
|
|
344
|
+
# below, which is real network I/O (up to 5s timeout *per*
|
|
345
|
+
# relay, see post_event) that resuming several hosts
|
|
346
|
+
# sequentially has no reason to serialize on
|
|
347
|
+
with _lock:
|
|
348
|
+
_hosts[host_id]['status'] = 'bound'
|
|
332
349
|
ott_status = node.ott_commit_status(archive_dir)
|
|
333
350
|
announced = []
|
|
334
351
|
for entry in entries:
|
|
@@ -354,7 +371,8 @@ def _run_host_job(host_id, archive_dir, file_name, port, price, relay_urls, adve
|
|
|
354
371
|
all_leaves[entry['sha256']], file_path, price),
|
|
355
372
|
kwargs={'use_tls': use_tls, 'quiet': True, 'ln_node': ln_node},
|
|
356
373
|
daemon=True).start()
|
|
357
|
-
node.run_host_server(archive_dir, file_name, port, quiet=True, price=price, ln_node=ln_node
|
|
374
|
+
node.run_host_server(archive_dir, file_name, port, quiet=True, price=price, ln_node=ln_node,
|
|
375
|
+
sock=bound_sock)
|
|
358
376
|
except SystemExit as e:
|
|
359
377
|
with _lock:
|
|
360
378
|
_hosts[host_id].update(status='error', error=_with_captured_detail(str(e), captured))
|
|
@@ -374,6 +392,11 @@ def _run_download_job(job_id, content_hash, relay_urls, out_path, k, use_lightni
|
|
|
374
392
|
try:
|
|
375
393
|
t0 = time.time()
|
|
376
394
|
with _quiet() as captured:
|
|
395
|
+
# registered as soon as it's the real live buffer (not the
|
|
396
|
+
# placeholder above) so /api/download/<job_id> can read
|
|
397
|
+
# node.py's real prints as they happen, not just after the
|
|
398
|
+
# job finishes — see the GET handler's use of _job_logs
|
|
399
|
+
_job_logs[job_id] = captured
|
|
377
400
|
path = node.download_with_auction(content_hash, relay_urls, out_path=out_path, k=k,
|
|
378
401
|
use_lightning=use_lightning, lightning_node=lightning_node,
|
|
379
402
|
on_progress=on_progress)
|
|
@@ -458,9 +481,16 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
458
481
|
'subscriptions': list(_library['subscriptions']),
|
|
459
482
|
})
|
|
460
483
|
if path.startswith('/api/download/'):
|
|
484
|
+
job_id = path[len('/api/download/'):]
|
|
461
485
|
with _lock:
|
|
462
|
-
job = _jobs.get(
|
|
463
|
-
|
|
486
|
+
job = _jobs.get(job_id)
|
|
487
|
+
job = dict(job) if job else None
|
|
488
|
+
if not job:
|
|
489
|
+
return self._json({'error': 'no such job'}, status=404)
|
|
490
|
+
log_buf = _job_logs.get(job_id)
|
|
491
|
+
if log_buf is not None:
|
|
492
|
+
job['log'] = log_buf.getvalue()
|
|
493
|
+
return self._json(job)
|
|
464
494
|
if path.startswith('/api/reputation/'):
|
|
465
495
|
from poc_reputation import ReputationStore
|
|
466
496
|
pubkey = path[len('/api/reputation/'):]
|
|
@@ -755,6 +785,13 @@ def run_web_ui(port=8080, bind_host='127.0.0.1', quiet=False, advertise_host=Non
|
|
|
755
785
|
_lan_url = f'http://{reachable_host}:{port}/'
|
|
756
786
|
|
|
757
787
|
if not quiet:
|
|
788
|
+
# answers "is this container actually running the code I think it
|
|
789
|
+
# is" directly in `docker compose logs`/`make node`'s own output —
|
|
790
|
+
# see node.weed_banner()'s own docstring for exactly the debugging
|
|
791
|
+
# session that motivated adding a commit hash to it in the first
|
|
792
|
+
# place; printing it here means the same question doesn't need a
|
|
793
|
+
# docker exec + grep to answer for the web UI specifically
|
|
794
|
+
print(f"[web:{port}] {node.weed_banner()}", flush=True)
|
|
758
795
|
print(f"[web:{port}] weed control UI at http://{bind_host}:{port}/", flush=True)
|
|
759
796
|
if bind_host == '127.0.0.1':
|
|
760
797
|
print(" bound to localhost only -- pass --bind 0.0.0.0 to reach this from your "
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: weed-cli
|
|
3
|
-
Version: 1.6.
|
|
3
|
+
Version: 1.6.2
|
|
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
|
|
@@ -180,6 +180,8 @@ somewhere other than a laptop.
|
|
|
180
180
|
make node # build + run, http://127.0.0.1:8080
|
|
181
181
|
make node-down # stop it — data persists
|
|
182
182
|
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
|
+
docker compose -f docker-compose.node.yml exec node python3 weed.py discover # or a one-off CLI command
|
|
183
185
|
```
|
|
184
186
|
|
|
185
187
|
- Identity key, reputation store, and library manifest all persist in a
|
|
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
|
|
File without changes
|
|
File without changes
|