Skip to main content

port_file/
write.rs

1use atomicwrites::{AtomicFile, DisallowOverwrite};
2use camino::{Utf8Path, Utf8PathBuf};
3use std::{
4    fmt::{self, Display},
5    io::{self, Write},
6};
7
8/// The stage of an atomic write that failed, returned by
9/// [`WriteError::stage`].
10#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub enum WriteStage {
12    /// Writing the serialized value to the temporary file failed.
13    Write,
14
15    /// Creating the temporary file, syncing it to disk, or atomically
16    /// renaming it to the destination failed.
17    Persist,
18}
19
20/// An error that occurred while writing the port file.
21#[derive(Debug)]
22pub struct WriteError {
23    path: Utf8PathBuf,
24    stage: WriteStage,
25    source: io::Error,
26}
27
28impl WriteError {
29    fn new(path: &Utf8Path, error: atomicwrites::Error<io::Error>) -> Self {
30        let (stage, source) = match error {
31            atomicwrites::Error::Internal(source) => {
32                (WriteStage::Persist, source)
33            }
34            atomicwrites::Error::User(source) => (WriteStage::Write, source),
35        };
36        WriteError { path: path.to_owned(), stage, source }
37    }
38
39    /// Returns the file path we failed to write.
40    pub fn path(&self) -> &Utf8Path {
41        &self.path
42    }
43
44    /// Returns the stage of the atomic write that failed.
45    pub fn stage(&self) -> WriteStage {
46        self.stage
47    }
48}
49
50impl fmt::Display for WriteError {
51    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52        match self.stage {
53            WriteStage::Write => write!(
54                f,
55                "writing value to temporary file for port file {}",
56                self.path
57            ),
58            WriteStage::Persist => write!(
59                f,
60                "creating, syncing, or renaming temporary file for port \
61                 file {}",
62                self.path
63            ),
64        }
65    }
66}
67
68impl std::error::Error for WriteError {
69    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
70        Some(&self.source)
71    }
72}
73
74/// Writes a value such as a [`SocketAddr`] to the path.
75///
76/// # Notes
77///
78/// The path must not already exist. A write to an existing file will fail with
79/// a [`WriteStage::Persist`] error.
80///
81/// The value is written atomically via a temporary file and a rename. This
82/// means that readers will not ever see torn or partial writes.
83///
84/// `write` is generic over [`Display`], so callers can publish any value with a
85/// [`FromStr`] counterpart on the reader, assuming [`Display`] and [`FromStr`]
86/// roundtrip. In most cases, you will pass in a [`SocketAddr`] here.
87///
88/// A single trailing newline is written after the value. The read side of this
89/// crate expects (and will strip) exactly one trailing newline.
90///
91/// [`SocketAddr`]: std::net::SocketAddr
92/// [`FromStr`]: std::str::FromStr
93pub fn write<T: Display>(path: &Utf8Path, value: T) -> Result<(), WriteError> {
94    AtomicFile::new(path, DisallowOverwrite)
95        .write(|f| writeln!(f, "{value}"))
96        .map_err(|error| WriteError::new(path, error))
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    #[test]
104    fn user_error_is_write_stage() {
105        let path = Utf8Path::new("port");
106        let error = atomicwrites::Error::User(io::Error::from(
107            io::ErrorKind::StorageFull,
108        ));
109        let err = WriteError::new(path, error);
110        assert_eq!(err.path(), path);
111        assert_eq!(err.stage(), WriteStage::Write);
112        assert!(
113            err.to_string().contains("writing value to temporary file"),
114            "unexpected error: {err}"
115        );
116        assert_eq!(err.source.kind(), io::ErrorKind::StorageFull);
117    }
118
119    #[test]
120    fn internal_error_is_persist_stage() {
121        let path = Utf8Path::new("port");
122        let error = atomicwrites::Error::Internal(io::Error::from(
123            io::ErrorKind::PermissionDenied,
124        ));
125        let err = WriteError::new(path, error);
126        assert_eq!(err.path(), path);
127        assert_eq!(err.stage(), WriteStage::Persist);
128        assert!(
129            err.to_string()
130                .contains("creating, syncing, or renaming temporary file"),
131            "unexpected error: {err}"
132        );
133        assert_eq!(err.source.kind(), io::ErrorKind::PermissionDenied);
134    }
135}