Fixed docker issue temporarily
This commit is contained in:
10
.env
10
.env
@@ -2,8 +2,7 @@ RUST_LOG=warn,api=info
|
|||||||
|
|
||||||
HTTPD_DOMAIN=localhost
|
HTTPD_DOMAIN=localhost
|
||||||
HTTPD_PROTOCOL=http
|
HTTPD_PROTOCOL=http
|
||||||
HTTPD_HTTP_PORT=8080
|
HTTPD_PORT=8080
|
||||||
HTTPD_HTTPS_PORT=8443
|
|
||||||
HTTPD_MINIO_HOST=host.docker.internal
|
HTTPD_MINIO_HOST=host.docker.internal
|
||||||
HTTPD_API_HOST=host.docker.internal
|
HTTPD_API_HOST=host.docker.internal
|
||||||
HTTPD_UI_HOST=host.docker.internal
|
HTTPD_UI_HOST=host.docker.internal
|
||||||
@@ -24,18 +23,19 @@ MINIO_BUCKET=aviation
|
|||||||
MINIO_PROTOCOL=http
|
MINIO_PROTOCOL=http
|
||||||
MINIO_PORT=9000
|
MINIO_PORT=9000
|
||||||
MINIO_PORT_INTERNAL=9001
|
MINIO_PORT_INTERNAL=9001
|
||||||
MINIO_BROWSER_REDIRECT_URL=${HTTPD_PROTOCOL}://${HTTPD_DOMAIN}:${HTTPD_HTTP_PORT}/minio/
|
MINIO_BROWSER_REDIRECT_URL=${HTTPD_PROTOCOL}://${HTTPD_DOMAIN}:${HTTPD_PORT}/minio/
|
||||||
|
|
||||||
UI_PROTOCOL=http
|
UI_PROTOCOL=http
|
||||||
UI_PORT=3000
|
UI_PORT=3000
|
||||||
|
|
||||||
API_PROTOCOL=http
|
API_PROTOCOL=http
|
||||||
|
API_HOST=0.0.0.0
|
||||||
API_PORT=5000
|
API_PORT=5000
|
||||||
|
|
||||||
VITE_API_URL=${HTTPD_PROTOCOL}://${HTTPD_DOMAIN}:${HTTPD_HTTP_PORT}/api
|
VITE_API_URL=${HTTPD_PROTOCOL}://${HTTPD_DOMAIN}:${HTTPD_PORT}/api
|
||||||
|
|
||||||
ENVIRONMENT=development
|
ENVIRONMENT=development
|
||||||
ADMIN_EMAIL=admin@example.com
|
ADMIN_EMAIL=admin@example.com
|
||||||
ADMIN_PASSWORD=CHANGEME
|
ADMIN_PASSWORD=CHANGEME
|
||||||
|
|
||||||
GOV_API_URL=https://aviationweather.gov/cgi-bin/data
|
AVIATION_WEATHER_URL=https://aviationweather.gov/api/data
|
||||||
|
|||||||
@@ -11,26 +11,14 @@ COPY Cargo.toml ./
|
|||||||
RUN apt-get update && apt-get install -y cmake
|
RUN apt-get update && apt-get install -y cmake
|
||||||
RUN cargo build --release
|
RUN cargo build --release
|
||||||
|
|
||||||
# ======
|
|
||||||
# Keys
|
|
||||||
# ======
|
|
||||||
FROM debian:bookworm-slim AS keys
|
|
||||||
WORKDIR /keys
|
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y openssl libpq-dev
|
|
||||||
RUN openssl genrsa -out access.pem 4096
|
|
||||||
RUN openssl rsa -in access.pem -pubout -outform PEM -out access.pem.pub
|
|
||||||
RUN openssl genrsa -out refresh.pem 4096
|
|
||||||
RUN openssl rsa -in refresh.pem -pubout -outform PEM -out refresh.pem.pub
|
|
||||||
|
|
||||||
# =========
|
# =========
|
||||||
# Runtime
|
# Runtime
|
||||||
# =========
|
# =========
|
||||||
FROM keys AS runtime
|
FROM debian:bookworm-slim AS runtime
|
||||||
WORKDIR /api
|
WORKDIR /api
|
||||||
|
RUN apt-get update && apt-get install -y openssl libpq-dev
|
||||||
USER root
|
USER root
|
||||||
|
|
||||||
COPY --from=builder /builder/target/release/api /usr/local/bin/api
|
COPY --from=builder /builder/target/release/api /usr/local/bin/api
|
||||||
COPY --from=keys /keys /keys
|
|
||||||
|
|
||||||
CMD ["api"]
|
CMD ["api"]
|
||||||
|
|||||||
@@ -17,6 +17,15 @@ CREATE TABLE IF NOT EXISTS airports (
|
|||||||
public BOOLEAN DEFAULT false
|
public BOOLEAN DEFAULT false
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE INDEX ON airports (iata);
|
||||||
|
CREATE INDEX ON airports (local);
|
||||||
|
CREATE INDEX ON airports (name);
|
||||||
|
CREATE INDEX ON airports (category);
|
||||||
|
CREATE INDEX ON airports (iso_country);
|
||||||
|
CREATE INDEX ON airports (iso_region);
|
||||||
|
CREATE INDEX ON airports (municipality);
|
||||||
|
CREATE INDEX ON airports (longitude, latitude);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS runways (
|
CREATE TABLE IF NOT EXISTS runways (
|
||||||
id UUID PRIMARY KEY NOT NULL,
|
id UUID PRIMARY KEY NOT NULL,
|
||||||
icao TEXT NOT NULL,
|
icao TEXT NOT NULL,
|
||||||
@@ -26,6 +35,9 @@ CREATE TABLE IF NOT EXISTS runways (
|
|||||||
surface TEXT NOT NULL
|
surface TEXT NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE INDEX ON runways (icao);
|
||||||
|
CREATE INDEX ON runways (surface);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS frequencies (
|
CREATE TABLE IF NOT EXISTS frequencies (
|
||||||
id UUID PRIMARY KEY NOT NULL,
|
id UUID PRIMARY KEY NOT NULL,
|
||||||
icao TEXT NOT NULL,
|
icao TEXT NOT NULL,
|
||||||
@@ -33,6 +45,9 @@ CREATE TABLE IF NOT EXISTS frequencies (
|
|||||||
frequency_mhz REAL NOT NULL
|
frequency_mhz REAL NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE INDEX ON frequencies (icao);
|
||||||
|
CREATE INDEX ON frequencies (frequency_mhz);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS metars (
|
CREATE TABLE IF NOT EXISTS metars (
|
||||||
icao TEXT NOT NULL,
|
icao TEXT NOT NULL,
|
||||||
observation_time TIMESTAMPTZ NOT NULL,
|
observation_time TIMESTAMPTZ NOT NULL,
|
||||||
@@ -40,6 +55,8 @@ CREATE TABLE IF NOT EXISTS metars (
|
|||||||
data JSONB NOT NULL
|
data JSONB NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE INDEX ON metars (observation_time DESC);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
email TEXT PRIMARY KEY NOT NULL,
|
email TEXT PRIMARY KEY NOT NULL,
|
||||||
password_hash TEXT NOT NULL,
|
password_hash TEXT NOT NULL,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use futures_util::try_join;
|
use futures_util::try_join;
|
||||||
|
use reqwest::Client;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::{Postgres, QueryBuilder};
|
use sqlx::{Postgres, QueryBuilder};
|
||||||
use crate::airports::{
|
use crate::airports::{
|
||||||
@@ -194,7 +195,7 @@ impl From<AirportRow> for Airport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Airport {
|
impl Airport {
|
||||||
pub async fn select(icao: &str, metar: bool) -> Option<Self> {
|
pub async fn select(client: &Client, icao: &str, metar: bool) -> Option<Self> {
|
||||||
let pool = db::pool();
|
let pool = db::pool();
|
||||||
|
|
||||||
let airport_fut = async {
|
let airport_fut = async {
|
||||||
@@ -206,7 +207,7 @@ impl Airport {
|
|||||||
|
|
||||||
let metar_fut = async {
|
let metar_fut = async {
|
||||||
if metar {
|
if metar {
|
||||||
match Metar::find_all(&vec![icao.to_string()]).await {
|
match Metar::find_all(client, &vec![icao.to_string()], &false).await {
|
||||||
Ok(m) => Some(m.into_iter().nth(0)),
|
Ok(m) => Some(m.into_iter().nth(0)),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::error!("{}", err);
|
log::error!("{}", err);
|
||||||
@@ -269,7 +270,7 @@ impl Airport {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn select_all(query: &AirportQuery) -> ApiResult<Vec<Self>> {
|
pub async fn select_all(client: &Client, query: &AirportQuery) -> ApiResult<Vec<Self>> {
|
||||||
let pool = db::pool();
|
let pool = db::pool();
|
||||||
|
|
||||||
let mut builder = QueryBuilder::<Postgres>::new("SELECT * FROM ");
|
let mut builder = QueryBuilder::<Postgres>::new("SELECT * FROM ");
|
||||||
@@ -337,7 +338,7 @@ impl Airport {
|
|||||||
let runway_future = Runway::select_all_map(icaos.clone());
|
let runway_future = Runway::select_all_map(icaos.clone());
|
||||||
let frequency_future = Frequency::select_all_map(icaos.clone());
|
let frequency_future = Frequency::select_all_map(icaos.clone());
|
||||||
let metar_future = if query.metars.unwrap_or(false) {
|
let metar_future = if query.metars.unwrap_or(false) {
|
||||||
Some(Metar::find_all(&icaos))
|
Some(Metar::find_all(client, &icaos, &false))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use crate::{
|
|||||||
airports::Airport,
|
airports::Airport,
|
||||||
db::Paged,
|
db::Paged,
|
||||||
auth::{Auth, verify_role},
|
auth::{Auth, verify_role},
|
||||||
|
AppState,
|
||||||
};
|
};
|
||||||
use actix_multipart::Multipart;
|
use actix_multipart::Multipart;
|
||||||
use actix_web::{delete, get, post, put, web, HttpResponse, HttpRequest, ResponseError};
|
use actix_web::{delete, get, post, put, web, HttpResponse, HttpRequest, ResponseError};
|
||||||
@@ -53,7 +54,7 @@ async fn import_airports(mut payload: Multipart, auth: Auth) -> HttpResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("")]
|
#[get("")]
|
||||||
async fn get_airports(req: HttpRequest) -> HttpResponse {
|
async fn get_airports(data: web::Data<AppState>, req: HttpRequest) -> HttpResponse {
|
||||||
let mut query = match web::Query::<AirportQuery>::from_query(req.query_string()) {
|
let mut query = match web::Query::<AirportQuery>::from_query(req.query_string()) {
|
||||||
Ok(q) => q.into_inner(),
|
Ok(q) => q.into_inner(),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@@ -71,7 +72,8 @@ async fn get_airports(req: HttpRequest) -> HttpResponse {
|
|||||||
query.limit = Some(limit);
|
query.limit = Some(limit);
|
||||||
query.page = Some(page);
|
query.page = Some(page);
|
||||||
|
|
||||||
match Airport::select_all(&query).await {
|
let client = &data.client;
|
||||||
|
match Airport::select_all(client, &query).await {
|
||||||
Ok(airports) => HttpResponse::Ok().json(Paged {
|
Ok(airports) => HttpResponse::Ok().json(Paged {
|
||||||
data: airports,
|
data: airports,
|
||||||
page,
|
page,
|
||||||
@@ -86,7 +88,11 @@ async fn get_airports(req: HttpRequest) -> HttpResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/{icao}")]
|
#[get("/{icao}")]
|
||||||
async fn get_airport(icao: web::Path<String>, req: HttpRequest) -> HttpResponse {
|
async fn get_airport(
|
||||||
|
data: web::Data<AppState>,
|
||||||
|
icao: web::Path<String>,
|
||||||
|
req: HttpRequest,
|
||||||
|
) -> HttpResponse {
|
||||||
let metar = match web::Query::<AirportQuery>::from_query(req.query_string()) {
|
let metar = match web::Query::<AirportQuery>::from_query(req.query_string()) {
|
||||||
Ok(q) => q.metars.unwrap_or_else(|| false),
|
Ok(q) => q.metars.unwrap_or_else(|| false),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@@ -95,7 +101,8 @@ async fn get_airport(icao: web::Path<String>, req: HttpRequest) -> HttpResponse
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
match Airport::select(&icao.into_inner(), metar).await {
|
let client = &data.client;
|
||||||
|
match Airport::select(client, &icao.into_inner(), metar).await {
|
||||||
Some(airport) => HttpResponse::Ok().json(airport),
|
Some(airport) => HttpResponse::Ok().json(airport),
|
||||||
None => HttpResponse::NotFound().finish(),
|
None => HttpResponse::NotFound().finish(),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,7 +97,18 @@ impl From<std::env::VarError> for Error {
|
|||||||
|
|
||||||
impl From<reqwest::Error> for Error {
|
impl From<reqwest::Error> for Error {
|
||||||
fn from(error: reqwest::Error) -> Self {
|
fn from(error: reqwest::Error) -> Self {
|
||||||
Self::new(500, format!("Unknown reqwest error: {}", error))
|
match error.status() {
|
||||||
|
Some(status_code) => {
|
||||||
|
if status_code.is_client_error() {
|
||||||
|
Self::new(500, format!("Client reqwest error: {}", error))
|
||||||
|
} else if status_code.is_server_error() {
|
||||||
|
Self::new(500, format!("Server reqwest error: {}", error))
|
||||||
|
} else {
|
||||||
|
Self::new(500, format!("Unknown reqwest error: {:?}", error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => Self::new(500, format!("Unknown reqwest error: {:?}", error)),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use std::env;
|
use std::env;
|
||||||
|
use std::time::Duration;
|
||||||
use actix_cors::Cors;
|
use actix_cors::Cors;
|
||||||
use actix_web::{App, HttpServer, middleware::Logger, web};
|
use actix_web::{App, HttpServer, middleware::Logger, web};
|
||||||
use dotenv::from_filename;
|
use dotenv::from_filename;
|
||||||
@@ -14,15 +14,17 @@ mod metars;
|
|||||||
mod scheduler;
|
mod scheduler;
|
||||||
mod users;
|
mod users;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct AppState {
|
||||||
|
client: reqwest::Client,
|
||||||
|
}
|
||||||
|
|
||||||
#[actix_web::main]
|
#[actix_web::main]
|
||||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
initialize_environment()?;
|
initialize_environment()?;
|
||||||
db::initialize().await?;
|
db::initialize().await?;
|
||||||
// scheduler::update_airports();
|
// scheduler::update_airports();
|
||||||
|
|
||||||
let host = "0.0.0.0".to_string();
|
|
||||||
let port = env::var("API_PORT").unwrap_or("5000".to_string());
|
|
||||||
|
|
||||||
// Initialize admin user
|
// Initialize admin user
|
||||||
let admin_email = env::var("ADMIN_EMAIL");
|
let admin_email = env::var("ADMIN_EMAIL");
|
||||||
let admin_password = env::var("ADMIN_PASSWORD");
|
let admin_password = env::var("ADMIN_PASSWORD");
|
||||||
@@ -55,6 +57,17 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let client = reqwest::Client::builder()
|
||||||
|
.timeout(Duration::from_secs(10))
|
||||||
|
.no_proxy()
|
||||||
|
.danger_accept_invalid_certs(true)
|
||||||
|
.build()
|
||||||
|
.expect("Failed to create reqwest client");
|
||||||
|
|
||||||
|
let state = AppState { client };
|
||||||
|
let host = env::var("API_HOST").unwrap_or("localhost".to_string());
|
||||||
|
let port = env::var("API_PORT").unwrap_or("5000".to_string());
|
||||||
|
|
||||||
let server = match HttpServer::new(move || {
|
let server = match HttpServer::new(move || {
|
||||||
let cors = Cors::default()
|
let cors = Cors::default()
|
||||||
.allow_any_origin()
|
.allow_any_origin()
|
||||||
@@ -62,18 +75,22 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
.allow_any_header()
|
.allow_any_header()
|
||||||
.supports_credentials()
|
.supports_credentials()
|
||||||
.max_age(3600);
|
.max_age(3600);
|
||||||
App::new().wrap(cors).wrap(Logger::default()).service(
|
App::new()
|
||||||
web::scope("api")
|
.wrap(cors)
|
||||||
.configure(airports::init_routes)
|
.wrap(Logger::default())
|
||||||
.configure(metars::init_routes)
|
.app_data(web::Data::new(state.clone()))
|
||||||
.configure(auth::init_routes)
|
.service(
|
||||||
.configure(users::init_routes),
|
web::scope("api")
|
||||||
)
|
.configure(airports::init_routes)
|
||||||
|
.configure(metars::init_routes)
|
||||||
|
.configure(auth::init_routes)
|
||||||
|
.configure(users::init_routes),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.bind(format!("{}:{}", host, port))
|
.bind(format!("{}:{}", host, port))
|
||||||
{
|
{
|
||||||
Ok(b) => {
|
Ok(b) => {
|
||||||
log::info!("Binding server to {}:{}", host, port);
|
log::info!("Server bound to {}:{}", host, port);
|
||||||
b
|
b
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use crate::{error::ApiResult, db};
|
|||||||
use chrono::{DateTime, Datelike, Utc};
|
use chrono::{DateTime, Datelike, Utc};
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use redis::{AsyncCommands, RedisResult};
|
use redis::{AsyncCommands, RedisResult};
|
||||||
|
use reqwest::Client;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use crate::db::redis_async_connection;
|
use crate::db::redis_async_connection;
|
||||||
|
|
||||||
@@ -845,8 +846,8 @@ impl Metar {
|
|||||||
missing_metar_icaos
|
missing_metar_icaos
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_remote_metars(icaos: &[&str]) -> ApiResult<Vec<Metar>> {
|
async fn get_remote_metars(client: &Client, icaos: &[&str]) -> ApiResult<Vec<Metar>> {
|
||||||
let gov_api_url = std::env::var("GOV_API_URL").expect("GOV_API_URL must be set");
|
let base_url = std::env::var("AVIATION_WEATHER_URL").expect("GOV_API_URL must be set");
|
||||||
// Query the remote API for the missing METAR data 10 at a time
|
// Query the remote API for the missing METAR data 10 at a time
|
||||||
let icao_chunks = icaos
|
let icao_chunks = icaos
|
||||||
.chunks(10)
|
.chunks(10)
|
||||||
@@ -854,14 +855,14 @@ impl Metar {
|
|||||||
.collect::<Vec<String>>();
|
.collect::<Vec<String>>();
|
||||||
let mut metars: Vec<Metar> = vec![];
|
let mut metars: Vec<Metar> = vec![];
|
||||||
for icao_chunk in icao_chunks {
|
for icao_chunk in icao_chunks {
|
||||||
let url = format!("{}/metar.php?ids={}", gov_api_url, icao_chunk);
|
let url = format!("{}/metar?ids={}&order=id", base_url, icao_chunk);
|
||||||
let mut m = match reqwest::get(url).await {
|
let mut m = match client.get(url).send().await {
|
||||||
Ok(r) => {
|
Ok(r) => {
|
||||||
// Check if the status code is 200
|
// Check if the status code is 200
|
||||||
if r.status() != 200 {
|
if r.status() != 200 {
|
||||||
return Err(Error::new(
|
return Err(Error::new(
|
||||||
500,
|
500,
|
||||||
format!("Unable to get METAR request: {}", r.status()),
|
format!("Request returned status {}", r.status()),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
match r.text().await {
|
match r.text().await {
|
||||||
@@ -876,20 +877,10 @@ impl Metar {
|
|||||||
Err(err) => return Err(err),
|
Err(err) => return Err(err),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => return Err(Error::new(500, format!("METAR parse failed: {}", err))),
|
||||||
return Err(Error::new(
|
|
||||||
500,
|
|
||||||
format!("Unable to parse METAR request: {}", err),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => return Err(err.into()),
|
||||||
return Err(Error::new(
|
|
||||||
500,
|
|
||||||
format!("Unable to get METAR request: {}", err),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
metars.append(&mut m);
|
metars.append(&mut m);
|
||||||
}
|
}
|
||||||
@@ -911,7 +902,11 @@ impl Metar {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn find_all(icao_list: &Vec<String>) -> ApiResult<Vec<Self>> {
|
pub async fn find_all(
|
||||||
|
client: &Client,
|
||||||
|
icao_list: &Vec<String>,
|
||||||
|
force: &bool,
|
||||||
|
) -> ApiResult<Vec<Self>> {
|
||||||
if icao_list.is_empty() {
|
if icao_list.is_empty() {
|
||||||
return Ok(Vec::new());
|
return Ok(Vec::new());
|
||||||
}
|
}
|
||||||
@@ -937,17 +932,21 @@ impl Metar {
|
|||||||
if !missing_icao_list.is_empty() {
|
if !missing_icao_list.is_empty() {
|
||||||
let mut updated_missing_icao_list: Vec<&str> = Vec::new();
|
let mut updated_missing_icao_list: Vec<&str> = Vec::new();
|
||||||
for icao in &missing_icao_list {
|
for icao in &missing_icao_list {
|
||||||
let result: RedisResult<Option<bool>> = conn.get(icao).await;
|
if *force {
|
||||||
match result {
|
updated_missing_icao_list.push(icao);
|
||||||
Ok(Some(value)) => {
|
} else {
|
||||||
if value {
|
let result: RedisResult<Option<bool>> = conn.get(icao).await;
|
||||||
|
match result {
|
||||||
|
Ok(Some(value)) => {
|
||||||
|
if value {
|
||||||
|
updated_missing_icao_list.push(icao);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None) => {
|
||||||
updated_missing_icao_list.push(icao);
|
updated_missing_icao_list.push(icao);
|
||||||
}
|
}
|
||||||
|
Err(err) => return Err(err.into()),
|
||||||
}
|
}
|
||||||
Ok(None) => {
|
|
||||||
updated_missing_icao_list.push(icao);
|
|
||||||
}
|
|
||||||
Err(err) => return Err(err.into()),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !updated_missing_icao_list.is_empty() {
|
if !updated_missing_icao_list.is_empty() {
|
||||||
@@ -955,7 +954,7 @@ impl Metar {
|
|||||||
"Retrieving missing METAR data for {:?}",
|
"Retrieving missing METAR data for {:?}",
|
||||||
updated_missing_icao_list
|
updated_missing_icao_list
|
||||||
);
|
);
|
||||||
let mut missing_icao_list = Self::get_remote_metars(&updated_missing_icao_list)
|
let mut missing_icao_list = Self::get_remote_metars(client, &updated_missing_icao_list)
|
||||||
.await
|
.await
|
||||||
.unwrap_or_else(|err| {
|
.unwrap_or_else(|err| {
|
||||||
log::warn!("Unable to get remote METAR data; {}", err);
|
log::warn!("Unable to get remote METAR data; {}", err);
|
||||||
|
|||||||
@@ -2,14 +2,16 @@ use crate::metars::Metar;
|
|||||||
use actix_web::{get, web, HttpResponse, HttpRequest};
|
use actix_web::{get, web, HttpResponse, HttpRequest};
|
||||||
use log::error;
|
use log::error;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use crate::AppState;
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
struct FindAllParameters {
|
struct FindAllParameters {
|
||||||
icaos: Option<String>,
|
icaos: Option<String>,
|
||||||
|
force: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("metars")]
|
#[get("metars")]
|
||||||
async fn find_all(req: HttpRequest) -> HttpResponse {
|
async fn find_all(data: web::Data<AppState>, req: HttpRequest) -> HttpResponse {
|
||||||
let parameters = web::Query::<FindAllParameters>::from_query(req.query_string()).unwrap();
|
let parameters = web::Query::<FindAllParameters>::from_query(req.query_string()).unwrap();
|
||||||
let icao_option = ¶meters.icaos;
|
let icao_option = ¶meters.icaos;
|
||||||
let icao_string = match icao_option {
|
let icao_string = match icao_option {
|
||||||
@@ -17,8 +19,10 @@ async fn find_all(req: HttpRequest) -> HttpResponse {
|
|||||||
None => return HttpResponse::UnprocessableEntity().body("Missing icaos parameter"),
|
None => return HttpResponse::UnprocessableEntity().body("Missing icaos parameter"),
|
||||||
};
|
};
|
||||||
let icaos: Vec<String> = icao_string.split(',').map(|s| s.to_string()).collect();
|
let icaos: Vec<String> = icao_string.split(',').map(|s| s.to_string()).collect();
|
||||||
|
let force = ¶meters.force.unwrap_or(false);
|
||||||
|
|
||||||
let metars = match Metar::find_all(&icaos).await {
|
let client = &data.client;
|
||||||
|
let metars = match Metar::find_all(client, &icaos, force).await {
|
||||||
Ok(a) => a,
|
Ok(a) => a,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("{}", err);
|
error!("{}", err);
|
||||||
|
|||||||
@@ -5,11 +5,12 @@ meta {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
url: {{API_URL}}/metars?icaos=KJYO,KOKV,KMRB,KHEF,KIAD
|
url: {{API_URL}}/metars?icaos=KJYO,KOKV,KMRB,KHEF,KIAD&force=true
|
||||||
body: none
|
body: none
|
||||||
auth: none
|
auth: none
|
||||||
}
|
}
|
||||||
|
|
||||||
params:query {
|
params:query {
|
||||||
icaos: KJYO,KOKV,KMRB,KHEF,KIAD
|
icaos: KJYO,KOKV,KMRB,KHEF,KIAD
|
||||||
|
force: true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
vars {
|
vars {
|
||||||
BASE_URL: http://localhost:8080
|
BASE_URL: http://localhost:8080
|
||||||
|
~BASE_URL: http://localhost:5000
|
||||||
|
~BASE_URL: http://127.0.0.1:5000
|
||||||
|
~BASE_URL: http://[::1]:5000
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ x-env_file: &env
|
|||||||
- path: .env.local
|
- path: .env.local
|
||||||
required: false
|
required: false
|
||||||
|
|
||||||
|
x-restart: &default_restart
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
name: aviation
|
name: aviation
|
||||||
services:
|
services:
|
||||||
httpd:
|
httpd:
|
||||||
@@ -14,12 +17,11 @@ services:
|
|||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
env_file: *env
|
env_file: *env
|
||||||
ports:
|
ports:
|
||||||
- "${HTTPD_HTTP_PORT:-8080}:80"
|
- "${HTTPD_PORT:-8080}:80"
|
||||||
- "${HTTPD_HTTPS_PORT:-8443}:443"
|
|
||||||
networks:
|
networks:
|
||||||
- frontend
|
- frontend
|
||||||
- backend
|
- backend
|
||||||
restart: unless-stopped
|
<<: *default_restart
|
||||||
|
|
||||||
postgres:
|
postgres:
|
||||||
image: postgis/postgis:17-3.5
|
image: postgis/postgis:17-3.5
|
||||||
@@ -38,7 +40,7 @@ services:
|
|||||||
- backend
|
- backend
|
||||||
profiles:
|
profiles:
|
||||||
- backend
|
- backend
|
||||||
restart: unless-stopped
|
<<: *default_restart
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
image: redis:8.0-M03 # Replace with valkey?
|
image: redis:8.0-M03 # Replace with valkey?
|
||||||
@@ -56,7 +58,7 @@ services:
|
|||||||
- backend
|
- backend
|
||||||
profiles:
|
profiles:
|
||||||
- backend
|
- backend
|
||||||
restart: unless-stopped
|
<<: *default_restart
|
||||||
|
|
||||||
minio:
|
minio:
|
||||||
image: minio/minio:RELEASE.2025-02-28T09-55-16Z
|
image: minio/minio:RELEASE.2025-02-28T09-55-16Z
|
||||||
@@ -76,7 +78,7 @@ services:
|
|||||||
profiles:
|
profiles:
|
||||||
- backend
|
- backend
|
||||||
command: server --console-address ":9001" /data
|
command: server --console-address ":9001" /data
|
||||||
restart: unless-stopped
|
<<: *default_restart
|
||||||
|
|
||||||
api:
|
api:
|
||||||
image: aviation-api:latest
|
image: aviation-api:latest
|
||||||
@@ -86,6 +88,7 @@ services:
|
|||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
env_file: *env
|
env_file: *env
|
||||||
environment:
|
environment:
|
||||||
|
API_HOST: 0.0.0.0
|
||||||
POSTGRES_HOST: aviation-postgres
|
POSTGRES_HOST: aviation-postgres
|
||||||
POSTGRES_PORT: 5432
|
POSTGRES_PORT: 5432
|
||||||
REDIS_HOST: aviation-redis
|
REDIS_HOST: aviation-redis
|
||||||
@@ -103,7 +106,7 @@ services:
|
|||||||
- backend
|
- backend
|
||||||
profiles:
|
profiles:
|
||||||
- api
|
- api
|
||||||
restart: unless-stopped
|
<<: *default_restart
|
||||||
|
|
||||||
ui:
|
ui:
|
||||||
image: aviation-ui:latest
|
image: aviation-ui:latest
|
||||||
@@ -125,7 +128,7 @@ services:
|
|||||||
profiles:
|
profiles:
|
||||||
- frontend
|
- frontend
|
||||||
command: ["npm", "run", "dev"]
|
command: ["npm", "run", "dev"]
|
||||||
restart: unless-stopped
|
<<: *default_restart
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres:
|
postgres:
|
||||||
|
|||||||
@@ -158,14 +158,14 @@ LoadModule proxy_http_module modules/mod_proxy_http.so
|
|||||||
#LoadModule session_dbd_module modules/mod_session_dbd.so
|
#LoadModule session_dbd_module modules/mod_session_dbd.so
|
||||||
#LoadModule slotmem_shm_module modules/mod_slotmem_shm.so
|
#LoadModule slotmem_shm_module modules/mod_slotmem_shm.so
|
||||||
#LoadModule slotmem_plain_module modules/mod_slotmem_plain.so
|
#LoadModule slotmem_plain_module modules/mod_slotmem_plain.so
|
||||||
LoadModule ssl_module modules/mod_ssl.so
|
#LoadModule ssl_module modules/mod_ssl.so
|
||||||
#LoadModule optional_hook_export_module modules/mod_optional_hook_export.so
|
#LoadModule optional_hook_export_module modules/mod_optional_hook_export.so
|
||||||
#LoadModule optional_hook_import_module modules/mod_optional_hook_import.so
|
#LoadModule optional_hook_import_module modules/mod_optional_hook_import.so
|
||||||
#LoadModule optional_fn_import_module modules/mod_optional_fn_import.so
|
#LoadModule optional_fn_import_module modules/mod_optional_fn_import.so
|
||||||
#LoadModule optional_fn_export_module modules/mod_optional_fn_export.so
|
#LoadModule optional_fn_export_module modules/mod_optional_fn_export.so
|
||||||
#LoadModule dialup_module modules/mod_dialup.so
|
#LoadModule dialup_module modules/mod_dialup.so
|
||||||
LoadModule http2_module modules/mod_http2.so
|
#LoadModule http2_module modules/mod_http2.so
|
||||||
LoadModule proxy_http2_module modules/mod_proxy_http2.so
|
#LoadModule proxy_http2_module modules/mod_proxy_http2.so
|
||||||
#LoadModule md_module modules/mod_md.so
|
#LoadModule md_module modules/mod_md.so
|
||||||
#LoadModule lbmethod_byrequests_module modules/mod_lbmethod_byrequests.so
|
#LoadModule lbmethod_byrequests_module modules/mod_lbmethod_byrequests.so
|
||||||
#LoadModule lbmethod_bytraffic_module modules/mod_lbmethod_bytraffic.so
|
#LoadModule lbmethod_bytraffic_module modules/mod_lbmethod_bytraffic.so
|
||||||
@@ -229,7 +229,7 @@ Group www-data
|
|||||||
# e-mailed. This address appears on some server-generated pages, such
|
# e-mailed. This address appears on some server-generated pages, such
|
||||||
# as error documents. e.g. admin@your-domain.com
|
# as error documents. e.g. admin@your-domain.com
|
||||||
#
|
#
|
||||||
ServerAdmin you@example.com
|
ServerAdmin ben@bensherrif.com
|
||||||
|
|
||||||
#
|
#
|
||||||
# ServerName gives the name and port that the server uses to identify itself.
|
# ServerName gives the name and port that the server uses to identify itself.
|
||||||
|
|||||||
@@ -50,7 +50,9 @@ body,
|
|||||||
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
transition: background-color 0.2s, color 0.2s;
|
transition:
|
||||||
|
background-color 0.2s,
|
||||||
|
color 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.map-button.active {
|
.map-button.active {
|
||||||
|
|||||||
@@ -42,14 +42,14 @@ function App() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (showRadar) {
|
if (showRadar) {
|
||||||
getWeatherMapUrl().then(url => {
|
getWeatherMapUrl().then((url) => {
|
||||||
setRainViewerUrl(url);
|
setRainViewerUrl(url);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [showRadar]);
|
}, [showRadar]);
|
||||||
|
|
||||||
function toggleRadar() {
|
function toggleRadar() {
|
||||||
setShowRadar(prev => {
|
setShowRadar((prev) => {
|
||||||
const newValue = !prev;
|
const newValue = !prev;
|
||||||
Cookies.set('showRadar', newValue.toString(), { expires: 7 });
|
Cookies.set('showRadar', newValue.toString(), { expires: 7 });
|
||||||
return newValue;
|
return newValue;
|
||||||
@@ -96,7 +96,7 @@ function App() {
|
|||||||
<TileLayer url={darkLayerUrl} />
|
<TileLayer url={darkLayerUrl} />
|
||||||
</LayersControl.BaseLayer>
|
</LayersControl.BaseLayer>
|
||||||
</LayersControl>
|
</LayersControl>
|
||||||
{rainViewerUrl && showRadar && <TileLayer url={rainViewerUrl} opacity={0.5} zIndex={5} />}
|
{rainViewerUrl && showRadar && <TileLayer url={rainViewerUrl} opacity={0.5} zIndex={10} />}
|
||||||
<ZoomControl position={'bottomright'} />
|
<ZoomControl position={'bottomright'} />
|
||||||
<AirportLayer setAirport={setAirport} />
|
<AirportLayer setAirport={setAirport} />
|
||||||
<BaseLayerChangeHandler />
|
<BaseLayerChangeHandler />
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export default function AirportMarker({
|
|||||||
mouseout: () => markerRef.current?.closePopup()
|
mouseout: () => markerRef.current?.closePopup()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Popup closeButton={false}>
|
<Popup closeButton={false} autoPan={false}>
|
||||||
{airport.icao} - {airport.name}
|
{airport.icao} - {airport.name}
|
||||||
</Popup>
|
</Popup>
|
||||||
</Marker>
|
</Marker>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const weatherMapsUrl = 'https://api.rainviewer.com/public/weather-maps.json';
|
|||||||
|
|
||||||
async function getWeatherMaps(): Promise<WeatherMaps | undefined> {
|
async function getWeatherMaps(): Promise<WeatherMaps | undefined> {
|
||||||
const response = await fetch(`${weatherMapsUrl}`, {
|
const response = await fetch(`${weatherMapsUrl}`, {
|
||||||
method: 'GET',
|
method: 'GET'
|
||||||
});
|
});
|
||||||
if (response?.status === 200) {
|
if (response?.status === 200) {
|
||||||
return response.json();
|
return response.json();
|
||||||
@@ -18,16 +18,16 @@ export async function getWeatherMapUrl(): Promise<string | null> {
|
|||||||
if (weatherMaps != undefined) {
|
if (weatherMaps != undefined) {
|
||||||
let url = weatherMaps.host;
|
let url = weatherMaps.host;
|
||||||
// url = 'https://cdn.rainviewer.com';
|
// url = 'https://cdn.rainviewer.com';
|
||||||
let latest = "";
|
let latest = '';
|
||||||
if (weatherMaps.radar.past.length > 0) {
|
if (weatherMaps.radar.past.length > 0) {
|
||||||
latest = weatherMaps.radar.past[weatherMaps.radar.past.length - 1].path;
|
latest = weatherMaps.radar.past[weatherMaps.radar.past.length - 1].path;
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
url += latest + "/256/{z}/{x}/{y}/2/1_1.png";
|
url += latest + '/256/{z}/{x}/{y}/2/1_1.png';
|
||||||
// url += latest + "/256/{z}/{x}/{y}/255/1_1_1_0.webp";
|
// url += latest + "/256/{z}/{x}/{y}/255/1_1_1_0.webp";
|
||||||
return url;
|
return url;
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,4 +18,4 @@ export interface SatelliteObject {
|
|||||||
export interface FrameObject {
|
export interface FrameObject {
|
||||||
time: number;
|
time: number;
|
||||||
path: string;
|
path: string;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user