summaryrefslogtreecommitdiffstats
path: root/src/ops.rs
diff options
context:
space:
mode:
authormurilo ijanc2026-03-25 23:23:10 -0300
committermurilo ijanc2026-03-25 23:23:10 -0300
commita96da5f0704c50e8a4e4f047dcd3fb7c73fdf600 (patch)
treef98bb20411dbc6a49e8c286b054a88d89eea795f /src/ops.rs
downloadtesseras-url-main.tar.gz
Initial implementation of tesseras-urlHEADmain
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.rs140
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(())
+}