feat(hosted-file): implement hash-based download action and flexible auth

- Adds GET /v3/action/hosted_file/hash/{sha256}/download for direct content-addressable storage access.
- Updates V3 authentication dependencies to support 'api_key' in the query parameter (alias 'api_key').
- Implements auth_method: 'api_key' for machine-to-machine requests that provide a valid key but no user/account context.
- Updates GUIDE__V3_FRONTEND_API.md with the new endpoint and auth options.
This commit is contained in:
Scott Idem
2026-02-03 13:44:07 -05:00
parent faa6de866d
commit 07609bae9a
3 changed files with 71 additions and 9 deletions

View File

@@ -276,6 +276,50 @@ async def download_file_action(
return FileResponse(full_file_path, filename=target_filename, media_type=media_type)
@router.get('/hash/{sha256}/download')
async def download_file_by_hash_action(
response: Response,
sha256: str = Path(min_length=64, max_length=64, regex='^[a-f0-9]{64}$'),
filename: Optional[str] = Query(None, min_length=4, max_length=255),
account: AccountContext = Depends(get_account_context_optional),
delay: DelayParams = Depends(),
):
"""
Direct hash-based download (Content-Addressable).
- Skips DB lookup for path resolution.
- Requires a valid API Key (via header or ?api_key=).
- Ideal for local caching systems like Events Launcher.
"""
if delay.sleep_time_s > 0: await asyncio.sleep(delay.sleep_time_s)
# 1. Mandatory Auth Check
# For now, we strictly require a valid machine API key (auth_method will not be 'guest')
if account.auth_method == 'guest':
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Valid API Key required for hash-based downloads."
)
# 2. Path Resolution (Deterministic)
hosted_files_path = settings.FILES_PATH['hosted_files_root']
subdir = sha256[0:2]
hash_filename = f"{sha256}.file"
full_file_path = os.path.join(hosted_files_path, subdir, hash_filename)
if not os.path.exists(full_file_path):
# Fallback to root (legacy structure)
full_file_path = os.path.join(hosted_files_path, hash_filename)
if not os.path.exists(full_file_path):
log.error(f"Hash-based file not found: {sha256}")
raise HTTPException(status_code=404, detail="File not found on server.")
# 3. Serve File
target_filename = filename or f"file_{sha256[:8]}.bin"
media_type = mimetypes.guess_type(target_filename)[0] or 'application/octet-stream'
return FileResponse(full_file_path, filename=target_filename, media_type=media_type)
@router.delete('/{hosted_file_id}', response_model=Resp_Body_Base)
async def delete_file_action(
hosted_file_id: str = Path(min_length=11, max_length=22),