feat: add read-only resource and route discovery

This commit is contained in:
2026-07-22 16:34:29 +00:00
parent 1219117403
commit f0555703cb
10 changed files with 713 additions and 3 deletions
+104
View File
@@ -0,0 +1,104 @@
import hashlib
import unittest
from datetime import datetime, timezone
from archive_clients.bencode import BencodeError, decode_metainfo, encode
from archive_clients.resources import (
ResourceError,
build_content_tree,
normalize_resource,
)
from archive_control.v1 import resource_pb2
class ResourceTests(unittest.TestCase):
def test_hybrid_identity_and_selection_normalization(self):
info = {
b"file tree": {
b"a.txt": {b"": {b"length": 3}},
b"b.bin": {b"": {b"attr": b"p", b"length": 5}},
},
b"files": [
{b"length": 3, b"path": [b"a.txt"]},
{b"attr": b"p", b"length": 5, b"path": [b"b.bin"]},
],
b"meta version": 2,
b"name": b"resource",
b"piece length": 16384,
b"pieces": b"x" * 20,
}
metainfo_bytes = encode({b"announce": b"http://tracker", b"info": info})
decoded = decode_metainfo(metainfo_bytes)
encoded_info = encode(info)
self.assertEqual(
decoded.info_hash_v1_hex, hashlib.sha1(encoded_info).hexdigest()
)
self.assertEqual(
decoded.info_hash_v2_hex, hashlib.sha256(encoded_info).hexdigest()
)
self.assertEqual(decoded.files[1].path, "resource/b.bin")
self.assertTrue(decoded.files[1].padding)
observed = datetime(2026, 1, 2, tzinfo=timezone.utc)
normalized = normalize_resource(
{
"hash": decoded.info_hash_v1_hex,
"name": "resource",
"state": "stoppedUP",
},
[
{
"index": 0, "name": "resource/a.txt", "size": 3,
"progress": 1.0, "priority": 1,
},
{
"index": 1, "name": "resource/b.bin", "size": 5,
"progress": 0.5, "priority": 0,
},
],
metainfo_bytes,
observed,
)
summary = normalized.summary
self.assertEqual(
summary.runtime_state, resource_pb2.TORRENT_RUNTIME_STATE_STOPPED
)
self.assertEqual(summary.selected_files.ranges[0].first, 0)
self.assertEqual(summary.selected_complete_files.ranges[0].last, 0)
self.assertEqual(summary.selected_logical_bytes, 3)
self.assertTrue(summary.canonical_paths)
tree = build_content_tree(normalized.files, {0})
root = next(entry for entry in tree if entry.canonical_path == "resource")
self.assertEqual(root.available_file_count, 1)
self.assertEqual(root.available_logical_bytes, 3)
def test_noncanonical_and_unsafe_paths_are_distinct(self):
info = {
b"length": 3,
b"name": b"a.txt",
b"piece length": 16384,
b"pieces": b"x" * 20,
}
metainfo = encode({b"info": info})
torrent = {
"hash": hashlib.sha1(encode(info)).hexdigest(),
"name": "renamed", "state": "uploading",
}
renamed = normalize_resource(torrent, [{
"index": 0, "name": "renamed.txt", "size": 3,
"progress": 1.0, "priority": 1,
}], metainfo)
self.assertFalse(renamed.summary.canonical_paths)
with self.assertRaisesRegex(ResourceError, "unsafe"):
normalize_resource(torrent, [{
"index": 0, "name": "../escape", "size": 3,
"progress": 1.0, "priority": 1,
}], metainfo)
def test_noncanonical_bencode_is_rejected(self):
with self.assertRaisesRegex(BencodeError, "unsorted"):
decode_metainfo(b"d4:infod1:b1:x1:a1:yee")
if __name__ == "__main__":
unittest.main()
+45
View File
@@ -0,0 +1,45 @@
import tempfile
import unittest
from datetime import datetime, timezone
from pathlib import Path, PurePosixPath
from archive_clients.config import RootMapping
from archive_clients.routes import discover_routes
from archive_control.v1 import route_pb2
class RouteDiscoveryTests(unittest.TestCase):
def test_pairwise_routes_are_discovered_and_unsafe_folders_ignored(self):
with tempfile.TemporaryDirectory() as directory:
roots = RootMapping(PurePosixPath("/sync"), Path(directory))
configuration = {"folders": [
{
"id": "route-b", "path": "/sync/routes/b",
"type": "sendreceive",
"devices": [{"deviceID": "LOCAL"}, {"deviceID": "PEER"}],
"label": "Archive Control route-b",
},
{
"id": "route-a", "path": "/sync/routes/a",
"type": "sendonly",
"devices": [{"deviceID": "LOCAL"}, {"deviceID": "PEER"}],
},
{
"id": "outside", "path": "/other/path",
"type": "sendreceive", "devices": [],
},
]}
routes = discover_routes(
configuration, "LOCAL", roots, True,
datetime(2026, 1, 2, tzinfo=timezone.utc),
)
self.assertEqual([route.route_id for route in routes], ["route-a", "route-b"])
self.assertEqual(routes[0].state, route_pb2.ROUTE_STATE_UNSUPPORTED)
self.assertEqual(routes[1].state, route_pb2.ROUTE_STATE_DISCOVERED)
self.assertEqual(list(routes[1].peer_syncthing_device_ids), ["PEER"])
self.assertEqual(routes[1].local_relative_path, "routes/b")
self.assertFalse(routes[1].archive_control_created)
if __name__ == "__main__":
unittest.main()
+1
View File
@@ -68,6 +68,7 @@ class ServiceProbeTests(unittest.TestCase):
opener = _Opener([
'{"version":"v2.0.1","longVersion":"syncthing v2.0.1"}',
'{"myID":"DEVICE-ID"}',
'{"folders":[]}',
])
with patch(
"archive_clients.services.request.build_opener",