hashtree_cli/server/
mod.rs1mod auth;
2pub mod blossom;
3mod git;
4mod handlers;
5mod mime;
6pub mod stun;
7mod ui;
8
9use anyhow::Result;
10use axum::{
11 extract::DefaultBodyLimit,
12 middleware,
13 routing::{get, post, put},
14 Router,
15};
16use crate::storage::HashtreeStore;
17use crate::webrtc::WebRTCState;
18use hashtree_git::GitStorage;
19use std::collections::HashSet;
20use std::sync::Arc;
21
22pub use auth::{AppState, AuthCredentials};
23
24pub struct HashtreeServer {
25 state: AppState,
26 git_storage: Option<Arc<GitStorage>>,
27 local_pubkey: Option<String>,
28 addr: String,
29}
30
31impl HashtreeServer {
32 pub fn new(store: Arc<HashtreeStore>, addr: String) -> Self {
33 Self {
34 state: AppState {
35 store,
36 auth: None,
37 webrtc_peers: None,
38 max_upload_bytes: 5 * 1024 * 1024, public_writes: true, allowed_pubkeys: HashSet::new(), },
42 git_storage: None,
43 local_pubkey: None,
44 addr,
45 }
46 }
47
48 pub fn with_max_upload_bytes(mut self, bytes: usize) -> Self {
50 self.state.max_upload_bytes = bytes;
51 self
52 }
53
54 pub fn with_public_writes(mut self, public: bool) -> Self {
57 self.state.public_writes = public;
58 self
59 }
60
61 pub fn with_webrtc_peers(mut self, webrtc_state: Arc<WebRTCState>) -> Self {
63 self.state.webrtc_peers = Some(webrtc_state);
64 self
65 }
66
67 pub fn with_git(mut self, storage: Arc<GitStorage>, local_pubkey: String) -> Self {
69 self.git_storage = Some(storage);
70 self.local_pubkey = Some(local_pubkey);
71 self
72 }
73
74 pub fn with_auth(mut self, username: String, password: String) -> Self {
75 self.state.auth = Some(AuthCredentials { username, password });
76 self
77 }
78
79 pub fn with_allowed_pubkeys(mut self, pubkeys: HashSet<String>) -> Self {
81 self.state.allowed_pubkeys = pubkeys;
82 self
83 }
84
85 pub async fn run(self) -> Result<()> {
86 let mut public_routes = Router::new()
90 .route("/", get(handlers::serve_root))
91 .route("/n/:pubkey/:treename", get(handlers::resolve_and_serve))
93 .route("/npub1:rest", get(handlers::serve_npub))
95 .route("/nhash1:rest", get(handlers::serve_nhash))
96 .route("/:id", get(handlers::serve_content_or_blob)
98 .head(blossom::head_blob)
99 .delete(blossom::delete_blob)
100 .options(blossom::cors_preflight))
101 .route("/upload", put(blossom::upload_blob)
102 .options(blossom::cors_preflight))
103 .route("/list/:pubkey", get(blossom::list_blobs)
104 .options(blossom::cors_preflight))
105 .route("/api/pins", get(handlers::list_pins))
107 .route("/api/stats", get(handlers::storage_stats))
108 .route("/api/peers", get(handlers::webrtc_peers))
109 .route("/api/socialgraph", get(handlers::socialgraph_stats))
110 .route("/api/resolve/:pubkey/:treename", get(handlers::resolve_to_hash))
112 .route("/api/trees/:pubkey", get(handlers::list_trees))
113 .with_state(self.state.clone());
114
115 if let Some(git_storage) = self.git_storage {
117 let local_pubkey = self.local_pubkey.unwrap_or_default();
118 let git_state = git::GitState { storage: git_storage, local_pubkey };
119 let git_routes = Router::new()
120 .route("/git/:pubkey/:repo/info/refs", get(git::info_refs))
121 .route("/git/:pubkey/:repo/git-upload-pack", post(git::upload_pack))
122 .route("/git/:pubkey/:repo/git-receive-pack", post(git::receive_pack))
123 .route("/api/git/repos", get(git::list_repos))
124 .with_state(git_state);
125 public_routes = public_routes.merge(git_routes);
126 }
127
128 let protected_routes = Router::new()
130 .route("/upload", post(handlers::upload_file))
131 .route("/api/pin/:cid", post(handlers::pin_cid))
132 .route("/api/unpin/:cid", post(handlers::unpin_cid))
133 .route("/api/gc", post(handlers::garbage_collect))
134 .layer(middleware::from_fn_with_state(
135 self.state.clone(),
136 auth::auth_middleware,
137 ))
138 .with_state(self.state);
139
140 let app = public_routes
141 .merge(protected_routes)
142 .layer(DefaultBodyLimit::max(10 * 1024 * 1024 * 1024)); let listener = tokio::net::TcpListener::bind(&self.addr).await?;
145 axum::serve(
146 listener,
147 app.into_make_service_with_connect_info::<std::net::SocketAddr>(),
148 ).await?;
149
150 Ok(())
151 }
152
153 pub fn addr(&self) -> &str {
154 &self.addr
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161 use crate::storage::HashtreeStore;
162 use tempfile::TempDir;
163 use std::path::Path;
164
165 #[tokio::test]
166 async fn test_server_serve_file() -> Result<()> {
167 let temp_dir = TempDir::new()?;
168 let store = Arc::new(HashtreeStore::new(temp_dir.path().join("db"))?);
169
170 let test_file = temp_dir.path().join("test.txt");
172 std::fs::write(&test_file, b"Hello, Hashtree!")?;
173
174 let cid = store.upload_file(&test_file)?;
175
176 let content = store.get_file(&cid)?;
178 assert!(content.is_some());
179 assert_eq!(content.unwrap(), b"Hello, Hashtree!");
180
181 Ok(())
182 }
183
184 #[tokio::test]
185 async fn test_server_list_pins() -> Result<()> {
186 let temp_dir = TempDir::new()?;
187 let store = Arc::new(HashtreeStore::new(temp_dir.path().join("db"))?);
188
189 let test_file = temp_dir.path().join("test.txt");
190 std::fs::write(&test_file, b"Test")?;
191
192 let cid = store.upload_file(&test_file)?;
193
194 let pins = store.list_pins()?;
195 assert_eq!(pins.len(), 1);
196 assert_eq!(pins[0], cid);
197
198 Ok(())
199 }
200}