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.
Files changed (26) hide show
  1. {weed_cli-1.8.7 → weed_cli-1.9.3}/PKG-INFO +1 -1
  2. {weed_cli-1.8.7 → weed_cli-1.9.3}/node.py +13 -14
  3. {weed_cli-1.8.7 → weed_cli-1.9.3}/pyproject.toml +1 -1
  4. {weed_cli-1.8.7 → weed_cli-1.9.3}/tests/test_node_manifest.py +31 -8
  5. {weed_cli-1.8.7 → weed_cli-1.9.3}/tests/test_web_ui_api.py +22 -1
  6. {weed_cli-1.8.7 → weed_cli-1.9.3}/tests/testutil.py +19 -8
  7. {weed_cli-1.8.7 → weed_cli-1.9.3}/web_ui.py +17 -11
  8. {weed_cli-1.8.7 → weed_cli-1.9.3}/weed_cli.egg-info/PKG-INFO +1 -1
  9. {weed_cli-1.8.7 → weed_cli-1.9.3}/LICENSE +0 -0
  10. {weed_cli-1.8.7 → weed_cli-1.9.3}/README.md +0 -0
  11. {weed_cli-1.8.7 → weed_cli-1.9.3}/dht.py +0 -0
  12. {weed_cli-1.8.7 → weed_cli-1.9.3}/discovery_relay.py +0 -0
  13. {weed_cli-1.8.7 → weed_cli-1.9.3}/lightning_settle.py +0 -0
  14. {weed_cli-1.8.7 → weed_cli-1.9.3}/poc_reputation.py +0 -0
  15. {weed_cli-1.8.7 → weed_cli-1.9.3}/setup.cfg +0 -0
  16. {weed_cli-1.8.7 → weed_cli-1.9.3}/shell.py +0 -0
  17. {weed_cli-1.8.7 → weed_cli-1.9.3}/tests/test_dht.py +0 -0
  18. {weed_cli-1.8.7 → weed_cli-1.9.3}/tests/test_discovery_relay.py +0 -0
  19. {weed_cli-1.8.7 → weed_cli-1.9.3}/tests/test_host_live_reload.py +0 -0
  20. {weed_cli-1.8.7 → weed_cli-1.9.3}/tunnel_relay.py +0 -0
  21. {weed_cli-1.8.7 → weed_cli-1.9.3}/weed.py +0 -0
  22. {weed_cli-1.8.7 → weed_cli-1.9.3}/weed_cli.egg-info/SOURCES.txt +0 -0
  23. {weed_cli-1.8.7 → weed_cli-1.9.3}/weed_cli.egg-info/dependency_links.txt +0 -0
  24. {weed_cli-1.8.7 → weed_cli-1.9.3}/weed_cli.egg-info/entry_points.txt +0 -0
  25. {weed_cli-1.8.7 → weed_cli-1.9.3}/weed_cli.egg-info/requires.txt +0 -0
  26. {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.8.7
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 chunk data
238
- (load_leaves) and per-chunk byte math (entry['chunk_size']), and ott
239
- only ever writes either for video-type entries — everything else
240
- (photos, or any file whose extension ott's is_video() doesn't
241
- recognize, which is also where a plain .mp3 lands, since ott only has
242
- two types) has chunk_size: None and no .ott/chunks/<hash>.json at all.
243
- Filtering here, the one function every hosting path (weed.py,
244
- shell.py, web_ui.py) goes through, means one non-video file sitting
245
- in an archive_dir no longer poison-pills hosting everything else 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') == 'video']
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} non-video entr{'y' if n == 1 else 'ies'} "
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.8.7"
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 actual bug: an mp3 (typed 'image' by ott's extension-based
23
- is_video()) sitting in the same archive_dir used to poison-pill
24
- hosting the whole directory -- host <dir> with no --file should just
25
- skip it and host the real video."""
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='song.mp3', video=False)
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='song.mp3', video=False)
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), 'song.mp3')
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). video=False produces
98
- an 'image'-typed entry with no chunks file, for regression-testing the
99
- mp3/photo-mixed-into-an-archive fix (load_manifest_entries filtering
100
- to video-only)."""
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 video:
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': 'video',
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': 'image',
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-only, matching ott's own is_video() -- a non-video upload
750
- would just be a manifest entry that can never actually be
751
- hosted (see load_manifest_entries' own video-only filter, added
752
- after exactly that silently broke `host` for everything else in
753
- the same archive_dir), so it's rejected up front instead."""
754
- from ott import is_video, chunk_hashes, merkle_root, OttStore
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 not is_video(safe_name):
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 video '
771
- 'files can be hosted (see ott.is_video)'}, status=400)
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': 'video', 'n_chunks': len(chunks), 'chunk_size': chunk_size,
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.8.7
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