This commit is contained in:
2025-05-23 09:20:08 -04:00
parent ed98140d22
commit 6ad2afe6dd
16 changed files with 5180 additions and 177 deletions

View File

@@ -8,9 +8,9 @@ use rand_chacha::ChaCha20Rng;
mod auth;
mod email_token;
mod model;
mod routes;
mod session;
mod model;
pub use auth::*;
pub use routes::init_routes;

View File

@@ -80,53 +80,53 @@ impl Default for AirportQuery {
}
}
impl AirportQuery {
pub fn builder() -> AirportQueryBuilder {
AirportQueryBuilder::new()
}
}
// impl AirportQuery {
// pub fn builder() -> AirportQueryBuilder {
// AirportQueryBuilder::new()
// }
// }
pub struct AirportQueryBuilder {
inner: AirportQuery,
}
impl AirportQueryBuilder {
/// start the builder
pub fn new() -> Self {
AirportQueryBuilder {
inner: AirportQuery::default(),
}
}
pub fn page(mut self, page: u32) -> Self {
self.inner.page = Some(page);
self
}
pub fn limit(mut self, limit: u32) -> Self {
self.inner.limit = Some(limit);
self
}
pub fn icaos<T: Into<String>>(mut self, v: T) -> Self {
self.inner.icaos = Some(v.into());
self
}
pub fn iatas<T: Into<String>>(mut self, v: T) -> Self {
self.inner.iatas = Some(v.into());
self
}
pub fn metars(mut self, v: bool) -> Self {
self.inner.metars = Some(v);
self
}
pub fn build(self) -> AirportQuery {
self.inner
}
}
// pub struct AirportQueryBuilder {
// inner: AirportQuery,
// }
//
// impl AirportQueryBuilder {
// /// start the builder
// pub fn new() -> Self {
// AirportQueryBuilder {
// inner: AirportQuery::default(),
// }
// }
//
// pub fn page(mut self, page: u32) -> Self {
// self.inner.page = Some(page);
// self
// }
//
// pub fn limit(mut self, limit: u32) -> Self {
// self.inner.limit = Some(limit);
// self
// }
//
// pub fn icaos<T: Into<String>>(mut self, v: T) -> Self {
// self.inner.icaos = Some(v.into());
// self
// }
//
// pub fn iatas<T: Into<String>>(mut self, v: T) -> Self {
// self.inner.iatas = Some(v.into());
// self
// }
//
// pub fn metars(mut self, v: bool) -> Self {
// self.inner.metars = Some(v);
// self
// }
//
// pub fn build(self) -> AirportQuery {
// self.inner
// }
// }
#[derive(Debug, Deserialize, ToSchema)]
pub struct Bounds {

View File

@@ -83,7 +83,8 @@ impl HttpClient {
if response.status() != 200 {
return Err(Error::new(
response.status().as_u16(),
format!("Request returned status {}", response.status())));
format!("Request returned status {}", response.status()),
));
}
Ok(response)

View File

@@ -35,7 +35,11 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Arc::new(HttpClient::default()?);
let scheduler_client = client.clone();
scheduler::update_metars(scheduler_client, 600);
let interval = env::var("METAR_INTERVAL")
.unwrap_or("300".to_string())
.parse::<u64>()
.unwrap_or(300);
scheduler::update_metars(scheduler_client, interval);
// Initialize admin user
let admin_username = env::var("ADMIN_USERNAME");

View File

