gabriel / musehub public
feat BREAKING storage task/phase1-per-repo-object-store #1 / 1
AI Agent gabriel · 142 days ago · May 1, 2026 · Diff

feat(storage): Phase 1 — per-repo object directory isolation

Each repo gets its own isolated object store under musehub_repos_dir: /data/repos/<owner>/<slug>/objects/sha256/<2-hex>/<62-hex>

LocalBackend._path/put/get/exists/delete/uri_for/stream/get_batch all accept optional repo_root: Path | None — backward compat when None (global store).

Path computation delegates to muse.core.object_store.object_path with objects_base=server_objects_dir(repo_root), giving the server the same algo-namespaced + 2-char sharded layout as the local client (.muse/objects/) without the .muse/ wrapper (bare repo). Phases 1 and 2 from issue #1 collapse into a single change — the sharding is free via the shared object_path.

repo_root_for(owner, slug) delegates to muse.core.paths.server_repo_root for path traversal validation and canonical layout. musehub_repos_dir added to Settings (default /data/repos).

44 new tests across 7 tiers in test_per_repo_object_store.py.

sha256:7c6330ae59ed5955168f7ca748e5faa3ab4800579073c31a764720bf620bb709 sha
+63 ~10 symbols
sha256:5eba47d77d7bcd434d536b983a7fe3dfcbc5a56929d5cbe4f82d7168fee6bd07 snapshot
+63
symbols added
~10
symbols modified
0
dead code introduced
Semantic Changes 73 symbols
~ tests/test_per_repo_object_store.py .py 62 symbols added
+ TestDataIntegrity class class TestDataIntegrity L382–446
+ test_delete_with_repo_root_only_targets_that_repo method async_method test_delete_with_repo_root_only_targets_that_repo L416–428
+ test_file_inside_repo_algo_shard_structure method async_method test_file_inside_repo_algo_shard_structure L430–440
+ test_global_and_per_repo_paths_do_not_collide method async_method test_global_and_per_repo_paths_do_not_collide L403–414
+ test_put_idempotent_with_repo_root method async_method test_put_idempotent_with_repo_root L383–391
+ test_repo_root_for_path_structure method async_method test_repo_root_for_path_structure L442–446
+ test_uri_for_with_repo_root_matches_put_uri method async_method test_uri_for_with_repo_root_matches_put_uri L393–401
+ TestE2E class class TestE2E L275–314
+ test_binary_content_preserved method async_method test_binary_content_preserved L307–314
+ test_disk_path_resolves_to_algo_shard_structure method async_method test_disk_path_resolves_to_algo_shard_structure L294–305
+ test_full_lifecycle_via_repo_root_for method async_method test_full_lifecycle_via_repo_root_for L278–292
+ TestIntegration class class TestIntegration L167–267
+ test_delete_from_one_repo_leaves_other_intact method async_method test_delete_from_one_repo_leaves_other_intact L238–249
+ test_delete_with_repo_root_removes_file method async_method test_delete_with_repo_root_removes_file L204–212
+ test_exists_with_repo_root_true_after_put method async_method test_exists_with_repo_root_true_after_put L195–202
+ test_get_missing_with_repo_root_returns_none method async_method test_get_missing_with_repo_root_returns_none L262–267
+ test_get_with_repo_root_reads_from_repo_dir method async_method test_get_with_repo_root_reads_from_repo_dir L184–193
+ test_put_creates_algo_shard_dirs_automatically method async_method test_put_creates_algo_shard_dirs_automatically L251–260
+ test_put_with_repo_root_creates_file_in_repo_dir method async_method test_put_with_repo_root_creates_file_in_repo_dir L170–182
+ test_put_without_repo_root_still_works method async_method test_put_without_repo_root_still_works L214–222
+ test_two_repos_same_oid_isolated method async_method test_two_repos_same_oid_isolated L224–236
+ TestPerformance class class TestPerformance L526–570
+ test_50_sequential_puts_with_repo_root_under_budget method async_method test_50_sequential_puts_with_repo_root_under_budget L538–548
+ test_exists_50_calls_with_repo_root_under_budget method async_method test_exists_50_calls_with_repo_root_under_budget L550–561
+ test_put_with_repo_root_latency method async_method test_put_with_repo_root_latency L527–536
+ test_repo_root_for_1000_calls_under_1_second method async_method test_repo_root_for_1000_calls_under_1_second L563–570
+ TestSecurity class class TestSecurity L454–518
+ test_exists_with_invalid_object_id_raises method async_method test_exists_with_invalid_object_id_raises L487–493
+ test_file_immutable_after_put_with_repo_root method async_method test_file_immutable_after_put_with_repo_root L508–518
+ test_get_with_invalid_object_id_raises method async_method test_get_with_invalid_object_id_raises L479–485
+ test_invalid_object_id_raises_with_repo_root method method test_invalid_object_id_raises_with_repo_root L463–469
+ test_path_traversal_in_object_id_raises_with_repo_root method method test_path_traversal_in_object_id_raises_with_repo_root L455–461
+ test_put_with_invalid_object_id_raises method async_method test_put_with_invalid_object_id_raises L471–477
+ test_repo_root_for_does_not_allow_traversal_in_owner method method test_repo_root_for_does_not_allow_traversal_in_owner L495–500
+ test_repo_root_for_does_not_allow_traversal_in_slug method method test_repo_root_for_does_not_allow_traversal_in_slug L502–506
+ TestStress class class TestStress L322–374
+ test_20_repos_under_5_seconds method async_method test_20_repos_under_5_seconds L360–374
+ test_50_objects_per_repo_all_isolated method async_method test_50_objects_per_repo_all_isolated L323–341
+ test_concurrent_puts_to_different_repos method async_method test_concurrent_puts_to_different_repos L343–358
+ TestUnit class class TestUnit L59–159
+ test_path_traversal_in_object_id_raises_with_repo_root method method test_path_traversal_in_object_id_raises_with_repo_root L124–130
+ test_path_two_repos_different_directories method method test_path_two_repos_different_directories L93–104
+ test_path_with_repo_root_no_colon_on_disk method method test_path_with_repo_root_no_colon_on_disk L84–91
+ test_path_with_repo_root_same_oid_same_path method method test_path_with_repo_root_same_oid_same_path L151–159
+ test_path_with_repo_root_uses_algo_sharded_layout method method test_path_with_repo_root_uses_algo_sharded_layout L62–74
+ test_path_without_repo_root_uses_global_root method method test_path_without_repo_root_uses_global_root L76–82
+ test_repo_root_for_returns_correct_path method method test_repo_root_for_returns_correct_path L138–142
+ test_repo_root_for_uses_settings_default method method test_repo_root_for_uses_settings_default L144–149
+ test_settings_has_musehub_repos_dir method method test_settings_has_musehub_repos_dir L132–136
+ test_uri_for_with_repo_root_contains_algo_and_objects method method test_uri_for_with_repo_root_contains_algo_and_objects L106–114
+ test_uri_for_without_repo_root_uses_global method method test_uri_for_without_repo_root_uses_global L116–122
+ _backend function function _backend L43–45
+ _oid function function _oid L38–40
+ _repo_root function function _repo_root L48–51
+ _uid function function _uid L34–35
+ Path import import Path L24–24
+ annotations import import annotations L19–19
+ asyncio import import asyncio L21–21
+ patch import import patch L25–25
+ pytest import import pytest L27–27
+ time import import time L22–22
+ uuid import import uuid L23–23
~ musehub/config.py .py 1 symbol modified
~ musehub/storage/backends.py .py 1 symbol added, 9 symbols modified
+ repo_root_for function function repo_root_for L586–599
~ _path
~ delete
~ exists
~ get
~ put
~ stream
← Older Oldest on task/phase1-per-repo-object-store
All commits
Newer → Latest on task/phase1-per-repo-object-store

0 comments

No comments yet. Be the first to start the discussion.

To add a comment, use the Muse CLI: muse hub commit comment sha256:7c6330ae59ed5955168f7ca748e5faa3ab4800579073c31a764720bf620bb709 --body "your comment"