weed-cli 1.0.2__tar.gz → 1.1.3__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.0.2 → weed_cli-1.1.3}/PKG-INFO +1 -1
- {weed_cli-1.0.2 → weed_cli-1.1.3}/node.py +20 -0
- {weed_cli-1.0.2 → weed_cli-1.1.3}/pyproject.toml +1 -1
- {weed_cli-1.0.2 → weed_cli-1.1.3}/shell.py +1 -1
- {weed_cli-1.0.2 → weed_cli-1.1.3}/web_ui.py +84 -6
- {weed_cli-1.0.2 → weed_cli-1.1.3}/weed.py +3 -1
- {weed_cli-1.0.2 → weed_cli-1.1.3}/weed_cli.egg-info/PKG-INFO +1 -1
- {weed_cli-1.0.2 → weed_cli-1.1.3}/LICENSE +0 -0
- {weed_cli-1.0.2 → weed_cli-1.1.3}/README.md +0 -0
- {weed_cli-1.0.2 → weed_cli-1.1.3}/dht.py +0 -0
- {weed_cli-1.0.2 → weed_cli-1.1.3}/discovery_relay.py +0 -0
- {weed_cli-1.0.2 → weed_cli-1.1.3}/lightning_settle.py +0 -0
- {weed_cli-1.0.2 → weed_cli-1.1.3}/poc_reputation.py +0 -0
- {weed_cli-1.0.2 → weed_cli-1.1.3}/setup.cfg +0 -0
- {weed_cli-1.0.2 → weed_cli-1.1.3}/tunnel_relay.py +0 -0
- {weed_cli-1.0.2 → weed_cli-1.1.3}/weed_cli.egg-info/SOURCES.txt +0 -0
- {weed_cli-1.0.2 → weed_cli-1.1.3}/weed_cli.egg-info/dependency_links.txt +0 -0
- {weed_cli-1.0.2 → weed_cli-1.1.3}/weed_cli.egg-info/entry_points.txt +0 -0
- {weed_cli-1.0.2 → weed_cli-1.1.3}/weed_cli.egg-info/requires.txt +0 -0
- {weed_cli-1.0.2 → weed_cli-1.1.3}/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.
|
|
3
|
+
Version: 1.1.3
|
|
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
|
|
@@ -39,6 +39,26 @@ IDENTITY_PATH = os.path.expanduser('~/.weed_identity.key')
|
|
|
39
39
|
IDENTITY_ARMOR_HEADER = '-----BEGIN WEED IDENTITY KEY-----'
|
|
40
40
|
IDENTITY_ARMOR_FOOTER = '-----END WEED IDENTITY KEY-----'
|
|
41
41
|
|
|
42
|
+
TAGLINE = 'we do in 4 what others do in 5'
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def weed_version():
|
|
46
|
+
"""Installed package version (pyproject.toml's source of truth), with a
|
|
47
|
+
fallback for running straight from source without `pip install -e .`."""
|
|
48
|
+
try:
|
|
49
|
+
from importlib.metadata import version, PackageNotFoundError
|
|
50
|
+
try:
|
|
51
|
+
return version('weed-cli')
|
|
52
|
+
except PackageNotFoundError:
|
|
53
|
+
pass
|
|
54
|
+
except ImportError:
|
|
55
|
+
pass
|
|
56
|
+
return '0.0.0-dev'
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def weed_banner():
|
|
60
|
+
return f'weed v{weed_version()} — {TAGLINE}'
|
|
61
|
+
|
|
42
62
|
|
|
43
63
|
def _armor_identity(raw_bytes):
|
|
44
64
|
"""Base64 text with a header/footer — NOT real OpenPGP armor (no
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "weed-cli"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.1.3"
|
|
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" }
|
|
@@ -20,6 +20,7 @@ import mimetypes
|
|
|
20
20
|
import os
|
|
21
21
|
import sys
|
|
22
22
|
import threading
|
|
23
|
+
import time
|
|
23
24
|
import uuid
|
|
24
25
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
25
26
|
from urllib.parse import urlparse, parse_qs
|
|
@@ -35,6 +36,7 @@ except ImportError:
|
|
|
35
36
|
|
|
36
37
|
WEB_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'web')
|
|
37
38
|
DEFAULT_RELAY = 'http://127.0.0.1:9101'
|
|
39
|
+
LIBRARY_PATH = os.path.expanduser('~/.weed_library.json')
|
|
38
40
|
|
|
39
41
|
|
|
40
42
|
class _JobStdout:
|
|
@@ -82,6 +84,53 @@ def _quiet():
|
|
|
82
84
|
_hosts = {} # host_id -> dict describing an actively-hosted file
|
|
83
85
|
_jobs = {} # job_id -> dict describing a download's progress/result
|
|
84
86
|
_lock = threading.Lock()
|
|
87
|
+
|
|
88
|
+
# what's been downloaded/liked/subscribed, persisted to disk so a page
|
|
89
|
+
# reload -- or a server restart -- doesn't forget any of it. Downloads
|
|
90
|
+
# are keyed by content_hash (one record per piece of content, most
|
|
91
|
+
# recent job wins); likes/subscriptions are just lists of hashes/pubkeys.
|
|
92
|
+
# All access goes through _lock, same as _hosts/_jobs.
|
|
93
|
+
_library = {'downloads': {}, 'likes': [], 'subscriptions': []}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _load_library():
|
|
97
|
+
global _library
|
|
98
|
+
try:
|
|
99
|
+
with open(LIBRARY_PATH) as f:
|
|
100
|
+
data = json.load(f)
|
|
101
|
+
_library = {
|
|
102
|
+
'downloads': data.get('downloads') or {},
|
|
103
|
+
'likes': data.get('likes') or [],
|
|
104
|
+
'subscriptions': data.get('subscriptions') or [],
|
|
105
|
+
}
|
|
106
|
+
except (FileNotFoundError, json.JSONDecodeError):
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _save_library():
|
|
111
|
+
"""Caller must hold _lock. Written to a tmp file + os.replace so a
|
|
112
|
+
crash mid-write can't leave a half-written, unparseable JSON file
|
|
113
|
+
behind -- this runs on every like/subscribe/download-finish, not just
|
|
114
|
+
at shutdown."""
|
|
115
|
+
tmp = LIBRARY_PATH + '.tmp'
|
|
116
|
+
with open(tmp, 'w') as f:
|
|
117
|
+
json.dump(_library, f)
|
|
118
|
+
os.replace(tmp, LIBRARY_PATH)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _rehydrate_jobs_from_library():
|
|
122
|
+
"""Lets a restarted server's Downloads tab and Discover "already
|
|
123
|
+
downloaded" check keep working against the same job_ids as before --
|
|
124
|
+
_jobs is otherwise purely in-memory and would forget every finished
|
|
125
|
+
download on restart even though the files (and _library) are still
|
|
126
|
+
there."""
|
|
127
|
+
for content_hash, rec in _library['downloads'].items():
|
|
128
|
+
_jobs[rec['job_id']] = {
|
|
129
|
+
'status': 'done', 'idx': 0, 'n_chunks': None,
|
|
130
|
+
'content_hash': content_hash, 'path': rec['path'],
|
|
131
|
+
'title': rec.get('title'), 'size': rec.get('size'), 'bps': rec.get('bps'),
|
|
132
|
+
'error': None,
|
|
133
|
+
}
|
|
85
134
|
_lan_url = None # set once in run_web_ui() -- the base URL a phone on the
|
|
86
135
|
# same LAN can actually reach this server at, or None if
|
|
87
136
|
# it can't (bound to 127.0.0.1). The client can't compute
|
|
@@ -154,17 +203,26 @@ def _run_host_job(host_id, archive_dir, file_name, port, price, relay_urls, adve
|
|
|
154
203
|
_hosts[host_id].update(status='error', error=f'{type(e).__name__}: {e}')
|
|
155
204
|
|
|
156
205
|
|
|
157
|
-
def _run_download_job(job_id, content_hash, relay_urls, out_path, k, use_lightning):
|
|
206
|
+
def _run_download_job(job_id, content_hash, relay_urls, out_path, k, use_lightning, title=None):
|
|
158
207
|
def on_progress(idx, n_chunks):
|
|
159
208
|
with _lock:
|
|
160
209
|
_jobs[job_id].update(idx=idx, n_chunks=n_chunks)
|
|
161
210
|
|
|
162
211
|
try:
|
|
212
|
+
t0 = time.time()
|
|
163
213
|
with _quiet():
|
|
164
214
|
path = node.download_with_auction(content_hash, relay_urls, out_path=out_path, k=k,
|
|
165
215
|
use_lightning=use_lightning, on_progress=on_progress)
|
|
216
|
+
elapsed = time.time() - t0
|
|
217
|
+
size = os.path.getsize(path)
|
|
218
|
+
bps = size / elapsed if elapsed > 0 else None
|
|
166
219
|
with _lock:
|
|
167
|
-
_jobs[job_id].update(status='done', path=path)
|
|
220
|
+
_jobs[job_id].update(status='done', path=path, size=size, bps=bps)
|
|
221
|
+
_library['downloads'][content_hash] = {
|
|
222
|
+
'content_hash': content_hash, 'job_id': job_id, 'path': path,
|
|
223
|
+
'title': title, 'downloaded_at': time.time(), 'size': size, 'bps': bps,
|
|
224
|
+
}
|
|
225
|
+
_save_library()
|
|
168
226
|
except SystemExit as e:
|
|
169
227
|
with _lock:
|
|
170
228
|
_jobs[job_id].update(status='error', error=str(e))
|
|
@@ -202,6 +260,13 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
202
260
|
if path == '/api/hosts':
|
|
203
261
|
with _lock:
|
|
204
262
|
return self._json({'hosts': list(_hosts.values())})
|
|
263
|
+
if path == '/api/library':
|
|
264
|
+
with _lock:
|
|
265
|
+
return self._json({
|
|
266
|
+
'downloads': list(_library['downloads'].values()),
|
|
267
|
+
'likes': list(_library['likes']),
|
|
268
|
+
'subscriptions': list(_library['subscriptions']),
|
|
269
|
+
})
|
|
205
270
|
if path.startswith('/api/download/'):
|
|
206
271
|
with _lock:
|
|
207
272
|
job = _jobs.get(path[len('/api/download/'):])
|
|
@@ -268,13 +333,14 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
268
333
|
out_path = body.get('out_path') or f'download_{content_hash[:16]}'
|
|
269
334
|
k = int(body.get('k') or 3)
|
|
270
335
|
use_lightning = bool(body.get('lightning'))
|
|
336
|
+
title = body.get('title')
|
|
271
337
|
|
|
272
338
|
job_id = uuid.uuid4().hex[:12]
|
|
273
339
|
with _lock:
|
|
274
340
|
_jobs[job_id] = {'status': 'running', 'idx': 0, 'n_chunks': None,
|
|
275
|
-
'content_hash': content_hash, 'path': None, 'error': None}
|
|
341
|
+
'content_hash': content_hash, 'path': None, 'title': title, 'error': None}
|
|
276
342
|
threading.Thread(target=_run_download_job,
|
|
277
|
-
args=(job_id, content_hash, relay_urls, out_path, k, use_lightning),
|
|
343
|
+
args=(job_id, content_hash, relay_urls, out_path, k, use_lightning, title),
|
|
278
344
|
daemon=True).start()
|
|
279
345
|
self._json({'job_id': job_id})
|
|
280
346
|
|
|
@@ -284,7 +350,12 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
284
350
|
return self._json({'error': 'content_hash required'}, status=400)
|
|
285
351
|
identity = _identity()
|
|
286
352
|
event = identity.sign_event('like', content_hash=content_hash)
|
|
287
|
-
|
|
353
|
+
result = node.post_event(body.get('relay') or DEFAULT_RELAY, event)
|
|
354
|
+
with _lock:
|
|
355
|
+
if content_hash not in _library['likes']:
|
|
356
|
+
_library['likes'].append(content_hash)
|
|
357
|
+
_save_library()
|
|
358
|
+
self._json({'result': result})
|
|
288
359
|
|
|
289
360
|
def _handle_subscribe(self, body):
|
|
290
361
|
target_pubkey = body.get('target_pubkey')
|
|
@@ -292,7 +363,12 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
292
363
|
return self._json({'error': 'target_pubkey required'}, status=400)
|
|
293
364
|
identity = _identity()
|
|
294
365
|
event = identity.sign_event('subscribe', target_pubkey=target_pubkey)
|
|
295
|
-
|
|
366
|
+
result = node.post_event(body.get('relay') or DEFAULT_RELAY, event)
|
|
367
|
+
with _lock:
|
|
368
|
+
if target_pubkey not in _library['subscriptions']:
|
|
369
|
+
_library['subscriptions'].append(target_pubkey)
|
|
370
|
+
_save_library()
|
|
371
|
+
self._json({'result': result})
|
|
296
372
|
|
|
297
373
|
def _handle_stream(self, job_id):
|
|
298
374
|
"""Serve an already-downloaded job's file with real HTTP range
|
|
@@ -398,6 +474,8 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
398
474
|
|
|
399
475
|
def run_web_ui(port=8080, bind_host='127.0.0.1', quiet=False):
|
|
400
476
|
global _lan_url
|
|
477
|
+
_load_library()
|
|
478
|
+
_rehydrate_jobs_from_library()
|
|
401
479
|
srv = ThreadingHTTPServer((bind_host, port), Handler)
|
|
402
480
|
|
|
403
481
|
reachable_host = _detect_lan_ip() if bind_host == '0.0.0.0' else bind_host
|
|
@@ -49,9 +49,11 @@ def run_make(target):
|
|
|
49
49
|
|
|
50
50
|
|
|
51
51
|
def build_parser():
|
|
52
|
+
import node
|
|
52
53
|
parser = argparse.ArgumentParser(
|
|
53
54
|
prog='weed',
|
|
54
|
-
description='
|
|
55
|
+
description=f'{node.weed_banner()} — real mechanisms behind the #all-pdx brainstorm.')
|
|
56
|
+
parser.add_argument('--version', action='version', version=node.weed_banner())
|
|
55
57
|
sub = parser.add_subparsers(dest='command', required=False)
|
|
56
58
|
|
|
57
59
|
sub.add_parser('shell', help='interactive shell with tab completion (also the default with no command)')
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: weed-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.1.3
|
|
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
|
|
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
|