ekv-fs 0.2.0

A chunked, #![no_std] Virtual File System built on top of the Embassy ekv key-value store.
Documentation
//! Filesystem operations: write, stat, open, and delete.

use ekv::{Database, WriteTransaction, flash::Flash};
use embassy_sync::blocking_mutex::raw::RawMutex;
use heapless::String;

use crate::error::Error;
use crate::file::EkvFile;
use crate::key::{self, KEY_BUF_CAP, KEY_SUFFIX_OVERHEAD};
use crate::meta::{FileMeta, META_BUF_LEN};

/// Chunked virtual file system backed by an [`ekv::Database`].
///
/// Configure capacity at compile time with const generics:
///
/// - `MAX_PATH_LEN`: longest path string (bytes) you will store.
/// - `CHUNK_SIZE`: payload size per flash write/read unit.
pub struct EkvFs<'a, F: Flash, M: RawMutex, const MAX_PATH_LEN: usize, const CHUNK_SIZE: usize> {
    db: &'a Database<F, M>,
}

impl<'a, F: Flash, M: RawMutex, const MAX_PATH_LEN: usize, const CHUNK_SIZE: usize>
    EkvFs<'a, F, M, MAX_PATH_LEN, CHUNK_SIZE>
{
    const _KEY_LEN_OK: () = assert!(MAX_PATH_LEN + KEY_SUFFIX_OVERHEAD <= KEY_BUF_CAP);

    /// Creates a filesystem handle over an existing `ekv` database.
    pub const fn new(db: &'a Database<F, M>) -> Self {
        Self { db }
    }

    /// Returns metadata for a file at `path`, or [`Error::NotFound`].
    pub async fn stat(&self, path: &str) -> Result<FileMeta, Error> {
        key::check_path(path, MAX_PATH_LEN)?;
        self.read_meta(path).await
    }

    /// Opens a file for sequential reading from the beginning.
    pub async fn open(
        &self,
        path: &str,
    ) -> Result<EkvFile<'a, F, M, MAX_PATH_LEN, CHUNK_SIZE>, Error> {
        key::check_path(path, MAX_PATH_LEN)?;
        let meta = self.read_meta(path).await?;

        let mut safe_path = String::<MAX_PATH_LEN>::new();
        safe_path.push_str(path).map_err(|_| Error::PathTooLong)?;

        Ok(EkvFile::new(self.db, safe_path, meta))
    }

    /// Writes (or replaces) the entire file at `path` with `data`.
    ///
    /// If a file already exists, its previous chunk keys are removed in the same
    /// write transaction before the new payload is stored.
    pub async fn write_file(&self, path: &str, data: &[u8]) -> Result<(), Error> {
        key::check_path(path, MAX_PATH_LEN)?;

        if self.read_meta(path).await.is_ok() {
            self.delete_file(path).await?;
        }

        let mut tx = self.db.write_transaction().await;

        let chunk_count = data.chunks(CHUNK_SIZE).count();
        let mut key_buf = String::<KEY_BUF_CAP>::new();

        for (i, chunk_data) in data.chunks(CHUNK_SIZE).enumerate() {
            key::format_chunk_key(&mut key_buf, path, i)?;
            tx.write(key_buf.as_bytes(), chunk_data)
                .await
                .map_err(Error::from_db)?;
        }

        let meta = FileMeta {
            size: data.len(),
            chunks: chunk_count,
        };

        key::format_meta_key(&mut key_buf, path)?;
        let mut meta_buf = [0u8; META_BUF_LEN];
        let meta_bytes = postcard::to_slice(&meta, &mut meta_buf).map_err(|_| Error::Serialize)?;

        tx.write(key_buf.as_bytes(), meta_bytes)
            .await
            .map_err(Error::from_db)?;

        tx.commit().await.map_err(Error::from_db)?;

        Ok(())
    }

    /// Deletes the file at `path`, or returns [`Error::NotFound`].
    pub async fn delete_file(&self, path: &str) -> Result<(), Error> {
        key::check_path(path, MAX_PATH_LEN)?;
        let meta = self.read_meta(path).await?;

        let mut tx = self.db.write_transaction().await;
        Self::remove_chunks(&mut tx, path, meta.chunks).await?;
        Self::remove_meta(&mut tx, path).await?;
        tx.commit().await.map_err(Error::from_db)?;

        Ok(())
    }

    async fn read_meta(&self, path: &str) -> Result<FileMeta, Error> {
        let mut key_buf = String::<KEY_BUF_CAP>::new();
        key::format_meta_key(&mut key_buf, path)?;

        let mut buf = [0u8; META_BUF_LEN];
        let tx = self.db.read_transaction().await;

        match tx.read(key_buf.as_bytes(), &mut buf).await {
            Ok(len) => postcard::from_bytes(&buf[..len]).map_err(|_| Error::Serialize),
            Err(_) => Err(Error::NotFound),
        }
    }

    async fn remove_chunks(
        tx: &mut WriteTransaction<'_, F, M>,
        path: &str,
        chunk_count: usize,
    ) -> Result<(), Error> {
        let mut key_buf = String::<KEY_BUF_CAP>::new();
        for i in 0..chunk_count {
            key::format_chunk_key(&mut key_buf, path, i)?;
            tx.delete(key_buf.as_bytes())
                .await
                .map_err(Error::from_db)?;
        }
        Ok(())
    }

    async fn remove_meta(tx: &mut WriteTransaction<'_, F, M>, path: &str) -> Result<(), Error> {
        let mut key_buf = String::<KEY_BUF_CAP>::new();
        key::format_meta_key(&mut key_buf, path)?;
        tx.delete(key_buf.as_bytes())
            .await
            .map_err(Error::from_db)?;
        Ok(())
    }
}