This commit is contained in:
43
src/main.rs
43
src/main.rs
@@ -16,7 +16,7 @@ mod util;
|
|||||||
struct AppState {
|
struct AppState {
|
||||||
db: Database,
|
db: Database,
|
||||||
firmwares_path: PathBuf,
|
firmwares_path: PathBuf,
|
||||||
hostname: String
|
hostname: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/telemetry/{device_id}")]
|
#[post("/telemetry/{device_id}")]
|
||||||
@@ -128,14 +128,20 @@ async fn get_devices(data: web::Data<AppState>) -> impl Responder {
|
|||||||
|
|
||||||
// Upload Firmware file
|
// Upload Firmware file
|
||||||
#[put("/firmware/{device}/{config}/{version}")]
|
#[put("/firmware/{device}/{config}/{version}")]
|
||||||
async fn upload_firmware(data: web::Data<AppState>, path: web::Path<(String, String, String)>, body: web::Bytes) -> impl Responder {
|
async fn upload_firmware(
|
||||||
|
data: web::Data<AppState>,
|
||||||
|
path: web::Path<(String, String, String)>,
|
||||||
|
body: web::Bytes,
|
||||||
|
) -> impl Responder {
|
||||||
let (device, config, version) = path.into_inner();
|
let (device, config, version) = path.into_inner();
|
||||||
let version = version.replace(".", "-");
|
let version = version.replace(".", "-");
|
||||||
|
|
||||||
let firmware_root_path = &data.firmwares_path;
|
let firmware_root_path = &data.firmwares_path;
|
||||||
|
|
||||||
let firmware_folder = firmware_root_path.join(&device);
|
let firmware_folder = firmware_root_path.join(&device);
|
||||||
let firmware_path = firmware_folder.join(format!("firmware_{config}_{version}")).with_extension("bin");
|
let firmware_path = firmware_folder
|
||||||
|
.join(format!("firmware_{config}_{version}"))
|
||||||
|
.with_extension("bin");
|
||||||
|
|
||||||
if firmware_path.is_file() {
|
if firmware_path.is_file() {
|
||||||
warn!("Firmware with product: {device}, config: {config} and version: {version} at path {firmware_path:?} already exists, cant upload");
|
warn!("Firmware with product: {device}, config: {config} and version: {version} at path {firmware_path:?} already exists, cant upload");
|
||||||
@@ -143,12 +149,15 @@ async fn upload_firmware(data: web::Data<AppState>, path: web::Path<(String, Str
|
|||||||
}
|
}
|
||||||
|
|
||||||
info!("Uploading firmware with product: {device}, config: {config} and version: {version} to {firmware_path:?}");
|
info!("Uploading firmware with product: {device}, config: {config} and version: {version} to {firmware_path:?}");
|
||||||
|
|
||||||
fs::create_dir_all(&firmware_folder).unwrap();
|
fs::create_dir_all(&firmware_folder).unwrap();
|
||||||
let x = tokio::fs::write(&firmware_path, &body).await;
|
let x = tokio::fs::write(&firmware_path, &body).await;
|
||||||
debug!("{x:?}");
|
debug!("{x:?}");
|
||||||
|
|
||||||
HttpResponse::Ok().body(format!("Firmware version {} uploaded successfully", version))
|
HttpResponse::Ok().body(format!(
|
||||||
|
"Firmware version {} uploaded successfully",
|
||||||
|
version
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/firmware/{device}")]
|
#[get("/firmware/{device}")]
|
||||||
@@ -157,18 +166,23 @@ async fn get_firmware_json(data: web::Data<AppState>, path: web::Path<String>) -
|
|||||||
let fw_path = &data.firmwares_path.join(device);
|
let fw_path = &data.firmwares_path.join(device);
|
||||||
if fw_path.is_dir() {
|
if fw_path.is_dir() {
|
||||||
match get_files(fw_path, &data.hostname) {
|
match get_files(fw_path, &data.hostname) {
|
||||||
Ok(cfg) => HttpResponse::Ok().json(OTAConfigurationList{configurations: cfg} ),
|
Ok(cfg) => HttpResponse::Ok().json(OTAConfigurationList {
|
||||||
Err(e) => HttpResponse::InternalServerError().body(e.to_string())
|
configurations: cfg,
|
||||||
|
}),
|
||||||
|
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
HttpResponse::Ok().json(OTAConfigurationList{configurations: vec![]} )
|
HttpResponse::Ok().json(OTAConfigurationList {
|
||||||
|
configurations: vec![],
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[get("/firmware/{product}/{config}/{version}.bin")]
|
#[get("/firmware/{product}/{config}/{version}.bin")]
|
||||||
async fn serve_firmware(path: web::Path<(String, String, String)>, data: web::Data<AppState>) -> impl Responder {
|
async fn serve_firmware(
|
||||||
|
path: web::Path<(String, String, String)>,
|
||||||
|
data: web::Data<AppState>,
|
||||||
|
) -> impl Responder {
|
||||||
let (product, config, version) = path.into_inner();
|
let (product, config, version) = path.into_inner();
|
||||||
let version = version.replace(".", "-").replace("_", "-");
|
let version = version.replace(".", "-").replace("_", "-");
|
||||||
let fw_root_path = &data.firmwares_path;
|
let fw_root_path = &data.firmwares_path;
|
||||||
@@ -198,7 +212,6 @@ async fn main() -> std::io::Result<()> {
|
|||||||
env_logger::init();
|
env_logger::init();
|
||||||
info!("Starting");
|
info!("Starting");
|
||||||
|
|
||||||
|
|
||||||
let db_url = match env::var("DATABASE_URL") {
|
let db_url = match env::var("DATABASE_URL") {
|
||||||
Ok(url) => url,
|
Ok(url) => url,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -213,7 +226,11 @@ async fn main() -> std::io::Result<()> {
|
|||||||
|
|
||||||
HttpServer::new(move || {
|
HttpServer::new(move || {
|
||||||
App::new()
|
App::new()
|
||||||
.app_data(web::Data::new(AppState { db: db.clone(), firmwares_path: PathBuf::from("./fw"), hostname: "127.0.0.1:8282".to_string() }))
|
.app_data(web::Data::new(AppState {
|
||||||
|
db: db.clone(),
|
||||||
|
firmwares_path: PathBuf::from("./fw"),
|
||||||
|
hostname: "127.0.0.1:8282".to_string(),
|
||||||
|
}))
|
||||||
.app_data(web::PayloadConfig::new(256 * 1024 * 1024)) //256MB
|
.app_data(web::PayloadConfig::new(256 * 1024 * 1024)) //256MB
|
||||||
.service(receive_telemetry)
|
.service(receive_telemetry)
|
||||||
.service(get_telemetry)
|
.service(get_telemetry)
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ impl Serialize for Device {
|
|||||||
#[derive(serde::Serialize)]
|
#[derive(serde::Serialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
#[serde(rename_all = "PascalCase")]
|
||||||
pub struct OTAConfigurationList {
|
pub struct OTAConfigurationList {
|
||||||
pub configurations: Vec<OTAConfiguration>
|
pub configurations: Vec<OTAConfiguration>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Serialize, PartialEq, Debug)]
|
#[derive(serde::Serialize, PartialEq, Debug)]
|
||||||
@@ -75,18 +75,17 @@ pub struct OTAConfiguration {
|
|||||||
pub version: String,
|
pub version: String,
|
||||||
pub url: String,
|
pub url: String,
|
||||||
pub board: Option<BoardType>,
|
pub board: Option<BoardType>,
|
||||||
pub config: Option<BoardConfig>
|
pub config: Option<BoardConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[derive(serde::Serialize, EnumString, PartialEq, Debug, Display)]
|
#[derive(serde::Serialize, EnumString, PartialEq, Debug, Display)]
|
||||||
#[strum(serialize_all = "snake_case")]
|
#[strum(serialize_all = "snake_case")]
|
||||||
pub enum BoardType {
|
pub enum BoardType {
|
||||||
Waterlevel
|
Waterlevel,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Serialize, EnumString, PartialEq, Debug, Display)]
|
#[derive(serde::Serialize, EnumString, PartialEq, Debug, Display)]
|
||||||
pub enum BoardConfig {
|
pub enum BoardConfig {
|
||||||
INA226,
|
INA226,
|
||||||
INA233
|
INA233,
|
||||||
}
|
}
|
||||||
|
|||||||
59
src/util.rs
59
src/util.rs
@@ -1,14 +1,11 @@
|
|||||||
use std::{fs, path::PathBuf};
|
use std::{fs, path::PathBuf};
|
||||||
|
|
||||||
use log::info;
|
use log::info;
|
||||||
use thiserror::Error;
|
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::schemas::{BoardConfig, BoardType, OTAConfiguration};
|
use crate::schemas::{BoardConfig, BoardType, OTAConfiguration};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
pub fn parse_mac_address(mac: &str) -> Result<[u8; 6], MacAddressError> {
|
pub fn parse_mac_address(mac: &str) -> Result<[u8; 6], MacAddressError> {
|
||||||
if mac.len() != 12 {
|
if mac.len() != 12 {
|
||||||
return Err(MacAddressError::Length(mac.len()));
|
return Err(MacAddressError::Length(mac.len()));
|
||||||
@@ -40,13 +37,19 @@ pub enum GetFilesError {
|
|||||||
#[error("Error getting extension (.bin)")]
|
#[error("Error getting extension (.bin)")]
|
||||||
Extension,
|
Extension,
|
||||||
#[error("Strum parse Error")]
|
#[error("Strum parse Error")]
|
||||||
Conversion(#[from] strum::ParseError)
|
Conversion(#[from] strum::ParseError),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_files(root_path: &PathBuf, hostname: &str) -> Result<Vec<OTAConfiguration>, GetFilesError>{
|
pub fn get_files(
|
||||||
|
root_path: &PathBuf,
|
||||||
|
hostname: &str,
|
||||||
|
) -> Result<Vec<OTAConfiguration>, GetFilesError> {
|
||||||
info!("Getting all files from path {root_path:?}");
|
info!("Getting all files from path {root_path:?}");
|
||||||
let mut configs = Vec::new();
|
let mut configs = Vec::new();
|
||||||
let product_name = root_path.file_name().ok_or(GetFilesError::Filename)?.to_string_lossy();
|
let product_name = root_path
|
||||||
|
.file_name()
|
||||||
|
.ok_or(GetFilesError::Filename)?
|
||||||
|
.to_string_lossy();
|
||||||
let entries = fs::read_dir(root_path)?;
|
let entries = fs::read_dir(root_path)?;
|
||||||
|
|
||||||
for entry in entries.flatten() {
|
for entry in entries.flatten() {
|
||||||
@@ -55,13 +58,28 @@ pub fn get_files(root_path: &PathBuf, hostname: &str) -> Result<Vec<OTAConfigura
|
|||||||
if path.is_file() {
|
if path.is_file() {
|
||||||
info!("processing file: {:?}", path);
|
info!("processing file: {:?}", path);
|
||||||
// Splits the filename at the underscores. This is safe to do as names get sanitized on upload and are only uploaded by the pipeline
|
// Splits the filename at the underscores. This is safe to do as names get sanitized on upload and are only uploaded by the pipeline
|
||||||
let split_name: Vec<_> = path.file_name().ok_or(GetFilesError::Filename)?.to_str().ok_or(GetFilesError::Filename)?.split("_").collect();
|
let split_name: Vec<_> = path
|
||||||
let version = split_name[2].strip_suffix(".bin").ok_or(GetFilesError::Extension)?.replace("-", ".");
|
.file_name()
|
||||||
|
.ok_or(GetFilesError::Filename)?
|
||||||
|
.to_str()
|
||||||
|
.ok_or(GetFilesError::Filename)?
|
||||||
|
.split("_")
|
||||||
|
.collect();
|
||||||
|
let version = split_name[2]
|
||||||
|
.strip_suffix(".bin")
|
||||||
|
.ok_or(GetFilesError::Extension)?
|
||||||
|
.replace("-", ".");
|
||||||
let board_config = BoardConfig::from_str(split_name[1])?;
|
let board_config = BoardConfig::from_str(split_name[1])?;
|
||||||
let board_type = BoardType::from_str(&product_name).unwrap();
|
let board_type = BoardType::from_str(&product_name).unwrap();
|
||||||
let version_replaced = version.replace(".", "_");
|
let version_replaced = version.replace(".", "_");
|
||||||
let fw_url = format!("{hostname}/firmware/{board_type}/{board_config}/{version_replaced}.bin");
|
let fw_url =
|
||||||
let cfg = OTAConfiguration{version: version.to_string(), url: fw_url, board: Some(board_type), config: Some(board_config) };
|
format!("{hostname}/firmware/{board_type}/{board_config}/{version_replaced}.bin");
|
||||||
|
let cfg = OTAConfiguration {
|
||||||
|
version: version.to_string(),
|
||||||
|
url: fw_url,
|
||||||
|
board: Some(board_type),
|
||||||
|
config: Some(board_config),
|
||||||
|
};
|
||||||
configs.push(cfg);
|
configs.push(cfg);
|
||||||
} else if path.is_dir() {
|
} else if path.is_dir() {
|
||||||
println!("Directory: {:?}", path);
|
println!("Directory: {:?}", path);
|
||||||
@@ -69,21 +87,28 @@ pub fn get_files(root_path: &PathBuf, hostname: &str) -> Result<Vec<OTAConfigura
|
|||||||
}
|
}
|
||||||
|
|
||||||
Ok(configs)
|
Ok(configs)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_file_loading() {
|
fn test_file_loading() {
|
||||||
let expected_1 = OTAConfiguration{ version: "1.2.3".to_string(), url: "example.com/firmware/waterlevel/INA233/1_2_3.bin".to_string(), board: Some(BoardType::Waterlevel), config: Some(BoardConfig::INA233) };
|
let expected_1 = OTAConfiguration {
|
||||||
let expected_2 = OTAConfiguration{ version: "4.5.6".to_string(), url: "example.com/firmware/waterlevel/INA226/4_5_6.bin".to_string(), board: Some(BoardType::Waterlevel), config: Some(BoardConfig::INA226) };
|
version: "1.2.3".to_string(),
|
||||||
|
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(),
|
||||||
|
url: "example.com/firmware/waterlevel/INA226/4_5_6.bin".to_string(),
|
||||||
|
board: Some(BoardType::Waterlevel),
|
||||||
|
config: Some(BoardConfig::INA226),
|
||||||
|
};
|
||||||
let loaded_configs = get_files(&PathBuf::from("./test/waterlevel"), "example.com").unwrap();
|
let loaded_configs = get_files(&PathBuf::from("./test/waterlevel"), "example.com").unwrap();
|
||||||
|
|
||||||
assert_eq!(loaded_configs[1], expected_1);
|
assert_eq!(loaded_configs[1], expected_1);
|
||||||
assert_eq!(loaded_configs[0], expected_2);
|
assert_eq!(loaded_configs[0], expected_2);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user