from __future__ import annotations import base64 import mimetypes from pathlib import Path from urllib.parse import quote import httpx def content_type(path: Path) -> str: return mimetypes.guess_type(path.name)[0] or 'application/octet-stream' def upload(client, node: str, path: Path, remote_name: str, note: str, expires: str, remote_path: str = '') -> dict: content_b64 = base64.b64encode(path.read_bytes()).decode('ascii') params = { 'file_name': remote_name, 'content_b64': content_b64, 'content_type': content_type(path), 'note': note, } if remote_path: params['file_path'] = remote_path if expires: params['expires'] = expires result = client.call(node, 'files.upload', params) return result if isinstance(result, dict) else {'result': result} def update_remote(client, node: str, file_id: str, file_name: str, file_path: str, note: str | None, expires: str) -> None: params = { 'file_id': file_id, 'file_name': file_name, 'file_path': file_path, } if note is not None: params['note'] = note if expires: params['expires'] = expires client.call(node, 'files.update', params) def delete_remote(client, node: str, file_id: str) -> None: client.call(node, 'files.delete', {'file_id': file_id}) def list_remote(client, node: str) -> list[dict]: result = client.call(node, 'files.list', {}) files = result.get('files', []) if isinstance(result, dict) else [] return files if isinstance(files, list) else [] def node_url(client, node: str, path: str) -> str: base = client.registry.get(node) if not base: raise RuntimeError(f'unknown node: {node}') return f"{base.rstrip('/')}{path}" def download_url(client, node: str, file_id: str) -> str: return node_url(client, node, f"/api/files/{quote(file_id, safe='')}/download") def events_url(client, node: str) -> str: return node_url(client, node, '/api/files/events') def download(client, node: str, file_id: str, path: Path) -> None: path.parent.mkdir(parents=True, exist_ok=True) headers = {'Accept': '*/*', **client.auth_headers()} with httpx.Client(timeout=client.timeout) as http: with http.stream('GET', download_url(client, node, file_id), headers=headers) as response: if response.status_code == 404: raise FileNotFoundError(file_id) if response.status_code == 410: raise RuntimeError(f'remote file expired: {file_id}') response.raise_for_status() tmp = path.with_name(f'{path.name}.{Path.cwd().stat().st_ino}.tmp') with tmp.open('wb') as handle: for chunk in response.iter_bytes(): handle.write(chunk) tmp.replace(path)