Compare commits

...
5 Commits
Author SHA1 Message Date
Slavasil 2f3212527e format main.rs 2026-03-09 13:00:18 +03:00
Slavasil f6dd3d9ca1 change host to xurl.pw 2026-03-09 12:42:17 +03:00
Slavasil a430e7f895 make SERVER_ADDR env variable override the listening address 2026-03-09 12:40:40 +03:00
Slavasil f510b73d05 cargo fmt 2026-03-09 12:36:43 +03:00
Slavasil a269a4bff9 add Dockerfile 2026-03-09 12:36:02 +03:00
4 changed files with 100 additions and 24 deletions
+10
View File
@@ -0,0 +1,10 @@
FROM rust:latest as build
WORKDIR /usr/src/app
COPY Cargo.toml Cargo.lock secret.txt postgres_password.txt .
COPY src src
RUN rustup target add x86_64-unknown-linux-musl
RUN cargo build --target=x86_64-unknown-linux-musl --release
FROM scratch
COPY --from=build /usr/src/app/target/x86_64-unknown-linux-musl/release/shortener /
CMD ["/shortener"]
+32 -10
View File
@@ -13,37 +13,59 @@ pub const N_STATEMENTS: usize = 4;
pub async fn get_link(state: &Mutex<GlobalState>, link: &str) -> Result<Option<String>, Error> {
let lock = state.lock().await;
Ok(lock.db_client.query_opt(&lock.statements[STATEMENT_GET_LINK], &[&link]).await?.map(|row| row.get(0)))
Ok(lock
.db_client
.query_opt(&lock.statements[STATEMENT_GET_LINK], &[&link])
.await?
.map(|row| row.get(0)))
}
pub async fn add_link(state: &Mutex<GlobalState>, link: &str, url: &str) -> Result<(), Error> {
let lock = state.lock().await;
lock.db_client.execute(&lock.statements[STATEMENT_ADD_LINK], &[&link, &url]).await?;
lock.db_client
.execute(&lock.statements[STATEMENT_ADD_LINK], &[&link, &url])
.await?;
Ok(())
}
pub async fn add_temporary_link(state: &Mutex<GlobalState>, link: &str, url: &str, ttl: u64) -> Result<(), Error> {
pub async fn add_temporary_link(
state: &Mutex<GlobalState>,
link: &str,
url: &str,
ttl: u64,
) -> Result<(), Error> {
let time = SystemTime::now() + Duration::from_secs(ttl);
let lock = state.lock().await;
lock.db_client.execute(&lock.statements[STATEMENT_ADD_TEMP_LINK], &[&link, &url, &time]).await?;
lock.db_client
.execute(
&lock.statements[STATEMENT_ADD_TEMP_LINK],
&[&link, &url, &time],
)
.await?;
Ok(())
}
pub async fn delete_expired_links(state: &Mutex<GlobalState>) -> Result<(), Error> {
let lock = state.lock().await;
lock.db_client.execute(&lock.statements[STATEMENT_DELETE_EXPIRED], &[]).await?;
lock.db_client
.execute(&lock.statements[STATEMENT_DELETE_EXPIRED], &[])
.await?;
Ok(())
}
pub async fn prepare_statements(db: &Client) -> Result<[Statement; N_STATEMENTS], Error> {
Ok([db.prepare("SELECT url FROM links WHERE id = $1").await?,
db.prepare("INSERT INTO links (id, url) VALUES ($1, $2)").await?,
db.prepare("INSERT INTO links (id, url, valid_until) VALUES ($1, $2, $3)").await?,
db.prepare("DELETE FROM links WHERE valid_until < NOW()").await?
Ok([
db.prepare("SELECT url FROM links WHERE id = $1").await?,
db.prepare("INSERT INTO links (id, url) VALUES ($1, $2)")
.await?,
db.prepare("INSERT INTO links (id, url, valid_until) VALUES ($1, $2, $3)")
.await?,
db.prepare("DELETE FROM links WHERE valid_until < NOW()")
.await?,
])
}
pub async fn prepare_tables(db: &Client) -> Result<(), Error> {
db.execute("CREATE TABLE IF NOT EXISTS links (id TEXT PRIMARY KEY, url TEXT NOT NULL, valid_until TIMESTAMP NOT NULL)", &[]).await?;
Ok(())
}
}
+7 -2
View File
@@ -5,10 +5,15 @@ pub fn generate_link_id(length: u32) -> String {
let alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_";
let mut link = String::with_capacity(length as usize);
for _ in 0..length {
link.push(alphabet.chars().nth(rng.sample(Uniform::new(0u8, 64u8)) as usize).unwrap());
link.push(
alphabet
.chars()
.nth(rng.sample(Uniform::new(0u8, 64u8)) as usize)
.unwrap(),
);
}
if link == "create" || link == "p" {
return generate_link_id(length);
}
return link;
}
}
+51 -12
View File
@@ -1,13 +1,23 @@
use db::N_STATEMENTS;
use rocket::{futures::lock::Mutex, get, fs::FileServer, http::Status, response::Redirect, response::content::RawHtml, routes, State};
use rocket::{
fs::FileServer, futures::lock::Mutex, get, http::Status, response::content::RawHtml,
response::Redirect, routes, State,
};
use std::{
env,
fs::File,
net::{Ipv4Addr, SocketAddr, SocketAddrV4},
str::FromStr,
sync::Arc,
time::Duration,
};
use tokio_postgres::{Client, NoTls, Statement};
use std::{fs::File, sync::Arc, time::Duration};
mod db;
mod linkgen;
const DEFAULT_LINK_LENGTH: u32 = 6;
const LINK_PREFIX: &str = "https://slavasil.ru/";
const LINK_PREFIX: &str = "https://xurl.pw/";
pub struct GlobalState {
db_client: Client,
@@ -16,7 +26,8 @@ pub struct GlobalState {
}
#[tokio::main]
async fn main() {
let postgres_config = "host=localhost user=shortener password=".to_owned() + include_str!("../postgres_password.txt").trim();
let postgres_config = "host=localhost user=shortener password=".to_owned()
+ include_str!("../postgres_password.txt").trim();
let (client, conn) = match tokio_postgres::connect(&postgres_config, NoTls).await {
Ok((client, conn)) => (client, conn),
Err(e) => {
@@ -31,9 +42,21 @@ async fn main() {
});
db::prepare_tables(&client).await.unwrap();
let statements = db::prepare_statements(&client).await.unwrap();
let default_listen_addr = SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 3020).into();
let listen_addr = env::var("SERVER_ADDR")
.ok()
.map(|v| SocketAddr::from_str(&v).ok())
.flatten()
.unwrap_or(default_listen_addr);
let mut config = rocket::Config::default();
config.port = 3020;
let state = GlobalState { db_client: client, statements };
config.address = listen_addr.ip();
config.port = listen_addr.port();
let state = GlobalState {
db_client: client,
statements,
};
let state_mutex = Arc::new(Mutex::new(state));
{
let state_mutex = state_mutex.clone();
@@ -44,16 +67,25 @@ async fn main() {
}
});
}
rocket::build()
.mount("/", routes![create, go_to_link])
.manage(state_mutex.clone())
.configure(config)
.launch()
.await.unwrap();
.await
.unwrap();
}
#[get("/create?<url>&<secret>&<length>&<link>&<ttl>")]
async fn create(state: &State<Mutex<GlobalState>>, url: &str, secret: Option<&str>, length: Option<u32>, link: Option<&str>, ttl: Option<u64>) -> (Status, String) {
async fn create(
state: &State<Arc<Mutex<GlobalState>>>,
url: &str,
secret: Option<&str>,
length: Option<u32>,
link: Option<&str>,
ttl: Option<u64>,
) -> (Status, String) {
let mut allow_secret_options = false;
if let Some(secret) = secret {
if secret == include_str!("../secret.txt").trim() {
@@ -68,7 +100,11 @@ async fn create(state: &State<Mutex<GlobalState>>, url: &str, secret: Option<&st
custom_link = true;
}
}
let actual_len = if allow_secret_options && length.is_some() { length.unwrap() } else { DEFAULT_LINK_LENGTH };
let actual_len = if allow_secret_options && length.is_some() {
length.unwrap()
} else {
DEFAULT_LINK_LENGTH
};
if actual_len < 1 || actual_len > 64 {
return (Status::BadRequest, "invalid length".to_owned());
}
@@ -86,7 +122,7 @@ async fn create(state: &State<Mutex<GlobalState>>, url: &str, secret: Option<&st
if custom_link {
return (Status::BadRequest, "link already exists".to_owned());
}
},
}
None => {
actual_link = Some(link_id.clone());
break;
@@ -101,7 +137,7 @@ async fn create(state: &State<Mutex<GlobalState>>, url: &str, secret: Option<&st
let res = match ttl {
Some(ttl) => db::add_temporary_link(&state, actual_link.as_ref().unwrap(), url, ttl).await,
None => db::add_link(&state, actual_link.as_ref().unwrap(), url).await
None => db::add_link(&state, actual_link.as_ref().unwrap(), url).await,
};
match res {
Ok(_) => (),
@@ -116,7 +152,10 @@ async fn create(state: &State<Mutex<GlobalState>>, url: &str, secret: Option<&st
}
#[get("/<link>")]
pub async fn go_to_link(state: &State<Mutex<GlobalState>>, link: &str) -> Result<Option<Redirect>, Status> {
pub async fn go_to_link(
state: &State<Arc<Mutex<GlobalState>>>,
link: &str,
) -> Result<Option<Redirect>, Status> {
match db::get_link(&state, link).await {
Ok(url) => match url {
Some(url) => Ok(Some(Redirect::to(url))),