Add a route to upload update to the platform
This commit is contained in:
		
							
								
								
									
										95
									
								
								central_backend/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										95
									
								
								central_backend/Cargo.lock
									
									
									
										generated
									
									
									
								
							@@ -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"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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"] }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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())
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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"];
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1 +1,2 @@
 | 
				
			|||||||
 | 
					pub mod ota_manager;
 | 
				
			||||||
pub mod ota_update;
 | 
					pub mod ota_update;
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										24
									
								
								central_backend/src/ota/ota_manager.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								central_backend/src/ota/ota_manager.rs
									
									
									
									
									
										Normal 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(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -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,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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."))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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"
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
		Reference in New Issue
	
	Block a user