rust-web-server 18.10.0

Single-binary web server and reverse proxy: automatic HTTPS (Let's Encrypt), HTTP/1.1, HTTP/2 and HTTP/3, load balancing, auth and rate limiting, configured in one TOML file. Also a Rust library: routing, middleware, MCP server, ORM, background jobs. No hyper, no Tower; HTTP/2 and HTTP/3 framing via h2 and quinn.
Documentation
use super::LocalStorage;
use crate::storage::Storage;

fn temp_dir(name: &str) -> std::path::PathBuf {
    std::env::temp_dir().join(format!("rws_local_storage_{name}_{}", std::process::id()))
}

#[test]
fn put_then_get_roundtrips() {
    let dir = temp_dir("roundtrip");
    let store = LocalStorage::new(dir.to_str().unwrap());

    let key = store.put("file.txt", b"hello world", "text/plain").unwrap();
    assert_eq!("file.txt", key);
    assert_eq!(b"hello world".to_vec(), store.get("file.txt").unwrap());

    std::fs::remove_dir_all(&dir).ok();
}

#[test]
fn put_creates_nested_directories() {
    let dir = temp_dir("nested");
    let store = LocalStorage::new(dir.to_str().unwrap());

    store.put("avatars/2026/42.png", b"...png...", "image/png").unwrap();
    assert_eq!(b"...png...".to_vec(), store.get("avatars/2026/42.png").unwrap());

    std::fs::remove_dir_all(&dir).ok();
}

#[test]
fn delete_removes_file_and_is_idempotent() {
    let dir = temp_dir("delete");
    let store = LocalStorage::new(dir.to_str().unwrap());

    store.put("file.txt", b"data", "text/plain").unwrap();
    store.delete("file.txt").unwrap();
    assert!(store.get("file.txt").is_err());

    // Deleting an already-missing key is a no-op success (matches S3 semantics).
    store.delete("file.txt").unwrap();

    std::fs::remove_dir_all(&dir).ok();
}

#[test]
fn get_missing_key_is_an_error() {
    let dir = temp_dir("missing");
    let store = LocalStorage::new(dir.to_str().unwrap());
    assert!(store.get("nope.txt").is_err());
    std::fs::remove_dir_all(&dir).ok();
}

#[test]
fn rejects_path_traversal() {
    let dir = temp_dir("traversal");
    let store = LocalStorage::new(dir.to_str().unwrap());

    assert!(store.put("../escape.txt", b"data", "text/plain").is_err());
    assert!(store.get("../escape.txt").is_err());
    assert!(store.delete("../escape.txt").is_err());

    std::fs::remove_dir_all(&dir).ok();
}

#[test]
fn url_without_base_url_falls_back_to_filesystem_path() {
    let dir = temp_dir("url_fallback");
    let store = LocalStorage::new(dir.to_str().unwrap());
    let url = store.url("file.txt");
    assert!(url.ends_with("file.txt"));
    assert!(url.contains(dir.to_str().unwrap()));
}

#[test]
fn url_with_base_url_uses_prefix() {
    let dir = temp_dir("url_prefix");
    let store = LocalStorage::new(dir.to_str().unwrap()).with_base_url("/uploads");
    assert_eq!("/uploads/avatars/42.png", store.url("avatars/42.png"));
}

#[test]
fn put_stream_writes_as_it_reads_and_replaces_atomically() {
    let dir = crate::test_env::TempPath::new("local_put_stream");
    std::fs::create_dir_all(&*dir).unwrap();
    let store = LocalStorage::new(dir.as_str());
    let data: Vec<u8> = (0..300_000u32).map(|i| (i % 251) as u8).collect();
    store.put_stream("big/file.bin", &mut &data[..], "application/octet-stream").unwrap();
    assert_eq!(data, store.get("big/file.bin").unwrap());

    // A reader that fails midway leaves the existing object as it was.
    struct Failing(usize);
    impl std::io::Read for Failing {
        fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
            if self.0 == 0 {
                return Err(std::io::Error::other("client went away"));
            }
            let n = buf.len().min(self.0);
            buf[..n].fill(b'x');
            self.0 -= n;
            Ok(n)
        }
    }
    assert!(store.put_stream("big/file.bin", &mut Failing(1000), "application/octet-stream").is_err());
    assert_eq!(data, store.get("big/file.bin").unwrap());
    let leftovers: Vec<_> = std::fs::read_dir(dir.join("big")).unwrap().filter_map(|e| e.ok()).map(|e| e.file_name()).collect();
    assert_eq!(1, leftovers.len(), "{leftovers:?}");
}