diff --git a/Cargo.lock b/Cargo.lock index 4e9e60f..0503d00 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,17 +2,6 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "ahash" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" -dependencies = [ - "getrandom", - "once_cell", - "version_check", -] - [[package]] name = "aho-corasick" version = "0.7.18" @@ -22,12 +11,52 @@ dependencies = [ "memchr", ] +[[package]] +name = "argh" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb41d85d92dfab96cb95ab023c265c5e4261bb956c0fb49ca06d90c570f1958" +dependencies = [ + "argh_derive", + "argh_shared", +] + +[[package]] +name = "argh_derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be69f70ef5497dd6ab331a50bd95c6ac6b8f7f17a7967838332743fbd58dc3b5" +dependencies = [ + "argh_shared", + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "argh_shared" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6f8c380fa28aa1b36107cd97f0196474bb7241bb95a453c5c01a15ac74b2eac" + [[package]] name = "arrayvec" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" +[[package]] +name = "async-trait" +version = "0.1.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed6aa3524a2dfcf9fe180c51eae2b58738348d819517ceadf95789c51fff7600" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "atomic" version = "0.5.1" @@ -110,6 +139,20 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "combine" +version = "4.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b727aacc797f9fc28e355d21f34709ac4fc9adecfe470ad07b8f4464f53062" +dependencies = [ + "bytes", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", +] + [[package]] name = "cow-utils" version = "0.1.2" @@ -160,6 +203,12 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dtoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0" + [[package]] name = "educe" version = "0.4.19" @@ -192,18 +241,6 @@ dependencies = [ "syn", ] -[[package]] -name = "fallible-iterator" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" - -[[package]] -name = "fallible-streaming-iterator" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" - [[package]] name = "fastrand" version = "1.7.0" @@ -330,18 +367,6 @@ name = "hashbrown" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" -dependencies = [ - "ahash", -] - -[[package]] -name = "hashlink" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf" -dependencies = [ - "hashbrown", -] [[package]] name = "headers" @@ -368,6 +393,15 @@ dependencies = [ "http", ] +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "hermit-abi" version = "0.1.19" @@ -385,7 +419,7 @@ checksum = "31f4c6746584866f0feabcc69893c5b51beef3831656a968ed7ae254cdc4fd03" dependencies = [ "bytes", "fnv", - "itoa", + "itoa 1.0.1", ] [[package]] @@ -426,7 +460,7 @@ dependencies = [ "http-body", "httparse", "httpdate", - "itoa", + "itoa 1.0.1", "pin-project-lite", "socket2", "tokio", @@ -480,6 +514,12 @@ dependencies = [ "either", ] +[[package]] +name = "itoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + [[package]] name = "itoa" version = "1.0.1" @@ -511,16 +551,6 @@ version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efaa7b300f3b5fe8eb6bf21ce3895e1751d9665086af2d64b42f19701015ff4f" -[[package]] -name = "libsqlite3-sys" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb644c388dfaefa18035c12614156d285364769e818893da0dda9030c80ad2ba" -dependencies = [ - "pkg-config", - "vcpkg", -] - [[package]] name = "linked-hash-map" version = "0.5.4" @@ -549,12 +579,13 @@ dependencies = [ name = "lonk" version = "0.1.0" dependencies = [ + "argh", "base64", - "either", "figment", - "rusqlite", + "redis", "serde", "tokio", + "toml", "validators", "warp", ] @@ -819,12 +850,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" -[[package]] -name = "pkg-config" -version = "0.3.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe" - [[package]] name = "ppv-lite86" version = "0.2.16" @@ -907,6 +932,26 @@ dependencies = [ "getrandom", ] +[[package]] +name = "redis" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a80b5f38d7f5a020856a0e16e40a9cfabf88ae8f0e4c2dcd8a3114c1e470852" +dependencies = [ + "async-trait", + "bytes", + "combine", + "dtoa", + "futures-util", + "itoa 0.4.8", + "percent-encoding", + "pin-project-lite", + "sha1", + "tokio", + "tokio-util", + "url", +] + [[package]] name = "redox_syscall" version = "0.2.11" @@ -954,21 +999,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "rusqlite" -version = "0.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85127183a999f7db96d1a976a309eebbfb6ea3b0b400ddd8340190129de6eb7a" -dependencies = [ - "bitflags", - "fallible-iterator", - "fallible-streaming-iterator", - "hashlink", - "libsqlite3-sys", - "memchr", - "smallvec", -] - [[package]] name = "rustc_version" version = "0.4.0" @@ -1034,7 +1064,7 @@ version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95" dependencies = [ - "itoa", + "itoa 1.0.1", "ryu", "serde", ] @@ -1046,7 +1076,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", - "itoa", + "itoa 1.0.1", "ryu", "serde", ] @@ -1075,6 +1105,21 @@ dependencies = [ "digest 0.10.3", ] +[[package]] +name = "sha1" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770" +dependencies = [ + "sha1_smol", +] + +[[package]] +name = "sha1_smol" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" + [[package]] name = "signal-hook-registry" version = "1.4.0" @@ -1360,6 +1405,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-segmentation" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" + [[package]] name = "unicode-xid" version = "0.2.2" @@ -1429,12 +1480,6 @@ dependencies = [ "enum-ordinalize", ] -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - [[package]] name = "version_check" version = "0.9.4" diff --git a/Cargo.toml b/Cargo.toml index 81f38b2..81836d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,11 +6,12 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +argh = "0.1.7" base64 = "0.13.0" -either = "1.6.1" figment = { version = "0.10.6", features = ["toml", "env"] } -rusqlite = "0.27.0" +redis = { version = "~0.21.5", features = ["tokio-comp"] } serde = { version = "~1.0.136", features = ["derive"] } tokio = { version = "~1.17.0", features = ["full"] } +toml = "0.5.8" validators = { version = "~0.24.1", features = ["url-dep"] } warp = "0.3.2" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2fa4889 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +# Create the build container to compile +FROM rust:latest as builder +RUN USER=root cargo new --bin lonk +WORKDIR lonk + +# Compile dependencies + +COPY ./Cargo.lock ./Cargo.lock +COPY ./Cargo.toml ./Cargo.toml + +RUN cargo build --release +RUN src/*.rs + +# Compile the source +COPY ./src ./src +RUN rm ./target/release/deps/lonk* +RUN cargo build + +# Execution container +FROM scratch +COPY --from=build /lonk/target/release/lonk . +CMD ["./lonk"] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a367687 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,17 @@ +version: '3.9' +services: + lonk: + build: . + environment: + - PROFILE: release + - LONK_CONFIG: /data/config.toml + volumes: + - ./data:/data + redis: + image: 'redis:alpine' + command: redis-server --save 20 1 --loglevel warning + volumes: + - redis:/data +volumes: + redis: + driver: local \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 6d6858f..8c7c331 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,13 @@ +use argh::FromArgs; use figment::{ providers::{Format, Toml}, Figment, }; -use rusqlite::Connection; -use std::{collections::BTreeSet, str::FromStr, sync::Arc}; +use serde::{Deserialize, Serialize}; +use std::{collections::BTreeSet, path::PathBuf, str::FromStr, sync::Arc}; +use tokio::sync::mpsc::{self, UnboundedSender}; use validators::prelude::*; -use warp::{hyper::StatusCode, Filter}; +use warp::{filters::BoxedFilter, hyper::StatusCode, Filter}; macro_rules! unwrap_or_unwrap_err { ($x:expr) => { @@ -16,62 +18,73 @@ macro_rules! unwrap_or_unwrap_err { }; } -mod config { - use serde::{Deserialize, Serialize}; - use std::{path::PathBuf, str::FromStr}; - use warp::{filters::BoxedFilter, Filter}; +#[derive(Serialize, Deserialize, Debug, Validator, Clone)] +#[validator(domain(ipv4(Allow), local(NotAllow), at_least_two_labels(Allow), port(Allow)))] +struct Url { + domain: String, + port: Option, +} - #[derive(Deserialize, Serialize, Debug, Clone)] - pub struct Config { - pub db_location: PathBuf, - pub slug_rules: SlugRules, - pub serve_rules: ServeRules, +#[derive(Deserialize, Serialize, Debug, Clone)] +struct DbConfig { + pub address: Url, +} + +impl Default for DbConfig { + fn default() -> Self { + Self { address: Url::parse_str("redis://127.0.0.1/").unwrap() } } +} - #[derive(Deserialize, Serialize, Debug, Clone)] - pub struct SlugRules { - pub length: usize, - pub chars: String, - } +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct SlugRules { + pub length: usize, + pub chars: String, +} - #[derive(Deserialize, Serialize, Debug, Clone)] - pub enum ServeRules { - File(PathBuf), - Dir(PathBuf), - } - - impl ServeRules { - pub fn to_filter(&self) -> BoxedFilter<(warp::fs::File,)> { - match self { - ServeRules::File(file) => warp::fs::file(file.clone()).boxed(), - ServeRules::Dir(dir) => warp::fs::dir(dir.clone()).boxed(), - } +impl Default for SlugRules { + fn default() -> Self { + Self { + length: 5, + chars: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-".to_string(), } } +} - impl Default for SlugRules { - fn default() -> Self { - Self { - length: 5, - chars: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-" - .to_string(), - } +#[derive(Deserialize, Serialize, Debug, Clone)] +pub enum ServeRules { + File(PathBuf), + Dir(PathBuf), +} + +impl ServeRules { + pub fn to_filter(&self) -> BoxedFilter<(warp::fs::File,)> { + match self { + ServeRules::File(file) => warp::fs::file(file.clone()).boxed(), + ServeRules::Dir(dir) => warp::fs::dir(dir.clone()).boxed(), } } +} - impl Default for ServeRules { - fn default() -> Self { - ServeRules::Dir(PathBuf::from_str("/etc/lonk/served").unwrap()) - } +impl Default for ServeRules { + fn default() -> Self { + ServeRules::Dir(PathBuf::from_str("/etc/lonk/served").unwrap()) } +} - impl Default for Config { - fn default() -> Self { - Self { - db_location: PathBuf::from_str("/etc/lonk/data.db").unwrap(), - slug_rules: Default::default(), - serve_rules: Default::default(), - } +#[derive(Deserialize, Serialize, Debug, Clone)] +struct Config { + pub db: DbConfig, + pub slug_rules: SlugRules, + pub serve_rules: ServeRules, +} + +impl Default for Config { + fn default() -> Self { + Self { + db: Default::default(), + slug_rules: Default::default(), + serve_rules: Default::default(), } } } @@ -80,13 +93,6 @@ mod config { #[validator(base64_url(padding(NotAllow)))] struct Base64WithoutPaddingUrl(String); -#[derive(Validator)] -#[validator(domain(ipv4(Allow), local(Allow), at_least_two_labels(Allow), port(Allow)))] -struct Url { - domain: String, - port: Option, -} - impl FromStr for Base64WithoutPaddingUrl { type Err = ::Error; @@ -95,17 +101,25 @@ impl FromStr for Base64WithoutPaddingUrl { } } -struct SlugDatabase(rusqlite::Connection); +#[derive(Debug)] +struct SlugDatabase { + tx: UnboundedSender, +} + +#[derive(Debug)] +enum SlugDbMessage { + Add(Slug, Url), +} impl SlugDatabase { - fn from_connection(connection: rusqlite::Connection) -> Self { - // TODO: Check that the database has the necessary format - - SlugDatabase(connection) + fn from_client(client: redis::Client) -> Self { + todo!() } - fn insert_slug(slug: Slug, url: Url) -> Result<(), ()> { - todo!(); + fn insert_slug(&self, slug: Slug, url: Url) -> Result<(), ()> { + self.tx + .send(SlugDbMessage::Add(slug, url)) + .expect("Could not send message."); Ok(()) } } @@ -115,6 +129,7 @@ struct SlugFactory { slug_chars: BTreeSet, } +#[derive(Debug)] struct Slug(String); enum InvalidSlug { @@ -123,7 +138,7 @@ enum InvalidSlug { } impl SlugFactory { - fn from_rules(rules: config::SlugRules) -> Self { + fn from_rules(rules: SlugRules) -> Self { let mut slug_chars = BTreeSet::::new(); slug_chars.extend(rules.chars.chars()); @@ -152,7 +167,11 @@ impl SlugFactory { } } -fn shorten<'s>(slug_factory: &SlugFactory, db: SlugDatabase, b64url: &'s str) -> Result { +fn shorten( + slug_factory: &SlugFactory, + db: &SlugDatabase, + b64url: &str, +) -> Result { let url = { let raw = base64::decode_config(b64url, base64::URL_SAFE_NO_PAD) .map_err(|_| warp::http::StatusCode::BAD_REQUEST)?; @@ -166,10 +185,10 @@ fn shorten<'s>(slug_factory: &SlugFactory, db: SlugDatabase, b64url: &'s str) -> } #[tokio::main] -async fn main() { +async fn serve() { // Read configuration let config_file = std::env::var("LONK_CONFIG").unwrap_or("lonk.toml".to_string()); - let config: config::Config = Figment::new() + let config: Config = Figment::new() .merge(Toml::file(&config_file)) .extract() .expect("Could not parse configuration file."); @@ -178,7 +197,16 @@ async fn main() { let slug_factory = Arc::new(SlugFactory::from_rules(config.slug_rules)); // Initialize database - let db = Connection::open(config.db_location); + let db = { + let client = if let Some(port) = config.db.address.port { + redis::Client::open((config.db.address.domain, port)) + } else { + redis::Client::open(config.db.address.domain) + }; + let client = client.expect("Error opening Redis database."); + //let conn = Connection::open(config.db_location).expect("Could not open database."); + Arc::new(SlugDatabase::from_client(client)) + }; // GET / let homepage = warp::path::end().and(config.serve_rules.to_filter()); @@ -188,7 +216,7 @@ async fn main() { move |link: Base64WithoutPaddingUrl| { warp::reply::with_status( warp::reply(), - unwrap_or_unwrap_err!(shorten(&slug_factory, &link.0)), + unwrap_or_unwrap_err!(shorten(&slug_factory, &db, &link.0)), ) } }); @@ -202,3 +230,25 @@ async fn main() { warp::serve(routes).run(([127, 0, 0, 1], 8892)).await; } + +#[derive(FromArgs, PartialEq, Debug)] +/// Start lonk. +struct Run { + /// write a default configuration to stdout and quit + #[argh(switch)] + print_default_config: bool, +} + +fn main() { + let run = argh::from_env::(); + + if run.print_default_config { + println!( + "{}", + toml::to_string(&Config::default()) + .expect("Default configuration should always be TOML serializable") + ); + } else { + serve(); + } +}