atomic-ops 0.1.1

Performs atomic operations in the filesystem
Documentation
use std::{
    fs::OpenOptions,
    io::Read,
    path::{Path, PathBuf},
    sync::Arc,
};

use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde_flow::{
    encoder::{bincode, FlowEncoder},
    flow::Bytes,
};

use crate::{
    error::{OpsError, OpsResult},
    operation::LoaderFn,
    process::{constants::SERIALIZE_FILE_ID, Process},
};

const SER_REMOVE_FILE: &str = ".ser_rm";

#[derive(Serialize, Deserialize, Default)]
pub struct Op {
    to: PathBuf,
    content: Vec<u8>,

    #[serde(skip)]
    bytes: Vec<u8>,
}

impl Op {
    pub fn new<T: Serialize + DeserializeOwned + Bytes<T>>(
        to: &Path,
        entity: &T,
    ) -> OpsResult<Self> {
        let mut object = Self {
            to: to.to_path_buf(),
            content: entity
                .encode::<bincode::Encoder>()
                .map_err(|_| OpsError::SerializeFailed)?,
            bytes: Vec::new(),
        };
        object.bytes =
            bincode::Encoder::serialize(&object).map_err(|_| OpsError::SerializeFailed)?;
        Ok(object)
    }

    pub fn from_bytes(bytes: &[u8]) -> OpsResult<Self> {
        let mut decoded: Op =
            bincode::Encoder::deserialize(bytes).map_err(|_| OpsError::SerializeFailed)?;
        decoded.bytes = bytes.to_vec();
        Ok(decoded)
    }

    #[must_use]
    #[inline]
    fn remove_name(&self) -> PathBuf {
        let remove_filename = format!("{}{SER_REMOVE_FILE}", self.to.to_string_lossy());
        PathBuf::from(remove_filename)
    }
}
pub fn load(bytes: &[u8]) -> OpsResult<Box<dyn Process>> {
    let value = Op::from_bytes(bytes)?;
    Ok(Box::new(value))
}

pub fn loader() -> (u8, LoaderFn) {
    (SERIALIZE_FILE_ID, Arc::new(load))
}

impl Process for Op {
    fn prepare(&self) -> OpsResult<()> {
        if self.to.exists() {
            let remove_filename = self.remove_name();
            std::fs::rename(self.to.as_path(), remove_filename)?;
        }
        Ok(())
    }

    fn run(&self) -> OpsResult<()> {
        std::fs::write(self.to.as_path(), &self.content)?;

        let checksum = serde_flow::encoder::CASTAGNOLI.checksum(&self.content);
        let mut attempts = 3;
        while attempts > 0 {
            let mut written_bytes = Vec::new();
            let mut to_file = OpenOptions::new().read(true).open(&self.to)?;
            to_file.read_to_end(&mut written_bytes)?;

            let written_checksum = serde_flow::encoder::CASTAGNOLI.checksum(&written_bytes);
            if checksum == written_checksum {
                return Ok(());
            }
            attempts -= 1;
        }
        Ok(())
    }

    fn clean(&self) -> OpsResult<()> {
        let remove_filename = self.remove_name();
        if remove_filename.exists() {
            std::fs::remove_file(remove_filename)?;
        }
        Ok(())
    }

    fn revert_prepare(&self) -> OpsResult<()> {
        let remove_filename = self.remove_name();
        if remove_filename.exists() {
            std::fs::rename(remove_filename, self.to.as_path())?;
        }
        Ok(())
    }

    fn revert_run(&self) -> OpsResult<()> {
        let remove_filename = self.remove_name();
        // tolerate this error, because it's not very important
        let _ = std::fs::remove_file(self.to.as_path());
        if remove_filename.exists() {
            // this must be executed
            std::fs::rename(remove_filename, self.to.as_path())?;
        }
        Ok(())
    }

    fn as_bytes(&self) -> &[u8] {
        &self.bytes
    }

    fn id() -> u8
    where
        Self: Sized,
    {
        SERIALIZE_FILE_ID
    }
}

