Files
rustpad/src/file_urls.rs
T
2026-08-05 09:53:28 +02:00

92 lines
2.6 KiB
Rust

/*
* Copyright (C) 2026 Mateusz Gruszczyński @linuxiarz.pl
* Source-Available Code / Dual-Licensed.
*
* Free for non-commercial and evaluation use under terms of BSL/GPLv3.
* Commercial or production use requires a valid paid license.
* See LICENSE file in repository root for details.
*/
use std::error::Error;
const FILE_ROUTE_PREFIX: &str = "/f/";
pub fn normalize_public_base(value: Option<String>) -> Result<Option<String>, Box<dyn Error>> {
let Some(value) = value else {
return Ok(None);
};
let value = value.trim();
if value.is_empty() {
return Ok(None);
}
let candidate = if value.starts_with("http://") || value.starts_with("https://") {
value.trim_end_matches('/').to_owned()
} else {
format!("https://{}", value.trim_end_matches('/'))
};
let authority = candidate
.strip_prefix("https://")
.or_else(|| candidate.strip_prefix("http://"))
.ok_or("FILES_PUBLIC_URL must use http:// or https://")?;
if authority.is_empty()
|| authority.chars().any(|character| {
matches!(character, '/' | '\\' | '?' | '#' | '@') || character.is_whitespace()
})
{
return Err(
"FILES_PUBLIC_URL must be a domain or HTTP(S) origin without a path, query, credentials, or fragment"
.into(),
);
}
Ok(Some(candidate))
}
pub fn canonical_file_path(value: &str) -> Option<String> {
let value = value.trim();
let path = if value.starts_with('/') {
value
} else {
let (_, after_scheme) = value.split_once("://")?;
let path_start = after_scheme.find('/')?;
&after_scheme[path_start..]
};
let path = path
.split(|character| matches!(character, '?' | '#'))
.next()
.unwrap_or(path);
if !path.starts_with(FILE_ROUTE_PREFIX) {
return None;
}
let mut parts = path.trim_start_matches('/').split('/');
let route = parts.next()?;
let token = parts.next()?;
let filename = parts.next()?;
if route != "f" || token.is_empty() || filename.is_empty() || parts.next().is_some() {
return None;
}
Some(format!("/f/{token}/{filename}"))
}
pub fn stored_file_path(token: &str, filename: &str) -> String {
format!("/f/{token}/{filename}")
}
pub fn public_file_url(public_base: Option<&str>, stored_url: &str) -> String {
let Some(canonical) = canonical_file_path(stored_url) else {
return stored_url.to_owned();
};
match public_base {
Some(base) => format!("{}{canonical}", base.trim_end_matches('/')),
None => canonical,
}
}
#[cfg(test)]
#[path = "tests/file_urls.rs"]
mod tests;