#![deny(missing_docs)]
mod append;
mod copy;
mod create;
mod delete;
mod r#move;
mod touch;
mod write;
#[cfg(feature = "tokio")]
pub mod async_;
#[cfg(feature = "tokio")]
pub use async_::{
AsyncAppendFile, AsyncCopyDirectory, AsyncCopyFile, AsyncCreateDirectory, AsyncCreateFile,
AsyncDeleteDirectory, AsyncDeleteFile, AsyncMoveDirectory, AsyncMoveFile, AsyncMoveOperation,
AsyncOpFuture, AsyncRollbackableOperation, AsyncTouchFile, AsyncTransaction, AsyncWriteFile,
};
use std::env;
use std::fs;
use std::io::{self, Error};
use std::path::{Path, PathBuf};
use uuid::Uuid;
pub use append::AppendFile;
pub use copy::{CopyDirectory, CopyFile};
pub use create::{CreateDirectory, CreateFile};
pub use delete::{DeleteDirectory, DeleteFile};
pub use r#move::{MoveDirectory, MoveFile, MoveOperation};
pub use touch::TouchFile;
pub use write::WriteFile;
pub trait RollbackableOperation {
fn execute(&mut self) -> io::Result<()>;
fn rollback(&mut self) -> io::Result<()>;
}
pub trait DirectoryOperation: RollbackableOperation {
fn get_path(&self) -> &Path;
fn get_backup_path(&self) -> &Path;
fn set_backup_path<S: AsRef<Path>>(&mut self, path: S);
fn get_temp_dir(&self) -> &Path;
fn dispose(&self) -> io::Result<()> {
let bp = self.get_backup_path();
if bp.as_os_str().is_empty() || !bp.exists() {
return Ok(());
}
fs::remove_dir_all(bp)
}
fn create_backup_folder(&mut self) -> io::Result<()> {
fs::create_dir_all(self.get_temp_dir())?;
let backup_path = self.get_temp_dir().join(Uuid::new_v4().to_string());
copy_dir(self.get_path(), &backup_path)?;
self.set_backup_path(backup_path);
Ok(())
}
}
pub trait SingleFileOperation: RollbackableOperation {
fn get_path(&self) -> &Path;
fn get_backup_path(&self) -> &Path;
fn set_backup_path<S: AsRef<Path>>(&mut self, path: S);
fn get_temp_dir(&self) -> &Path;
fn dispose(&self) -> io::Result<()> {
let bp = self.get_backup_path();
if bp.as_os_str().is_empty() || !bp.exists() {
return Ok(());
}
fs::remove_file(bp)
}
fn create_backup_file(&mut self) -> io::Result<()> {
fs::create_dir_all(self.get_temp_dir())?;
let backup_path = self.get_temp_dir().join(Uuid::new_v4().to_string());
fs::copy(self.get_path(), &backup_path)?;
self.set_backup_path(&backup_path);
Ok(())
}
}
pub(crate) fn copy_dir<U: AsRef<Path>, V: AsRef<Path>>(from: U, to: V) -> io::Result<()> {
let mut stack = vec![from.as_ref().to_path_buf()];
let output_root = to.as_ref().to_path_buf();
let input_root = from.as_ref().components().count();
while let Some(working_path) = stack.pop() {
let src: PathBuf = working_path.components().skip(input_root).collect();
let dest = if src.components().count() == 0 {
output_root.clone()
} else {
output_root.join(&src)
};
fs::create_dir_all(&dest)?;
for entry in fs::read_dir(&working_path)? {
let entry = entry?;
let path = entry.path();
if entry.file_type()?.is_dir() {
stack.push(path);
} else {
match path.file_name() {
Some(filename) => fs::copy(&path, dest.join(filename)).map(|_| ())?,
None => {
return Err(Error::other("could not extract filename from path"))
}
}
}
}
}
Ok(())
}
#[must_use = "build then call .execute()"]
pub struct Transaction {
ops: Vec<Box<dyn RollbackableOperation>>,
execution_count: usize,
temp_dir: PathBuf,
}
impl Transaction {
pub fn new() -> Self {
Self {
ops: vec![],
execution_count: 0,
temp_dir: env::temp_dir(),
}
}
pub fn with_temp_dir<P: AsRef<Path>>(temp_dir: P) -> Self {
Self {
ops: vec![],
execution_count: 0,
temp_dir: temp_dir.as_ref().to_path_buf(),
}
}
pub fn create_file<S: AsRef<Path>>(mut self, path: S) -> Self {
self.ops.push(Box::new(CreateFile::new(path)));
self
}
pub fn create_dir<S: AsRef<Path>>(mut self, path: S) -> Self {
self.ops.push(Box::new(CreateDirectory::new(path)));
self
}
pub fn append_file<S: AsRef<Path>>(mut self, path: S, data: Vec<u8>) -> Self {
self.ops
.push(Box::new(AppendFile::with_temp_dir(path, self.temp_dir.clone(), data)));
self
}
pub fn copy_file<S: AsRef<Path>, T: AsRef<Path>>(mut self, source: S, dest: T) -> Self {
self.ops
.push(Box::new(CopyFile::with_temp_dir(source, dest, self.temp_dir.clone())));
self
}
pub fn copy_dir<S: AsRef<Path>, T: AsRef<Path>>(mut self, source: S, dest: T) -> Self {
self.ops
.push(Box::new(CopyDirectory::with_temp_dir(source, dest, self.temp_dir.clone())));
self
}
pub fn delete_file<S: AsRef<Path>>(mut self, source: S) -> Self {
self.ops
.push(Box::new(DeleteFile::with_temp_dir(source, self.temp_dir.clone())));
self
}
pub fn delete_dir<S: AsRef<Path>>(mut self, source: S) -> Self {
self.ops
.push(Box::new(DeleteDirectory::with_temp_dir(source, self.temp_dir.clone())));
self
}
pub fn move_file<S: AsRef<Path>, T: AsRef<Path>>(mut self, source: S, dest: T) -> Self {
self.ops.push(Box::new(MoveFile::new(source, dest)));
self
}
pub fn move_dir<S: AsRef<Path>, T: AsRef<Path>>(mut self, source: S, dest: T) -> Self {
self.ops.push(Box::new(MoveDirectory::new(source, dest)));
self
}
pub fn write_file<S: AsRef<Path>>(mut self, path: S, data: Vec<u8>) -> Self {
self.ops
.push(Box::new(WriteFile::with_temp_dir(path, self.temp_dir.clone(), data)));
self
}
pub fn touch_file<S: AsRef<Path>>(mut self, path: S) -> Self {
self.ops.push(Box::new(TouchFile::new(path)));
self
}
}
impl Default for Transaction {
fn default() -> Self {
Self::new()
}
}
impl RollbackableOperation for Transaction {
fn execute(&mut self) -> io::Result<()> {
self.execution_count = 0;
for i in 0..self.ops.len() {
self.ops[i].execute()?;
self.execution_count += 1;
}
Ok(())
}
fn rollback(&mut self) -> io::Result<()> {
for i in (0..self.execution_count).rev() {
self.ops[i].rollback()?;
}
self.execution_count = 0;
Ok(())
}
}
#[cfg(test)]
mod tests {
use tempfile::tempdir;
use super::*;
#[test]
fn transaction_works() {
let dir = tempdir().unwrap();
let d = dir.path();
let mut tr = Transaction::with_temp_dir(d)
.create_file(d.join("file.txt"))
.create_file(d.join("for_delete.txt"))
.create_dir(d.join("inner/sub"))
.create_dir(d.join("for_delete_dir"))
.create_dir(d.join("magic_dir"))
.write_file(d.join("file.txt"), b"Hello World".to_vec())
.append_file(d.join("file.txt"), b"Hello World".to_vec())
.copy_file(d.join("file.txt"), d.join("inner/file.txt"))
.copy_dir(d.join("magic_dir"), d.join("inner/magic_dir"))
.delete_file(d.join("for_delete.txt"))
.delete_dir(d.join("for_delete_dir"))
.move_file(d.join("inner/file.txt"), d.join("inner/magic_dir/file.txt"))
.create_dir(d.join("for_moving"))
.move_dir(d.join("for_moving"), d.join("inner/magic_dir/for_moving"));
tr.execute().expect("Cannot execute");
tr.rollback().expect("Cannot rollback");
}
}