file up & download and delete

This commit is contained in:
Empire Phoenix 2025-09-18 01:39:32 +02:00
parent cd63e76469
commit 584d6df2d0
6 changed files with 341 additions and 205 deletions

View File

@ -159,7 +159,7 @@ edge-nal-embassy = "0.6.0"
static_cell = "2.1.1"
cfg-if = "1.0.3"
edge-http = { version = "0.6.1", features = ["log"] }
littlefs2 = { version = "0.6.1", features = ["c-stubs", "alloc", "serde"] }
littlefs2 = { version = "0.6.1", features = ["c-stubs", "alloc"] }
littlefs2-core = "0.1.1"

View File

@ -6,7 +6,7 @@ use anyhow::{anyhow, bail, Context};
use chrono::{DateTime, Utc};
use serde::Serialize;
use crate::hal::LittleFS2StorageAdapter::LittleFs2Filesystem;
use crate::hal::little_fs2storage_adapter::LittleFs2Filesystem;
use alloc::string::ToString;
use alloc::sync::Arc;
use alloc::{format, string::String, vec::Vec};
@ -34,7 +34,7 @@ use esp_wifi::wifi::{
ScanTypeConfig, WifiController, WifiDevice, WifiEvent, WifiState,
};
use littlefs2::fs::Filesystem;
use littlefs2_core::{FileType, PathBuf};
use littlefs2_core::{DynFile, FileType, OpenSeekFrom, Path, PathBuf, SeekFrom};
use log::{info, warn};
#[link_section = ".rtc.data"]
@ -113,6 +113,79 @@ macro_rules! mk_static {
}
impl Esp<'_> {
pub(crate) async fn delete_file(&self, filename: String) -> anyhow::Result<()> {
let file = PathBuf::try_from(filename.as_str()).unwrap();
let access = self.fs.lock().await;
access
.remove(&*file)
.map_err(|err| anyhow!("Could not delete file: {:?}", err))?;
Ok(())
}
pub(crate) async fn write_file(
&mut self,
filename: String,
offset: u32,
buf: &[u8],
) -> anyhow::Result<()> {
let file = PathBuf::try_from(filename.as_str()).unwrap();
let access = self.fs.lock().await;
info!("write file {} at offset {}", filename, offset);
match access.open_file_with_options_and_then(
|options| options.read(true).write(true).create(true),
&*file,
|file| {
file.seek(SeekFrom::Start(offset))?;
file.write(buf)?;
Ok(())
},
) {
Ok(_) => Ok(()),
Err(err) => {
bail!(format!("{err:?}"))
}
}
}
pub(crate) async fn get_file(
&mut self,
filename: String,
chunk: u32,
) -> anyhow::Result<([u8; 128], usize)> {
use littlefs2::io::Error as lfs2Error;
let file = PathBuf::try_from(filename.as_str()).unwrap();
let access = self.fs.lock().await;
let mut buf = [0_u8; 128];
let mut read = 0;
let offset = chunk * 128;
info!("read file {} at offset {}", filename, offset);
match access.open_file_with_options_and_then(
|options| options.read(true),
&*file,
|file| {
let length = file.len()? as u32;
info!("file length {}", length);
if length == 0 {
Err(lfs2Error::IO)
} else if length > offset {
file.seek(SeekFrom::Start(offset))?;
info!("seek to {}", offset);
read = file.read(&mut buf)?;
info!("read {} bytes", read);
Ok(())
} else {
//exactly at end, do nothing
Ok(())
}
},
) {
Ok(_) => {}
Err(err) => {
bail!(format!("{err:?}"))
}
}
Ok((buf, read))
}
pub(crate) fn get_ota_slot(&mut self) -> String {
match self.ota.current_slot() {
Ok(slot) => {
@ -435,11 +508,12 @@ impl Esp<'_> {
match self.fs.lock().await.read_dir_and_then(&path, |dir| {
for entry in dir {
let e = entry?;
result.files.push(FileInfo {
filename: e.path().to_string(),
size: e.metadata().len(),
});
if e.file_type() == FileType::File {
result.files.push(FileInfo {
filename: e.path().to_string(),
size: e.metadata().len(),
});
}
}
Result::Ok(())
}) {
@ -450,16 +524,7 @@ impl Esp<'_> {
}
Ok(result)
}
pub(crate) async fn delete_file(&self, _filename: &str) -> anyhow::Result<()> {
bail!("todo");
// let filepath = Path::new(Self::BASE_PATH).join(Path::new(filename));
// match fs::remove_file(filepath) {
// OkStd(_) => anyhow::Ok(()),
// Err(err) => {
// bail!(format!("{err:?}"))
// }
// }
}
// pub(crate) async fn get_file_handle(
// &self,
// filename: &str,
@ -473,13 +538,17 @@ impl Esp<'_> {
// })
// }
pub(crate) fn init_rtc_deepsleep_memory(&self, init_rtc_store: bool, to_config_mode: bool) {
pub(crate) async fn init_rtc_deepsleep_memory(
&self,
init_rtc_store: bool,
to_config_mode: bool,
) {
if init_rtc_store {
unsafe {
LAST_WATERING_TIMESTAMP = [0; PLANT_COUNT];
CONSECUTIVE_WATERING_PLANT = [0; PLANT_COUNT];
LOW_VOLTAGE_DETECTED = false;
crate::log::init();
crate::log::init().await;
RESTART_TO_CONF = to_config_mode;
};
} else {
@ -493,14 +562,16 @@ impl Esp<'_> {
0,
"",
"",
);
)
.await;
log(
LogMessage::LowVoltage,
LOW_VOLTAGE_DETECTED as u32,
0,
"",
"",
);
)
.await;
for i in 0..PLANT_COUNT {
log::info!(
"LAST_WATERING_TIMESTAMP[{}] = UTC {}",

View File

@ -1,8 +1,8 @@
use embedded_storage::{ReadStorage, Storage};
use esp_bootloader_esp_idf::partitions::FlashRegion;
use esp_storage::FlashStorage;
use littlefs2::consts::U1 as lfs2Array1;
use littlefs2::consts::U512 as lfs2Array512;
use littlefs2::consts::U512 as lfsCache;
use littlefs2::consts::U512 as lfsLookahead;
use littlefs2::driver::Storage as lfs2Storage;
use littlefs2::fs::Filesystem as lfs2Filesystem;
use littlefs2::io::Error as lfs2Error;
@ -14,20 +14,15 @@ pub struct LittleFs2Filesystem {
}
impl lfs2Storage for LittleFs2Filesystem {
const READ_SIZE: usize = 512;
const READ_SIZE: usize = 256;
const WRITE_SIZE: usize = 512;
const BLOCK_SIZE: usize = 1024; //usually optimal for flash access
const BLOCK_COUNT: usize = 8 * 1024 * 1024 / 1024; //8mb in 32kb blocks
const BLOCK_SIZE: usize = 512; //usually optimal for flash access
const BLOCK_COUNT: usize = 8 * 1024 * 1024 / 512; //8mb in 32kb blocks
const BLOCK_CYCLES: isize = 100;
type CACHE_SIZE = lfs2Array512;
type LOOKAHEAD_SIZE = lfs2Array1;
type CACHE_SIZE = lfsCache;
type LOOKAHEAD_SIZE = lfsLookahead;
fn read(&mut self, off: usize, buf: &mut [u8]) -> lfs2Result<usize> {
info!(
"Littlefs2Filesystem read at offset {} with len {}",
off,
buf.len()
);
let read_size: usize = Self::READ_SIZE;
assert_eq!(off % read_size, 0);
assert_eq!(buf.len() % read_size, 0);

View File

@ -1,7 +1,7 @@
mod LittleFS2StorageAdapter;
pub(crate) mod battery;
pub mod esp;
mod initial_hal;
mod little_fs2storage_adapter;
mod rtc;
//mod water;
@ -34,12 +34,10 @@ use esp_bootloader_esp_idf::partitions::{
};
use esp_hal::clock::CpuClock;
use esp_hal::gpio::{Input, InputConfig, Pull};
use esp_println::println;
use measurements::{Current, Voltage};
use crate::hal::LittleFS2StorageAdapter::LittleFs2Filesystem;
use crate::hal::little_fs2storage_adapter::LittleFs2Filesystem;
use embassy_sync::mutex::Mutex;
use embedded_storage::nor_flash::{NorFlash, ReadNorFlash};
use esp_alloc as _;
use esp_backtrace as _;
use esp_bootloader_esp_idf::ota::Slot;
@ -182,6 +180,7 @@ impl PlantHal {
let peripherals: Peripherals = esp_hal::init(config);
esp_alloc::heap_allocator!(size: 64 * 1024);
esp_alloc::heap_allocator!(#[link_section = ".dram2_uninit"] size: 64000);
let systimer = SystemTimer::new(peripherals.SYSTIMER);
let boot_button = Input::new(
@ -364,9 +363,11 @@ impl PlantHal {
to_config_mode as u32,
"",
&format!("{reasons:?}"),
);
)
.await;
esp.init_rtc_deepsleep_memory(init_rtc_store, to_config_mode);
esp.init_rtc_deepsleep_memory(init_rtc_store, to_config_mode)
.await;
let config = esp.load_config().await;
@ -453,7 +454,8 @@ impl PlantHal {
0,
"",
&err.to_string(),
);
)
.await;
HAL {
board_hal: initial_hal::create_initial_board(
free_pins,

View File

@ -31,8 +31,6 @@ use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_sync::mutex::{Mutex, MutexGuard};
use embassy_sync::once_lock::OnceLock;
use embassy_time::Timer;
use esp_alloc::heap_allocator;
use esp_bootloader_esp_idf::ota::OtaImageState;
use esp_hal::rom::ets_delay_us;
use esp_hal::system::software_reset;
use esp_println::{logger, println};
@ -317,8 +315,7 @@ async fn safe_main(spawner: Spawner) -> anyhow::Result<()> {
info!("executing config mode override");
//config upload will trigger reboot!
let reboot_now = Arc::new(AtomicBool::new(false));
//TODO
//let _webserver = httpd(reboot_now.clone());
//spawner.spawn(httpd(reboot_now.clone(), stack))?;
let board = BOARD_ACCESS.get().await.lock().await;
wait_infinity(board, WaitType::ConfigButton, reboot_now.clone()).await;
} else {
@ -667,7 +664,8 @@ pub async fn do_secure_pump(
current_ma as u32,
plant_config.max_pump_current_ma.to_string().as_str(),
step.to_string().as_str(),
);
)
.await;
board.board_hal.general_fault(true).await;
board.board_hal.fault(plant_id, true).await?;
if !plant_config.ignore_current_error {
@ -686,7 +684,8 @@ pub async fn do_secure_pump(
current_ma as u32,
plant_config.min_pump_current_ma.to_string().as_str(),
step.to_string().as_str(),
);
)
.await;
board.board_hal.general_fault(true).await;
board.board_hal.fault(plant_id, true).await?;
if !plant_config.ignore_current_error {
@ -706,7 +705,8 @@ pub async fn do_secure_pump(
0,
"",
"",
);
)
.await;
error = true;
break;
} else {
@ -840,7 +840,7 @@ async fn update_charge_indicator(board: &mut MutexGuard<'_, CriticalSectionRawMu
// }
async fn publish_firmware_info(
mut board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>,
board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>,
version: VersionInfo,
ip_address: &String,
timezone_time: &String,
@ -867,17 +867,6 @@ async fn publish_firmware_info(
let _ = esp.mqtt_publish("/state", "online".as_bytes()).await;
}
fn state_to_string(state: OtaImageState) -> &'static str {
match state {
OtaImageState::New => "New",
OtaImageState::PendingVerify => "PendingVerify",
OtaImageState::Valid => "Valid",
OtaImageState::Invalid => "Invalid",
OtaImageState::Aborted => "Aborted",
OtaImageState::Undefined => "Undefined",
}
}
async fn try_connect_wifi_sntp_mqtt() -> NetworkMode {
let board = &mut BOARD_ACCESS.get().await.lock().await;
let nw_conf = &board.board_hal.get_config().network.clone();

View File

@ -3,6 +3,8 @@
use crate::config::PlantControllerConfig;
use crate::{get_version, log::LogMessage, BOARD_ACCESS};
use alloc::borrow::ToOwned;
use alloc::fmt::format;
use alloc::format;
use alloc::string::{String, ToString};
use alloc::sync::Arc;
use alloc::vec::Vec;
@ -21,6 +23,7 @@ use embassy_net::Stack;
use embassy_time::Instant;
use embedded_io_async::{Read, Write};
use esp_println::println;
use littlefs2_core::Path;
use log::info;
use serde::{Deserialize, Serialize};
@ -294,15 +297,6 @@ pub struct NightLampCommand {
// anyhow::Ok(None)
// }
//
// fn query_param(uri: &str, param_name: &str) -> Option<std::string::String> {
// log::info!("{uri} get {param_name}");
// let parsed = Url::parse(&format!("http://127.0.0.1/{uri}")).unwrap();
// let value = parsed.query_pairs().find(|it| it.0 == param_name);
// match value {
// Some(found) => Some(found.1.into_owned()),
// None => None,
// }
// }
struct HttpHandler {
reboot_now: Arc<AtomicBool>,
@ -324,43 +318,147 @@ impl Handler for HttpHandler {
let method = headers.method;
let path = headers.path;
let status = match method {
Method::Get => match path {
"/favicon.ico" => {
conn.initiate_response(200, Some("OK"), &[("Content-Type", "image/x-icon")])
let prefix = "/file?filename=";
let status = if path.starts_with(prefix) {
let filename = &path[prefix.len()..];
let mut board = BOARD_ACCESS.get().await.lock().await;
info!("file request for {} with method {}", filename, method);
match method {
Method::Delete => {
board
.board_hal
.get_esp()
.delete_file(filename.to_owned())
.await
.unwrap();
Some(200)
}
Method::Get => {
let disp = format!("attachment; filename=\"{filename}\"");
conn.initiate_response(
200,
Some("OK"),
&[
("Content-Type", "application/octet-stream"),
("Content-Disposition", disp.as_str()),
],
)
.await?;
let mut chunk = 0;
loop {
let read_chunk = board
.board_hal
.get_esp()
.get_file(filename.to_owned(), chunk)
.await
.unwrap();
let length = read_chunk.1;
info!("read {} bytes for file request for {}", length, filename);
if length == 0 {
info!("file request for {} finished", filename);
break;
}
let data = &read_chunk.0[0..length];
conn.write_all(data).await?;
if length < 128 {
info!("file request for {} finished", filename);
break;
}
chunk = chunk + 1;
}
Some(200)
}
Method::Post => {
//ensure file is deleted, 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;
loop {
let mut buf = [0_u8; 1024];
let to_write = conn.read(&mut buf).await?;
if to_write == 0 {
info!("file request for {} finished", filename);
break;
} else {
info!(
"writing {} bytes for file request for {}",
to_write, filename
);
board
.board_hal
.get_esp()
.write_file(filename.to_owned(), offset as u32, &buf[0..to_write])
.await
.unwrap();
}
offset = offset + to_write
}
Some(200)
}
_ => None,
}
} else {
match method {
Method::Get => match path {
"/favicon.ico" => {
conn.initiate_response(
200,
Some("OK"),
&[("Content-Type", "image/x-icon")],
)
.await?;
conn.write_all(include_bytes!("favicon.ico")).await?;
Some(200)
}
"/" => {
conn.initiate_response(200, Some("OK"), &[("Content-Type", "text/html")])
conn.write_all(include_bytes!("favicon.ico")).await?;
Some(200)
}
"/" => {
conn.initiate_response(200, Some("OK"), &[("Content-Type", "text/html")])
.await?;
conn.write_all(include_bytes!("index.html")).await?;
Some(200)
}
"/bundle.js" => {
conn.initiate_response(
200,
Some("OK"),
&[("Content-Type", "text/javascript")],
)
.await?;
conn.write_all(include_bytes!("index.html")).await?;
Some(200)
}
"/bundle.js" => {
conn.initiate_response(200, Some("OK"), &[("Content-Type", "text/javascript")])
.await?;
conn.write_all(include_bytes!("bundle.js")).await?;
Some(200)
}
"/reboot" => {
let mut board = BOARD_ACCESS.get().await.lock().await;
board.board_hal.get_esp().set_restart_to_conf(true);
self.reboot_now.store(true, Ordering::Relaxed);
Some(200)
}
&_ => {
conn.write_all(include_bytes!("bundle.js")).await?;
Some(200)
}
"/reboot" => {
let mut board = BOARD_ACCESS.get().await.lock().await;
board.board_hal.get_esp().set_restart_to_conf(true);
self.reboot_now.store(true, Ordering::Relaxed);
Some(200)
}
&_ => {
let json = match path {
"/version" => Some(get_version_web(conn).await),
"/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),
"/log" => Some(get_log(conn).await),
"/wifiscan" => Some(wifi_scan(conn).await),
_ => None,
};
match json {
None => None,
Some(json) => Some(handle_json(conn, json).await?),
}
}
},
Method::Post => {
let json = match path {
"/version" => Some(get_version_web(conn).await),
"/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),
"/log" => Some(get_log(conn).await),
"/wifiscan" => Some(wifi_scan(conn).await),
"/set_config" => Some(set_config(conn).await),
_ => None,
};
match json {
@ -368,20 +466,9 @@ impl Handler for HttpHandler {
Some(json) => Some(handle_json(conn, json).await?),
}
}
},
Method::Post => {
let json = match path {
"/wifiscan" => Some(wifi_scan(conn).await),
"/set_config" => Some(set_config(conn).await),
_ => None,
};
match json {
None => None,
Some(json) => Some(handle_json(conn, json).await?),
}
Method::Options | Method::Delete | Method::Head | Method::Put => None,
_ => None,
}
Method::Options | Method::Delete | Method::Head | Method::Put => None,
_ => None,
};
let code = match status {
None => {
@ -390,6 +477,7 @@ impl Handler for HttpHandler {
}
Some(code) => code,
};
conn.complete().await?;
let response_time = Instant::now().duration_since(start).as_millis();
@ -398,16 +486,87 @@ impl Handler for HttpHandler {
}
}
// fn set_config(
// request: &mut Request<&mut EspHttpConnection>,
// ) -> Result<Option<std::string::String>, anyhow::Error> {
// let all = read_up_to_bytes_from_request(request, Some(4096))?;
// let config: PlantControllerConfig = serde_json::from_slice(&all)?;
// .fn_handler("/file", Method::Get, move |request| {
// let filename = query_param(request.uri(), "filename").unwrap();
// let file_handle = BOARD_ACCESS
// .lock()
// .unwrap()
// .board_hal
// .get_esp()
// .get_file_handle(&filename, false);
// match file_handle {
// Ok(mut file_handle) => {
// let headers = [("Access-Control-Allow-Origin", "*")];
// let mut response = request.into_response(200, None, &headers)?;
// const BUFFER_SIZE: usize = 512;
// let mut buffer: [u8; BUFFER_SIZE] = [0; BUFFER_SIZE];
// let mut total_read: usize = 0;
// loop {
// unsafe { vTaskDelay(1) };
// let read = std::io::Read::read(&mut file_handle, &mut buffer)?;
// total_read += read;
// let to_write = &buffer[0..read];
// response.write(to_write)?;
// if read == 0 {
// break;
// }
// }
// log::info!("wrote {total_read} for file {filename}");
// drop(file_handle);
// response.flush()?;
// }
// Err(err) => {
// //todo set headers here for filename to be error
// let error_text = err.to_string();
// log::info!("error handling get file {}", error_text);
// cors_response(request, 500, &error_text)?;
// }
// }
// anyhow::Ok(())
// })
// .unwrap();
// server
// .fn_handler("/file", Method::Post, move |mut request| {
// let filename = query_param(request.uri(), "filename").unwrap();
// let mut board = BOARD_ACCESS.lock().unwrap();
// let file_handle = board.board_hal.get_esp().get_file_handle(&filename, true);
// match file_handle {
// //TODO get free filesystem size, check against during write if not to large
// Ok(mut file_handle) => {
// const BUFFER_SIZE: usize = 512;
// let mut buffer: [u8; BUFFER_SIZE] = [0; BUFFER_SIZE];
// let mut total_read: usize = 0;
// let mut lastiter = 0;
// loop {
// let iter = (total_read / 1024) % 8;
// if iter != lastiter {
// for i in 0..PLANT_COUNT {
// let _ = board.board_hal.fault(i, iter == i);
// }
// lastiter = iter;
// }
//
// let mut board = BOARD_ACCESS.lock().expect("board access");
// board.board_hal.set_config(config)?;
// anyhow::Ok(Some("saved".to_owned()))
// }
// let read = request.read(&mut buffer)?;
// total_read += read;
// let to_write = &buffer[0..read];
// std::io::Write::write(&mut file_handle, to_write)?;
// if read == 0 {
// break;
// }
// }
// cors_response(request, 200, &format!("saved {total_read} bytes"))?;
// }
// Err(err) => {
// //todo set headers here for filename to be error
// let error_text = err.to_string();
// log::info!("error handling get file {}", error_text);
// cors_response(request, 500, &error_text)?;
// }
// }
// drop(board);
// anyhow::Ok(())
// })
// .unwrap();
async fn set_config<T, const N: usize>(
request: &mut Connection<'_, T, N>,
@ -703,87 +862,7 @@ pub async fn httpd(reboot_now: Arc<AtomicBool>, stack: Stack<'static>) {
// })
// .unwrap();
// server
// .fn_handler("/file", Method::Get, move |request| {
// let filename = query_param(request.uri(), "filename").unwrap();
// let file_handle = BOARD_ACCESS
// .lock()
// .unwrap()
// .board_hal
// .get_esp()
// .get_file_handle(&filename, false);
// match file_handle {
// Ok(mut file_handle) => {
// let headers = [("Access-Control-Allow-Origin", "*")];
// let mut response = request.into_response(200, None, &headers)?;
// const BUFFER_SIZE: usize = 512;
// let mut buffer: [u8; BUFFER_SIZE] = [0; BUFFER_SIZE];
// let mut total_read: usize = 0;
// loop {
// unsafe { vTaskDelay(1) };
// let read = std::io::Read::read(&mut file_handle, &mut buffer)?;
// total_read += read;
// let to_write = &buffer[0..read];
// response.write(to_write)?;
// if read == 0 {
// break;
// }
// }
// log::info!("wrote {total_read} for file {filename}");
// drop(file_handle);
// response.flush()?;
// }
// Err(err) => {
// //todo set headers here for filename to be error
// let error_text = err.to_string();
// log::info!("error handling get file {}", error_text);
// cors_response(request, 500, &error_text)?;
// }
// }
// anyhow::Ok(())
// })
// .unwrap();
// server
// .fn_handler("/file", Method::Post, move |mut request| {
// let filename = query_param(request.uri(), "filename").unwrap();
// let mut board = BOARD_ACCESS.lock().unwrap();
// let file_handle = board.board_hal.get_esp().get_file_handle(&filename, true);
// match file_handle {
// //TODO get free filesystem size, check against during write if not to large
// Ok(mut file_handle) => {
// const BUFFER_SIZE: usize = 512;
// let mut buffer: [u8; BUFFER_SIZE] = [0; BUFFER_SIZE];
// let mut total_read: usize = 0;
// let mut lastiter = 0;
// loop {
// let iter = (total_read / 1024) % 8;
// if iter != lastiter {
// for i in 0..PLANT_COUNT {
// let _ = board.board_hal.fault(i, iter == i);
// }
// lastiter = iter;
// }
//
// let read = request.read(&mut buffer)?;
// total_read += read;
// let to_write = &buffer[0..read];
// std::io::Write::write(&mut file_handle, to_write)?;
// if read == 0 {
// break;
// }
// }
// cors_response(request, 200, &format!("saved {total_read} bytes"))?;
// }
// Err(err) => {
// //todo set headers here for filename to be error
// let error_text = err.to_string();
// log::info!("error handling get file {}", error_text);
// cors_response(request, 500, &error_text)?;
// }
// }
// drop(board);
// anyhow::Ok(())
// })
// .unwrap();
//
// server
// .fn_handler("/file", Method::Delete, move |request| {