remove: eliminate file management and LittleFS-based filesystem, implement savegame management for JSON config slots with wear-leveling
This commit is contained in:
@@ -1,160 +0,0 @@
|
||||
use crate::fat_error::{FatError, FatResult};
|
||||
use crate::webserver::read_up_to_bytes_from_request;
|
||||
use crate::BOARD_ACCESS;
|
||||
use alloc::borrow::ToOwned;
|
||||
use alloc::format;
|
||||
use alloc::string::String;
|
||||
use edge_http::io::server::Connection;
|
||||
use edge_http::Method;
|
||||
use edge_nal::io::{Read, Write};
|
||||
use log::info;
|
||||
|
||||
pub(crate) async fn list_files<T, const N: usize>(
|
||||
_request: &mut Connection<'_, T, N>,
|
||||
) -> FatResult<Option<String>> {
|
||||
let mut board = BOARD_ACCESS.get().await.lock().await;
|
||||
let result = board.board_hal.get_esp().list_files().await?;
|
||||
let file_list_json = serde_json::to_string(&result)?;
|
||||
Ok(Some(file_list_json))
|
||||
}
|
||||
pub(crate) async fn file_operations<T, const N: usize>(
|
||||
conn: &mut Connection<'_, T, { N }>,
|
||||
method: Method,
|
||||
path: &&str,
|
||||
prefix: &&str,
|
||||
) -> Result<Option<u32>, FatError>
|
||||
where
|
||||
T: Read + Write,
|
||||
{
|
||||
let filename = &path[prefix.len()..];
|
||||
info!("file request for {filename} with method {method}");
|
||||
Ok(match method {
|
||||
Method::Delete => {
|
||||
let mut board = BOARD_ACCESS.get().await.lock().await;
|
||||
board
|
||||
.board_hal
|
||||
.get_esp()
|
||||
.delete_file(filename.to_owned())
|
||||
.await?;
|
||||
conn.initiate_response(
|
||||
200,
|
||||
Some("OK"),
|
||||
&[
|
||||
("Access-Control-Allow-Origin", "*"),
|
||||
("Access-Control-Allow-Headers", "*"),
|
||||
("Access-Control-Allow-Methods", "*"),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
Some(200)
|
||||
}
|
||||
Method::Get => {
|
||||
let disposition = format!("attachment; filename=\"{filename}\"");
|
||||
let size = {
|
||||
let mut board = BOARD_ACCESS.get().await.lock().await;
|
||||
board
|
||||
.board_hal
|
||||
.get_esp()
|
||||
.get_size(filename.to_owned())
|
||||
.await?
|
||||
};
|
||||
|
||||
conn.initiate_response(
|
||||
200,
|
||||
Some("OK"),
|
||||
&[
|
||||
("Content-Type", "application/octet-stream"),
|
||||
("Content-Disposition", disposition.as_str()),
|
||||
("Content-Length", &format!("{size}")),
|
||||
("Access-Control-Allow-Origin", "*"),
|
||||
("Access-Control-Allow-Headers", "*"),
|
||||
("Access-Control-Allow-Methods", "*"),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut chunk = 0;
|
||||
loop {
|
||||
let mut board = BOARD_ACCESS.get().await.lock().await;
|
||||
board.board_hal.progress(chunk).await;
|
||||
let read_chunk = board
|
||||
.board_hal
|
||||
.get_esp()
|
||||
.get_file(filename.to_owned(), chunk)
|
||||
.await?;
|
||||
let length = read_chunk.1;
|
||||
if length == 0 {
|
||||
info!("file request for {filename} finished");
|
||||
break;
|
||||
}
|
||||
let data = &read_chunk.0[0..length];
|
||||
conn.write_all(data).await?;
|
||||
if length < read_chunk.0.len() {
|
||||
info!("file request for {filename} finished");
|
||||
break;
|
||||
}
|
||||
chunk += 1;
|
||||
}
|
||||
BOARD_ACCESS
|
||||
.get()
|
||||
.await
|
||||
.lock()
|
||||
.await
|
||||
.board_hal
|
||||
.clear_progress()
|
||||
.await;
|
||||
Some(200)
|
||||
}
|
||||
Method::Post => {
|
||||
{
|
||||
let mut board = BOARD_ACCESS.get().await.lock().await;
|
||||
//ensure the file is deleted first; otherwise we would need to truncate the file which will not work with streaming
|
||||
let _ = board
|
||||
.board_hal
|
||||
.get_esp()
|
||||
.delete_file(filename.to_owned())
|
||||
.await;
|
||||
}
|
||||
|
||||
let mut offset = 0_usize;
|
||||
let mut chunk = 0;
|
||||
loop {
|
||||
let buf = read_up_to_bytes_from_request(conn, Some(4096)).await?;
|
||||
if buf.is_empty() {
|
||||
info!("file request for {filename} finished");
|
||||
break;
|
||||
} else {
|
||||
let mut board = BOARD_ACCESS.get().await.lock().await;
|
||||
board.board_hal.progress(chunk as u32).await;
|
||||
board
|
||||
.board_hal
|
||||
.get_esp()
|
||||
.write_file(filename.to_owned(), offset as u32, &buf)
|
||||
.await?;
|
||||
}
|
||||
offset += buf.len();
|
||||
chunk += 1;
|
||||
}
|
||||
BOARD_ACCESS
|
||||
.get()
|
||||
.await
|
||||
.lock()
|
||||
.await
|
||||
.board_hal
|
||||
.clear_progress()
|
||||
.await;
|
||||
conn.initiate_response(
|
||||
200,
|
||||
Some("OK"),
|
||||
&[
|
||||
("Access-Control-Allow-Origin", "*"),
|
||||
("Access-Control-Allow-Headers", "*"),
|
||||
("Access-Control-Allow-Methods", "*"),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
Some(200)
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
@@ -114,12 +114,39 @@ pub(crate) async fn get_version_web<T, const N: usize>(
|
||||
Ok(Some(serde_json::to_string(&get_version(&mut board).await)?))
|
||||
}
|
||||
|
||||
/// Return the current in-memory config, or — when `saveidx` is `Some(idx)` —
|
||||
/// the JSON stored in save slot `idx`.
|
||||
pub(crate) async fn get_config<T, const N: usize>(
|
||||
_request: &mut Connection<'_, T, N>,
|
||||
saveidx: Option<usize>,
|
||||
) -> FatResult<Option<String>> {
|
||||
let mut board = BOARD_ACCESS.get().await.lock().await;
|
||||
let json = match saveidx {
|
||||
None => serde_json::to_string(board.board_hal.get_config())?,
|
||||
Some(idx) => {
|
||||
board.board_hal.get_esp().load_config_slot(idx).await?
|
||||
}
|
||||
};
|
||||
Ok(Some(json))
|
||||
}
|
||||
|
||||
/// Return a JSON array describing every valid save slot on flash.
|
||||
pub(crate) async fn list_saves<T, const N: usize>(
|
||||
_request: &mut Connection<'_, T, N>,
|
||||
) -> FatResult<Option<String>> {
|
||||
let mut board = BOARD_ACCESS.get().await.lock().await;
|
||||
let json = serde_json::to_string(&board.board_hal.get_config())?;
|
||||
Ok(Some(json))
|
||||
let saves = board.board_hal.get_esp().list_saves().await?;
|
||||
Ok(Some(serde_json::to_string(&saves)?))
|
||||
}
|
||||
|
||||
/// Erase (delete) a single save slot by index.
|
||||
pub(crate) async fn delete_save<T, const N: usize>(
|
||||
_request: &mut Connection<'_, T, N>,
|
||||
idx: usize,
|
||||
) -> FatResult<Option<String>> {
|
||||
let mut board = BOARD_ACCESS.get().await.lock().await;
|
||||
board.board_hal.get_esp().delete_save_slot(idx).await?;
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub(crate) async fn get_battery_state<T, const N: usize>(
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
//offer ota and config mode
|
||||
|
||||
mod backup_manager;
|
||||
mod file_manager;
|
||||
mod get_json;
|
||||
mod get_log;
|
||||
mod get_static;
|
||||
@@ -10,10 +9,9 @@ mod post_json;
|
||||
|
||||
use crate::fat_error::{FatError, FatResult};
|
||||
use crate::webserver::backup_manager::{backup_config, backup_info, get_backup_config};
|
||||
use crate::webserver::file_manager::{file_operations, list_files};
|
||||
use crate::webserver::get_json::{
|
||||
get_battery_state, get_config, get_live_moisture, get_log_localization_config, get_solar_state,
|
||||
get_time, get_timezones, get_version_web, tank_info,
|
||||
delete_save, get_time, get_timezones, get_version_web, list_saves, tank_info,
|
||||
};
|
||||
use crate::webserver::get_log::get_log;
|
||||
use crate::webserver::get_static::{serve_bundle, serve_favicon, serve_index};
|
||||
@@ -60,10 +58,7 @@ impl Handler for HTTPRequestRouter {
|
||||
let method = headers.method;
|
||||
let path = headers.path;
|
||||
|
||||
let prefix = "/file?filename=";
|
||||
let status = if path.starts_with(prefix) {
|
||||
file_operations(conn, method, &path, &prefix).await?
|
||||
} else if path == "/ota" {
|
||||
let status = if path == "/ota" {
|
||||
ota_operations(conn, method).await.map_err(|e| {
|
||||
error!("Error handling ota: {e}");
|
||||
e
|
||||
@@ -82,13 +77,20 @@ impl Handler for HTTPRequestRouter {
|
||||
"/time" => Some(get_time(conn).await),
|
||||
"/battery" => Some(get_battery_state(conn).await),
|
||||
"/solar" => Some(get_solar_state(conn).await),
|
||||
"/get_config" => Some(get_config(conn).await),
|
||||
"/files" => Some(list_files(conn).await),
|
||||
"/log_localization" => Some(get_log_localization_config(conn).await),
|
||||
"/tank" => Some(tank_info(conn).await),
|
||||
"/backup_info" => Some(backup_info(conn).await),
|
||||
"/timezones" => Some(get_timezones().await),
|
||||
"/moisture" => Some(get_live_moisture(conn).await),
|
||||
"/list_saves" => Some(list_saves(conn).await),
|
||||
// /get_config accepts an optional ?saveidx=N query parameter
|
||||
p if p == "/get_config" || p.starts_with("/get_config?") => {
|
||||
let saveidx: Option<usize> = p
|
||||
.find("saveidx=")
|
||||
.and_then(|pos| p[pos + 8..].split('&').next())
|
||||
.and_then(|s| s.parse().ok());
|
||||
Some(get_config(conn, saveidx).await)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
match json {
|
||||
@@ -127,7 +129,26 @@ impl Handler for HTTPRequestRouter {
|
||||
Some(json) => Some(handle_json(conn, json).await?),
|
||||
}
|
||||
}
|
||||
Method::Options | Method::Delete | Method::Head | Method::Put => None,
|
||||
Method::Delete => {
|
||||
// DELETE /delete_save?idx=N
|
||||
let json = if path == "/delete_save" || path.starts_with("/delete_save?") {
|
||||
let idx: Option<usize> = path
|
||||
.find("idx=")
|
||||
.and_then(|pos| path[pos + 4..].split('&').next())
|
||||
.and_then(|s| s.parse().ok());
|
||||
match idx {
|
||||
Some(idx) => Some(delete_save(conn, idx).await),
|
||||
None => Some(Err(FatError::String { error: "missing idx parameter".into() })),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
match json {
|
||||
None => None,
|
||||
Some(json) => Some(handle_json(conn, json).await?),
|
||||
}
|
||||
}
|
||||
Method::Options | Method::Head | Method::Put => None,
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user