Skip to content

Commit 86f8a34

Browse files
Merge pull request #179 from ThomasWaldmann/fixes
Misc. fixes
2 parents 5534dbc + 860653c commit 86f8a34

5 files changed

Lines changed: 79 additions & 17 deletions

File tree

src/borgstore/backends/rest.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ def request(self, method, url, params=None, data=None, headers=None, timeout=Non
160160
response_headers[key.strip()] = value.strip()
161161

162162
content_length = int(response_headers.get("Content-Length", "0"))
163-
response_body = self.process.stdout.read(content_length) if content_length else b""
163+
response_body = self.process.stdout.read(content_length) if method.upper() != "HEAD" and content_length else b""
164164

165165
response = requests.Response()
166166
response.status_code = status_code
@@ -376,7 +376,9 @@ def info(self, name: str) -> ItemInfo:
376376
self._handle_response(response, name) # raises!
377377
exists = response.status_code == HTTP.OK
378378
is_dir = response.headers.get("X-BorgStore-Is-Directory") == "true"
379-
return ItemInfo(name=name, exists=exists, size=int(response.headers.get("Content-Length", 0)), directory=is_dir)
379+
atime = float(response.headers.get("X-BorgStore-Atime", 0))
380+
size = int(response.headers.get("Content-Length", 0)) if exists else 0
381+
return ItemInfo(name=name, exists=exists, size=size, directory=is_dir, atime=atime)
380382

381383
def load(self, name: str, *, size=None, offset=0) -> bytes:
382384
self._assert_open()
@@ -460,4 +462,10 @@ def list(self, name: str) -> Iterator[ItemInfo]:
460462
response = self._request("get", self._url(name) + "/") # trailing "/" needed to get list
461463
self._handle_response(response, name)
462464
for entry in response.json():
463-
yield ItemInfo(name=entry["name"], exists=True, size=entry["size"], directory=entry.get("directory", False))
465+
yield ItemInfo(
466+
name=entry["name"],
467+
exists=True,
468+
size=entry["size"],
469+
directory=entry.get("directory", False),
470+
atime=entry.get("atime", 0),
471+
)

src/borgstore/server/rest.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -296,10 +296,6 @@ def do_DELETE(self):
296296

297297
@checks_and_logging
298298
def do_HEAD(self):
299-
if not self.name:
300-
self.send_error(HTTP.BAD_REQUEST, "Bad Request")
301-
return
302-
303299
try:
304300
with self.server.backend:
305301
info = self.server.backend.info(self.name)
@@ -310,6 +306,7 @@ def do_HEAD(self):
310306
headers={
311307
"Content-Length": str(info.size),
312308
"X-BorgStore-Is-Directory": "true" if info.directory else "false",
309+
"X-BorgStore-Atime": str(info.atime),
313310
},
314311
)
315312
except Exception as e:
@@ -324,7 +321,7 @@ def do_GET(self):
324321
# [{"name": "...", "size": ...}, ...]
325322
with self.server.backend:
326323
items = (
327-
{"name": item.name, "size": item.size, "directory": item.directory}
324+
{"name": item.name, "size": item.size, "directory": item.directory, "atime": item.atime}
328325
for item in self.server.backend.list(self.name)
329326
)
330327
json_data = json.dumps(list(items), indent=2)

src/borgstore/utils/nesting.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def split_key(name: str) -> tuple[Optional[str], str]:
4141

4242
def nest(name: str, levels: int, *, add_suffix: Optional[str] = None) -> str:
4343
"""namespace/12345678 --2 levels--> namespace/12/34/12345678"""
44-
if levels > 0:
44+
if levels > 0 and name:
4545
namespace, key = split_key(name)
4646
parts = [key[2 * level : 2 * level + 2] for level in range(levels)]
4747
parts.append(key)

tests/test_nesting.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ def test_split_key(name, base, key):
2626
("data/12345678", 2, False, "data/12/34/12345678"),
2727
("data/12345678", 3, False, "data/12/34/56/12345678"),
2828
("data/12345678", 3, True, "data/12/34/56/12345678.del"),
29+
("", 1, False, ""),
30+
("", 2, False, ""),
2931
],
3032
)
3133
def test_nest(key, levels, deleted, nested_key):

