commit 4d27ceb1174d981e56db963ff092a95086109a9a Author: litoq Date: Sun Mar 15 13:11:57 2026 +0300 Initial implementation of IPFS CLI tool diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..5a42912 --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +IPFS_API_URL=http://127.0.0.1:5001 +# Optional: +# IPFS_API_TOKEN=token_here diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a3242ee --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +# Python +__pycache__/ +*.py[cod] +*.pyo +*.pyd +.Python + +# Virtual environments +.venv/ +venv/ +env/ + +# Environment variables +.env + +# OS-specific +.DS_Store +Thumbs.db + +# IDE / Editor +.vscode/ +.idea/ + diff --git a/README.md b/README.md new file mode 100644 index 0000000..43482ed --- /dev/null +++ b/README.md @@ -0,0 +1,57 @@ +### IPFS CLI (Python) + +Простая утилита для загрузки файлов в IPFS и скачивания по CID через HTTP API Kubo (`http://127.0.0.1:5001` по умолчанию). + +### Установка + +```bash +python -m venv .venv + +# Windows PowerShell +.\.venv\Scripts\Activate.ps1 + +# Linux / macOS / WSL +source .venv/bin/activate + +pip install -r requirements.txt +``` + + +### Конфигурация + +Создайте файл `.env` в корне проекта (можно скопировать из `.env.example`): + + +```bash +cp .env.example .env +``` + +По умолчанию используется: + +```text +IPFS_API_URL = http://127.0.0.1:5001 +IPFS_API_TOKEN = (пусто) +``` + +Перед работой IPFS‑узел должен быть запущен: + +```bash +ipfs init # один раз +ipfs daemon # при каждом запуске +``` + + +### Использование + +Загрузка файла: + +```bash +python ipfs_cli.py upload path/to/file.txt +# вывод: Qm... (CID) +``` + +Скачивание файла по CID (сохранится в `./download/.bin`): + +```bash +python ipfs_cli.py download QmXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +``` \ No newline at end of file diff --git a/download/.gitkeep b/download/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/ipfs_cli.py b/ipfs_cli.py new file mode 100644 index 0000000..7f69e07 --- /dev/null +++ b/ipfs_cli.py @@ -0,0 +1,93 @@ +import argparse +import os +import sys +from pathlib import Path + +import requests + +API_URL = os.getenv("IPFS_API_URL", "http://127.0.0.1:5001").rstrip("/") +TOKEN = os.getenv("IPFS_API_TOKEN") + + +def _auth_headers() -> dict[str, str]: + return {"Authorization": f"Bearer {TOKEN}"} if TOKEN else {} + + +def upload_file(path: Path) -> str: + if not path.is_file(): + raise FileNotFoundError(f"Файл не найден: {path}") + + url = f"{API_URL}/api/v0/add" + with path.open("rb") as f: + files = {"file": (path.name, f)} + r = requests.post(url, headers=_auth_headers(), files=files, timeout=120) + + if r.status_code != 200: + raise RuntimeError(f"Ошибка загрузки (status={r.status_code}): {r.text}") + + data = r.json() + cid = data.get("Hash") + if not cid: + raise RuntimeError(f"Не удалось получить CID из ответа: {data}") + return cid + + +def download_file(cid: str) -> Path: + if not cid: + raise ValueError("CID пустой") + + download_dir = Path("download") + download_dir.mkdir(parents=True, exist_ok=True) + out_path = download_dir / f"{cid}.bin" + + url = f"{API_URL}/api/v0/cat" + params = {"arg": cid} + + with requests.post( + url, + headers=_auth_headers(), + params=params, + stream=True, + timeout=120, + ) as r: + if r.status_code != 200: + raise RuntimeError( + f"Ошибка скачивания (status={r.status_code}): {r.text}" + ) + with out_path.open("wb") as f: + for chunk in r.iter_content(chunk_size=8192): + if chunk: + f.write(chunk) + + return out_path + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(add_help=False) + sub = parser.add_subparsers(dest="command", required=True) + + up = sub.add_parser("upload", add_help=False) + up.add_argument("file") + + dl = sub.add_parser("download", add_help=False) + dl.add_argument("cid") + + args = parser.parse_args(argv) + + try: + if args.command == "upload": + cid = upload_file(Path(args.file)) + print(cid) + elif args.command == "download": + out = download_file(args.cid) + print(out) + else: + raise ValueError("Неизвестная команда") + return 0 + except Exception as exc: + print(f"Ошибка: {exc}", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + sys.exit(main()) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..1274c1b --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +python-dotenv>=1.0.0 +requests>=2.32.0