1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202
//! This module exports the [AtomicFile] struct, which is used to synchronize access to a file.
use crate::{error::DisplayErr, Result};
use btserde::{read_from, write_to};
use fd_lock::RwLock as FdLock;
use serde::{de::DeserializeOwned, Serialize};
use std::{
fs::{DirBuilder, File, OpenOptions},
io::{self, BufReader, BufWriter},
marker::PhantomData,
ops::{Deref, DerefMut},
os::unix::fs::PermissionsExt,
path::PathBuf,
sync::{RwLock, RwLockReadGuard, RwLockWriteGuard},
};
/// This struct provides access to a value of type `T` which can be persisted to a file in the
/// local filesystem. Access to `T` is synchronized within the current process, and access to the
/// file is synchronized between processes.
///
/// This struct is primarily intended to prevent the file storing `T` from becoming corrupted if
/// multiple processes attempt to write to it at once.
pub struct AtomicFile<T> {
file: SyncFile<T>,
value: RwLock<T>,
}
impl<T: TryDefault + Serialize + DeserializeOwned> AtomicFile<T> {
/// Creates a new [AtomicFile] stored at `path`.
///
/// If there is already a file at `path` then it is
/// opened and a value of type `T` is deserialized from it. If it does not exist, then a new
/// value of `T` is created using the [TryDefault] trait which is then written to a new file at
/// `path`.
///
/// If a new file is created then it's mode will be masked using `mask`. This means that if a
/// bit is set in `mask` then the corresponding mode bit will be unset in the file.
///
/// This method will recursively create all parent directories of `path` if they do not exist.
pub fn new(path: PathBuf, mask: u32) -> Result<Self> {
let file = SyncFile::new(path, mask);
let value = RwLock::new(file.load_or_init()?);
Ok(AtomicFile { file, value })
}
/// Locks the value of `T` for reading in the current process and returns a struct which
/// dereferences to it.
pub fn read(&self) -> Result<AtomicFileReadGuard<'_, T>> {
Ok(AtomicFileReadGuard {
guard: self.value.read().display_err()?,
})
}
/// Locks the value of `T` for writing in the current process and returns a struct which
/// dereferences to it.
///
/// This struct can be used to save the current value of `T` to the file.
pub fn write(&self) -> Result<AtomicFileWriteGuard<'_, T>> {
Ok(AtomicFileWriteGuard {
file: &self.file,
guard: self.value.write().display_err()?,
})
}
}
pub struct AtomicFileReadGuard<'a, T> {
guard: RwLockReadGuard<'a, T>,
}
impl<'a, T> Deref for AtomicFileReadGuard<'a, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
self.guard.deref()
}
}
pub struct AtomicFileWriteGuard<'a, T> {
file: &'a SyncFile<T>,
guard: RwLockWriteGuard<'a, T>,
}
impl<'a, T: Serialize> AtomicFileWriteGuard<'a, T> {
/// Saves the current value of `T` to the file.
///
/// If another process calls `save` concurrently, then one will block waiting for the other
/// to finish. Note that in this case one process will overwrite the value written
/// by the other, but whichever value is contained in the file after both have finished
/// is internally consistent.
pub fn save(&mut self) -> Result<()> {
self.file.save(&self.guard)
}
}
impl<'a, T> Deref for AtomicFileWriteGuard<'a, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
self.guard.deref()
}
}
impl<'a, T> DerefMut for AtomicFileWriteGuard<'a, T> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.guard.deref_mut()
}
}
/// This struct provides synchronized access to a file such that two different processes cannot
/// concurrently write to the same path.
pub struct SyncFile<T> {
path: PathBuf,
/// The bits which are set in this field will cause the corresponding mode bits to be set to
/// zero when the file is created.
mask: u32,
phantom_data: PhantomData<T>,
}
impl<T> SyncFile<T> {
/// Create a new [SyncFile] backed by `path`. If there is no file at `path`, then it will be
/// created and it's mode adjusted according to `mask`.
pub fn new(path: PathBuf, mask: u32) -> Self {
SyncFile {
path,
mask,
phantom_data: PhantomData,
}
}
}
impl<T: Serialize> SyncFile<T> {
/// Serialize `value` and save the resulting bytes in the file.
pub fn save(&self, value: &T) -> Result<()> {
let file = OpenOptions::new()
.write(true)
.create(false)
.open(&self.path)?;
let mut lock = FdLock::new(file);
let mut guard = lock.write()?;
let mut writer = BufWriter::new(guard.deref_mut());
write_to(value, &mut writer).map_err(|err| err.into())
}
}
impl<T: DeserializeOwned> SyncFile<T> {
fn load(file: File) -> Result<T> {
let lock = FdLock::new(file);
let guard = lock.read()?;
let mut reader = BufReader::new(guard.deref());
read_from(&mut reader).map_err(|err| err.into())
}
}
impl<T: TryDefault + Serialize + DeserializeOwned> SyncFile<T> {
fn init(&self) -> Result<T> {
if let Some(dir_path) = self.path.parent() {
DirBuilder::new().recursive(true).create(dir_path)?;
}
let file = OpenOptions::new()
.write(true)
.create_new(true)
.open(&self.path)?;
let metadata = file.metadata()?;
let mut permissions = metadata.permissions();
if permissions.mode() & self.mask != 0 {
permissions.set_mode(permissions.mode() & !self.mask);
file.set_permissions(permissions)?;
}
let state = T::try_default()?;
let mut writer = BufWriter::new(file);
write_to(&state, &mut writer)?;
Ok(state)
}
/// Load the current value from the file, or create a new value using the [TryDefault] trait,
/// save it to the file and return it.
pub fn load_or_init(&self) -> Result<T> {
match OpenOptions::new().read(true).create(false).open(&self.path) {
Ok(file) => Self::load(file),
Err(err) => {
if io::ErrorKind::NotFound != err.kind() {
return Err(err.into());
}
self.init()
}
}
}
}
/// Trait for types which can produce a default value, but which may fail when doing so.
pub trait TryDefault: Sized {
/// Attempt to produce a default value for this type.
fn try_default() -> Result<Self>;
}
/// Constants for setting file permissions.
pub mod mask {
/// A mask which only allows the owner to access a file.
pub const OWNER_ONLY: u32 = 0o077;
/// A mask which only allows the owner or group owner to access a file.
pub const OWNER_AND_GROUP: u32 = 0o007;
}