Skip to main content

ferripfs_network/
daemon.rs

1// Ported from: kubo/cmd/ipfs/kubo/daemon.go
2// Kubo version: v0.39.0
3//
4// Original work: Copyright (c) Protocol Labs, Inc.
5// Port: Copyright (c) 2026 ferripfs contributors
6// SPDX-License-Identifier: MIT OR Apache-2.0
7
8//! Daemon process management.
9
10use std::fs;
11use std::path::{Path, PathBuf};
12use std::sync::Arc;
13
14use libp2p::identity::Keypair;
15use tokio::sync::oneshot;
16
17use ferripfs_config::Config;
18use ferripfs_repo::{FsRepo, RepoLock, API_FILE};
19
20use crate::{
21    host::{HostConfig, NetworkHost},
22    NetworkError, NetworkResult, PeerInfo,
23};
24
25/// Configuration for the daemon.
26#[derive(Debug, Clone)]
27pub struct DaemonConfig {
28    /// Repository path
29    pub repo_path: PathBuf,
30    /// Run in offline mode (no network)
31    pub offline: bool,
32    /// Enable mDNS discovery
33    pub enable_mdns: bool,
34    /// Enable pubsub
35    pub enable_pubsub: bool,
36    /// Enable IPNS pubsub
37    pub enable_ipns_pubsub: bool,
38    /// Routing mode (dht, dhtclient, dhtserver, none)
39    pub routing: String,
40}
41
42impl Default for DaemonConfig {
43    fn default() -> Self {
44        Self {
45            repo_path: PathBuf::new(),
46            offline: false,
47            enable_mdns: true,
48            enable_pubsub: false,
49            enable_ipns_pubsub: false,
50            routing: "dht".into(),
51        }
52    }
53}
54
55/// Handle to a running daemon.
56pub struct DaemonHandle {
57    /// Shutdown sender
58    shutdown_tx: Option<oneshot::Sender<()>>,
59    /// Network host
60    host: Arc<NetworkHost>,
61    /// Repository lock
62    _lock: RepoLock,
63    /// Repository path
64    repo_path: PathBuf,
65}
66
67impl DaemonHandle {
68    /// Get the network host.
69    pub fn host(&self) -> &NetworkHost {
70        &self.host
71    }
72
73    /// Get peer info.
74    pub async fn peer_info(&self) -> NetworkResult<PeerInfo> {
75        self.host.peer_info().await
76    }
77
78    /// Shutdown the daemon.
79    pub async fn shutdown(mut self) -> NetworkResult<()> {
80        // Signal shutdown
81        if let Some(tx) = self.shutdown_tx.take() {
82            let _ = tx.send(());
83        }
84
85        // Shutdown host
86        self.host.shutdown().await?;
87
88        // Remove API file
89        let api_file = self.repo_path.join(API_FILE);
90        let _ = fs::remove_file(&api_file);
91
92        Ok(())
93    }
94}
95
96impl Drop for DaemonHandle {
97    fn drop(&mut self) {
98        // Clean up API file
99        let api_file = self.repo_path.join(API_FILE);
100        let _ = fs::remove_file(&api_file);
101    }
102}
103
104/// The ferripfs daemon.
105pub struct Daemon;
106
107impl Daemon {
108    /// Start the daemon.
109    pub async fn start(config: DaemonConfig) -> NetworkResult<DaemonHandle> {
110        let repo_path = config.repo_path.clone();
111
112        // Check if repo is initialized
113        if !FsRepo::is_initialized(&repo_path) {
114            return Err(NetworkError::Daemon(format!(
115                "Repository not initialized at {}. Run 'ferripfs init' first.",
116                repo_path.display()
117            )));
118        }
119
120        // Acquire repository lock
121        let lock = RepoLock::acquire(&repo_path).map_err(|e| {
122            if matches!(e, ferripfs_repo::RepoError::Locked) {
123                NetworkError::AlreadyRunning
124            } else {
125                NetworkError::Repo(e)
126            }
127        })?;
128
129        // Open repository
130        let repo = FsRepo::open(&repo_path)?;
131        let ferripfs_config = repo.config().clone();
132
133        // Load keypair from identity
134        let keypair = Self::load_keypair(&ferripfs_config)?;
135
136        // Create host config
137        let host_config = if config.offline {
138            // Offline mode - no listening
139            HostConfig {
140                keypair,
141                listen_addrs: Vec::new(),
142                bootstrap_peers: Vec::new(),
143                swarm_config: crate::swarm::SwarmConfig {
144                    mdns_enabled: false,
145                    relay_enabled: false,
146                    ..Default::default()
147                },
148            }
149        } else {
150            HostConfig::from_config(&ferripfs_config, keypair)
151        };
152
153        // Start network host
154        let host = NetworkHost::start(host_config).await?;
155        let host = Arc::new(host);
156
157        // Write API file
158        let api_addr = ferripfs_config
159            .addresses
160            .api
161            .first()
162            .cloned()
163            .unwrap_or_else(|| "/ip4/127.0.0.1/tcp/5001".into());
164        let api_file = repo_path.join(API_FILE);
165        fs::write(&api_file, &api_addr).map_err(NetworkError::Io)?;
166
167        // Create shutdown channel
168        let (shutdown_tx, _shutdown_rx) = oneshot::channel();
169
170        Ok(DaemonHandle {
171            shutdown_tx: Some(shutdown_tx),
172            host,
173            _lock: lock,
174            repo_path,
175        })
176    }
177
178    /// Load keypair from config identity.
179    fn load_keypair(config: &Config) -> NetworkResult<Keypair> {
180        // Get private key from config
181        let priv_key_str = config
182            .identity
183            .priv_key
184            .as_ref()
185            .ok_or_else(|| NetworkError::Init("Identity.PrivKey not found in config".into()))?;
186
187        // Decode base64 private key
188        use base64::Engine;
189        let key_bytes = base64::engine::general_purpose::STANDARD
190            .decode(priv_key_str)
191            .map_err(|e| NetworkError::Init(format!("Invalid private key encoding: {}", e)))?;
192
193        // Parse as protobuf-encoded keypair
194        let keypair = Keypair::from_protobuf_encoding(&key_bytes)
195            .map_err(|e| NetworkError::Init(format!("Invalid keypair: {}", e)))?;
196
197        Ok(keypair)
198    }
199
200    /// Check if daemon is running for a repository.
201    pub fn is_running(repo_path: impl AsRef<Path>) -> bool {
202        RepoLock::is_locked(repo_path)
203    }
204
205    /// Read API address from running daemon.
206    pub fn read_api_addr(repo_path: impl AsRef<Path>) -> Option<String> {
207        let api_file = repo_path.as_ref().join(API_FILE);
208        fs::read_to_string(api_file).ok()
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215
216    #[test]
217    fn test_daemon_config_default() {
218        let config = DaemonConfig::default();
219        assert!(!config.offline);
220        assert!(config.enable_mdns);
221        assert_eq!(config.routing, "dht");
222    }
223}