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("/api/pins", get(handlers::list_pins))
93 .route("/api/stats", get(handlers::storage_stats))
94 .route("/api/peers", get(handlers::webrtc_peers))
95 .route("/api/socialgraph", get(handlers::socialgraph_stats))
96 .route("/api/resolve/:pubkey/:treename", get(handlers::resolve_to_hash))
98 .route("/api/trees/:pubkey", get(handlers::list_trees))
99 .with_state(self.state.clone());
100
101 let protected_routes = Router::new()
103 .route("/upload", post(handlers::upload_file))
104 .route("/api/pin/:cid", post(handlers::pin_cid))
105 .route("/api/unpin/:cid", post(handlers::unpin_cid))
106 .route("/api/gc", post(handlers::garbage_collect))
107 .layer(middleware::from_fn_with_state(
108 self.state.clone(),
109 auth::auth_middleware,
110 ))
111 .with_state(self.state);
112
113 let app = public_routes
114 .merge(protected_routes)
115 .layer(DefaultBodyLimit::max(10 * 1024 * 1024 * 1024)); let listener = tokio::net::TcpListener::bind(&self.addr).await?;
118 axum::serve(
119 listener,
120 app.into_make_service_with_connect_info::<std::net::SocketAddr>(),
121 ).await?;
122
123 Ok(())
124 }
125
126 pub fn addr(&self) -> &str {
127 &self.addr
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134 use crate::storage::HashtreeStore;
135 use tempfile::TempDir;
136 use std::path::Path;
137 use hashtree_core::from_hex;
138
139 #[tokio::test]
140 async fn test_server_serve_file() -> Result<()> {
141 let temp_dir = TempDir::new()?;
142 let store = Arc::new(HashtreeStore::new(temp_dir.path().join("db"))?);
143
144 let test_file = temp_dir.path().join("test.txt");
146 std::fs::write(&test_file, b"Hello, Hashtree!")?;
147
148 let cid = store.upload_file(&test_file)?;
149 let hash = from_hex(&cid)?;
150
151 let content = store.get_file(&hash)?;
153 assert!(content.is_some());
154 assert_eq!(content.unwrap(), b"Hello, Hashtree!");
155
156 Ok(())
157 }
158
159 #[tokio::test]
160 async fn test_server_list_pins() -> Result<()> {
161 let temp_dir = TempDir::new()?;
162 let store = Arc::new(HashtreeStore::new(temp_dir.path().join("db"))?);
163
164 let test_file = temp_dir.path().join("test.txt");
165 std::fs::write(&test_file, b"Test")?;
166
167 let cid = store.upload_file(&test_file)?;
168 let hash = from_hex(&cid)?;
169
170 let pins = store.list_pins_raw()?;
171 assert_eq!(pins.len(), 1);
172 assert_eq!(pins[0], hash);
173
174 Ok(())
175 }
176}