weed-cli 1.8.7__tar.gz → 1.9.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.8.7 → weed_cli-1.9.3}/PKG-INFO +1 -1
- {weed_cli-1.8.7 → weed_cli-1.9.3}/node.py +13 -14
- {weed_cli-1.8.7 → weed_cli-1.9.3}/pyproject.toml +1 -1
- {weed_cli-1.8.7 → weed_cli-1.9.3}/tests/test_node_manifest.py +31 -8
- {weed_cli-1.8.7 → weed_cli-1.9.3}/tests/test_web_ui_api.py +22 -1
- {weed_cli-1.8.7 → weed_cli-1.9.3}/tests/testutil.py +19 -8
- {weed_cli-1.8.7 → weed_cli-1.9.3}/web_ui.py +17 -11
- {weed_cli-1.8.7 → weed_cli-1.9.3}/weed_cli.egg-info/PKG-INFO +1 -1
- {weed_cli-1.8.7 → weed_cli-1.9.3}/LICENSE +0 -0
- {weed_cli-1.8.7 → weed_cli-1.9.3}/README.md +0 -0
- {weed_cli-1.8.7 → weed_cli-1.9.3}/dht.py +0 -0
- {weed_cli-1.8.7 → weed_cli-1.9.3}/discovery_relay.py +0 -0
- {weed_cli-1.8.7 → weed_cli-1.9.3}/lightning_settle.py +0 -0
- {weed_cli-1.8.7 → weed_cli-1.9.3}/poc_reputation.py +0 -0
- {weed_cli-1.8.7 → weed_cli-1.9.3}/setup.cfg +0 -0
- {weed_cli-1.8.7 → weed_cli-1.9.3}/shell.py +0 -0
- {weed_cli-1.8.7 → weed_cli-1.9.3}/tests/test_dht.py +0 -0
- {weed_cli-1.8.7 → weed_cli-1.9.3}/tests/test_discovery_relay.py +0 -0
- {weed_cli-1.8.7 → weed_cli-1.9.3}/tests/test_host_live_reload.py +0 -0
- {weed_cli-1.8.7 → weed_cli-1.9.3}/tunnel_relay.py +0 -0
- {weed_cli-1.8.7 → weed_cli-1.9.3}/weed.py +0 -0
- {weed_cli-1.8.7 → weed_cli-1.9.3}/weed_cli.egg-info/SOURCES.txt +0 -0
- {weed_cli-1.8.7 → weed_cli-1.9.3}/weed_cli.egg-info/dependency_links.txt +0 -0
- {weed_cli-1.8.7 → weed_cli-1.9.3}/weed_cli.egg-info/entry_points.txt +0 -0
- {weed_cli-1.8.7 → weed_cli-1.9.3}/weed_cli.egg-info/requires.txt +0 -0
- {weed_cli-1.8.7 → weed_cli-1.9.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.9.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
|
|
@@ -234,16 +234,15 @@ def load_manifest_entries(archive_dir, file_name=None):
|
|
|
234
234
|
no --file only ever served the single most-recently-added file out of a
|
|
235
235
|
45-video archive. Dedupes by name (last-write-wins, same convention).
|
|
236
236
|
|
|
237
|
-
Only 'video' entries are returned. Hosting depends on
|
|
238
|
-
(load_leaves) and per-chunk byte math (entry['chunk_size']),
|
|
239
|
-
only ever writes either for video-type entries
|
|
240
|
-
(photos, or any file whose extension
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
in
|
|
246
|
-
it with 'no chunks file at ...'."""
|
|
237
|
+
Only 'video' and 'audio' entries are returned. Hosting depends on
|
|
238
|
+
chunk data (load_leaves) and per-chunk byte math (entry['chunk_size']),
|
|
239
|
+
and ott only ever writes either for video/audio-type entries (see its
|
|
240
|
+
own cmd_add) — everything else (photos, or any file whose extension
|
|
241
|
+
ott's is_video()/is_audio() don't recognize) has chunk_size: None and
|
|
242
|
+
no .ott/chunks/<hash>.json at all. Filtering here, the one function
|
|
243
|
+
every hosting path (weed.py, shell.py, web_ui.py) goes through, means
|
|
244
|
+
one non-chunked file sitting in an archive_dir no longer poison-pills
|
|
245
|
+
hosting everything else in it with 'no chunks file at ...'."""
|
|
247
246
|
archive_dir = os.path.expanduser(archive_dir)
|
|
248
247
|
manifest_path = os.path.join(archive_dir, '.ott', 'manifest.jsonl')
|
|
249
248
|
if not os.path.exists(manifest_path):
|
|
@@ -256,14 +255,14 @@ def load_manifest_entries(archive_dir, file_name=None):
|
|
|
256
255
|
for e in raw:
|
|
257
256
|
by_name[e['name']] = e
|
|
258
257
|
all_entries = list(by_name.values())
|
|
259
|
-
entries = [e for e in all_entries if e.get('type')
|
|
258
|
+
entries = [e for e in all_entries if e.get('type') in ('video', 'audio')]
|
|
260
259
|
if not entries:
|
|
261
260
|
if all_entries:
|
|
262
261
|
n = len(all_entries)
|
|
263
|
-
sys.exit(f"no hostable video file found in {archive_dir}" +
|
|
262
|
+
sys.exit(f"no hostable video/audio file found in {archive_dir}" +
|
|
264
263
|
(f" matching {file_name}" if file_name else "") +
|
|
265
|
-
f" — found {n}
|
|
266
|
-
f"(only video files can be hosted; see ott's is_video())")
|
|
264
|
+
f" — found {n} other entr{'y' if n == 1 else 'ies'} "
|
|
265
|
+
f"(only video/audio files can be hosted; see ott's is_video()/is_audio())")
|
|
267
266
|
sys.exit(f"no archived file found in {archive_dir}" + (f" matching {file_name}" if file_name else ""))
|
|
268
267
|
return entries
|
|
269
268
|
|
|
@@ -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.9.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" }
|
|
@@ -19,23 +19,46 @@ def test_load_manifest_entries_single_video(tmp_path):
|
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
def test_load_manifest_entries_filters_non_video(tmp_path):
|
|
22
|
-
"""The
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
"""The original bug this filter exists for: a non-chunked file
|
|
23
|
+
(originally an mp3, back when ott's extension-based is_video() typed
|
|
24
|
+
it 'image' for lack of any 'audio' type at all -- see
|
|
25
|
+
test_load_manifest_entries_includes_audio_alongside_video below for
|
|
26
|
+
that no longer being true) sitting in the same archive_dir used to
|
|
27
|
+
poison-pill hosting the whole directory -- host <dir> with no --file
|
|
28
|
+
should just skip a genuinely non-hostable entry (a real photo, here)
|
|
29
|
+
and host the real video."""
|
|
26
30
|
video = make_fake_archive(tmp_path, name='good.mp4')
|
|
27
|
-
make_fake_archive(tmp_path, name='
|
|
31
|
+
make_fake_archive(tmp_path, name='photo.jpg', content_type='image')
|
|
28
32
|
|
|
29
33
|
entries = node.load_manifest_entries(str(tmp_path))
|
|
30
34
|
assert [e['sha256'] for e in entries] == [video['sha256']]
|
|
31
35
|
|
|
32
36
|
|
|
37
|
+
def test_load_manifest_entries_includes_audio_alongside_video(tmp_path):
|
|
38
|
+
"""Real ask: support audio (mp3, etc.) in addition to video. ott's
|
|
39
|
+
own cmd_add now chunks audio the same way it always has video (see
|
|
40
|
+
its own is_audio()), so an audio-typed manifest entry has real chunk
|
|
41
|
+
data too -- this is the one place every hosting path (weed.py,
|
|
42
|
+
shell.py, web_ui.py) filters on 'is this actually hostable', and it
|
|
43
|
+
needs to say yes to both now, not just video."""
|
|
44
|
+
video = make_fake_archive(tmp_path, name='good.mp4')
|
|
45
|
+
audio = make_fake_archive(tmp_path, name='song.mp3', content_type='audio')
|
|
46
|
+
|
|
47
|
+
entries = node.load_manifest_entries(str(tmp_path))
|
|
48
|
+
assert {e['sha256'] for e in entries} == {video['sha256'], audio['sha256']}
|
|
49
|
+
# the audio entry got real chunk data, not the image-style single
|
|
50
|
+
# whole-file hash with no chunks
|
|
51
|
+
audio_entry = next(e for e in entries if e['sha256'] == audio['sha256'])
|
|
52
|
+
assert audio_entry['n_chunks'] > 1
|
|
53
|
+
assert node.load_leaves(str(tmp_path), audio['sha256']) is not None
|
|
54
|
+
|
|
55
|
+
|
|
33
56
|
def test_load_manifest_entries_explicit_non_video_file_errors_clearly(tmp_path):
|
|
34
57
|
make_fake_archive(tmp_path, name='good.mp4')
|
|
35
|
-
make_fake_archive(tmp_path, name='
|
|
58
|
+
make_fake_archive(tmp_path, name='photo.jpg', content_type='image')
|
|
36
59
|
|
|
37
|
-
with pytest.raises(SystemExit, match='no hostable video file found'):
|
|
38
|
-
node.load_manifest_entries(str(tmp_path), '
|
|
60
|
+
with pytest.raises(SystemExit, match='no hostable video/audio file found'):
|
|
61
|
+
node.load_manifest_entries(str(tmp_path), 'photo.jpg')
|
|
39
62
|
|
|
40
63
|
|
|
41
64
|
def test_load_manifest_entries_no_manifest_at_all(tmp_path):
|
|
@@ -201,10 +201,31 @@ def test_upload_rejects_non_video_extension(web_server, tmp_path):
|
|
|
201
201
|
archive_dir = str(tmp_path / 'archive')
|
|
202
202
|
status, resp = http_post_raw(_upload_url(web_server, 'notes.txt', archive_dir), b'hello')
|
|
203
203
|
assert status == 400
|
|
204
|
-
assert 'not a recognized video extension' in resp['error']
|
|
204
|
+
assert 'not a recognized video or audio extension' in resp['error']
|
|
205
205
|
assert not os.path.exists(archive_dir) # never even created
|
|
206
206
|
|
|
207
207
|
|
|
208
|
+
def test_upload_audio_produces_a_real_hostable_archive(web_server, tmp_path):
|
|
209
|
+
"""Real ask: support audio (mp3, etc.) in addition to video --
|
|
210
|
+
uploading one should archive it exactly like a video does (real
|
|
211
|
+
chunk data, type: 'audio'), not get rejected the way it used to
|
|
212
|
+
before ott/node.py recognized audio as its own hostable type."""
|
|
213
|
+
archive_dir = str(tmp_path / 'archive')
|
|
214
|
+
data = os.urandom(200_000)
|
|
215
|
+
status, resp = http_post_raw(_upload_url(web_server, 'song.mp3', archive_dir), data)
|
|
216
|
+
|
|
217
|
+
assert status == 200
|
|
218
|
+
assert resp['ok'] is True
|
|
219
|
+
assert resp['type'] == 'audio'
|
|
220
|
+
assert resp['n_chunks'] >= 1
|
|
221
|
+
|
|
222
|
+
entries = node.load_manifest_entries(archive_dir)
|
|
223
|
+
assert len(entries) == 1
|
|
224
|
+
assert entries[0]['type'] == 'audio'
|
|
225
|
+
leaves = node.load_leaves(archive_dir, resp['content_hash'])
|
|
226
|
+
assert len(leaves) == resp['n_chunks']
|
|
227
|
+
|
|
228
|
+
|
|
208
229
|
def test_upload_sanitizes_path_traversal_in_filename(web_server, tmp_path):
|
|
209
230
|
archive_dir = str(tmp_path / 'archive')
|
|
210
231
|
outside_target = tmp_path / 'evil.mp4'
|
|
@@ -89,15 +89,22 @@ def _split_url(url):
|
|
|
89
89
|
return host, int(port), '/' + path
|
|
90
90
|
|
|
91
91
|
|
|
92
|
-
def make_fake_archive(archive_dir, name='clip.mp4', size=200_000, chunk_size=65_536, video=True
|
|
92
|
+
def make_fake_archive(archive_dir, name='clip.mp4', size=200_000, chunk_size=65_536, video=True,
|
|
93
|
+
content_type=None):
|
|
93
94
|
"""Hand-writes a minimal but real .ott/manifest.jsonl + chunks file --
|
|
94
95
|
real sha256 chunk hashes and a real merkle root via the same ott
|
|
95
96
|
helpers node.py itself calls, just skipping ott's own add/stage/commit
|
|
96
97
|
workflow (irrelevant to what node.py/web_ui.py actually read: the
|
|
97
|
-
final manifest.jsonl + chunks/<hash>.json shape).
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
98
|
+
final manifest.jsonl + chunks/<hash>.json shape).
|
|
99
|
+
|
|
100
|
+
content_type overrides video/False entirely when given ('video',
|
|
101
|
+
'audio', or 'image') -- video=True/False is kept as the original
|
|
102
|
+
shorthand for the two cases every existing test already uses.
|
|
103
|
+
'image' (or any other non-chunkable type) produces no chunks file,
|
|
104
|
+
for regression-testing the photo-mixed-into-an-archive fix
|
|
105
|
+
(load_manifest_entries filtering to video/audio only); 'video' and
|
|
106
|
+
'audio' both get real chunk data, same as ott's own cmd_add does for
|
|
107
|
+
either since a bare mp3 stopped being lumped in with 'image'."""
|
|
101
108
|
os.makedirs(archive_dir, exist_ok=True)
|
|
102
109
|
ott_dir = os.path.join(archive_dir, '.ott')
|
|
103
110
|
os.makedirs(os.path.join(ott_dir, 'chunks'), exist_ok=True)
|
|
@@ -105,12 +112,16 @@ def make_fake_archive(archive_dir, name='clip.mp4', size=200_000, chunk_size=65_
|
|
|
105
112
|
with open(file_path, 'wb') as f:
|
|
106
113
|
f.write(os.urandom(size))
|
|
107
114
|
|
|
108
|
-
if
|
|
115
|
+
if content_type is None:
|
|
116
|
+
content_type = 'video' if video else 'image'
|
|
117
|
+
chunkable = content_type in ('video', 'audio')
|
|
118
|
+
|
|
119
|
+
if chunkable:
|
|
109
120
|
chunks = chunk_hashes(file_path, chunk_size)
|
|
110
121
|
digest = merkle_root(chunks)
|
|
111
122
|
entry = {
|
|
112
123
|
'sha256': digest, 'name': name, 'orig_path': name, 'last_path': file_path,
|
|
113
|
-
'size': size, 'added': '2026-01-01T00:00:00Z', 'type':
|
|
124
|
+
'size': size, 'added': '2026-01-01T00:00:00Z', 'type': content_type,
|
|
114
125
|
'n_chunks': len(chunks), 'chunk_size': chunk_size,
|
|
115
126
|
}
|
|
116
127
|
with open(os.path.join(ott_dir, 'chunks', f'{digest}.json'), 'w') as f:
|
|
@@ -119,7 +130,7 @@ def make_fake_archive(archive_dir, name='clip.mp4', size=200_000, chunk_size=65_
|
|
|
119
130
|
digest = hashlib.sha256(open(file_path, 'rb').read()).hexdigest()
|
|
120
131
|
entry = {
|
|
121
132
|
'sha256': digest, 'name': name, 'orig_path': name, 'last_path': file_path,
|
|
122
|
-
'size': size, 'added': '2026-01-01T00:00:00Z', 'type':
|
|
133
|
+
'size': size, 'added': '2026-01-01T00:00:00Z', 'type': content_type,
|
|
123
134
|
'n_chunks': 1, 'chunk_size': None,
|
|
124
135
|
}
|
|
125
136
|
|
|
@@ -746,12 +746,13 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
746
746
|
with no separate `ott add` step -- same reasoning as the rest of
|
|
747
747
|
this UI existing at all: don't make someone learn a second tool
|
|
748
748
|
just to do the thing this one already knows how to do.
|
|
749
|
-
Video
|
|
750
|
-
would just be a manifest entry that can never
|
|
751
|
-
hosted (see load_manifest_entries' own video-
|
|
752
|
-
after exactly that silently broke `host` for
|
|
753
|
-
the same archive_dir), so it's rejected up
|
|
754
|
-
|
|
749
|
+
Video or audio, matching ott's own is_video()/is_audio() -- an
|
|
750
|
+
upload of neither would just be a manifest entry that can never
|
|
751
|
+
actually be hosted (see load_manifest_entries' own video/audio-
|
|
752
|
+
only filter, added after exactly that silently broke `host` for
|
|
753
|
+
everything else in the same archive_dir), so it's rejected up
|
|
754
|
+
front instead."""
|
|
755
|
+
from ott import is_video, is_audio, chunk_hashes, merkle_root, OttStore
|
|
755
756
|
|
|
756
757
|
qs = parse_qs(urlparse(self.path).query)
|
|
757
758
|
raw_name = (qs.get('name') or [''])[0]
|
|
@@ -765,10 +766,15 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
765
766
|
if not safe_name or safe_name in ('.', '..'):
|
|
766
767
|
return self._json({'error': f'invalid file name: {raw_name!r}'}, status=400)
|
|
767
768
|
|
|
768
|
-
if
|
|
769
|
+
if is_video(safe_name):
|
|
770
|
+
content_type = 'video'
|
|
771
|
+
elif is_audio(safe_name):
|
|
772
|
+
content_type = 'audio'
|
|
773
|
+
else:
|
|
769
774
|
return self._json(
|
|
770
|
-
{'error': f'{safe_name}: not a recognized video extension -- only
|
|
771
|
-
'files can be hosted (see ott.is_video)'},
|
|
775
|
+
{'error': f'{safe_name}: not a recognized video or audio extension -- only '
|
|
776
|
+
'video/audio files can be hosted (see ott.is_video/ott.is_audio)'},
|
|
777
|
+
status=400)
|
|
772
778
|
|
|
773
779
|
archive_dir = os.path.expanduser(archive_dir)
|
|
774
780
|
os.makedirs(archive_dir, exist_ok=True)
|
|
@@ -814,7 +820,7 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
814
820
|
entry = {
|
|
815
821
|
'sha256': digest, 'name': safe_name, 'orig_path': safe_name, 'last_path': dest_path,
|
|
816
822
|
'size': written, 'added': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()),
|
|
817
|
-
'type':
|
|
823
|
+
'type': content_type, 'n_chunks': len(chunks), 'chunk_size': chunk_size,
|
|
818
824
|
}
|
|
819
825
|
|
|
820
826
|
# _lock (not just for _jobs/_hosts/_library, see its own comment
|
|
@@ -857,7 +863,7 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
857
863
|
f.write(existing + json.dumps(entry) + '\n')
|
|
858
864
|
os.replace(manifest_tmp, manifest_path)
|
|
859
865
|
|
|
860
|
-
self._json({'ok': True, 'name': safe_name, 'content_hash': digest,
|
|
866
|
+
self._json({'ok': True, 'name': safe_name, 'content_hash': digest, 'type': content_type,
|
|
861
867
|
'size': written, 'n_chunks': len(chunks), 'archive_dir': archive_dir})
|
|
862
868
|
|
|
863
869
|
def _handle_host(self, body):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: weed-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.9.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|