Skip to main content

strop_core/buffer/
io.rs

1//! Owned file writes: preparing and accepting are pure; execute belongs on a worker.
2use super::Buffer;
3use crate::id::BufferRevision;
4use ropey::Rope;
5use std::fs;
6use std::io::{self, Write};
7use std::path::{Path, PathBuf};
8use std::time::SystemTime;
9
10pub struct SaveRequest {
11    text: Rope,
12    origin: Option<PathBuf>,
13    target: PathBuf,
14    revision: BufferRevision,
15    baseline: Option<SystemTime>,
16    new_name: bool,
17    force: bool,
18}
19
20#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
21pub struct SaveReceipt {
22    #[serde(with = "crate::path_serde::option")]
23    origin: Option<PathBuf>,
24    #[serde(with = "crate::path_serde")]
25    target: PathBuf,
26    #[serde(with = "crate::path_serde")]
27    canonical: PathBuf,
28    revision: BufferRevision,
29    stamp: Option<SystemTime>,
30}
31
32impl Buffer {
33    pub fn prepare_save(&self, target: Option<PathBuf>, force: bool) -> io::Result<SaveRequest> {
34        if self.readonly && !force {
35            return Err(io::Error::new(
36                io::ErrorKind::PermissionDenied,
37                "readonly buffer — :w! to force",
38            ));
39        }
40        let new_name = target.is_some();
41        let target = target.or_else(|| self.path.clone()).ok_or_else(|| {
42            io::Error::new(
43                io::ErrorKind::NotFound,
44                "no file name — :w {path} to name it",
45            )
46        })?;
47        Ok(SaveRequest {
48            text: self.snapshot(),
49            origin: self.path.clone(),
50            target,
51            revision: self.revision(),
52            baseline: self.disk_stamp,
53            new_name,
54            force,
55        })
56    }
57
58    /// A write of an older snapshot may update the on-disk baseline, never clear
59    /// dirty text or rename an edited document. Caller also checks request identity.
60    pub fn accept_save(&mut self, receipt: SaveReceipt) -> bool {
61        if self.path != receipt.origin {
62            return false;
63        }
64        let current = self.acknowledge_saved_revision(receipt.revision);
65        if current {
66            self.path = Some(receipt.target);
67            self.disk_stamp = receipt.stamp;
68            self.file_identity = Some(receipt.canonical);
69        } else if self.path.as_ref() == Some(&receipt.target) {
70            self.disk_stamp = receipt.stamp;
71        }
72        current
73    }
74
75    /// Pure acknowledgment for an externally owned save. The caller validates
76    /// document/request identity; no local filesystem path is created or changed.
77    pub fn acknowledge_saved_revision(&mut self, revision: BufferRevision) -> bool {
78        let current = self.revision() == revision;
79        if current {
80            self.dirty = false;
81        }
82        current
83    }
84}
85
86impl SaveRequest {
87    /// Blocking filesystem work; no editor borrow crosses this boundary.
88    pub fn execute(self) -> io::Result<SaveReceipt> {
89        let target = if self.new_name {
90            self.target.clone()
91        } else {
92            fs::canonicalize(&self.target).unwrap_or_else(|_| self.target.clone())
93        };
94        let current = match fs::metadata(&target) {
95            Ok(metadata) => Some(metadata.modified()?),
96            Err(error) if error.kind() == io::ErrorKind::NotFound => None,
97            Err(error) => return Err(error),
98        };
99        if !self.force && self.new_name && current.is_some() {
100            return Err(io::Error::new(
101                io::ErrorKind::PermissionDenied,
102                "file exists — :w! to overwrite",
103            ));
104        }
105        if !self.force && !self.new_name && current != self.baseline {
106            return Err(io::Error::new(
107                io::ErrorKind::PermissionDenied,
108                "file changed on disk — :w! to force",
109            ));
110        }
111        write_atomic(&target, &self.text, !self.new_name || self.force)?;
112        let stamp = Some(fs::metadata(&target)?.modified()?);
113        let canonical = fs::canonicalize(&target)?;
114        Ok(SaveReceipt {
115            origin: self.origin,
116            target: self.target,
117            canonical,
118            revision: self.revision,
119            stamp,
120        })
121    }
122}
123
124fn write_atomic(target: &Path, contents: &Rope, overwrite: bool) -> io::Result<()> {
125    let parent = target
126        .parent()
127        .filter(|path| !path.as_os_str().is_empty())
128        .unwrap_or_else(|| Path::new("."));
129    let mut temporary = tempfile::NamedTempFile::new_in(parent)?;
130    match fs::metadata(target) {
131        Ok(metadata) => temporary
132            .as_file()
133            .set_permissions(metadata.permissions())?,
134        Err(error) if error.kind() == io::ErrorKind::NotFound => {}
135        Err(error) => return Err(error),
136    }
137    for chunk in contents.chunks() {
138        temporary.write_all(chunk.as_bytes())?;
139    }
140    temporary.as_file().sync_all()?;
141    let result = if overwrite {
142        temporary.persist(target)
143    } else {
144        temporary.persist_noclobber(target)
145    };
146    result.map_err(|error| error.error)?;
147    #[cfg(unix)]
148    fs::File::open(parent)?.sync_all()?;
149    Ok(())
150}