#[cfg(test)]
mod tests {
    use serde::{Deserialize, Serialize};
    use serde_flow::Flow;
    use tempfile::tempdir;

    use super::*;

    #[derive(Serialize, Deserialize, Flow)]
    #[flow(variant = 1, bytes)]
    pub struct TestNumber {
        pub number: u32,
    }

    #[test]
    fn test_prepare_none() {
        let tempdir = tempdir().unwrap();
        let path = tempdir.path().join("new_file");
        let saved_path = tempdir.path().join(format!("new_file{SER_REMOVE_FILE}"));

        let entity = TestNumber { number: 1234 };
        let ops = Op::new(path.as_path(), &entity).unwrap();
        // Run
        ops.prepare().unwrap();
        // Test
        assert!(!path.exists());
        assert!(!saved_path.exists());
    }

    #[test]
    fn test_prepare_exists() {
        let tempdir = tempdir().unwrap();
        let path = tempdir.path().join("new_file");
        let saved_path = tempdir.path().join(format!("new_file{SER_REMOVE_FILE}"));

        let old_entity = TestNumber { number: 5678 };
        std::fs::write(&path, old_entity.encode::<bincode::Encoder>().unwrap()).unwrap();

        // new entity
        let entity = TestNumber { number: 1234 };
        let ops = Op::new(path.as_path(), &entity).unwrap();
        // Run
        ops.prepare().unwrap();

        // Test
        assert!(saved_path.exists());
        let entity_bytes = std::fs::read(saved_path).unwrap();
        let entity_decoded = TestNumber::decode::<bincode::Encoder>(&entity_bytes).unwrap();
        assert_eq!(5678, entity_decoded.number);
    }

    #[test]
    fn test_run_none() {
        let tempdir = tempdir().unwrap();
        let path = tempdir.path().join("new_file");

        let entity = TestNumber { number: 1234 };
        let ops = Op::new(path.as_path(), &entity).unwrap();
        // Run
        ops.prepare().unwrap();
        ops.run().unwrap();

        // Test
        assert!(path.exists());
        let entity_bytes = std::fs::read(&path).unwrap();
        let entity_decoded = TestNumber::decode::<bincode::Encoder>(&entity_bytes).unwrap();
        assert_eq!(1234, entity_decoded.number);
    }

    #[test]
    fn test_run_exists() {
        let tempdir = tempdir().unwrap();
        let path = tempdir.path().join("new_file");
        let saved_path = tempdir.path().join(format!("new_file{SER_REMOVE_FILE}"));

        let old_entity = TestNumber { number: 5678 };
        std::fs::write(&path, old_entity.encode::<bincode::Encoder>().unwrap()).unwrap();

        // new entity
        let entity = TestNumber { number: 1234 };
        let ops = Op::new(path.as_path(), &entity).unwrap();
        // Run
        ops.prepare().unwrap();
        ops.run().unwrap();

        // Test
        assert!(saved_path.exists());
        let entity_bytes = std::fs::read(saved_path).unwrap();
        let entity_decoded = TestNumber::decode::<bincode::Encoder>(&entity_bytes).unwrap();
        assert_eq!(5678, entity_decoded.number);

        // new file must be present
        assert!(path.exists());
        let entity_bytes = std::fs::read(&path).unwrap();
        let entity_decoded = TestNumber::decode::<bincode::Encoder>(&entity_bytes).unwrap();
        assert_eq!(1234, entity_decoded.number);
    }

    #[test]
    fn test_clean_none() {
        let tempdir = tempdir().unwrap();
        let path = tempdir.path().join("new_file");
        let saved_path = tempdir.path().join(format!("new_file{SER_REMOVE_FILE}"));

        let entity = TestNumber { number: 1234 };
        let ops = Op::new(path.as_path(), &entity).unwrap();
        // Run
        ops.prepare().unwrap();
        ops.run().unwrap();
        ops.clean().unwrap();

        // Test
        assert!(path.exists());
        assert!(!saved_path.exists());
        let entity_bytes = std::fs::read(&path).unwrap();
        let entity_decoded = TestNumber::decode::<bincode::Encoder>(&entity_bytes).unwrap();
        assert_eq!(1234, entity_decoded.number);
    }

