"""Thin wrapper around the bits of Nextcloud's WebDAV and OCS Share API we need. Auth is HTTP Basic with an app password (Settings -> Security -> Devices & Sessions -> create app password), never the account's real password. """ from __future__ import annotations import requests from .config import Config def _auth() -> tuple[str, str]: return (Config.NC_USERNAME, Config.NC_APP_PASSWORD) def _webdav_root() -> str: return f"{Config.NC_BASE_URL}/remote.php/dav/files/{Config.NC_USERNAME}" def ensure_upload_folder() -> None: """MKCOL each segment of the configured upload folder path if missing. NC_UPLOAD_FOLDER may be nested (e.g. "Documents/Cospend/Assets"); MKCOL only creates one level at a time, so walk the path segment by segment. Idempotent. """ if not Config.NC_UPLOAD_FOLDER: return segments = Config.NC_UPLOAD_FOLDER.split("/") partial = "" for segment in segments: partial = f"{partial}/{segment}" if partial else segment url = f"{_webdav_root()}/{partial}" resp = requests.request("MKCOL", url, auth=_auth(), timeout=15) # 201 = created, 405 = already exists. Anything else is a real problem. if resp.status_code not in (201, 405): resp.raise_for_status() def upload_file(filename: str, content: bytes, content_type: str = "image/jpeg") -> str: """Uploads `content` to / via WebDAV PUT. Returns the server-relative path (e.g. "wgBill/receipt-123.jpg"), which is what the Share API expects as `path`. """ ensure_upload_folder() rel_path = f"{Config.NC_UPLOAD_FOLDER}/{filename}" if Config.NC_UPLOAD_FOLDER else filename url = f"{_webdav_root()}/{rel_path}" resp = requests.put( url, data=content, auth=_auth(), headers={"Content-Type": content_type}, timeout=30 ) resp.raise_for_status() return rel_path def create_public_share(rel_path: str) -> str: """Creates a public read-only link share for `rel_path`. Returns the share URL.""" url = f"{Config.NC_BASE_URL}/ocs/v2.php/apps/files_sharing/api/v1/shares" resp = requests.post( url, auth=_auth(), headers={"OCS-APIRequest": "true"}, data={ "path": f"/{rel_path}", "shareType": 3, # public link "permissions": 1, # read-only }, params={"format": "json"}, timeout=15, ) resp.raise_for_status() payload = resp.json() share_url = payload["ocs"]["data"]["url"] return share_url