@@ -4,6 +4,8 @@ use crate::http_client::HttpClient;
use crate::metars::MetarCheck;
use crate::{db, error::ApiResult};
use chrono::{DateTime, Datelike, NaiveDate, Utc};
use flate2::read::GzDecoder;
use reqwest::header::ETAG;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::env;
@@ -11,8 +13,6 @@ use std::fmt::Display;
use std::io::{Cursor, Read};
use std::str::FromStr;
use std::sync::OnceLock;
use flate2::read::GzDecoder;
use reqwest::header::ETAG;
use utoipa::ToSchema;
static TIME_OFFSET: OnceLock<i64> = OnceLock::new();
@@ -921,12 +921,17 @@ impl Metar {
),
)
})?;
let candidate_date = match candidate_date.and_hms_opt(observation_hour, observation_minute, 0) {
let candidate_date = match candidate_date.and_hms_opt(observation_hour, observation_minute, 0) {
Some(date) => date,
None => return Err(Error::new(
500,
format!("Invalid time for time '{}': hour {}, minute {}",
observation_time, observation_hour, observation_minute)))
None => {
return Err(Error::new(
500,
format!(
"Invalid time for time '{}': hour {}, minute {}",
observation_time, observation_hour, observation_minute
),
));
}
};
let obs_datetime = if candidate_date > current_time {
@@ -956,9 +961,11 @@ impl Metar {
Ok(obs_datetime.format("%Y-%m-%dT%H:%M:00Z").to_string())
}
pub async fn get_cached_remote_metars(client: &HttpClient, etag: Option<String>) -> ApiResult<(Vec<Self>, String)> {
let base_url = env::var("AVIATION_WEATHER_URL")
.expect("AVIATION_WEATHER_URL must be set");
pub async fn get_cached_remote_metars(
client: &HttpClient,
etag: Option<String>,
) -> ApiResult<(Vec<Self>, String)> {
let base_url = env::var("AVIATION_WEATHER_URL").expect("AVIATION_WEATHER_URL must be set");
let url = format!("{}/data/cache/metars.cache.csv.gz", base_url);
match client.get(&url, etag.clone()).await {
@@ -991,8 +998,8 @@ impl Metar {
Some(etag) => Ok((output, etag)),
None => match etag {
Some(etag) => Ok((output, etag)),
None => Ok((output, String::new()))
}
None => Ok((output, String::new())),
},
}
}
Err(err) => Err(err.into()),
@@ -1000,8 +1007,7 @@ impl Metar {
}
pub async fn get_remote_metars(client: &HttpClient, icaos: &Vec<String>) -> ApiResult<Vec<Self>> {
let base_url = env::var("AVIATION_WEATHER_URL")
.expect("AVIATION_WEATHER_URL must be set");
let base_url = env::var("AVIATION_WEATHER_URL").expect("AVIATION_WEATHER_URL must be set");
// Query the remote API for the missing METAR data 10 at a time
let icao_chunks = icaos
.chunks(10)
@@ -1014,22 +1020,20 @@ impl Metar {
base_url, icao_chunk
);
let mut m = match client.get(&url, None).await {
Ok(r) => {
match r.text().await {
Ok(r) => {
let metar_chunk = r
.trim()
.split("\n")
.filter(|m| !m.trim().is_empty())
.collect();
match Self::parse_multiple(&metar_chunk) {
Ok(m) => m,
Err(err) => return Err(err),
}
Ok(r) => match r.text().await {
Ok(r) => {
let metar_chunk = r
.trim()
.split("\n")
.filter(|m| !m.trim().is_empty())
.collect();
match Self::parse_multiple(&metar_chunk) {
Ok(m) => m,
Err(err) => return Err(err),
}
Err(err) => return Err(Error::new(500, format!("METAR parse failed: {}", err))),
}
}
Err(err) => return Err(Error::new(500, format!("METAR parse failed: {}", err))),
},
Err(err) => return Err(err.into()),
};
metars.append(&mut m);

View File

@@ -1,4 +1,5 @@
use crate::AppState;
use crate::account::Auth;
use crate::metars::Metar;
use actix_web::{HttpRequest, HttpResponse, get, put, web};
use log::error;
@@ -6,7 +7,6 @@ use serde::Deserialize;
use utoipa::{IntoParams, ToSchema};
use utoipa_actix_web::scope;
use utoipa_actix_web::service_config::ServiceConfig;
use crate::account::Auth;
#[derive(Debug, Deserialize, ToSchema, IntoParams)]
#[into_params(parameter_in = Query)]
@@ -85,37 +85,10 @@ async fn refresh_metars(data: web::Data<AppState>, req: HttpRequest, _auth: Auth
HttpResponse::Ok().json(metars)
}
#[utoipa::path(
tag = "metar",
responses(
(status = 200, description = "Successful Response", body = Metar),
(status = 404, description = "Not Found"),
),
)]
#[get("/{icao}")]
async fn find(icao: web::Path<String>) -> HttpResponse {
let icao = vec![icao.to_uppercase()];
let metar = match Metar::get_all_distinct(&icao).await {
Ok(metars) => {
if metars.len() == 1 {
metars[0].clone()
} else {
return HttpResponse::NotFound().finish()
}
},
Err(err) => {
error!("{}", err);
return err.to_http_response();
}
};
HttpResponse::Ok().json(metar)
}
pub fn init_routes(config: &mut ServiceConfig) {
config.service(
scope::scope("/metars")
.service(find_all)
.service(refresh_metars)
.service(find)
.service(refresh_metars),
);
}

View File

@@ -22,7 +22,7 @@ pub fn update_metars(client: Arc<HttpClient>, seconds: u64) {
// Run the update
match Metar::update_metars(&client, etag.clone()).await {
Ok(new_etag) => etag = Some(new_etag),
Err(err) => log::error!("METAR update failed: {}", err)
Err(err) => log::error!("METAR update failed: {}", err),
}
let elapsed = start_monotonic.elapsed();

View File

@@ -21,7 +21,10 @@ pub struct SystemInfo {
async fn info() -> HttpResponse {
let healthy = true;
let version = env!("CARGO_PKG_VERSION");
let info = SystemInfo { version: version.to_string(), healthy };
let info = SystemInfo {
version: version.to_string(),
healthy,
};
HttpResponse::Ok().json(info)
}