diff --git a/ipfs_client.py b/ipfs_client.py index e7d1bc3..17b6e74 100644 --- a/ipfs_client.py +++ b/ipfs_client.py @@ -3,15 +3,16 @@ IPFS client for Art DAG L1 server. Provides functions to add, retrieve, and pin files on IPFS. -Uses local cache as hot storage with IPFS as durable backing store. +Uses direct HTTP API calls for compatibility with all Kubo versions. """ import logging import os +import re from pathlib import Path from typing import Optional -import ipfshttpclient +import requests logger = logging.getLogger(__name__) @@ -22,9 +23,26 @@ IPFS_API = os.getenv("IPFS_API", "/ip4/127.0.0.1/tcp/5001") IPFS_TIMEOUT = int(os.getenv("IPFS_TIMEOUT", "30")) -def get_client(): - """Get an IPFS client connection.""" - return ipfshttpclient.connect(IPFS_API, timeout=IPFS_TIMEOUT) +def _multiaddr_to_url(multiaddr: str) -> str: + """Convert IPFS multiaddr to HTTP URL.""" + # Handle /dns/hostname/tcp/port format + dns_match = re.match(r"/dns[46]?/([^/]+)/tcp/(\d+)", multiaddr) + if dns_match: + return f"http://{dns_match.group(1)}:{dns_match.group(2)}" + + # Handle /ip4/address/tcp/port format + ip4_match = re.match(r"/ip4/([^/]+)/tcp/(\d+)", multiaddr) + if ip4_match: + return f"http://{ip4_match.group(1)}:{ip4_match.group(2)}" + + # Fallback: assume it's already a URL or use default + if multiaddr.startswith("http"): + return multiaddr + return "http://127.0.0.1:5001" + + +# Base URL for IPFS API +IPFS_BASE_URL = _multiaddr_to_url(IPFS_API) def add_file(file_path: Path, pin: bool = True) -> Optional[str]: @@ -39,11 +57,18 @@ def add_file(file_path: Path, pin: bool = True) -> Optional[str]: IPFS CID (content identifier) or None on failure """ try: - with get_client() as client: - result = client.add(str(file_path), pin=pin) - cid = result["Hash"] - logger.info(f"Added to IPFS: {file_path.name} -> {cid}") - return cid + url = f"{IPFS_BASE_URL}/api/v0/add" + params = {"pin": str(pin).lower()} + + with open(file_path, "rb") as f: + files = {"file": (file_path.name, f)} + response = requests.post(url, params=params, files=files, timeout=IPFS_TIMEOUT) + + response.raise_for_status() + result = response.json() + cid = result["Hash"] + logger.info(f"Added to IPFS: {file_path.name} -> {cid}") + return cid except Exception as e: logger.error(f"Failed to add to IPFS: {e}") return None @@ -61,13 +86,17 @@ def add_bytes(data: bytes, pin: bool = True) -> Optional[str]: IPFS CID or None on failure """ try: - with get_client() as client: - result = client.add_bytes(data) - cid = result - if pin: - client.pin.add(cid) - logger.info(f"Added bytes to IPFS: {len(data)} bytes -> {cid}") - return cid + url = f"{IPFS_BASE_URL}/api/v0/add" + params = {"pin": str(pin).lower()} + files = {"file": ("data", data)} + + response = requests.post(url, params=params, files=files, timeout=IPFS_TIMEOUT) + response.raise_for_status() + result = response.json() + cid = result["Hash"] + + logger.info(f"Added bytes to IPFS: {len(data)} bytes -> {cid}") + return cid except Exception as e: logger.error(f"Failed to add bytes to IPFS: {e}") return None @@ -85,14 +114,14 @@ def get_file(cid: str, dest_path: Path) -> bool: True on success, False on failure """ try: - with get_client() as client: - # Get file content - data = client.cat(cid) - # Write to destination - dest_path.parent.mkdir(parents=True, exist_ok=True) - dest_path.write_bytes(data) - logger.info(f"Retrieved from IPFS: {cid} -> {dest_path}") - return True + data = get_bytes(cid) + if data is None: + return False + + dest_path.parent.mkdir(parents=True, exist_ok=True) + dest_path.write_bytes(data) + logger.info(f"Retrieved from IPFS: {cid} -> {dest_path}") + return True except Exception as e: logger.error(f"Failed to get from IPFS: {e}") return False @@ -109,10 +138,15 @@ def get_bytes(cid: str) -> Optional[bytes]: File content as bytes or None on failure """ try: - with get_client() as client: - data = client.cat(cid) - logger.info(f"Retrieved from IPFS: {cid} ({len(data)} bytes)") - return data + url = f"{IPFS_BASE_URL}/api/v0/cat" + params = {"arg": cid} + + response = requests.post(url, params=params, timeout=IPFS_TIMEOUT) + response.raise_for_status() + data = response.content + + logger.info(f"Retrieved from IPFS: {cid} ({len(data)} bytes)") + return data except Exception as e: logger.error(f"Failed to get bytes from IPFS: {e}") return None @@ -129,10 +163,14 @@ def pin(cid: str) -> bool: True on success, False on failure """ try: - with get_client() as client: - client.pin.add(cid) - logger.info(f"Pinned on IPFS: {cid}") - return True + url = f"{IPFS_BASE_URL}/api/v0/pin/add" + params = {"arg": cid} + + response = requests.post(url, params=params, timeout=IPFS_TIMEOUT) + response.raise_for_status() + + logger.info(f"Pinned on IPFS: {cid}") + return True except Exception as e: logger.error(f"Failed to pin on IPFS: {e}") return False @@ -149,10 +187,14 @@ def unpin(cid: str) -> bool: True on success, False on failure """ try: - with get_client() as client: - client.pin.rm(cid) - logger.info(f"Unpinned on IPFS: {cid}") - return True + url = f"{IPFS_BASE_URL}/api/v0/pin/rm" + params = {"arg": cid} + + response = requests.post(url, params=params, timeout=IPFS_TIMEOUT) + response.raise_for_status() + + logger.info(f"Unpinned on IPFS: {cid}") + return True except Exception as e: logger.error(f"Failed to unpin on IPFS: {e}") return False @@ -169,9 +211,14 @@ def is_pinned(cid: str) -> bool: True if pinned, False otherwise """ try: - with get_client() as client: - pins = client.pin.ls(type="recursive") - return cid in pins.get("Keys", {}) + url = f"{IPFS_BASE_URL}/api/v0/pin/ls" + params = {"arg": cid, "type": "recursive"} + + response = requests.post(url, params=params, timeout=IPFS_TIMEOUT) + if response.status_code == 200: + result = response.json() + return cid in result.get("Keys", {}) + return False except Exception as e: logger.error(f"Failed to check pin status: {e}") return False @@ -185,8 +232,25 @@ def is_available() -> bool: True if IPFS is available, False otherwise """ try: - with get_client() as client: - client.id() - return True + url = f"{IPFS_BASE_URL}/api/v0/id" + response = requests.post(url, timeout=5) + return response.status_code == 200 except Exception: return False + + +def get_node_id() -> Optional[str]: + """ + Get this IPFS node's peer ID. + + Returns: + Peer ID string or None on failure + """ + try: + url = f"{IPFS_BASE_URL}/api/v0/id" + response = requests.post(url, timeout=IPFS_TIMEOUT) + response.raise_for_status() + return response.json().get("ID") + except Exception as e: + logger.error(f"Failed to get node ID: {e}") + return None diff --git a/requirements.txt b/requirements.txt index 034da0a..7ef5926 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,6 +6,5 @@ uvicorn>=0.27.0 python-multipart>=0.0.6 PyYAML>=6.0 asyncpg>=0.29.0 -ipfshttpclient>=0.7.0 # Core artdag from GitHub git+https://github.com/gilesbradshaw/art-dag.git