1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
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(())
}
|