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:
150
ipfs_client.py
150
ipfs_client.py
@@ -3,15 +3,16 @@
|
|||||||
IPFS client for Art DAG L1 server.
|
IPFS client for Art DAG L1 server.
|
||||||
|
|
||||||
Provides functions to add, retrieve, and pin files on IPFS.
|
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 logging
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import ipfshttpclient
|
import requests
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
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"))
|
IPFS_TIMEOUT = int(os.getenv("IPFS_TIMEOUT", "30"))
|
||||||
|
|
||||||
|
|
||||||
def get_client():
|
def _multiaddr_to_url(multiaddr: str) -> str:
|
||||||
"""Get an IPFS client connection."""
|
"""Convert IPFS multiaddr to HTTP URL."""
|
||||||
return ipfshttpclient.connect(IPFS_API, timeout=IPFS_TIMEOUT)
|
# 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]:
|
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
|
IPFS CID (content identifier) or None on failure
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
with get_client() as client:
|
url = f"{IPFS_BASE_URL}/api/v0/add"
|
||||||
result = client.add(str(file_path), pin=pin)
|
params = {"pin": str(pin).lower()}
|
||||||
cid = result["Hash"]
|
|
||||||
logger.info(f"Added to IPFS: {file_path.name} -> {cid}")
|
with open(file_path, "rb") as f:
|
||||||
return cid
|
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:
|
except Exception as e:
|
||||||
logger.error(f"Failed to add to IPFS: {e}")
|
logger.error(f"Failed to add to IPFS: {e}")
|
||||||
return None
|
return None
|
||||||
@@ -61,13 +86,17 @@ def add_bytes(data: bytes, pin: bool = True) -> Optional[str]:
|
|||||||
IPFS CID or None on failure
|
IPFS CID or None on failure
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
with get_client() as client:
|
url = f"{IPFS_BASE_URL}/api/v0/add"
|
||||||
result = client.add_bytes(data)
|
params = {"pin": str(pin).lower()}
|
||||||
cid = result
|
files = {"file": ("data", data)}
|
||||||
if pin:
|
|
||||||
client.pin.add(cid)
|
response = requests.post(url, params=params, files=files, timeout=IPFS_TIMEOUT)
|
||||||
logger.info(f"Added bytes to IPFS: {len(data)} bytes -> {cid}")
|
response.raise_for_status()
|
||||||
return cid
|
result = response.json()
|
||||||
|
cid = result["Hash"]
|
||||||
|
|
||||||
|
logger.info(f"Added bytes to IPFS: {len(data)} bytes -> {cid}")
|
||||||
|
return cid
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to add bytes to IPFS: {e}")
|
logger.error(f"Failed to add bytes to IPFS: {e}")
|
||||||
return None
|
return None
|
||||||
@@ -85,14 +114,14 @@ def get_file(cid: str, dest_path: Path) -> bool:
|
|||||||
True on success, False on failure
|
True on success, False on failure
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
with get_client() as client:
|
data = get_bytes(cid)
|
||||||
# Get file content
|
if data is None:
|
||||||
data = client.cat(cid)
|
return False
|
||||||
# Write to destination
|
|
||||||
dest_path.parent.mkdir(parents=True, exist_ok=True)
|
dest_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
dest_path.write_bytes(data)
|
dest_path.write_bytes(data)
|
||||||
logger.info(f"Retrieved from IPFS: {cid} -> {dest_path}")
|
logger.info(f"Retrieved from IPFS: {cid} -> {dest_path}")
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to get from IPFS: {e}")
|
logger.error(f"Failed to get from IPFS: {e}")
|
||||||
return False
|
return False
|
||||||
@@ -109,10 +138,15 @@ def get_bytes(cid: str) -> Optional[bytes]:
|
|||||||
File content as bytes or None on failure
|
File content as bytes or None on failure
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
with get_client() as client:
|
url = f"{IPFS_BASE_URL}/api/v0/cat"
|
||||||
data = client.cat(cid)
|
params = {"arg": cid}
|
||||||
logger.info(f"Retrieved from IPFS: {cid} ({len(data)} bytes)")
|
|
||||||
return data
|
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:
|
except Exception as e:
|
||||||
logger.error(f"Failed to get bytes from IPFS: {e}")
|
logger.error(f"Failed to get bytes from IPFS: {e}")
|
||||||
return None
|
return None
|
||||||
@@ -129,10 +163,14 @@ def pin(cid: str) -> bool:
|
|||||||
True on success, False on failure
|
True on success, False on failure
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
with get_client() as client:
|
url = f"{IPFS_BASE_URL}/api/v0/pin/add"
|
||||||
client.pin.add(cid)
|
params = {"arg": cid}
|
||||||
logger.info(f"Pinned on IPFS: {cid}")
|
|
||||||
return True
|
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:
|
except Exception as e:
|
||||||
logger.error(f"Failed to pin on IPFS: {e}")
|
logger.error(f"Failed to pin on IPFS: {e}")
|
||||||
return False
|
return False
|
||||||
@@ -149,10 +187,14 @@ def unpin(cid: str) -> bool:
|
|||||||
True on success, False on failure
|
True on success, False on failure
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
with get_client() as client:
|
url = f"{IPFS_BASE_URL}/api/v0/pin/rm"
|
||||||
client.pin.rm(cid)
|
params = {"arg": cid}
|
||||||
logger.info(f"Unpinned on IPFS: {cid}")
|
|
||||||
return True
|
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:
|
except Exception as e:
|
||||||
logger.error(f"Failed to unpin on IPFS: {e}")
|
logger.error(f"Failed to unpin on IPFS: {e}")
|
||||||
return False
|
return False
|
||||||
@@ -169,9 +211,14 @@ def is_pinned(cid: str) -> bool:
|
|||||||
True if pinned, False otherwise
|
True if pinned, False otherwise
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
with get_client() as client:
|
url = f"{IPFS_BASE_URL}/api/v0/pin/ls"
|
||||||
pins = client.pin.ls(type="recursive")
|
params = {"arg": cid, "type": "recursive"}
|
||||||
return cid in pins.get("Keys", {})
|
|
||||||
|
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:
|
except Exception as e:
|
||||||
logger.error(f"Failed to check pin status: {e}")
|
logger.error(f"Failed to check pin status: {e}")
|
||||||
return False
|
return False
|
||||||
@@ -185,8 +232,25 @@ def is_available() -> bool:
|
|||||||
True if IPFS is available, False otherwise
|
True if IPFS is available, False otherwise
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
with get_client() as client:
|
url = f"{IPFS_BASE_URL}/api/v0/id"
|
||||||
client.id()
|
response = requests.post(url, timeout=5)
|
||||||
return True
|
return response.status_code == 200
|
||||||
except Exception:
|
except Exception:
|
||||||
return False
|
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
|
||||||
|
|||||||
@@ -6,6 +6,5 @@ uvicorn>=0.27.0
|
|||||||
python-multipart>=0.0.6
|
python-multipart>=0.0.6
|
||||||
PyYAML>=6.0
|
PyYAML>=6.0
|
||||||
asyncpg>=0.29.0
|
asyncpg>=0.29.0
|
||||||
ipfshttpclient>=0.7.0
|
|
||||||
# Core artdag from GitHub
|
# Core artdag from GitHub
|
||||||
git+https://github.com/gilesbradshaw/art-dag.git
|
git+https://github.com/gilesbradshaw/art-dag.git
|
||||||
|
|||||||
Reference in New Issue
Block a user