    #[test]
    fn test_clean_exists() {
        let tempdir = tempdir().unwrap();
        let path = tempdir.path().join("new_file");
        let saved_path = tempdir.path().join(format!("new_file{SER_REMOVE_FILE}"));

        let old_entity = TestNumber { number: 5678 };
        std::fs::write(&path, old_entity.encode::<bincode::Encoder>().unwrap()).unwrap();

        // new entity
        let entity = TestNumber { number: 1234 };
        let ops = Op::new(path.as_path(), &entity).unwrap();
        // Run
        ops.prepare().unwrap();
        ops.run().unwrap();
        ops.clean().unwrap();

        // Test
        assert!(!saved_path.exists());

        // new file must be present
        assert!(path.exists());
        let entity_bytes = std::fs::read(&path).unwrap();
        let entity_decoded = TestNumber::decode::<bincode::Encoder>(&entity_bytes).unwrap();
        assert_eq!(1234, entity_decoded.number);
    }

    #[test]
    fn test_revert_prepare_none() {
        let tempdir = tempdir().unwrap();
        let path = tempdir.path().join("new_file");
        let saved_path = tempdir.path().join(format!("new_file{SER_REMOVE_FILE}"));

        let entity = TestNumber { number: 1234 };
        let ops = Op::new(path.as_path(), &entity).unwrap();
        // Run
        ops.prepare().unwrap();
        // Test
        assert!(!path.exists());
        assert!(!saved_path.exists());
    }

    #[test]
    fn test_revert_prepare_exists() {
        let tempdir = tempdir().unwrap();
        let path = tempdir.path().join("new_file");
        let saved_path = tempdir.path().join(format!("new_file{SER_REMOVE_FILE}"));

        let old_entity = TestNumber { number: 5678 };
        std::fs::write(&path, old_entity.encode::<bincode::Encoder>().unwrap()).unwrap();

        // new entity
        let entity = TestNumber { number: 1234 };
        let ops = Op::new(path.as_path(), &entity).unwrap();

        // Run
        ops.prepare().unwrap();
        ops.revert_prepare().unwrap();

        // Test
        assert!(!saved_path.exists());
        let entity_bytes = std::fs::read(path).unwrap();
        let entity_decoded = TestNumber::decode::<bincode::Encoder>(&entity_bytes).unwrap();
        assert_eq!(5678, entity_decoded.number);
    }

    #[test]
    fn test_revert_run_none() {
        let tempdir = tempdir().unwrap();
        let path = tempdir.path().join("new_file");

        let entity = TestNumber { number: 1234 };
        let ops = Op::new(path.as_path(), &entity).unwrap();
        // Run
        ops.prepare().unwrap();
        ops.run().unwrap();
        ops.revert_run().unwrap();

        // Test
        assert!(!path.exists());
    }

    #[test]
    fn test_revert_run_exists() {
        let tempdir = tempdir().unwrap();
        let path = tempdir.path().join("new_file");
        let saved_path = tempdir.path().join(format!("new_file{SER_REMOVE_FILE}"));

        let old_entity = TestNumber { number: 5678 };
        std::fs::write(&path, old_entity.encode::<bincode::Encoder>().unwrap()).unwrap();

        // new entity
        let entity = TestNumber { number: 1234 };
        let ops = Op::new(path.as_path(), &entity).unwrap();
        // Run
        ops.prepare().unwrap();
        ops.run().unwrap();
        ops.revert_run().unwrap();

        // Test
        assert!(!saved_path.exists());
        // old file must be present
        assert!(path.exists());
        let entity_bytes = std::fs::read(&path).unwrap();
        let entity_decoded = TestNumber::decode::<bincode::Encoder>(&entity_bytes).unwrap();
        assert_eq!(5678, entity_decoded.number);
    }
}