Lightweight decentralized “knot” server prototype using Iroh + ATProto.
at master 304 lines 9.7 kB view raw
1use std::path::PathBuf; 2 3use bpaf::{Bpaf, ParseFailure}; 4 5use crate::config::Config; 6use crate::registry::AtProtoLocator; 7use crate::{Error, Result}; 8 9#[derive(Debug, Clone, Default, PartialEq, Eq)] 10pub struct AtProtoOverrides { 11 pub pds: Option<String>, 12 pub repo: Option<String>, 13 pub collection: Option<String>, 14 pub rkey: Option<String>, 15} 16 17#[derive(Debug, Clone, PartialEq, Eq)] 18pub enum Command { 19 Sync { 20 repo_id: String, 21 record_path: PathBuf, 22 cache_dir: PathBuf, 23 }, 24 Register { 25 path: Option<String>, 26 repo_id: Option<String>, 27 handle: Option<String>, 28 repo_name: Option<String>, 29 record_path: Option<PathBuf>, 30 registry_path: PathBuf, 31 atproto_host: Option<String>, 32 atproto_overrides: AtProtoOverrides, 33 data_dir: PathBuf, 34 }, 35 Daemon { 36 addr: String, 37 registry_path: PathBuf, 38 cache_root: PathBuf, 39 }, 40 Serve { 41 addr: String, 42 cache_dir: PathBuf, 43 }, 44 Publish { 45 repo_id: String, 46 repo_path: PathBuf, 47 git_path: String, 48 record_path: PathBuf, 49 registry_path: PathBuf, 50 cache_root: PathBuf, 51 atproto: Option<AtProtoLocator>, 52 }, 53 Seed { 54 record_path: PathBuf, 55 cache_root: PathBuf, 56 }, 57} 58 59#[derive(Debug, Clone, Bpaf, PartialEq, Eq)] 60#[bpaf(options, version)] 61enum CommandArgs { 62 #[bpaf(command("sync"))] 63 Sync { 64 #[bpaf(long("repo"))] 65 repo_id: String, 66 #[bpaf(long("record"))] 67 record_path: PathBuf, 68 #[bpaf(long("cache-dir"), argument("PATH"))] 69 cache_dir: Option<PathBuf>, 70 }, 71 #[bpaf(command("register"))] 72 Register { 73 #[bpaf(long("path"))] 74 path: Option<String>, 75 #[bpaf(long("repo"))] 76 repo_id: Option<String>, 77 #[bpaf(long("handle"))] 78 handle: Option<String>, 79 #[bpaf(long("repo-name"))] 80 repo_name: Option<String>, 81 #[bpaf(long("record"))] 82 record_path: Option<PathBuf>, 83 #[bpaf(long("registry"), argument("PATH"))] 84 registry_path: Option<PathBuf>, 85 #[bpaf(long("atproto-host"))] 86 atproto_host: Option<String>, 87 #[bpaf(long("atproto-pds"))] 88 atproto_pds: Option<String>, 89 #[bpaf(long("atproto-repo"))] 90 atproto_repo: Option<String>, 91 #[bpaf(long("atproto-collection"))] 92 atproto_collection: Option<String>, 93 #[bpaf(long("atproto-rkey"))] 94 atproto_rkey: Option<String>, 95 }, 96 #[bpaf(command("daemon"))] 97 Daemon { 98 #[bpaf(long("addr"), argument("ADDR"))] 99 addr: Option<String>, 100 #[bpaf(long("registry"), argument("PATH"))] 101 registry_path: Option<PathBuf>, 102 #[bpaf(long("cache-root"), argument("PATH"))] 103 cache_root: Option<PathBuf>, 104 }, 105 #[bpaf(command("serve"))] 106 Serve { 107 #[bpaf(long("addr"), argument("ADDR"))] 108 addr: Option<String>, 109 #[bpaf(long("cache-dir"), argument("PATH"))] 110 cache_dir: Option<PathBuf>, 111 }, 112 #[bpaf(command("publish"))] 113 Publish { 114 #[bpaf(long("repo-id"))] 115 repo_id: String, 116 #[bpaf(long("repo-path"), argument("PATH"))] 117 repo_path: PathBuf, 118 #[bpaf(long("git-path"), argument("PATH"))] 119 git_path: Option<String>, 120 #[bpaf(long("record-path"), argument("PATH"))] 121 record_path: Option<PathBuf>, 122 #[bpaf(long("registry"), argument("PATH"))] 123 registry_path: Option<PathBuf>, 124 #[bpaf(long("cache-root"), argument("PATH"))] 125 cache_root: Option<PathBuf>, 126 #[bpaf(long("atproto-pds"))] 127 atproto_pds: Option<String>, 128 #[bpaf(long("atproto-repo"))] 129 atproto_repo: Option<String>, 130 #[bpaf(long("atproto-collection"))] 131 atproto_collection: Option<String>, 132 #[bpaf(long("atproto-rkey"))] 133 atproto_rkey: Option<String>, 134 }, 135 #[bpaf(command("seed"))] 136 Seed { 137 #[bpaf(long("record-path"), argument("PATH"))] 138 record_path: PathBuf, 139 #[bpaf(long("cache-root"), argument("PATH"))] 140 cache_root: Option<PathBuf>, 141 }, 142} 143 144impl CommandArgs { 145 fn resolve(self, config: &Config) -> Result<Command> { 146 let command = match self { 147 CommandArgs::Sync { 148 repo_id, 149 record_path, 150 cache_dir, 151 } => Command::Sync { 152 repo_id, 153 record_path, 154 cache_dir: cache_dir.unwrap_or_else(|| config.cache_dir.clone()), 155 }, 156 CommandArgs::Register { 157 path, 158 repo_id, 159 handle, 160 repo_name, 161 record_path, 162 registry_path, 163 atproto_host, 164 atproto_pds, 165 atproto_repo, 166 atproto_collection, 167 atproto_rkey, 168 } => Command::Register { 169 path, 170 record_path, 171 registry_path: registry_path.unwrap_or_else(|| config.registry_path.clone()), 172 repo_id, 173 handle, 174 repo_name, 175 atproto_host, 176 atproto_overrides: AtProtoOverrides { 177 pds: atproto_pds, 178 repo: atproto_repo, 179 collection: atproto_collection, 180 rkey: atproto_rkey, 181 }, 182 data_dir: config.data_dir.clone(), 183 }, 184 CommandArgs::Daemon { 185 addr, 186 registry_path, 187 cache_root, 188 } => Command::Daemon { 189 addr: addr.unwrap_or_else(|| config.daemon_addr.clone()), 190 registry_path: registry_path.unwrap_or_else(|| config.registry_path.clone()), 191 cache_root: cache_root.unwrap_or_else(|| config.cache_dir.clone()), 192 }, 193 CommandArgs::Serve { addr, cache_dir } => Command::Serve { 194 addr: addr.unwrap_or_else(|| config.http_addr.clone()), 195 cache_dir: cache_dir.unwrap_or_else(|| config.cache_dir.clone()), 196 }, 197 CommandArgs::Publish { 198 repo_id, 199 repo_path, 200 git_path, 201 record_path, 202 registry_path, 203 cache_root, 204 atproto_pds, 205 atproto_repo, 206 atproto_collection, 207 atproto_rkey, 208 } => Command::Publish { 209 git_path: git_path.unwrap_or_else(|| default_git_path(&repo_id)), 210 record_path: record_path 211 .unwrap_or_else(|| default_record_path(&config.data_dir, &repo_id)), 212 registry_path: registry_path.unwrap_or_else(|| config.registry_path.clone()), 213 cache_root: cache_root.unwrap_or_else(|| config.cache_dir.clone()), 214 repo_id: repo_id.clone(), 215 repo_path, 216 atproto: build_atproto_locator_strict( 217 &repo_id, 218 atproto_pds, 219 atproto_repo, 220 atproto_collection, 221 atproto_rkey, 222 )?, 223 }, 224 CommandArgs::Seed { 225 record_path, 226 cache_root, 227 } => Command::Seed { 228 record_path, 229 cache_root: cache_root.unwrap_or_else(|| config.cache_dir.clone()), 230 }, 231 }; 232 Ok(command) 233 } 234} 235 236pub fn default_git_path(repo_id: &str) -> String { 237 let name = repo_id 238 .split('/') 239 .last() 240 .unwrap_or("repo"); 241 format!("/{name}.git") 242} 243 244pub fn default_record_path(data_dir: &std::path::Path, repo_id: &str) -> PathBuf { 245 let sanitized: String = repo_id 246 .chars() 247 .map(|ch| if ch.is_ascii_alphanumeric() { ch } else { '_' }) 248 .collect(); 249 data_dir.join("records").join(format!("{sanitized}.json")) 250} 251 252pub fn default_rkey(repo_id: &str) -> String { 253 let raw = repo_id 254 .split('/') 255 .last() 256 .unwrap_or("repo"); 257 raw.chars() 258 .map(|ch| if ch.is_ascii_alphanumeric() { ch } else { '_' }) 259 .collect() 260} 261 262fn build_atproto_locator_strict( 263 repo_id: &str, 264 pds: Option<String>, 265 repo: Option<String>, 266 collection: Option<String>, 267 rkey: Option<String>, 268) -> Result<Option<AtProtoLocator>> { 269 let any = pds.is_some() || repo.is_some() || collection.is_some() || rkey.is_some(); 270 if !any { 271 return Ok(None); 272 } 273 let pds = pds.ok_or_else(|| Error::Invalid("missing --atproto-pds".into()))?; 274 let repo = repo.ok_or_else(|| Error::Invalid("missing --atproto-repo".into()))?; 275 let collection = 276 collection.ok_or_else(|| Error::Invalid("missing --atproto-collection".into()))?; 277 let rkey = rkey.unwrap_or_else(|| default_rkey(repo_id)); 278 Ok(Some(AtProtoLocator { 279 pds, 280 repo, 281 collection, 282 rkey, 283 })) 284} 285 286pub fn parse_args(args: &[String]) -> Result<Command> { 287 let config = Config::from_env(); 288 parse_args_with_config(args, &config) 289} 290 291pub fn parse_args_with_config(args: &[String], config: &Config) -> Result<Command> { 292 let args: &[String] = if args.len() > 1 { &args[1..] } else { &[] }; 293 match command_args().run_inner(args) { 294 Ok(cmd) => Ok(cmd.resolve(config)?), 295 Err(err) => { 296 let msg = match err { 297 ParseFailure::Stderr(doc) => doc.monochrome(true), 298 ParseFailure::Stdout(doc, full) => doc.monochrome(full), 299 ParseFailure::Completion(text) => text, 300 }; 301 Err(Error::Invalid(msg)) 302 } 303 } 304}