diff options
| author | murilo ijanc | 2026-03-25 23:23:10 -0300 |
|---|---|---|
| committer | murilo ijanc | 2026-03-25 23:23:10 -0300 |
| commit | a96da5f0704c50e8a4e4f047dcd3fb7c73fdf600 (patch) | |
| tree | f98bb20411dbc6a49e8c286b054a88d89eea795f /src/ops.rs | |
| download | tesseras-url-main.tar.gz | |
Decentralized URL shortener built on tesseras-dht. Includes:
- tud daemon: DHT node, Unix socket API, HTTP 302 redirect server
- tu CLI: shorten, resolve, del, list, status commands
- Auto-generated slugs (8-byte SHA256, base58) or custom slugs
- TTL support (default: forever)
- Automatic re-join of bootstrap nodes when routing table is empty
- OpenBSD pledge(2) and unveil(2) sandboxing
- DNS SRV bootstrap discovery
- Verbose mode (-v) for both binaries
Diffstat (limited to 'src/ops.rs')
| -rw-r--r-- | src/ops.rs | 140 |
1 files changed, 140 insertions, 0 deletions
diff --git a/src/ops.rs b/src/ops.rs new file mode 100644 index 0000000..7143913 --- /dev/null +++ b/src/ops.rs @@ -0,0 +1,140 @@ +//! High-level URL shortener operations. +//! +//! Each function combines local storage and DHT interaction. + +use std::time::Duration; + +use tesseras_dht::Node; + +use crate::store::UrlStore; +use crate::url_entry::{MAX_URL_SIZE, UrlEntry}; + +/// Timeout for blocking DHT lookups. +const OP_TIMEOUT: Duration = Duration::from_secs(30); + +/// Errors from URL operations. +#[derive(Debug)] +pub enum UrlError { + InvalidSlug, + InvalidUrl, + NotFound, + Expired, + TooLarge, + Internal(String), +} + +impl std::fmt::Display for UrlError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::InvalidSlug => write!(f, "invalid slug"), + Self::InvalidUrl => write!(f, "invalid url"), + Self::NotFound => write!(f, "not found"), + Self::Expired => write!(f, "expired"), + Self::TooLarge => write!(f, "url too large"), + Self::Internal(msg) => write!(f, "internal: {msg}"), + } + } +} + +/// Create a shortened URL. Returns the slug. +pub fn shorten_url( + node: &mut Node, + store: &UrlStore, + target_url: &str, + ttl_secs: u64, + slug: &str, +) -> Result<String, UrlError> { + if target_url.len() > MAX_URL_SIZE { + return Err(UrlError::TooLarge); + } + if !target_url.starts_with("http://") + && !target_url.starts_with("https://") + { + return Err(UrlError::InvalidUrl); + } + + let slug = if slug == "auto" { + UrlEntry::auto_slug(target_url) + } else { + slug.to_string() + }; + + if slug.is_empty() { + return Err(UrlError::InvalidSlug); + } + + let dht_key = UrlEntry::dht_key(&slug); + let entry = UrlEntry::new(slug.clone(), target_url.to_string(), ttl_secs); + let serialized = entry.to_bytes(); + + store + .put_entry(&dht_key, &serialized) + .map_err(|e| UrlError::Internal(e.to_string()))?; + + let dht_ttl = if ttl_secs == 0 { + u16::MAX + } else { + std::cmp::min(ttl_secs, u16::MAX as u64) as u16 + }; + node.put(&dht_key, &serialized, dht_ttl, false); + + log::info!( + "shorten: {} -> {} (ttl={})", + slug, + target_url, + if ttl_secs == 0 { + "forever".to_string() + } else { + format!("{ttl_secs}s") + } + ); + + Ok(slug) +} + +/// Resolve a slug to its target URL. +/// Tries local store first, then falls back to DHT lookup. +pub fn resolve_url( + node: &mut Node, + store: &UrlStore, + slug: &str, +) -> Result<String, UrlError> { + let dht_key = UrlEntry::dht_key(slug); + + let data = if let Some(local) = store.get_entry(&dht_key) { + local + } else { + let vals = node.get_blocking(&dht_key, OP_TIMEOUT); + if vals.is_empty() { + return Err(UrlError::NotFound); + } + match vals.iter().find(|v| { + UrlEntry::from_bytes(v) + .map(|e| e.slug == slug) + .unwrap_or(false) + }) { + Some(v) => v.clone(), + None => return Err(UrlError::NotFound), + } + }; + + let entry = UrlEntry::from_bytes(&data).ok_or(UrlError::NotFound)?; + if entry.is_expired() { + return Err(UrlError::Expired); + } + + Ok(entry.target_url) +} + +/// Delete a URL entry from local store and the DHT. +pub fn delete_url( + node: &mut Node, + store: &UrlStore, + slug: &str, +) -> Result<(), UrlError> { + let dht_key = UrlEntry::dht_key(slug); + store.remove_entry(&dht_key); + node.delete(&dht_key); + log::info!("del: removed slug {slug}"); + Ok(()) +} |