Add atomic file saving

Adapted from the atomicwrites crate, but with directory fsync disabled
for performance.
This commit is contained in:
nyanpasu64 2021-06-11 07:52:00 -07:00
parent 478bae2ecd
commit 859121bcbc
5 changed files with 309 additions and 24 deletions

28
Cargo.lock generated
View file

@ -383,11 +383,13 @@ dependencies = [
"json",
"mime",
"mime-db",
"nix",
"once_cell",
"pest",
"pest_derive",
"serde",
"shlex",
"tempfile",
"thiserror",
"url",
"xdg",
@ -567,9 +569,9 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.94"
version = "0.2.96"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18794a8ad5b29321f790b55d93dfba91e125cb1a9edbd4f8e3150acc771c1a5e"
checksum = "5600b4e6efc5421841a2138a6b082e07fe12f9aaa12783d50e5d13325b26b4fc"
[[package]]
name = "log"
@ -598,6 +600,15 @@ version = "2.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525"
[[package]]
name = "memoffset"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59accc507f1338036a0477ef61afdae33cde60840f4dfe481319ce3ad116ddf9"
dependencies = [
"autocfg",
]
[[package]]
name = "mime"
version = "0.3.16"
@ -655,6 +666,19 @@ dependencies = [
"tempfile",
]
[[package]]
name = "nix"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c3728fec49d363a50a8828a190b379a446cc5cf085c06259bbbeb34447e4ec7"
dependencies = [
"bitflags",
"cc",
"cfg-if 1.0.0",
"libc",
"memoffset",
]
[[package]]
name = "nom"
version = "5.1.2"

View file

@ -27,6 +27,8 @@ xdg-mime = "0.3.3"
freedesktop_entry_parser = "1.1.1"
once_cell = "1.7.2"
aho-corasick = "0.7.15"
tempfile = "3.2.0"
nix = "0.21.0"
[profile.release]
opt-level=3

View file

@ -1,3 +1,6 @@
use crate::common::atomic_save::{
AtomicFile, AtomicSaveError, Durability, OverwriteBehavior,
};
use crate::{apps::SystemApps, common::Handler, Error, Result, CONFIG};
use mime::Mime;
use once_cell::sync::Lazy;
@ -179,12 +182,12 @@ impl MimeApps {
use itertools::Itertools;
use std::io::{prelude::*, BufWriter};
let f = std::fs::OpenOptions::new()
.read(true)
.create(true)
.write(true)
.truncate(true)
.open(Self::path()?)?;
let af = AtomicFile::new(
&Self::path()?,
OverwriteBehavior::AllowOverwrite,
Durability::DontSyncDir,
);
af.write(|f| -> Result<()> {
let mut writer = BufWriter::new(f);
writer.write_all(b"[Added Associations]\n")?;
@ -205,6 +208,12 @@ impl MimeApps {
writer.flush()?;
Ok(())
})
.map_err(|e| match e {
AtomicSaveError::Internal(e) => Error::Io(e),
AtomicSaveError::User(e) => e,
})?;
Ok(())
}
pub fn print(&self, detailed: bool) -> Result<()> {
use itertools::Itertools;

249
src/common/atomic_save.rs Normal file
View file

@ -0,0 +1,249 @@
//! Adapted from https://github.com/untitaker/rust-atomicwrites/blob/master/src/lib.rs.
use std::error::Error as ErrorTrait;
use std::fmt;
use std::fs;
use std::io;
use std::path;
pub use OverwriteBehavior::{AllowOverwrite, DisallowOverwrite};
/// Whether to allow overwriting if the target file exists.
#[derive(Clone, Copy)]
pub enum OverwriteBehavior {
/// Overwrite files silently.
AllowOverwrite,
/// Don't overwrite files. `AtomicFile.write` will raise errors for such conditions only after
/// you've already written your data.
DisallowOverwrite,
}
/// Whether to ensure durability after a system crash (guaranteed to contain the new data).
/// Regardless of the option you pick, the atomic write will be consistent after a crash
/// (will never contain partially-written data).
#[derive(Clone, Copy)]
pub enum Durability {
/// Faster, ensures either old or new file contents (but not half-written data)
/// will be present after system crash.
DontSyncDir,
/// Slower, ensures new file contents will be present after system crash.
/// Not possible on Windows.
SyncDir,
}
/// Represents an error raised by `AtomicFile.write`.
#[derive(Debug)]
pub enum AtomicSaveError<E> {
/// The error originated in the library itself, while it was either creating a temporary file
/// or moving the file into place.
Internal(io::Error),
/// The error originated in the user-supplied callback.
User(E),
}
/// If your callback returns a `std::io::Error`, you can unwrap this type to `std::io::Error`.
impl From<AtomicSaveError<io::Error>> for io::Error {
fn from(e: AtomicSaveError<io::Error>) -> Self {
match e {
AtomicSaveError::Internal(x) => x,
AtomicSaveError::User(x) => x,
}
}
}
impl<E: fmt::Display> fmt::Display for AtomicSaveError<E> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
AtomicSaveError::Internal(ref e) => e.fmt(f),
AtomicSaveError::User(ref e) => e.fmt(f),
}
}
}
impl<E: ErrorTrait> ErrorTrait for AtomicSaveError<E> {
fn cause(&self) -> Option<&dyn ErrorTrait> {
match *self {
AtomicSaveError::Internal(ref e) => Some(e),
AtomicSaveError::User(ref e) => Some(e),
}
}
}
fn safe_parent(p: &path::Path) -> Option<&path::Path> {
match p.parent() {
None => None,
Some(x) if x.as_os_str().len() == 0 => Some(&path::Path::new(".")),
x => x,
}
}
/// Create a file and write to it atomically, in a callback.
pub struct AtomicFile {
/// Path to the final file that is atomically written.
path: path::PathBuf,
overwrite: OverwriteBehavior,
durability: Durability,
/// Directory to which to write the temporary subdirectories.
tmpdir: path::PathBuf,
}
impl AtomicFile {
/// Helper for writing to the file at `path` atomically, in write-only mode.
///
/// If `OverwriteBehaviour::DisallowOverwrite` is given,
/// an `Error::Internal` containing an `std::io::ErrorKind::AlreadyExists`
/// will be returned from `self.write(...)` if the file exists.
///
/// The temporary file is written to a temporary subdirectory in `.`, to ensure
/// its on the same filesystem (so that the move is atomic).
pub fn new(
p: &path::Path,
overwrite: OverwriteBehavior,
durability: Durability,
) -> Self {
AtomicFile::new_with_tmpdir(
p,
overwrite,
durability,
safe_parent(p).unwrap_or(path::Path::new(".")),
)
}
/// Like `AtomicFile::new`, but the temporary file is written to a temporary subdirectory in `tmpdir`.
///
/// TODO: does `tmpdir` have to exist?
pub fn new_with_tmpdir(
path: &path::Path,
overwrite: OverwriteBehavior,
durability: Durability,
tmpdir: &path::Path,
) -> Self {
AtomicFile {
path: path.to_path_buf(),
overwrite,
durability,
tmpdir: tmpdir.to_path_buf(),
}
}
/// Move the file to `self.path()`. Not exposed!
fn commit(self, tmppath: &path::Path) -> io::Result<()> {
match self.overwrite {
AllowOverwrite => {
replace_atomic(tmppath, self.path(), self.durability)
}
DisallowOverwrite => {
move_atomic(tmppath, self.path(), self.durability)
}
}
}
/// Get the target filepath.
pub fn path(&self) -> &path::Path {
&self.path
}
/// Open a temporary file, call `f` on it (which is supposed to write to it), then move the
/// file atomically to `self.path`.
///
/// The temporary file is written to a randomized temporary subdirectory with prefix `.atomicwrite`.
pub fn write<T, E, F>(self, f: F) -> Result<T, AtomicSaveError<E>>
where
F: FnOnce(&mut fs::File) -> Result<T, E>,
{
let tmpdir = tempfile::Builder::new()
.prefix(".atomicwrite")
.tempdir_in(&self.tmpdir)
.map_err(AtomicSaveError::Internal)?;
let tmppath = tmpdir.path().join("tmpfile.tmp");
let rv = {
let mut tmpfile = fs::File::create(&tmppath)
.map_err(AtomicSaveError::Internal)?;
let r = f(&mut tmpfile).map_err(AtomicSaveError::User)?;
tmpfile.sync_all().map_err(AtomicSaveError::Internal)?;
r
};
self.commit(&tmppath).map_err(AtomicSaveError::Internal)?;
Ok(rv)
}
}
mod imp {
use super::{safe_parent, Durability};
use std::os::unix::io::AsRawFd;
use std::{fs, io, path};
fn fsync<T: AsRawFd>(f: T) -> io::Result<()> {
match nix::unistd::fsync(f.as_raw_fd()) {
Ok(()) => Ok(()),
Err(nix::Error::Sys(errno)) => Err(errno.into()),
Err(nix::Error::InvalidPath) => {
Err(io::Error::new(io::ErrorKind::Other, "invalid path"))
}
Err(nix::Error::InvalidUtf8) => {
Err(io::Error::new(io::ErrorKind::Other, "invalid utf-8"))
}
Err(nix::Error::UnsupportedOperation) => Err(io::Error::new(
io::ErrorKind::Other,
"unsupported operation",
)),
}
}
fn fsync_dir(x: &path::Path) -> io::Result<()> {
let f = fs::File::open(x)?;
fsync(f)
}
/// Move `src` to `dst`. If `dst` exists, it will be silently overwritten.
///
/// Both paths must reside on the same filesystem for the operation to be atomic.
pub fn replace_atomic(
src: &path::Path,
dst: &path::Path,
durability: Durability,
) -> io::Result<()> {
fs::rename(src, dst)?;
match durability {
Durability::SyncDir => {
let dst_directory = safe_parent(dst).unwrap();
fsync_dir(dst_directory)?;
}
Durability::DontSyncDir => {}
}
Ok(())
}
/// Move `src` to `dst`. An error will be returned if `dst` exists.
///
/// Both paths must reside on the same filesystem for the operation to be atomic.
pub fn move_atomic(
src: &path::Path,
dst: &path::Path,
durability: Durability,
) -> io::Result<()> {
fs::hard_link(src, dst)?;
fs::remove_file(src)?;
match durability {
Durability::SyncDir => {
let src_directory = safe_parent(src).unwrap();
let dst_directory = safe_parent(dst).unwrap();
fsync_dir(dst_directory)?;
if src_directory != dst_directory {
fsync_dir(src_directory)?;
}
}
Durability::DontSyncDir => {}
}
Ok(())
}
}
use imp::{move_atomic, replace_atomic};

View file

@ -1,3 +1,4 @@
pub mod atomic_save;
mod db;
mod desktop_entry;
mod handler;