hashtree_cli/server/
mod.rs1mod auth;
2pub mod blossom;
3mod handlers;
4mod mime;
5pub mod stun;
6mod ui;
7
8use anyhow::Result;
9use axum::{
10 extract::DefaultBodyLimit,
11 middleware,
12 routing::{get, post, put},
13 Router,
14};
15use crate::storage::HashtreeStore;
16use crate::webrtc::WebRTCState;
17use std::collections::HashSet;
18use std::sync::Arc;
19
20pub use auth::{AppState, AuthCredentials};
21
22pub struct HashtreeServer {
23 state: AppState,
24 addr: String,
25}
26
27impl HashtreeServer {
28 pub fn new(store: Arc<HashtreeStore>, addr: String) -> Self {
29 Self {
30 state: AppState {
31 store,
32 auth: None,
33 webrtc_peers: None,
34 max_upload_bytes: 5 * 1024 * 1024, public_writes: true, allowed_pubkeys: HashSet::new(), },
38 addr,
39 }
40 }
41
42 pub fn with_max_upload_bytes(mut self, bytes: usize) -> Self {
44 self.state.max_upload_bytes = bytes;
45 self
46 }
47
48 pub fn with_public_writes(mut self, public: bool) -> Self {
51 self.state.public_writes = public;
52 self
53 }
54
55 pub fn with_webrtc_peers(mut self, webrtc_state: Arc<WebRTCState>) -> Self {
57 self.state.webrtc_peers = Some(webrtc_state);
58 self
59 }
60
61 pub fn with_auth(mut self, username: String, password: String) -> Self {
62 self.state.auth = Some(AuthCredentials { username, password });
63 self
64 }
65
66 pub fn with_allowed_pubkeys(mut self, pubkeys: HashSet<String>) -> Self {
68 self.state.allowed_pubkeys = pubkeys;
69 self
70 }
71
72 pub async fn run(self) -> Result<()> {
73 let public_routes = Router::new()
77 .route("/", get(handlers::serve_root))
78 .route("/n/:pubkey/:treename", get(handlers::resolve_and_serve))
80 .route("/npub1:rest", get(handlers::serve_npub))
82 .route("/:id", get(handlers::serve_content_or_blob)
84 .head(blossom::head_blob)
85 .delete(blossom::delete_blob)
86 .options(blossom::cors_preflight))
87 .route("/upload", put(blossom::upload_blob)
88 .options(blossom::cors_preflight))
89 .route("/list/:pubkey", get(blossom::list_blobs)
90 .options(blossom::cors_preflight))
91 .route("/health", get(handlers::health_check))
93 .route("/api/pins", get(handlers::list_pins))
94 .route("/api/stats", get(handlers::storage_stats))
95 .route("/api/peers", get(handlers::webrtc_peers))
96 .route("/api/socialgraph", get(handlers::socialgraph_stats))
97 .route("/api/resolve/:pubkey/:treename", get(handlers::resolve_to_hash))
99 .route("/api/trees/:pubkey", get(handlers::list_trees))
100 .with_state(self.state.clone());
101
102 let protected_routes = Router::new()
104 .route("/upload", post(handlers::upload_file))
105 .route("/api/pin/:cid", post(handlers::pin_cid))
106 .route("/api/unpin/:cid", post(handlers::unpin_cid))
107 .route("/api/gc", post(handlers::garbage_collect))
108 .layer(middleware::from_fn_with_state(
109 self.state.clone(),
110 auth::auth_middleware,
111 ))
112 .with_state(self.state);
113
114 let app = public_routes
115 .merge(protected_routes)
116 .layer(DefaultBodyLimit::max(10 * 1024 * 1024 * 1024)); let listener = tokio::net::TcpListener::bind(&self.addr).await?;
119 axum::serve(
120 listener,
121 app.into_make_service_with_connect_info::<std::net::SocketAddr>(),
122 ).await?;
123
124 Ok(())
125 }
126
127 pub fn addr(&self) -> &str {
128 &self.addr
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135 use crate::storage::HashtreeStore;
136 use tempfile::TempDir;
137 use std::path::Path;
138 use hashtree_core::from_hex;
139
140 #[tokio::test]
141 async fn test_server_serve_file() -> Result<()> {
142 let temp_dir = TempDir::new()?;
143 let store = Arc::new(HashtreeStore::new(temp_dir.path().join("db"))?);
144
145 let test_file = temp_dir.path().join("test.txt");
147 std::fs::write(&test_file, b"Hello, Hashtree!")?;
148
149 let cid = store.upload_file(&test_file)?;
150 let hash = from_hex(&cid)?;
151
152 let content = store.get_file(&hash)?;
154 assert!(content.is_some());
155 assert_eq!(content.unwrap(), b"Hello, Hashtree!");
156
157 Ok(())
158 }
159
160 #[tokio::test]
161 async fn test_server_list_pins() -> Result<()> {
162 let temp_dir = TempDir::new()?;
163 let store = Arc::new(HashtreeStore::new(temp_dir.path().join("db"))?);
164
165 let test_file = temp_dir.path().join("test.txt");
166 std::fs::write(&test_file, b"Test")?;
167
168 let cid = store.upload_file(&test_file)?;
169 let hash = from_hex(&cid)?;
170
171 let pins = store.list_pins_raw()?;
172 assert_eq!(pins.len(), 1);
173 assert_eq!(pins[0], hash);
174
175 Ok(())
176 }
177}