Add a route to upload update to the platform

This commit is contained in:
Pierre HUBERT 2024-10-05 15:50:46 +02:00
parent e1a94acdcb
commit 2f971c0055
10 changed files with 211 additions and 5 deletions

View File

@ -125,6 +125,44 @@ dependencies = [
"syn", "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]] [[package]]
name = "actix-remote-ip" name = "actix-remote-ip"
version = "0.1.0" version = "0.1.0"
@ -622,6 +660,7 @@ dependencies = [
"actix", "actix",
"actix-cors", "actix-cors",
"actix-identity", "actix-identity",
"actix-multipart",
"actix-remote-ip", "actix-remote-ip",
"actix-session", "actix-session",
"actix-web", "actix-web",
@ -847,6 +886,41 @@ dependencies = [
"cipher", "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]] [[package]]
name = "deranged" name = "deranged"
version = "0.3.11" version = "0.3.11"
@ -1399,6 +1473,12 @@ dependencies = [
"cc", "cc",
] ]
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]] [[package]]
name = "idna" name = "idna"
version = "0.5.0" version = "0.5.0"
@ -1779,6 +1859,12 @@ dependencies = [
"windows-targets", "windows-targets",
] ]
[[package]]
name = "parse-size"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "487f2ccd1e17ce8c1bfab3a65c89525af41cfad4c8659021a1e9a2aacd73b89b"
[[package]] [[package]]
name = "paste" name = "paste"
version = "1.0.15" version = "1.0.15"
@ -2243,6 +2329,15 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "serde_plain"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ce1fc6db65a611022b23a0dec6975d63fb80a302cb3388835ff02c097258d50"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "serde_urlencoded" name = "serde_urlencoded"
version = "0.7.1" version = "0.7.1"

View File

@ -25,6 +25,7 @@ actix = "0.13.5"
actix-identity = "0.8.0" actix-identity = "0.8.0"
actix-session = { version = "0.10.1", features = ["cookie-session"] } actix-session = { version = "0.10.1", features = ["cookie-session"] }
actix-cors = "0.7.0" actix-cors = "0.7.0"
actix-multipart = { version ="0.7.2", features = ["derive"] }
actix-remote-ip = "0.1.0" actix-remote-ip = "0.1.0"
futures-util = "0.3.30" futures-util = "0.3.30"
uuid = { version = "1.10.0", features = ["v4", "serde"] } uuid = { version = "1.10.0", features = ["v4", "serde"] }

View File

@ -1,4 +1,5 @@
use crate::devices::device::{DeviceId, DeviceRelayID}; use crate::devices::device::{DeviceId, DeviceRelayID};
use crate::ota::ota_update::OTAPlatform;
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@ -296,12 +297,14 @@ impl AppConfig {
/// Get the directory that will store OTA updates /// Get the directory that will store OTA updates
pub fn ota_dir(&self) -> PathBuf { 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 /// Get the path to the file that will contain an OTA update
pub fn ota_of_device(&self, dev_ref: &str) -> PathBuf { pub fn path_ota_update(&self, platform: OTAPlatform, version: &semver::Version) -> PathBuf {
self.ota_dir().join(dev_ref) self.ota_dir()
.join(platform.to_string())
.join(version.to_string())
} }
} }

View File

@ -13,6 +13,9 @@ pub const MAX_INACTIVITY_DURATION: u64 = 3600;
/// Maximum session duration (1 day) /// Maximum session duration (1 day)
pub const MAX_SESSION_DURATION: u64 = 3600 * 24; 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 /// List of routes that do not require authentication
pub const ROUTES_WITHOUT_AUTH: [&str; 2] = pub const ROUTES_WITHOUT_AUTH: [&str; 2] =
["/web_api/server/config", "/web_api/auth/password_auth"]; ["/web_api/server/config", "/web_api/auth/password_auth"];

View File

@ -1 +1,2 @@
pub mod ota_manager;
pub mod ota_update; pub mod ota_update;

View File

@ -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<bool> {
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(())
}

View File

@ -1,5 +1,22 @@
use std::fmt::{Display, Formatter};
#[derive(serde::Serialize, serde::Deserialize, Debug, Copy, Clone, Eq, PartialEq)] #[derive(serde::Serialize, serde::Deserialize, Debug, Copy, Clone, Eq, PartialEq)]
pub enum OTAPlatform { pub enum OTAPlatform {
#[serde(rename = "Wt32-Eth01")] #[serde(rename = "Wt32-Eth01")]
Wt32Eth01, 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,
}

View File

@ -185,6 +185,10 @@ pub async fn secure_server(energy_actor: EnergyActorAddr) -> anyhow::Result<()>
"/web_api/ota/supported_platforms", "/web_api/ota/supported_platforms",
web::get().to(ota_controller::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 : upload a new software update
// TODO : list ota software update per platform // TODO : list ota software update per platform
// TODO : download a OTA file // TODO : download a OTA file

View File

@ -1,7 +1,51 @@
use crate::constants;
use crate::ota::ota_manager;
use crate::ota::ota_update::OTAPlatform; use crate::ota::ota_update::OTAPlatform;
use crate::server::custom_error::HttpResult; 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 { pub async fn supported_platforms() -> HttpResult {
Ok(HttpResponse::Ok().json(vec![OTAPlatform::Wt32Eth01])) Ok(HttpResponse::Ok().json(vec![OTAPlatform::Wt32Eth01]))
} }
#[derive(Debug, MultipartForm)]
pub struct UploadForm {
#[multipart(rename = "firmware")]
firmware: Vec<TempFile>,
}
#[derive(serde::Deserialize)]
pub struct UploadPath {
platform: OTAPlatform,
version: semver::Version,
}
pub async fn upload_firmware(
MultipartForm(form): MultipartForm<UploadForm>,
path: web::Path<UploadPath>,
) -> 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."))
}

View File

@ -1,3 +1,17 @@
# ESP32 device # ESP32 device
ESP32 client device, using `W32-ETH01` 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"
```