#rollback #temporary-files #mechanism #file #fs #operations #offers

fs_rollback

This crate offers a rollback mechanism in Rust for fs common operations

5 stable releases

new 2.0.0 Feb 16, 2025
1.0.3 Feb 3, 2025

#419 in Filesystem

Download history 96/week @ 2025-01-28 108/week @ 2025-02-04 123/week @ 2025-02-11

327 downloads per month

GPL-3.0 license

48KB
731 lines

CI Workflow Codecov Crates.io docs.rs

Description ๐Ÿ“–๐Ÿ“š

This crate introduces the Rollback struct, a whole rollback mechanism for file system transactions. Rollback allows to atomically create/modify files and to create directories, so the file system won't be affected by these changes unless the Rollback instance is committed.

Rollback uses temporary files under the hood. Changes should be applied to those files, as they will be committed to the original paths as soon as the Rollback instance is committed ๐Ÿฆพ.

The Rollback struct currently supports:

  • Modification of existing files.
  • Creation of new files.
  • Creation of new directories.

The crate docs should be considered the only source of truth for this crate usage.

Considerations โš ๏ธ โ—๏ธ

When a file is added to the rollback 'to be modified', or when a new file is added as 'to be created', a temporary file is created an remains open until the Rollback instance goes out of scope (a commit consumes the rollback). While it's unlikely that the limit of open files is reached, this is something worth to keep in mind.

Those temporary files are created using the tempfile crate, so the security considerations described here applies for this crate as well.

Examples ๐Ÿ“Œ๐Ÿ“

A successful execution of a program using the Rollback struct may look like the code below. As it's shown in the example, all the files and directories are successfully committed.

 use fs_rollback::Rollback;
 use std::fs::File;
     
 let tempdir = tempfile::tempdir().unwrap();

 // Some file that already exists
 let existing_file = tempdir.path().join("file.txt");
 File::create(&existing_file).unwrap();
 std::fs::write(&existing_file, "Hello world!").unwrap();
 assert_eq!("Hello world!", std::fs::read_to_string(&existing_file).unwrap());

 // Some dirs that don't exist yet
 let dir1 = tempdir.path().join("dir1");
 let dir2 = dir1.join("dir2");
 let dir3 = tempdir.path().join("dir3");
 assert!(!dir1.is_dir());
 assert!(!dir2.is_dir());
 assert!(!dir3.is_dir());

 // Some files that don't exist yet, even if they're contained in one of the non-existing dirs above
 let new_file1 = dir2.join("file1.txt");
 let new_file2 = tempdir.path().join("file2.txt");
 assert!(!new_file1.is_file());
 assert!(!new_file2.is_file());

 // Rollback instance with capacity for the needed paths
 let mut rollback = Rollback::with_capacity(1,2,3);
 rollback.note_file(&existing_file).unwrap();
 rollback.new_file(&new_file1).unwrap();
 rollback.new_file(&new_file2).unwrap();
 rollback.new_dir(&dir1).unwrap();
 rollback.new_dir(&dir2).unwrap();
 rollback.new_dir(&dir3).unwrap();

 // Some operations with the new files and the noted files.
 std::fs::write(rollback.get_noted_file(&existing_file).unwrap(),"Happy to commit this").unwrap();
 std::fs::write(rollback.get_new_file(&new_file1).unwrap(),"Happy to commit this").unwrap();

 // If everything went well, we can commit our changes to the fs
 rollback.commit().unwrap();

 // And everything's commited!
 assert!(dir1.is_dir());
 assert!(dir2.is_dir());
 assert!(dir3.is_dir());
 assert!(new_file1.is_file());
 assert!(new_file2.is_file());
 assert_eq!("Happy to commit this", std::fs::read_to_string(&existing_file).unwrap());
 assert_eq!("Happy to commit this", std::fs::read_to_string(&new_file1).unwrap());

There's plenty of reasons leading to a failing execution, all of them resulting in an unaltered file system. Visit the Rollback struct docs to learn about all the available methods, as well as their possible errors.

This example shows that a failed commit rollbacks everything.

 use fs_rollback::{Rollback, Error};
 use std::fs::File;
     
 let tempdir = tempfile::tempdir().unwrap();

 // Some file that already exists
 let existing_file = tempdir.path().join("file.txt");
 File::create(&existing_file).unwrap();
 std::fs::write(&existing_file, "Hello world!").unwrap();
 assert_eq!("Hello world!", std::fs::read_to_string(&existing_file).unwrap());

 // Some dirs that don't exist yet
 let dir1 = tempdir.path().join("dir1");
 let dir2 = dir1.join("dir2");
 assert!(!dir1.is_dir());
 assert!(!dir2.is_dir());

 // A file that doesn't exist and that cannot be committed due to its parent dir doesn't exist
 // and that won't be noted by the rollback. This file will cause that the rollback commit
 // fails.
 let new_file1 = dir2.join("file1.txt");
 assert!(!new_file1.is_file());

 // Rollback instance with capacity for the needed paths
 let mut rollback = Rollback::with_capacity(1,1,2);
 rollback.note_file(&existing_file).unwrap();
 rollback.new_file(&new_file1).unwrap();
 rollback.new_dir(&dir1).unwrap();

 // Some operations with the new files and the noted files.
 std::fs::write(rollback.get_noted_file(&existing_file).unwrap(),"Happy to commit this").unwrap();
 std::fs::write(rollback.get_new_file(&new_file1).unwrap(),"Happy to commit this").unwrap();

 // If everything went well, we can commit our changes to the fs
 match rollback.commit(){
     Err(Error::Descriptive(msg)) => {
         // The error specifies the uncommited file
         assert!(msg.contains(&format!("Committing the following file: {}",new_file1.display())));
         // As the error's originated by a not existing directory, the message also explains
         // that
         assert!(msg.contains("No such file or directory"));
     },
     _ => panic!("Unexpected error")
 }

 // And everything's rolled back!
 assert!(!dir1.is_dir());
 assert!(!dir2.is_dir());
 assert!(!new_file1.is_file());
 assert_eq!("Hello world!", std::fs::read_to_string(&existing_file).unwrap());

While this example shows that uncommitted changes are just discarded if the Rollback instance goes out of scope.

 use fs_rollback::Rollback;
 use std::path::PathBuf;

 let tempdir = tempfile::tempdir().unwrap();
 let new_file = tempdir.path().join("file.txt");
 let mut tempfile = PathBuf::new();
 assert!(!new_file.is_file());
 assert!(!tempfile.is_file());

 {
     let mut rollback = Rollback::default();
     rollback.new_file(&new_file).unwrap();
     tempfile = rollback.get_new_file(&new_file).unwrap().to_path_buf();
     assert!(tempfile.is_file());
     std::fs::write(&tempfile,"Hello world!").unwrap();
 }

 assert!(!new_file.is_file());
 // tempfile contains a path to the temporary file created by the rollback, but as the rollback
 // went out of scope, that path doesn't point anymore to an existing file. That file was
 // discarded.
 assert!(!tempfile.is_file() && tempfile != PathBuf::new());

Contributing ๐Ÿค๐Ÿš€

Any contribution is more than welcome! ๐Ÿค๐Ÿฆพ just open a PR with your changes and it'll be considered๐Ÿ˜ธ

Dependencies

~2โ€“11MB
~141K SLoC