mirror of
https://github.com/DeBrosOfficial/network.git
synced 2026-01-30 18:13:04 +00:00
322 lines
11 KiB
Go
322 lines
11 KiB
Go
package installers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
)
|
|
|
|
// IPFSInstaller handles IPFS (Kubo) installation
|
|
type IPFSInstaller struct {
|
|
*BaseInstaller
|
|
version string
|
|
}
|
|
|
|
// NewIPFSInstaller creates a new IPFS installer
|
|
func NewIPFSInstaller(arch string, logWriter io.Writer) *IPFSInstaller {
|
|
return &IPFSInstaller{
|
|
BaseInstaller: NewBaseInstaller(arch, logWriter),
|
|
version: "v0.38.2",
|
|
}
|
|
}
|
|
|
|
// IsInstalled checks if IPFS is already installed
|
|
func (ii *IPFSInstaller) IsInstalled() bool {
|
|
_, err := exec.LookPath("ipfs")
|
|
return err == nil
|
|
}
|
|
|
|
// Install downloads and installs IPFS (Kubo)
|
|
// Follows official steps from https://docs.ipfs.tech/install/command-line/
|
|
func (ii *IPFSInstaller) Install() error {
|
|
if ii.IsInstalled() {
|
|
fmt.Fprintf(ii.logWriter, " ✓ IPFS already installed\n")
|
|
return nil
|
|
}
|
|
|
|
fmt.Fprintf(ii.logWriter, " Installing IPFS (Kubo)...\n")
|
|
|
|
// Follow official installation steps in order
|
|
tarball := fmt.Sprintf("kubo_%s_linux-%s.tar.gz", ii.version, ii.arch)
|
|
url := fmt.Sprintf("https://dist.ipfs.tech/kubo/%s/%s", ii.version, tarball)
|
|
tmpDir := "/tmp"
|
|
tarPath := filepath.Join(tmpDir, tarball)
|
|
kuboDir := filepath.Join(tmpDir, "kubo")
|
|
|
|
// Step 1: Download the Linux binary from dist.ipfs.tech
|
|
fmt.Fprintf(ii.logWriter, " Step 1: Downloading Kubo %s...\n", ii.version)
|
|
if err := DownloadFile(url, tarPath); err != nil {
|
|
return fmt.Errorf("failed to download kubo from %s: %w", url, err)
|
|
}
|
|
|
|
// Verify tarball exists
|
|
if _, err := os.Stat(tarPath); err != nil {
|
|
return fmt.Errorf("kubo tarball not found after download at %s: %w", tarPath, err)
|
|
}
|
|
|
|
// Step 2: Unzip the file
|
|
fmt.Fprintf(ii.logWriter, " Step 2: Extracting Kubo archive...\n")
|
|
if err := ExtractTarball(tarPath, tmpDir); err != nil {
|
|
return fmt.Errorf("failed to extract kubo tarball: %w", err)
|
|
}
|
|
|
|
// Verify extraction
|
|
if _, err := os.Stat(kuboDir); err != nil {
|
|
return fmt.Errorf("kubo directory not found after extraction at %s: %w", kuboDir, err)
|
|
}
|
|
|
|
// Step 3: Move into the kubo folder (cd kubo)
|
|
fmt.Fprintf(ii.logWriter, " Step 3: Running installation script...\n")
|
|
|
|
// Step 4: Run the installation script (sudo bash install.sh)
|
|
installScript := filepath.Join(kuboDir, "install.sh")
|
|
if _, err := os.Stat(installScript); err != nil {
|
|
return fmt.Errorf("install.sh not found in extracted kubo directory at %s: %w", installScript, err)
|
|
}
|
|
|
|
cmd := exec.Command("bash", installScript)
|
|
cmd.Dir = kuboDir
|
|
if output, err := cmd.CombinedOutput(); err != nil {
|
|
return fmt.Errorf("failed to run install.sh: %v\n%s", err, string(output))
|
|
}
|
|
|
|
// Step 5: Test that Kubo has installed correctly
|
|
fmt.Fprintf(ii.logWriter, " Step 5: Verifying installation...\n")
|
|
cmd = exec.Command("ipfs", "--version")
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
// ipfs might not be in PATH yet in this process, check file directly
|
|
ipfsLocations := []string{"/usr/local/bin/ipfs", "/usr/bin/ipfs"}
|
|
found := false
|
|
for _, loc := range ipfsLocations {
|
|
if info, err := os.Stat(loc); err == nil && !info.IsDir() {
|
|
found = true
|
|
// Ensure it's executable
|
|
if info.Mode()&0111 == 0 {
|
|
os.Chmod(loc, 0755)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
return fmt.Errorf("ipfs binary not found after installation in %v", ipfsLocations)
|
|
}
|
|
} else {
|
|
fmt.Fprintf(ii.logWriter, " %s", string(output))
|
|
}
|
|
|
|
// Ensure PATH is updated for current process
|
|
os.Setenv("PATH", os.Getenv("PATH")+":/usr/local/bin")
|
|
|
|
fmt.Fprintf(ii.logWriter, " ✓ IPFS installed successfully\n")
|
|
return nil
|
|
}
|
|
|
|
// Configure is a placeholder for IPFS configuration
|
|
func (ii *IPFSInstaller) Configure() error {
|
|
// Configuration is handled by InitializeRepo
|
|
return nil
|
|
}
|
|
|
|
// InitializeRepo initializes an IPFS repository for a node (unified - no bootstrap/node distinction)
|
|
// If ipfsPeer is provided, configures Peering.Peers for peer discovery in private networks
|
|
func (ii *IPFSInstaller) InitializeRepo(ipfsRepoPath string, swarmKeyPath string, apiPort, gatewayPort, swarmPort int, ipfsPeer *IPFSPeerInfo) error {
|
|
configPath := filepath.Join(ipfsRepoPath, "config")
|
|
repoExists := false
|
|
if _, err := os.Stat(configPath); err == nil {
|
|
repoExists = true
|
|
fmt.Fprintf(ii.logWriter, " IPFS repo already exists, ensuring configuration...\n")
|
|
} else {
|
|
fmt.Fprintf(ii.logWriter, " Initializing IPFS repo...\n")
|
|
}
|
|
|
|
if err := os.MkdirAll(ipfsRepoPath, 0755); err != nil {
|
|
return fmt.Errorf("failed to create IPFS repo directory: %w", err)
|
|
}
|
|
|
|
// Resolve IPFS binary path
|
|
ipfsBinary, err := ResolveBinaryPath("ipfs", "/usr/local/bin/ipfs", "/usr/bin/ipfs")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Initialize IPFS if repo doesn't exist
|
|
if !repoExists {
|
|
cmd := exec.Command(ipfsBinary, "init", "--profile=server", "--repo-dir="+ipfsRepoPath)
|
|
if output, err := cmd.CombinedOutput(); err != nil {
|
|
return fmt.Errorf("failed to initialize IPFS: %v\n%s", err, string(output))
|
|
}
|
|
}
|
|
|
|
// Copy swarm key if present
|
|
swarmKeyExists := false
|
|
if data, err := os.ReadFile(swarmKeyPath); err == nil {
|
|
swarmKeyDest := filepath.Join(ipfsRepoPath, "swarm.key")
|
|
if err := os.WriteFile(swarmKeyDest, data, 0600); err != nil {
|
|
return fmt.Errorf("failed to copy swarm key: %w", err)
|
|
}
|
|
swarmKeyExists = true
|
|
}
|
|
|
|
// Configure IPFS addresses (API, Gateway, Swarm) by modifying the config file directly
|
|
// This ensures the ports are set correctly and avoids conflicts with RQLite on port 5001
|
|
fmt.Fprintf(ii.logWriter, " Configuring IPFS addresses (API: %d, Gateway: %d, Swarm: %d)...\n", apiPort, gatewayPort, swarmPort)
|
|
if err := ii.configureAddresses(ipfsRepoPath, apiPort, gatewayPort, swarmPort); err != nil {
|
|
return fmt.Errorf("failed to configure IPFS addresses: %w", err)
|
|
}
|
|
|
|
// Always disable AutoConf for private swarm when swarm.key is present
|
|
// This is critical - IPFS will fail to start if AutoConf is enabled on a private network
|
|
// We do this even for existing repos to fix repos initialized before this fix was applied
|
|
if swarmKeyExists {
|
|
fmt.Fprintf(ii.logWriter, " Disabling AutoConf for private swarm...\n")
|
|
cmd := exec.Command(ipfsBinary, "config", "--json", "AutoConf.Enabled", "false")
|
|
cmd.Env = append(os.Environ(), "IPFS_PATH="+ipfsRepoPath)
|
|
if output, err := cmd.CombinedOutput(); err != nil {
|
|
return fmt.Errorf("failed to disable AutoConf: %v\n%s", err, string(output))
|
|
}
|
|
|
|
// Clear AutoConf placeholders from config to prevent Kubo startup errors
|
|
// When AutoConf is disabled, 'auto' placeholders must be replaced with explicit values or empty
|
|
fmt.Fprintf(ii.logWriter, " Clearing AutoConf placeholders from IPFS config...\n")
|
|
|
|
type configCommand struct {
|
|
desc string
|
|
args []string
|
|
}
|
|
|
|
// List of config replacements to clear 'auto' placeholders
|
|
cleanup := []configCommand{
|
|
{"clearing Bootstrap peers", []string{"config", "Bootstrap", "--json", "[]"}},
|
|
{"clearing Routing.DelegatedRouters", []string{"config", "Routing.DelegatedRouters", "--json", "[]"}},
|
|
{"clearing Ipns.DelegatedPublishers", []string{"config", "Ipns.DelegatedPublishers", "--json", "[]"}},
|
|
{"clearing DNS.Resolvers", []string{"config", "DNS.Resolvers", "--json", "{}"}},
|
|
}
|
|
|
|
for _, step := range cleanup {
|
|
fmt.Fprintf(ii.logWriter, " %s...\n", step.desc)
|
|
cmd := exec.Command(ipfsBinary, step.args...)
|
|
cmd.Env = append(os.Environ(), "IPFS_PATH="+ipfsRepoPath)
|
|
if output, err := cmd.CombinedOutput(); err != nil {
|
|
return fmt.Errorf("failed while %s: %v\n%s", step.desc, err, string(output))
|
|
}
|
|
}
|
|
|
|
// Configure Peering.Peers if we have peer info (for private network discovery)
|
|
if ipfsPeer != nil && ipfsPeer.PeerID != "" && len(ipfsPeer.Addrs) > 0 {
|
|
fmt.Fprintf(ii.logWriter, " Configuring Peering.Peers for private network discovery...\n")
|
|
if err := ii.configurePeering(ipfsRepoPath, ipfsPeer); err != nil {
|
|
return fmt.Errorf("failed to configure IPFS peering: %w", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fix ownership (best-effort, don't fail if it doesn't work)
|
|
if err := exec.Command("chown", "-R", "debros:debros", ipfsRepoPath).Run(); err != nil {
|
|
fmt.Fprintf(ii.logWriter, " ⚠️ Warning: failed to chown IPFS repo: %v\n", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// configureAddresses configures the IPFS API, Gateway, and Swarm addresses in the config file
|
|
func (ii *IPFSInstaller) configureAddresses(ipfsRepoPath string, apiPort, gatewayPort, swarmPort int) error {
|
|
configPath := filepath.Join(ipfsRepoPath, "config")
|
|
|
|
// Read existing config
|
|
data, err := os.ReadFile(configPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read IPFS config: %w", err)
|
|
}
|
|
|
|
var config map[string]interface{}
|
|
if err := json.Unmarshal(data, &config); err != nil {
|
|
return fmt.Errorf("failed to parse IPFS config: %w", err)
|
|
}
|
|
|
|
// Get existing Addresses section or create new one
|
|
// This preserves any existing settings like Announce, AppendAnnounce, NoAnnounce
|
|
addresses, ok := config["Addresses"].(map[string]interface{})
|
|
if !ok {
|
|
addresses = make(map[string]interface{})
|
|
}
|
|
|
|
// Update specific address fields while preserving others
|
|
// Bind API and Gateway to localhost only for security
|
|
// Swarm binds to all interfaces for peer connections
|
|
addresses["API"] = []string{
|
|
fmt.Sprintf("/ip4/127.0.0.1/tcp/%d", apiPort),
|
|
}
|
|
addresses["Gateway"] = []string{
|
|
fmt.Sprintf("/ip4/127.0.0.1/tcp/%d", gatewayPort),
|
|
}
|
|
addresses["Swarm"] = []string{
|
|
fmt.Sprintf("/ip4/0.0.0.0/tcp/%d", swarmPort),
|
|
fmt.Sprintf("/ip6/::/tcp/%d", swarmPort),
|
|
}
|
|
|
|
config["Addresses"] = addresses
|
|
|
|
// Write config back
|
|
updatedData, err := json.MarshalIndent(config, "", " ")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal IPFS config: %w", err)
|
|
}
|
|
|
|
if err := os.WriteFile(configPath, updatedData, 0600); err != nil {
|
|
return fmt.Errorf("failed to write IPFS config: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// configurePeering configures Peering.Peers in the IPFS config for private network discovery
|
|
// This allows nodes in a private swarm to find each other even without bootstrap peers
|
|
func (ii *IPFSInstaller) configurePeering(ipfsRepoPath string, peer *IPFSPeerInfo) error {
|
|
configPath := filepath.Join(ipfsRepoPath, "config")
|
|
|
|
// Read existing config
|
|
data, err := os.ReadFile(configPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read IPFS config: %w", err)
|
|
}
|
|
|
|
var config map[string]interface{}
|
|
if err := json.Unmarshal(data, &config); err != nil {
|
|
return fmt.Errorf("failed to parse IPFS config: %w", err)
|
|
}
|
|
|
|
// Get existing Peering section or create new one
|
|
peering, ok := config["Peering"].(map[string]interface{})
|
|
if !ok {
|
|
peering = make(map[string]interface{})
|
|
}
|
|
|
|
// Create peer entry
|
|
peerEntry := map[string]interface{}{
|
|
"ID": peer.PeerID,
|
|
"Addrs": peer.Addrs,
|
|
}
|
|
|
|
// Set Peering.Peers
|
|
peering["Peers"] = []interface{}{peerEntry}
|
|
config["Peering"] = peering
|
|
|
|
fmt.Fprintf(ii.logWriter, " Adding peer: %s (%d addresses)\n", peer.PeerID, len(peer.Addrs))
|
|
|
|
// Write config back
|
|
updatedData, err := json.MarshalIndent(config, "", " ")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal IPFS config: %w", err)
|
|
}
|
|
|
|
if err := os.WriteFile(configPath, updatedData, 0600); err != nil {
|
|
return fmt.Errorf("failed to write IPFS config: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|