feat: add read-only resource and route discovery
This commit is contained in:
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user