From 2f971c0055010b6470a98d17befefbaa146340ee Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Sat, 5 Oct 2024 15:50:46 +0200 Subject: [PATCH] Add a route to upload update to the platform --- central_backend/Cargo.lock | 95 +++++++++++++++++++ central_backend/Cargo.toml | 1 + central_backend/src/app_config.rs | 11 ++- central_backend/src/constants.rs | 3 + central_backend/src/ota/mod.rs | 1 + central_backend/src/ota/ota_manager.rs | 24 +++++ central_backend/src/ota/ota_update.rs | 17 ++++ central_backend/src/server/servers.rs | 4 + .../src/server/web_api/ota_controller.rs | 46 ++++++++- esp32_device/README.md | 14 +++ 10 files changed, 211 insertions(+), 5 deletions(-) create mode 100644 central_backend/src/ota/ota_manager.rs diff --git a/central_backend/Cargo.lock b/central_backend/Cargo.lock index 77b2c81..8b78cd3 100644 --- a/central_backend/Cargo.lock +++ b/central_backend/Cargo.lock @@ -125,6 +125,44 @@ dependencies = [ "syn", ] +[[package]] +name = "actix-multipart" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5118a26dee7e34e894f7e85aa0ee5080ae4c18bf03c0e30d49a80e418f00a53" +dependencies = [ + "actix-multipart-derive", + "actix-utils", + "actix-web", + "derive_more 0.99.18", + "futures-core", + "futures-util", + "httparse", + "local-waker", + "log", + "memchr", + "mime", + "rand", + "serde", + "serde_json", + "serde_plain", + "tempfile", + "tokio", +] + +[[package]] +name = "actix-multipart-derive" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e11eb847f49a700678ea2fa73daeb3208061afa2b9d1a8527c03390f4c4a1c6b" +dependencies = [ + "darling", + "parse-size", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "actix-remote-ip" version = "0.1.0" @@ -622,6 +660,7 @@ dependencies = [ "actix", "actix-cors", "actix-identity", + "actix-multipart", "actix-remote-ip", "actix-session", "actix-web", @@ -847,6 +886,41 @@ dependencies = [ "cipher", ] +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core", + "quote", + "syn", +] + [[package]] name = "deranged" version = "0.3.11" @@ -1399,6 +1473,12 @@ dependencies = [ "cc", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.5.0" @@ -1779,6 +1859,12 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "parse-size" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "487f2ccd1e17ce8c1bfab3a65c89525af41cfad4c8659021a1e9a2aacd73b89b" + [[package]] name = "paste" version = "1.0.15" @@ -2243,6 +2329,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_plain" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1fc6db65a611022b23a0dec6975d63fb80a302cb3388835ff02c097258d50" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" diff --git a/central_backend/Cargo.toml b/central_backend/Cargo.toml index b79a99a..fc08e82 100644 --- a/central_backend/Cargo.toml +++ b/central_backend/Cargo.toml @@ -25,6 +25,7 @@ actix = "0.13.5" actix-identity = "0.8.0" actix-session = { version = "0.10.1", features = ["cookie-session"] } actix-cors = "0.7.0" +actix-multipart = { version ="0.7.2", features = ["derive"] } actix-remote-ip = "0.1.0" futures-util = "0.3.30" uuid = { version = "1.10.0", features = ["v4", "serde"] } diff --git a/central_backend/src/app_config.rs b/central_backend/src/app_config.rs index b2a21e8..50b421a 100644 --- a/central_backend/src/app_config.rs +++ b/central_backend/src/app_config.rs @@ -1,4 +1,5 @@ use crate::devices::device::{DeviceId, DeviceRelayID}; +use crate::ota::ota_update::OTAPlatform; use clap::{Parser, Subcommand}; use std::path::{Path, PathBuf}; @@ -296,12 +297,14 @@ impl AppConfig { /// Get the directory that will store OTA updates pub fn ota_dir(&self) -> PathBuf { - self.logs_dir().join("ota") + self.storage_path().join("ota") } - /// Get the directory that will store OTA updates of a given device reference - pub fn ota_of_device(&self, dev_ref: &str) -> PathBuf { - self.ota_dir().join(dev_ref) + /// Get the path to the file that will contain an OTA update + pub fn path_ota_update(&self, platform: OTAPlatform, version: &semver::Version) -> PathBuf { + self.ota_dir() + .join(platform.to_string()) + .join(version.to_string()) } } diff --git a/central_backend/src/constants.rs b/central_backend/src/constants.rs index a1a1c0c..e2bffe1 100644 --- a/central_backend/src/constants.rs +++ b/central_backend/src/constants.rs @@ -13,6 +13,9 @@ pub const MAX_INACTIVITY_DURATION: u64 = 3600; /// Maximum session duration (1 day) pub const MAX_SESSION_DURATION: u64 = 3600 * 24; +/// Maximum firmware size (in bytes) +pub const MAX_FIRMWARE_SIZE: usize = 50 * 1000 * 1000; + /// List of routes that do not require authentication pub const ROUTES_WITHOUT_AUTH: [&str; 2] = ["/web_api/server/config", "/web_api/auth/password_auth"]; diff --git a/central_backend/src/ota/mod.rs b/central_backend/src/ota/mod.rs index ea34968..fd51b4b 100644 --- a/central_backend/src/ota/mod.rs +++ b/central_backend/src/ota/mod.rs @@ -1 +1,2 @@ +pub mod ota_manager; pub mod ota_update; diff --git a/central_backend/src/ota/ota_manager.rs b/central_backend/src/ota/ota_manager.rs new file mode 100644 index 0000000..f954631 --- /dev/null +++ b/central_backend/src/ota/ota_manager.rs @@ -0,0 +1,24 @@ +use crate::app_config::AppConfig; +use crate::ota::ota_update::OTAPlatform; +use crate::utils::files_utils; + +/// Check out whether a given update exists or not +pub fn update_exists(platform: OTAPlatform, version: &semver::Version) -> anyhow::Result { + Ok(AppConfig::get() + .path_ota_update(platform, version) + .is_file()) +} + +/// Save a new firmware update +pub fn save_update( + platform: OTAPlatform, + version: &semver::Version, + update: &[u8], +) -> anyhow::Result<()> { + let path = AppConfig::get().path_ota_update(platform, version); + files_utils::create_directory_if_missing(path.parent().unwrap())?; + + std::fs::write(path, update)?; + + Ok(()) +} diff --git a/central_backend/src/ota/ota_update.rs b/central_backend/src/ota/ota_update.rs index 7c993ea..23f3254 100644 --- a/central_backend/src/ota/ota_update.rs +++ b/central_backend/src/ota/ota_update.rs @@ -1,5 +1,22 @@ +use std::fmt::{Display, Formatter}; + #[derive(serde::Serialize, serde::Deserialize, Debug, Copy, Clone, Eq, PartialEq)] pub enum OTAPlatform { #[serde(rename = "Wt32-Eth01")] Wt32Eth01, } + +impl Display for OTAPlatform { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let s = serde_json::to_string(&self).unwrap().replace('"', ""); + write!(f, "{s}") + } +} + +/// Single OTA update information +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Eq, PartialEq)] +pub struct OTAUpdate { + platform: OTAPlatform, + version: semver::Version, + file_size: usize, +} diff --git a/central_backend/src/server/servers.rs b/central_backend/src/server/servers.rs index 9fb7421..95d973c 100644 --- a/central_backend/src/server/servers.rs +++ b/central_backend/src/server/servers.rs @@ -185,6 +185,10 @@ pub async fn secure_server(energy_actor: EnergyActorAddr) -> anyhow::Result<()> "/web_api/ota/supported_platforms", web::get().to(ota_controller::supported_platforms), ) + .route( + "/web_api/ota/{platform}/{version}", + web::post().to(ota_controller::upload_firmware), + ) // TODO : upload a new software update // TODO : list ota software update per platform // TODO : download a OTA file diff --git a/central_backend/src/server/web_api/ota_controller.rs b/central_backend/src/server/web_api/ota_controller.rs index 9329ad3..89fde65 100644 --- a/central_backend/src/server/web_api/ota_controller.rs +++ b/central_backend/src/server/web_api/ota_controller.rs @@ -1,7 +1,51 @@ +use crate::constants; +use crate::ota::ota_manager; use crate::ota::ota_update::OTAPlatform; use crate::server::custom_error::HttpResult; -use actix_web::HttpResponse; +use actix_multipart::form::tempfile::TempFile; +use actix_multipart::form::MultipartForm; +use actix_web::{web, HttpResponse}; pub async fn supported_platforms() -> HttpResult { Ok(HttpResponse::Ok().json(vec![OTAPlatform::Wt32Eth01])) } + +#[derive(Debug, MultipartForm)] +pub struct UploadForm { + #[multipart(rename = "firmware")] + firmware: Vec, +} + +#[derive(serde::Deserialize)] +pub struct UploadPath { + platform: OTAPlatform, + version: semver::Version, +} + +pub async fn upload_firmware( + MultipartForm(form): MultipartForm, + path: web::Path, +) -> HttpResult { + if ota_manager::update_exists(path.platform, &path.version)? { + return Ok(HttpResponse::Conflict() + .json("A firmware with the same version has already been uploaded on the platform!")); + } + + let Some(file) = form.firmware.first() else { + return Ok(HttpResponse::BadRequest().json("No firmware specified!")); + }; + + if file.size == 0 { + return Ok(HttpResponse::BadRequest().json("Uploaded file is empty!")); + } + + if file.size > constants::MAX_FIRMWARE_SIZE { + return Ok(HttpResponse::BadRequest().json("Uploaded file is too heavy!")); + } + + let content = std::fs::read(file.file.path())?; + + ota_manager::save_update(path.platform, &path.version, &content)?; + + Ok(HttpResponse::Accepted().body("OTA update successfully saved.")) +} diff --git a/esp32_device/README.md b/esp32_device/README.md index 26f5a67..63e950c 100755 --- a/esp32_device/README.md +++ b/esp32_device/README.md @@ -1,3 +1,17 @@ # ESP32 device ESP32 client device, using `W32-ETH01` device + +## Some commands + +Create a new firmware build: + +```bash +idf.py build +``` + +Upload firmware to central backend, in dev mode: + +```bash +curl -k -X POST https://localhost:8443/web_api/ota/Wt32-Eth01/$(cat version.txt) --form firmware="@build/main.bin" +``` \ No newline at end of file