mural_server/
lib.rs

1use std::path::PathBuf;
2
3use actix_web::{App, HttpResponse, HttpServer, Responder, get, http::StatusCode, web};
4
5mod cli;
6
7mod config;
8pub(crate) use config::Config;
9
10mod env;
11
12mod error;
13pub(crate) use error::Error;
14
15mod pool;
16pub(crate) use pool::Pool;
17
18pub(crate) mod prelude;
19use prelude::*;
20
21mod state;
22pub(crate) use state::State;
23
24mod wallpaper;
25pub(crate) use wallpaper::Wallpaper;
26
27async fn not_found() -> impl Responder {
28    HttpResponse::build(StatusCode::NOT_FOUND).body("Not Found")
29}
30
31#[get("/api/interval")]
32async fn interval(state: web::Data<State>) -> impl Responder {
33    HttpResponse::build(StatusCode::OK).body(state.interval().to_string())
34}
35
36#[get("/api/pool/{pool_name}/digest")]
37async fn current_digest(path: web::Path<String>, state: web::Data<State>) -> impl Responder {
38    let pool_name = path.into_inner();
39    let wallpaper = match state.current_wallpaper(&pool_name) {
40        Ok(wallpaper) => wallpaper,
41        Err(Error::PoolNotFound(pool_name)) => {
42            return HttpResponse::build(StatusCode::NOT_FOUND)
43                .body(format!("there is no pool named '{}'", pool_name));
44        }
45        Err(Error::PoolEmpty(pool_name)) => {
46            error!("the current wallpaper of a pool was requested, but it is empty");
47            return HttpResponse::build(StatusCode::INTERNAL_SERVER_ERROR).body(format!(
48                "the pool '{}' does not contain any wallpapers",
49                pool_name
50            ));
51        }
52        Err(_) => todo!(
53            "current_wallpaper returned an error that was not expected; you should handle the new error here"
54        ),
55    };
56    let digest = wallpaper.digest().clone();
57    HttpResponse::build(StatusCode::OK).body(digest)
58}
59
60#[get("/api/pool/{pool_name}/wallpaper")]
61async fn current_wallpaper(path: web::Path<String>, state: web::Data<State>) -> impl Responder {
62    let pool_name = path.into_inner();
63    let wallpaper = match state.current_wallpaper(&pool_name) {
64        Ok(wallpaper) => wallpaper,
65        Err(Error::PoolNotFound(pool_name)) => {
66            return HttpResponse::build(StatusCode::NOT_FOUND)
67                .body(format!("there is no pool named '{}'", pool_name));
68        }
69        Err(Error::PoolEmpty(pool_name)) => {
70            error!("the current wallpaper of a pool was requested, but it is empty");
71            return HttpResponse::build(StatusCode::INTERNAL_SERVER_ERROR).body(format!(
72                "the pool '{}' does not contain any wallpapers",
73                pool_name
74            ));
75        }
76        Err(_) => todo!(
77            "current_wallpaper returned an error that was not expected; you should handle the new error here"
78        ),
79    };
80    let file_path = wallpaper.file_path().clone();
81    let extension = file_path
82        .extension()
83        .expect("files with no extension should be filtered out from the pool")
84        .to_string_lossy()
85        .to_string();
86    let image_content = match web::block(move || std::fs::read(file_path)).await {
87        Ok(Ok(image_content)) => image_content,
88        Ok(Err(e)) => {
89            error!("failed to read the wallpaper file: {}", e);
90            return HttpResponse::build(StatusCode::INTERNAL_SERVER_ERROR)
91                .body("failed to read the wallpaper file");
92        }
93        Err(e) => {
94            error!("blocking error: {}", e);
95            return HttpResponse::build(StatusCode::INTERNAL_SERVER_ERROR)
96                .body("internal server error");
97        }
98    };
99    HttpResponse::build(StatusCode::OK)
100        .content_type(format!("image/{}", extension))
101        .body(image_content)
102}
103
104pub async fn run() -> Result<()> {
105    env::load_dotenv()?;
106    let matches = cli::get_command().get_matches();
107    let custom_config_dir = matches.get_one::<PathBuf>("config-dir");
108    let config = Config::load(custom_config_dir)?;
109    let state = State::new(&config)?;
110
111    println!("pools:");
112    for (pool_name, pool) in state.pools() {
113        println!("  {}:", pool_name);
114        for wallpaper in pool.wallpapers() {
115            println!(
116                "  -> {}",
117                wallpaper
118                    .file_path()
119                    .file_stem()
120                    .unwrap()
121                    .to_string_lossy()
122                    .to_string()
123            );
124        }
125    }
126
127    HttpServer::new(move || {
128        App::new()
129            .app_data(web::Data::new(state.clone()))
130            .service(interval)
131            .service(current_digest)
132            .service(current_wallpaper)
133            .default_service(web::route().to(not_found))
134    })
135    .bind(("0.0.0.0", config.port()))
136    .map_err(Error::BindServer)?
137    .run()
138    .await
139    .map_err(|_| Error::RunServer)?;
140
141    Ok(())
142}