Expand description
Disk: serde + directories + various file formats as Traits.
This crate is for:
- (De)serializing various file formats (provided by
serde) - To/from disk locations that follow OS specifications (provided by
directories)
All errors returned will be an Error (re-exported anyhow::Error).
Implementing disk
use serde::{Serialize, Deserialize};
use disk::Toml;
#[derive(Serialize,Deserialize)] // <- Your data must implement `serde`.
struct State {
string: String,
number: u32,
}
// To make this struct a file, use the following macro:
//
// |- 1. The file format used will be TOML.
// |
// | |- 2. This is implemented for the struct "State".
// | |
// | | |- 3. It will be saved in the OS Data directory.
// | | |
// | | | |- 4. The main project directory is called "MyProject".
// | | | |
// | | | | |- 6. It won't be in any sub-directories.
// | | | | |
// | | | | | |- 7. The file name will be "state.toml".
// v v v v v v
disk::toml!(State, disk::Dir::Data, "MyProject", "", "state");Now our State struct implements the Toml trait.
The PATH the file would be saved in would be:
| OS | PATH |
|---|---|
| Windows | C:\Users\Alice\AppData\Roaming\My_Project\state.toml |
| macOS | /Users/Alice/Library/Application Support/My-Project/state.toml |
| Linux | /home/alice/.local/share/myproject/state.toml |
.save() and .from_file()
These two functions are the basic ways to:
- Save a struct to disk
- Create a struct from disk
// Create our struct.
let my_state = State { string: "Hello".to_string(), number: 123 };
// Save our `State` as a `Toml` file.
match my_state.save() {
Ok(_) => println!("We saved to disk"),
Err(e) => eprintln!("We failed to save to disk"),
}
// Let's create a new `State` by reading the file that we just created:
let from_disk = State::from_file().expect("Failed to read disk");
// These should be the same.
assert!(my_state == from_disk);.save_atomic()
disk provides an atomic version of .save().
Atomic in this context means, the data will be saved to a TEMPORARY file first, then renamed to the associated file.
This lowers the chance for data corruption on interrupt.
The temporary file is removed if the rename fails.
The temporary file name is: file_name + extension + .tmp, for example:
config.toml // <- Real file
config.toml.tmp // <- Temporary version
Already existing .tmp files will be overwritten.
.save_gzip() & .from_file_gzip()
disk provides gzip versions of .save() and .from_file().
This saves the file as a compressed file using gzip.
This will suffix the file with .gz, for example:
config.json // Normal file name with `.save()`
config.json.gz // File name when using `.save_gzip()`
To recover data from this file, you must also use the matching .from_file_gzip() when reading the data.
Sub-Directories
Either a single or multiple sub-directories can be specified with a / delimiter.
\ is also allowed but ONLY if building on Windows.
An empty string "" means NO sub directories.
// Windows ... C:\Users\Alice\AppData\Roaming\My_Project\sub1\sub2\state.toml
disk::toml!(State, Data, "MyProject", r"sub1\sub2", "state");
// macOS ... /Users/Alice/Library/Application Support/My-Project/sub1/sub2/state.json
disk::json!(State, Data, "MyProject", "sub1/sub2", "state");
// Linux ... /home/alice/.local/share/myproject/sub1/sub2/state.yml
disk::yaml!(State, Data, "MyProject", "sub1/sub2", "state");
// NO sub directory:
disk::toml!(State, Data, "MyProject", "", "state");bincode Header and Version
disk provides a custom header and versioning feature for the binary format, bincode.
The custom header is an arbitrary 24 byte array that is appended to the front of the file.
The version is a single u8 that comes after the header, representing a version from 0-255.
These must be passed to the implementation macro.
Example:
const HEADER: [u8; 24] = [1_u8; 24];
const VERSION: u8 = 5;
// Define.
disk::bincode!(State, disk::Dir::Data, "MyProject", "", "state", HEADER, VERSION);
#[derive(Serialize,Deserialize)]
struct State {
string: String,
number: u32,
}
// Save file.
let state = State { string: "Hello".to_string(), number: 123 };
state.save().unwrap();
// Assert the file's header+version on
// disk is correct and extract our version.
let version = State::file_version().unwrap();
assert!(version == State::VERSION);The header and version make up the first 25 bytes of the file, byte 1..=24 being the header and
byte 25 being the version. These bytes are checked upon using any .from_file() variant and will
return an error if it does not match your struct’s implementation.
bincode2
disk provides two bincode traits, Bincode & Bincode2.
bincode 2.0.0 (currently not stable) brings big performance improvements.
It also no longer requires serde, having it’s own Encode and Decode traits.
This means your type must implement these as well, e.g:
use bincode::{Encode, Decode};
#[derive(Encode, Decode)]
struct State;To implement bincode 2.x.x’s new traits, add it to Cargo.toml:
bincode = "2.0.0-rc.3"
and add #[derive(Encode, Decode)] to your types, like you would with serde.
Manually implementing disk
The macros verify and sanity check the input data at compile time,
while manual unsafe impl does not, and gives you full control over the data definitions,
allowing obvious mistakes like empty PATH’s and mismatching filenames to slip through.
It requires 9 constants to be defined:
unsafe impl disk::Toml for State {
const OS_DIRECTORY: disk::Dir = disk::Dir::Data;
const PROJECT_DIRECTORY: &'static str = "MyProject";
const SUB_DIRECTORIES: &'static str = "";
const FILE: &'static str = "state";
const FILE_EXT: &'static str = "toml";
const FILE_NAME: &'static str = "state.toml";
const FILE_NAME_GZIP: &'static str = "state.gzip";
const FILE_NAME_TMP: &'static str = "state.toml.tmp";
const FILE_NAME_GZIP_TMP: &'static str = "state.toml.gzip.tmp";
}A dangerous example:
unsafe impl disk::Toml for State {
const OS_DIRECTORY: disk::Dir = disk::Dir::Data;
const PROJECT_DIRECTORY: &'static str = "";
const SUB_DIRECTORIES: &'static str = "";
const FILE: &'static str = "";
[...]
}
// This deletes `~/.local/share`...!
State::rm_rf();Feature Flags
No file formats are enabled by default, you must enable them with feature flags.
Enabling the bytesize feature makes Metadata use bytesize for its [Display].
For example, println!("{metadata}") which normally looks like:
312445 bytes @ /my/file/path
will turn into:
312.4 KB @ /my/file/path
Use the full feature flag to enable everything.
| File Format | Feature flag to enable |
|---|---|
| Bincode | bincode |
| Bincode2 | bincode2 |
| Postcard | postcard |
| JSON | json |
| TOML | toml |
| YAML | yaml |
| Pickle | pickle |
| MessagePack | messagepack |
| BSON | bson |
| RON | ron |
| Plain Text | plain |
| Empty File | empty |
Macros
- Implement the
Bincodetrait - Implement the
Bincode2trait - Implement the
Bsontrait - Implement the
Emptytrait - Implement the
Jsontrait - Implement the
MessagePacktrait - Implement the
Pickletrait - Implement the
Plaintrait - Implement the
Postcardtrait - Implement the
Rontrait - Implement the
Tomltrait - Implement the
Yamltrait
Structs
- The
Errortype, a wrapper around a dynamic error type. - Metadata collected about a file/directory.
Enums
- The different types of OS directories, provided by
directories