From b869e1641bafe377716a9aaf0f0766e0d11b6bd5 Mon Sep 17 00:00:00 2001 From: Daniel Svitan Date: Sun, 5 Oct 2025 21:02:33 +0200 Subject: [PATCH] :sparkles: Adds image route --- src/api/image.rs | 82 +++++++++++++++++++++++++++++++++++++++++++++++ src/api/mod.rs | 1 + src/main.rs | 8 ++++- src/models/hit.rs | 5 +-- src/models/mod.rs | 4 ++- 5 files changed, 96 insertions(+), 4 deletions(-) create mode 100644 src/api/image.rs diff --git a/src/api/image.rs b/src/api/image.rs new file mode 100644 index 0000000..b7de390 --- /dev/null +++ b/src/api/image.rs @@ -0,0 +1,82 @@ +use crate::models::AppState; +use crate::models::hit::Hit; +use crate::models::tracker::Tracker; +use crate::schema::{hits, trackers}; +use chrono::Utc; +use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl}; +use rocket::fs::NamedFile; +use rocket::http::Status; +use rocket::request::{FromRequest, Outcome}; +use rocket::{Request, State}; +use std::path::Path; +use uuid::Uuid; + +pub struct ReqMeta { + pub ip: String, + pub agent: Option, + pub language: Option, +} + +#[rocket::async_trait] +impl<'r> FromRequest<'r> for ReqMeta { + type Error = (); + + async fn from_request(req: &'r Request<'_>) -> Outcome { + let headers = req.headers(); + let agent = headers.get_one("User-Agent").map(|s| s.to_string()); + let language = headers.get_one("Accept-Language").map(|s| s.to_string()); + + let ip = req + .client_ip() + .map(|ip| ip.to_string()) + .or_else(|| { + headers + .get_one("X-Forwarded-For") + .map(|s| s.split(',').next().unwrap_or("").trim().to_string()) + }) + .unwrap_or_else(|| "unknown".to_string()); + + Outcome::Success(ReqMeta { + ip, + agent, + language, + }) + } +} + +#[get("/")] +pub async fn get(id: String, meta: ReqMeta, state: &State) -> Result { + let id = match Uuid::parse_str(id.as_str()).ok() { + Some(id) => id, + None => return Err(Status::BadRequest), + }; + + { + let mut db = state.db.lock().unwrap(); + + let result = trackers::dsl::trackers + .filter(trackers::id.eq(id)) + .first::(&mut *db) + .ok(); + + if result.is_none() { + return Err(Status::NotFound); + } + + // TODO: handle possible error + let _ = diesel::insert_into(hits::table) + .values(&Hit { + id: Uuid::new_v4(), + tracker_id: id, + ip: meta.ip, + agent: meta.agent, + language: meta.language, + created_at: Utc::now().naive_utc(), + }) + .execute(&mut *db); + } + + NamedFile::open(Path::new(state.image_path.as_str())) + .await + .map_err(|_| Status::InternalServerError) +} diff --git a/src/api/mod.rs b/src/api/mod.rs index ef0b7fa..8708e92 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,2 +1,3 @@ pub mod tracker; pub mod hit; +pub mod image; diff --git a/src/main.rs b/src/main.rs index 211b25d..a28256d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,7 @@ mod models; mod schema; use crate::api::hit; +use crate::api::image; use crate::api::tracker; use diesel::{Connection, PgConnection}; use std::env; @@ -16,15 +17,19 @@ fn index() -> &'static str { "Hello world!" } +// TODO: add logging +// TODO: add auth + #[launch] fn rocket() -> _ { dotenv::dotenv().ok(); let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set"); + let image_path = env::var("IMAGE_PATH").expect("IMAGE_PATH must be set"); let db = PgConnection::establish(&database_url) .expect(&format!("Error connecting to {}", database_url)); - let app_data = models::AppState::new(db); + let app_data = models::AppState::new(db, image_path); rocket::build() .manage(app_data) .mount("/", routes![index]) @@ -38,4 +43,5 @@ fn rocket() -> _ { ], ) .mount("/hit", routes![hit::index, hit::get, hit::delete]) + .mount("/image", routes![image::get]) } diff --git a/src/models/hit.rs b/src/models/hit.rs index efd1faa..8d9770a 100644 --- a/src/models/hit.rs +++ b/src/models/hit.rs @@ -1,8 +1,8 @@ use chrono::NaiveDateTime; -use diesel::{Queryable, Selectable}; +use diesel::{Insertable, Queryable, Selectable}; use uuid::Uuid; -#[derive(Queryable, Selectable, Clone)] +#[derive(Queryable, Selectable, Insertable, Clone)] #[diesel(table_name = crate::schema::hits)] #[diesel(check_for_backend(diesel::pg::Pg))] pub struct Hit { @@ -13,3 +13,4 @@ pub struct Hit { pub language: Option, pub created_at: NaiveDateTime, } + diff --git a/src/models/mod.rs b/src/models/mod.rs index 52619f6..a6007e0 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -6,12 +6,14 @@ pub mod tracker; pub struct AppState { pub db: Arc>, + pub image_path: String, } impl AppState { - pub fn new(db: PgConnection) -> Self { + pub fn new(db: PgConnection, image_path: String) -> Self { AppState { db: Arc::new(Mutex::new(db)), + image_path, } } }