@zburn/core
Version:
Core utilities for the ZBURN ecosystem.
197 lines (168 loc) • 6.25 kB
JavaScript
const fs = require('fs');
const { promises } = require('fs');
const fsPromises = promises;
const path = require('path');
const { execSync, spawn } = require('child_process');
const { Blob } = require('buffer');
const { addToHistory, listHistory } = require('./history.js');
const { stimuleCidOnPublicGateways, pinEveryWhere } = require('./pin.js');
const kubo = require('kubo');
const kubo_path = kubo.path;
const IPFS_REPO_PATH = process.env.IPFS_PATH || path.join(process.env.HOME || process.env.USERPROFILE, '.ipfs_zburn');
const IPFS_API_URL = "http://127.0.0.1:5001/api/v0";
/**
* Recursively collect all files from a directory with their relative paths.
*/
function getAllFiles(dir, base = dir) {
let files = [];
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const fullPath = path.join(dir, entry.name);
const relPath = path.relative(base, fullPath).replace(/\\/g, '/');
if (entry.isDirectory()) {
files = files.concat(getAllFiles(fullPath, base));
} else if (entry.isFile()) {
files.push({ fullPath, relPath });
}
}
return files;
}
/**
* Generates an IPFS CID by adding a folder to a local IPFS node.
* Works on Node.js 18+ and Node.js 22 (with native fetch + FormData + Blob).
*/
async function generateCID(folderPath) {
const fetch = globalThis.fetch;
const stats = fs.existsSync(folderPath) && fs.statSync(folderPath);
if (!stats || !stats.isDirectory()) {
throw new Error('Invalid folder path');
}
const files = getAllFiles(folderPath);
if (files.length === 0) {
throw new Error('The folder is empty');
}
const form = new FormData();
for (const file of files) {
const content = fs.readFileSync(file.fullPath); // Read as buffer
const blob = new Blob([content], { type: 'application/octet-stream' });
form.append('file', blob, file.relPath); // relPath = filename/path in IPFS
}
const ipfsApiUrl = IPFS_API_URL+'/add?recursive=true&wrap-with-directory=true';
console.log("Upload to local IPFS daemon...")
let res;
try {
res = await fetch(ipfsApiUrl, {
method: 'POST',
body: form,
});
} catch (err) {
console.error(err);
return null;
}
if (!res.ok) {
const err = await res.text();
throw new Error(`IPFS upload failed: ${res.status} ${res.statusText}\n${err}`);
}
const responseText = await res.text();
const lines = responseText.trim().split('\n').map(line => JSON.parse(line));
const root = lines.find(line => line.Name === '');
if (!root || !root.Hash) {
throw new Error('Could not determine root CID');
}
console.log(`✅ Uploaded to local IPFS. Root CID: ${root.Hash}`);
addToHistory({
cid: root.Hash,
path: folderPath,
date: new Date().toISOString()
});
return root.Hash;
}
async function listLocalCIDs() {
const history = listHistory();
console.log('Local IPFS CIDs:');
history.forEach(el => {
console.log(`---------------------------------------------------\nCID: ${el.cid}\nPATH: ${el.path}\nDATE: ${el.date}\n`)
})
return history;
}
async function startIPFSDaemon() {
console.log("Starting IPFS daemon...");
const ipfsBinaryPath = kubo_path();
if (!ipfsBinaryPath) {
throw new Error("Kubo (IPFS) binary not found. Please ensure 'npm install kubo' was successful.");
}
console.log(`Using IPFS binary from: ${ipfsBinaryPath}`);
process.env.IPFS_PATH = IPFS_REPO_PATH;
try {
const repoExists = await fsPromises.stat(path.join(IPFS_REPO_PATH, 'config'))
.then(() => true)
.catch(() => false);
if (!repoExists) {
console.log(`IPFS repository not found at ${IPFS_REPO_PATH}. Initializing...`);
execSync(`${ipfsBinaryPath} init`, { stdio: 'inherit' });
console.log("IPFS repository initialized.");
} else {
console.log(`IPFS repository found at ${IPFS_REPO_PATH}. Skipping initialization.`);
}
console.log("Launching IPFS daemon in background...");
const daemonProcess = spawn(ipfsBinaryPath, ['daemon', '--enable-gc'], {
stdio: ['ignore', process.stdout, process.stderr],
env: { ...process.env, IPFS_PATH: IPFS_REPO_PATH }
});
console.log("Waiting for IPFS daemon to become ready...");
let attempts = 0;
const maxAttempts = 30; // Wait up to 30 * 1 second = 30 seconds
const pollInterval = 1000; // Poll every 1 second
while (attempts < maxAttempts) {
try {
const response = await fetch(IPFS_API_URL + '/id', {
method: 'POST'
});
if (response.ok) {
console.log("✅ IPFS daemon is now running and ready!");
return;
}
} catch (fetchError) {
}
await new Promise(resolve => setTimeout(resolve, pollInterval));
attempts++;
}
throw new Error(`IPFS daemon did not become ready after ${maxAttempts} attempts.`);
} catch (error) {
console.error("Failed to start IPFS daemon:", error);
throw error;
}
}
async function stopIPFSDaemon() {
console.log("Attempting to stop IPFS daemon...");
try {
const ipfsBinaryPath = kubo_path();
if (!ipfsBinaryPath) {
console.warn("Kubo (IPFS) binary path not found. Cannot stop daemon gracefully.");
return;
}
process.env.IPFS_PATH = IPFS_REPO_PATH; // Ensure IPFS_PATH is set for shutdown
execSync(`${ipfsBinaryPath} shutdown`, { stdio: 'inherit' });
console.log("IPFS daemon stopped successfully.");
} catch (error) {
console.error("Failed to stop IPFS daemon:", error.message);
}
}
async function deployIPFS(folderPath) {
try {
await startIPFSDaemon();
const cid = await generateCID(folderPath);
if (!cid)
throw new Error(`CID Generation failed, is IPFS daemon running on ${IPFS_API_URL} ?`);
await stimuleCidOnPublicGateways(cid);
await pinEveryWhere(cid);
await stopIPFSDaemon();
return cid;
} catch (err) {
console.error(err);
return null;
}
}
module.exports = {
deployIPFS,
listLocalCIDs
};