Use direct HTTP API for IPFS instead of ipfshttpclient

Replace ipfshttpclient library with direct HTTP requests to IPFS API.
This fixes compatibility with newer Kubo versions (0.39.0+) which are
not supported by the outdated ipfshttpclient library.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
gilesb
2026-01-08 19:39:49 +00:00
parent c73e79fe28
commit f52ec79860
2 changed files with 107 additions and 44 deletions

View File

@@ -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

View File

@@ -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