//! The files this node reads and writes for itself, and how much it did (`/admin/stats`, //! `disk`): the state file (`swarm::state_file`), the document file //! (`document::file`) and a backup to recover from (`swarm::bootstrap`). Not the //! Mycelium daemon's files, configuration or logs. //! //! Writes replace a file atomically: a temporary file next to it, flushed to disk, then //! renamed over it, so a crash leaves the old content or the new one. Appends add to the //! end and are flushed to disk before they count as done; a crash may leave the last one //! cut short, which the file's reader has to expect. use crate::locks::MutexExt; use std::io::Write; use std::path::{Path, PathBuf}; use std::sync::{Mutex, MutexGuard}; /// What a file is, for the counts. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) enum FileKind { State, Document, Backup, /// The input of a genesis remake (`--genesis --remake`). Remake, } impl FileKind { pub(crate) const ALL: [FileKind; 4] = [ FileKind::State, FileKind::Document, FileKind::Backup, FileKind::Remake, ]; pub(crate) fn as_str(self) -> &'static str { match self { FileKind::State => "state", FileKind::Document => "document", FileKind::Backup => "backup", FileKind::Remake => "remake", } } fn index(self) -> usize { self as usize } } /// Counts for one kind of file, since the node started. #[derive(Clone, Copy, Debug, Default)] pub(crate) struct Counts { pub(crate) reads: u64, pub(crate) read_bytes: u64, /// Whole files written. pub(crate) writes: u64, pub(crate) written_bytes: u64, pub(crate) appends: u64, pub(crate) appended_bytes: u64, /// Reads, writes or appends that failed (a missing file read is no failure). pub(crate) errors: u64, } /// Reads and writes this node's files, counting per kind. #[derive(Default)] pub(crate) struct Files { // A std mutex: every access is short and never spans an await. counts: Mutex<[Counts; FileKind::ALL.len()]>, } impl Files { fn lock(&self) -> MutexGuard<'_, [Counts; FileKind::ALL.len()]> { self.counts.lock_or_recover() } fn count(&self, kind: FileKind, f: impl FnOnce(&mut Counts)) { f(&mut self.lock()[kind.index()]); } /// The file's content; None if it doesn't exist. pub(crate) fn read(&self, kind: FileKind, path: &Path) -> std::io::Result>> { match std::fs::read(path) { Ok(bytes) => { self.count(kind, |counts| { counts.reads += 1; counts.read_bytes += bytes.len() as u64; }); Ok(Some(bytes)) } Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None), Err(error) => { self.count(kind, |counts| counts.errors += 1); Err(error) } } } /// Replaces the file's content atomically (see the module docs). pub(crate) fn write_atomic( &self, kind: FileKind, path: &Path, bytes: &[u8], ) -> std::io::Result<()> { let result = write_atomic(path, bytes); self.count(kind, |counts| match &result { Ok(()) => { counts.writes += 1; counts.written_bytes += bytes.len() as u64; } Err(_) => counts.errors += 1, }); result } /// Adds `bytes` to the end of the file (which must exist), flushed to disk. pub(crate) fn append(&self, kind: FileKind, path: &Path, bytes: &[u8]) -> std::io::Result<()> { let result = (|| { let mut file = std::fs::OpenOptions::new().append(true).open(path)?; file.write_all(bytes)?; file.sync_data() })(); self.count(kind, |counts| match &result { Ok(()) => { counts.appends += 1; counts.appended_bytes += bytes.len() as u64; } Err(_) => counts.errors += 1, }); result } /// Moves an unusable file aside (``), returning where to. pub(crate) fn move_aside(&self, path: &Path, suffix: &str) -> std::io::Result { let aside = with_suffix(path, suffix); std::fs::rename(path, &aside)?; Ok(aside) } /// The counts per kind. pub(crate) fn counts(&self) -> Vec<(FileKind, Counts)> { let counts = *self.lock(); FileKind::ALL .into_iter() .map(|kind| (kind, counts[kind.index()])) .collect() } } fn write_atomic(path: &Path, bytes: &[u8]) -> std::io::Result<()> { let temporary = with_suffix(path, ".tmp"); { let mut file = std::fs::File::create(&temporary)?; file.write_all(bytes)?; file.sync_all()?; } std::fs::rename(&temporary, path)?; sync_parent(path); Ok(()) } /// Makes a rename in `path`'s directory durable (best effort: not every platform syncs /// directories). fn sync_parent(path: &Path) { if let Some(dir) = path.parent().filter(|dir| !dir.as_os_str().is_empty()) && let Ok(dir) = std::fs::File::open(dir) { let _ = dir.sync_all(); } } /// A key file's bytes: read from `path`; or with `create`, if there is no file there, /// `len` random bytes in a new one only its owner may read (then also `true`). Without /// `create`, a missing file is an error saying to give `create_option` (`what`: which /// key; `hint`: what a new one means). Never created where a file is: a mistyped path /// stays an error unless asked. pub(crate) fn key_file( path: &Path, what: &str, create: bool, create_option: &str, hint: &str, len: usize, ) -> anyhow::Result<(Vec, bool)> { use anyhow::Context; use std::io::Write; match std::fs::read(path) { Ok(bytes) => { warn_if_readable_by_others(path, what); Ok((bytes, false)) } Err(error) if error.kind() == std::io::ErrorKind::NotFound => { if !create { anyhow::bail!( "The {what} file {} doesn't exist; add --{create_option} to create a new {what} there ({hint})", path.display() ); } let mut bytes = vec![0u8; len]; getrandom::fill(&mut bytes) .map_err(|error| anyhow::anyhow!("No randomness: {error}"))?; let mut options = std::fs::OpenOptions::new(); options.write(true).create_new(true); #[cfg(unix)] { use std::os::unix::fs::OpenOptionsExt; options.mode(0o600); } let written = (|| { let mut file = options.open(path)?; file.write_all(&bytes)?; file.sync_all() })(); written.with_context(|| format!("Creating the {what} file {}", path.display()))?; Ok((bytes, true)) } Err(error) => { Err(error).with_context(|| format!("Reading the {what} from {}", path.display())) } } } /// A key is a secret: a file others may read is worth a warning (not an error: a /// group may be deliberate, e.g. shared with a deployment tool). `what`: which key. pub(crate) fn warn_if_readable_by_others(path: &Path, what: &str) { #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; if let Ok(metadata) = std::fs::metadata(path) { let mode = metadata.permissions().mode(); if mode & 0o077 != 0 { tracing::warn!( path = %path.display(), mode = format!("{:o}", mode & 0o777), "The {what} file is readable by others than its owner; chmod 600 it" ); } } } #[cfg(not(unix))] let _ = (path, what); } fn with_suffix(path: &Path, suffix: &str) -> PathBuf { let mut name = path.as_os_str().to_owned(); name.push(suffix); PathBuf::from(name) } /// An empty directory for the test `name`: `continuum--` in the /// temporary directory, emptied first. #[cfg(test)] pub(crate) fn scratch_dir(name: &str) -> PathBuf { let dir = std::env::temp_dir().join(format!("continuum-{name}-{}", std::process::id())); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); dir } #[cfg(test)] mod tests { use super::*; #[test] fn writes_reads_and_counts() { let dir = scratch_dir("files"); let path = dir.join("state.json"); let files = Files::default(); assert_eq!(files.read(FileKind::State, &path).unwrap(), None); files.write_atomic(FileKind::State, &path, b"one").unwrap(); files .write_atomic(FileKind::State, &path, b"three") .unwrap(); assert_eq!( files.read(FileKind::State, &path).unwrap().as_deref(), Some(&b"three"[..]) ); assert!(!with_suffix(&path, ".tmp").exists()); let (kind, state) = files.counts()[0]; assert_eq!(kind, FileKind::State); assert_eq!((state.writes, state.written_bytes), (2, 8)); assert_eq!((state.reads, state.read_bytes, state.errors), (1, 5, 0)); files.append(FileKind::State, &path, b"!").unwrap(); assert_eq!( files.read(FileKind::State, &path).unwrap().as_deref(), Some(&b"three!"[..]) ); assert_eq!(files.counts()[0].1.appended_bytes, 1); assert_eq!(files.counts()[1].1.reads, 0); std::fs::remove_dir_all(&dir).unwrap(); } }