Files
simple-nanoshare/cli/nanoshare_client/client.py
T
daniel156161 2b0215cd94
Build and Push Docker Container / build-and-push (push) Successful in 1m52s
fix(cli): preserve notes during sync moves
- Leave sync and watch notes empty by default instead of writing placeholder text.
- Preserve existing remote notes when moving or adopting files without --note.
- Detect tracked local moves by SHA-256 and update remote metadata safely.
- Skip files that vanish during watch scans instead of aborting the sync cycle.
- Bump NanoShare to 1.24.0 and the standalone CLI to 0.3.0.
2026-07-27 21:40:30 +02:00

69 lines
2.4 KiB
Python

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 download_url(client, node: str, file_id: str) -> str:
base = client.registry.get(node)
if not base:
raise RuntimeError(f'unknown node: {node}')
return f"{base.rstrip('/')}/api/files/{quote(file_id, safe='')}/download"
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)