diff --git a/Cargo.toml b/Cargo.toml index 6fb3bd8..1847443 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ chrono = { version = "0.4.31", features = ["serde"] } dotenvy = "0.15" env_logger = "0.11" log = "0.4.20" +semver = "1.0.25" serde = "1.0.188" sqlx = { version = "0.8", features = [ "runtime-tokio", "tls-rustls", "postgres", "migrate", "chrono", "mac_address"] } sqlx-cli = "0.8" diff --git a/src/firmware_api.rs b/src/firmware_api.rs index 2022328..f36467e 100644 --- a/src/firmware_api.rs +++ b/src/firmware_api.rs @@ -2,11 +2,12 @@ use std::{fs, path::PathBuf}; use actix_web::{get, put, web, HttpResponse, Responder}; use log::{debug, info, warn}; +use semver::Version; use std::str::FromStr; use crate::{ schemas::{AppState, OTAConfigurationList, Services}, - util::get_files, + util::{get_files, prune_files}, }; // Upload Firmware file @@ -17,7 +18,11 @@ async fn upload_firmware( body: web::Bytes, ) -> impl Responder { let (service, device, config, version) = path.into_inner(); - let version = version.replace('.', "-"); + let Ok(version) = Version::parse(&version) else { + return HttpResponse::InternalServerError().body("Failed to parse version"); + }; + debug!("{version:?}"); + let version = format!("{}-{}-{}", version.major, version.minor, version.patch); let firmware_root_path = &data.firmwares_path; let Ok(service) = Services::from_str(&service) else { @@ -43,7 +48,11 @@ async fn upload_firmware( let x = tokio::fs::write(&firmware_path, &body).await; debug!("{x:?}"); + debug!("pruning now"); + prune_files(firmware_folder, service, 3); + HttpResponse::Ok().body(format!("Firmware version {version} uploaded successfully")) + } #[get("/{service}/{device}")] diff --git a/src/schemas.rs b/src/schemas.rs index ee95776..8040be4 100644 --- a/src/schemas.rs +++ b/src/schemas.rs @@ -1,6 +1,7 @@ use std::path::PathBuf; use chrono::NaiveDateTime; +use semver::Version; use serde::{ser::SerializeStruct, Deserialize, Serialize}; use sqlx::types::mac_address::MacAddress; use strum::{Display, EnumString}; @@ -76,7 +77,7 @@ pub struct OTAConfigurationList { #[derive(serde::Serialize, PartialEq, Debug)] #[serde(rename_all = "PascalCase")] pub struct OTAConfiguration { - pub version: String, + pub version: Version, #[serde(rename = "URL")] pub url: String, pub board: Option, diff --git a/src/util.rs b/src/util.rs index dc60aa4..6ecf9d4 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,10 +1,11 @@ -use std::{fs, path::PathBuf}; +use std::{cmp::Reverse, fs, path::PathBuf}; -use log::{debug, info}; +use log::{error, info}; +use semver::Version; use std::str::FromStr; use thiserror::Error; -use crate::schemas::{BoardConfig, BoardType, OTAConfiguration}; +use crate::schemas::{BoardConfig, BoardType, OTAConfiguration, Services}; pub fn parse_mac_address(mac: &str) -> Result<[u8; 6], MacAddressError> { if mac.len() != 12 { @@ -38,6 +39,8 @@ pub enum GetFilesError { Extension, #[error("Strum parse Error")] Conversion(#[from] strum::ParseError), + #[error("Failed to parse the version")] + VersionParse(#[from] semver::Error), } pub fn get_files( @@ -73,11 +76,12 @@ pub fn get_files( let board_config = BoardConfig::from_str(split_name[1])?; let service = split_name[0]; let board_type = BoardType::from_str(&product_name).unwrap(); - let version_replaced = version.replace('.', "_"); + let version = Version::parse(&version)?; + let version_replaced = format!("{}-{}-{}", version.major, version.minor, version.patch); let fw_url = format!("{hostname}/{service}/{board_type}/{board_config}/{version_replaced}.bin"); let cfg = OTAConfiguration { - version: version.to_string(), + version: version, url: fw_url, board: Some(board_type), config: Some(board_config), @@ -91,6 +95,25 @@ pub fn get_files( Ok(configs) } +pub fn prune_files(path: PathBuf, service: Services, keep_last: usize) { + let Ok(mut config_list) = get_files(&path, "irrelevant") else { + error!("failed to get file list for pruning"); + return + }; + config_list.sort_by_key(|x| x.version.clone()); + config_list.truncate(config_list.len() - keep_last); + for cfg in config_list { + println!("{cfg:?}"); + let path = path.to_string_lossy(); + let board_type = cfg. config.unwrap(); + let path_to_remove = format!("{path}/{service}_{}_{}-{}-{}.bin", board_type, cfg.version.major, cfg.version.minor, cfg.version.patch); + if let Err(e) = fs::remove_file(&path_to_remove) { + error!("Failed to delete {path_to_remove}, {e:?}"); + return; + }; + } +} + #[cfg(test)] mod tests { use super::*; @@ -98,13 +121,13 @@ mod tests { #[test] fn test_file_loading() { let expected_1 = OTAConfiguration { - version: "1.2.3".to_string(), + version: Version::parse("1.2.3").unwrap(), url: "example.com/firmware/waterlevel/INA233/1_2_3.bin".to_string(), board: Some(BoardType::Waterlevel), config: Some(BoardConfig::INA233), }; let expected_2 = OTAConfiguration { - version: "4.5.6".to_string(), + version: Version::parse("4.5.6").unwrap(), url: "example.com/firmware/waterlevel/INA226/4_5_6.bin".to_string(), board: Some(BoardType::Waterlevel), config: Some(BoardConfig::INA226),