Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions openblade/api/routes_upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,18 @@ async def download_file(
record = repo.get_nas_file_record(file_id)
path = _resolve_file_path(file_id, record)
if path is None:
if record is not None:
# The file exists in the catalog but is not staged/cached: it is offline
# on tape. Return an actionable 409 rather than a misleading 404 so a
# NAS client knows to hydrate it first, not that the file is gone.
state = str(record.get("status") or record.get("file_state") or "offline_on_tape")
raise HTTPException(
status_code=409,
detail=(
f"File {file_id} is {state} (offline on tape). "
"Hydrate it first (POST /virtual/hydrate), then download."
),
)
Comment on lines +392 to +403
raise HTTPException(status_code=404, detail=f"File {file_id} not found")
filename = _sanitize_filename(record.get("relative_path") if record else file_id)
return FileResponse(path=path, filename=filename)
Expand Down
38 changes: 38 additions & 0 deletions tests/integration/test_upload_download_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,3 +235,41 @@ def test_all_upload_download_require_auth(self, client: TestClient) -> None:
assert client.get("/api/files/00000000-0000-4000-8000-000000000001/download").status_code in (401, 403)
assert client.get("/api/pools/1/files").status_code in (401, 403)
assert client.delete("/api/files/00000000-0000-4000-8000-000000000001").status_code in (401, 403)


def test_download_offline_file_returns_409_with_hydrate_guidance(
client: TestClient, admin_auth_headers: dict[str, str]
) -> None:
# NAS-experience coherence: an offline-on-tape file (record exists, nothing
# staged/cached) must return an actionable 409 telling the client to hydrate,
# not a misleading 404 that reads as "gone".
from pathlib import Path

from openblade.api import routes_upload as ru
from openblade.bootstrap import get_context

upload = client.post(
"/api/pools/integration-pool/upload",
files={"file": ("offline.txt", b"tape-resident bytes", "text/plain")},
headers=admin_auth_headers,
)
assert upload.status_code == 200
file_id = upload.json()["file_id"]
assert client.get(f"/api/files/{file_id}/download", headers=admin_auth_headers).status_code == 200

# Simulate the file going offline: drop every local copy, keep the catalog record.
record = get_context().catalog.get_nas_file_record(file_id)
candidates = [
ru._safe_resolve(ru._staging_dir(), file_id),
ru._safe_resolve(ru._restore_dir(), file_id),
]
candidates += [
Path(str(record[key])) for key in ("cache_path", "source_path") if record and record.get(key)
]
Comment on lines +246 to +268
for candidate in candidates:
if candidate.exists():
candidate.unlink()

dl = client.get(f"/api/files/{file_id}/download", headers=admin_auth_headers)
assert dl.status_code == 409, dl.status_code
assert "hydrate" in dl.json()["detail"].lower()
Loading