cli/src/services/ipfs.ts
2025-04-02 13:37:54 +03:00

238 lines
6.1 KiB
TypeScript

import fs from "fs";
import path from "path";
import { execSync } from "child_process";
import { logger, startSpinner, stopSpinner } from "../utils/logger.js";
import { config } from "../utils/config.js";
import { initIpfs, getHelia, getLibp2p, stopIpfs } from "@debros/network";
// Interface to represent a Node in the network
export interface NodeInfo {
id: string;
address: string;
port: number;
load: number;
type: "k3s" | "docker";
}
// Interface for node list JSON
export interface NodeList {
nodes: NodeInfo[];
updatedAt: string;
}
/**
* Initialize IPFS instance using the @debros/network configuration
*/
export async function initializeIpfs() {
const spinner = startSpinner("Initializing IPFS with DeBros Network...");
try {
// Use the initIpfs function from @debros/network
await initIpfs();
const ipfs = getHelia();
const libp2p = getLibp2p();
if (!ipfs || !libp2p) {
throw new Error("Failed to initialize IPFS or libp2p");
}
// Get the peer ID
const peerId = libp2p.peerId.toString();
stopSpinner(`IPFS initialized with ID: ${peerId}`, true);
return { ipfs, libp2p };
} catch (error) {
stopSpinner(
`Failed to initialize IPFS: ${
error instanceof Error ? error.message : String(error)
}`,
false
);
throw error;
}
}
/**
* Upload a file to IPFS
*/
export async function uploadToIPFS(filePath: string) {
const spinner = startSpinner(`Uploading ${filePath} to IPFS...`);
try {
// Initialize IPFS
const { ipfs } = await initializeIpfs();
// Read the file
const fileContent = fs.readFileSync(filePath);
// Create a block for the file content
const encoder = new TextEncoder();
const bytes = encoder.encode(fileContent.toString());
// Add the content to IPFS
const { cid } = await ipfs.blockstore.put(bytes);
// Stop IPFS to clean up
await stopIpfs();
stopSpinner(`File uploaded with CID: ${cid.toString()}`, true);
return cid.toString();
} catch (error) {
stopSpinner(
`Failed to upload file: ${
error instanceof Error ? error.message : String(error)
}`,
false
);
throw error;
}
}
/**
* Generate IPNS key if it doesn't exist
*/
export async function ensureIPNSKey(name: string) {
const spinner = startSpinner(`Ensuring IPNS key exists for ${name}...`);
try {
// We'll use the ipfs CLI for this since the JS library doesn't expose key management well
const keyListOutput = execSync("ipfs key list -l").toString();
const keyExists = keyListOutput.includes(name);
if (!keyExists) {
logger.info(`Creating new IPNS key: ${name}`);
execSync(`ipfs key gen --type=rsa --size=2048 ${name}`);
spinner.text = `Created new IPNS key: ${name}`;
} else {
spinner.text = `IPNS key ${name} already exists`;
}
// Get the key ID
const keyInfo = execSync("ipfs key list -l")
.toString()
.split("\n")
.find((line) => line.includes(name));
if (!keyInfo) {
throw new Error(`Failed to find key info for ${name}`);
}
const keyId = keyInfo.split(" ")[0].trim();
stopSpinner(`IPNS key ready: ${keyId}`, true);
return keyId;
} catch (error) {
stopSpinner(
`Failed to ensure IPNS key: ${
error instanceof Error ? error.message : String(error)
}`,
false
);
throw error;
}
}
/**
* Publish content to IPNS
*/
export async function publishToIPNS(cid: string, keyName: string) {
const spinner = startSpinner(
`Publishing ${cid} to IPNS with key ${keyName}...`
);
try {
const output = execSync(
`ipfs name publish --key=${keyName} /ipfs/${cid}`
).toString();
const ipnsAddress = output.match(/to (.+)/)?.[1]?.trim() || "";
stopSpinner(`Published to IPNS: ${ipnsAddress}`, true);
return ipnsAddress;
} catch (error) {
stopSpinner(
`Failed to publish to IPNS: ${
error instanceof Error ? error.message : String(error)
}`,
false
);
throw error;
}
}
/**
* Create and publish node list JSON
*/
export async function publishNodeList(nodes: NodeInfo[], appName: string) {
const spinner = startSpinner(`Publishing node list for ${appName}...`);
try {
const nodeList: NodeList = {
nodes,
updatedAt: new Date().toISOString(),
};
const tempFile = path.join(config.ipfsDataPath, `${appName}-nodes.json`);
fs.writeFileSync(tempFile, JSON.stringify(nodeList, null, 2));
const cid = await uploadToIPFS(tempFile);
const keyName = `${appName}-nodes`;
await ensureIPNSKey(keyName);
const ipnsAddress = await publishToIPNS(cid, keyName);
fs.unlinkSync(tempFile);
stopSpinner(`Node list published to ${ipnsAddress}`, true);
return { cid, ipnsAddress };
} catch (error) {
stopSpinner(
`Failed to publish node list: ${
error instanceof Error ? error.message : String(error)
}`,
false
);
throw error;
}
}
/**
* Fetch node list from IPNS
*/
export async function fetchNodeList(ipnsPath: string): Promise<NodeList> {
const spinner = startSpinner(`Fetching node list from ${ipnsPath}...`);
try {
// Resolve IPNS to CID
const ipfsCid = execSync(`ipfs name resolve ${ipnsPath}`)
.toString()
.trim()
.replace("/ipfs/", "");
spinner.text = `Resolved IPNS to CID: ${ipfsCid}`;
// Get file from IPFS
const tempOutputPath = path.join(config.ipfsDataPath, "nodelist-temp.json");
execSync(`ipfs get ${ipfsCid} -o ${tempOutputPath}`);
// Read and parse the file
const nodeListContent = fs.readFileSync(tempOutputPath, "utf8");
const nodeList = JSON.parse(nodeListContent) as NodeList;
// Clean up temp file
fs.unlinkSync(tempOutputPath);
stopSpinner(`Fetched node list with ${nodeList.nodes.length} nodes`, true);
return nodeList;
} catch (error) {
stopSpinner(
`Failed to fetch node list: ${
error instanceof Error ? error.message : String(error)
}`,
false
);
throw new Error(
`Failed to fetch node list: ${
error instanceof Error ? error.message : String(error)
}`
);
}
}