tests/test_server_rest.py

Lines changed: 63 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from borgstore.backends.rest import get_rest_backend
1616
from borgstore.backends.posixfs import get_file_backend
1717
from borgstore.backends.errors import ObjectNotFound, BackendAlreadyExists, QuotaExceeded
18-
from borgstore.store import get_backend
18+
from borgstore.store import get_backend, Store
1919

2020

2121
def start_server(backend_url, address, port, username=None, password=None, permissions=None, quota=None):
@@ -76,7 +76,9 @@ def test_rest_server_basic_ops(rest_server_with_auth):
7676
be.delete("test/item1")
7777
with pytest.raises(ObjectNotFound):
7878
be.load("test/item1")
79-
assert not be.info("test/item1").exists
79+
info = be.info("test/item1")
80+
assert not info.exists
81+
assert info.size == 0
8082

8183
finally:
8284
be.close()
@@ -613,29 +615,82 @@ def do_request(method, path, body=b""):
613615

614616
# Read body
615617
resp_body = b""
616-
if "Content-Length" in resp_headers:
618+
if method.upper() != "HEAD" and "Content-Length" in resp_headers:
617619
resp_body = proc.stdout.read(int(resp_headers["Content-Length"]))
618-
return status, resp_body
620+
return status, resp_body, resp_headers
619621

620622
try:
621623
# 1. Create store
622-
status, body = do_request("POST", "/?cmd=create")
624+
status, body, headers = do_request("POST", "/?cmd=create")
623625
assert status == 200
624626

625627
# 2. Store something
626628
item_data = b"stdio data"
627-
status, body = do_request("POST", "/item1", body=item_data)
629+
status, body, headers = do_request("POST", "/item1", body=item_data)
628630
assert status == 200
629631

630632
# 3. List the store
631-
status, body = do_request("GET", "/")
633+
status, body, headers = do_request("GET", "/")
632634
assert status == 200
633635
items = json.loads(body.decode("utf-8"))
634-
assert any(item["name"] == "item1" for item in items)
636+
assert any(item["name"] == "item1" and item.get("atime", 0) > 0 for item in items)
637+
638+
# 4. Info (HEAD)
639+
status, body, headers = do_request("HEAD", "/item1")
640+
assert status == 200
641+
assert body == b""
642+
assert float(headers.get("X-BorgStore-Atime", 0)) > 0
643+
644+
# 5. Info for nonexistent (HEAD)
645+
status, body, headers = do_request("HEAD", "/nonexistent")
646+
assert status == 404
647+
assert body == b""
635648

636649
finally:
637650
proc.stdin.close()
638651
try:
639652
proc.wait(timeout=2)
640653
except subprocess.TimeoutExpired:
641654
proc.kill()
655+
656+
657+
def test_rest_url(tmp_path):
658+
repo_path = tmp_path / "repo"
659+
# Use rest: URL with stdio backend (empty host)
660+
url = f"rest:///{repo_path}"
661+
662+
# Use levels=0 to avoid root nesting issues if they arise
663+
config = {"": {"levels": [0]}}
664+
store = Store(url, config=config)
665+
store.create()
666+
667+
with store:
668+
item_name = "test-item"
669+
item_data = b"some data"
670+
store.store(item_name, item_data)
671+
672+
# Test Store.info which calls Backend.info (HEAD)
673+
# This used to hang.
674+
info = store.info(item_name)
675+
assert info.exists
676+
assert info.size == len(item_data)
677+
assert info.atime > 0
678+
679+
# Test listing
680+
items = list(store.list(""))
681+
assert len(items) == 1
682+
assert items[0].name == item_name
683+
assert items[0].atime > 0
684+
685+
# Test nonexistent item
686+
# This also used to hang if it returned a 404 with a body.
687+
info_none = store.info("nonexistent")
688+
assert not info_none.exists
689+
assert info_none.size == 0
690+
691+
# Test directory info (root)
692+
info_root = store.info("")
693+
assert info_root.exists
694+
assert info_root.directory
695+
696+
store.destroy()

0 commit comments

Comments
 (0)