summaryrefslogtreecommitdiffstats
path: root/src/files.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/files.rs')
-rw-r--r--src/files.rs213
1 files changed, 213 insertions, 0 deletions
diff --git a/src/files.rs b/src/files.rs
new file mode 100644
index 0000000..58fd043
--- /dev/null
+++ b/src/files.rs
@@ -0,0 +1,213 @@
+//! 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 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()
+ .unwrap_or_else(|poisoned| poisoned.into_inner())
+ }
+
+ 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<Option<Vec<u8>>> {
+ 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 (`<path><suffix>`), returning where to.
+ pub(crate) fn move_aside(&self, path: &Path, suffix: &str) -> std::io::Result<PathBuf> {
+ 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()) {
+ if let Ok(dir) = std::fs::File::open(dir) {
+ let _ = dir.sync_all();
+ }
+ }
+}
+
+fn with_suffix(path: &Path, suffix: &str) -> PathBuf {
+ let mut name = path.as_os_str().to_owned();
+ name.push(suffix);
+ PathBuf::from(name)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn writes_reads_and_counts() {
+ let dir = std::env::temp_dir().join(format!("continuum-files-{}", std::process::id()));
+ std::fs::create_dir_all(&dir).unwrap();
+ 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();
+ }
+}