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.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            self.dirty = false;
70        } else if self.path.as_ref() == Some(&receipt.target) {
71            self.disk_stamp = receipt.stamp;
72        }
73        current
74    }
75}
76
77impl SaveRequest {
78    /// Blocking filesystem work; no editor borrow crosses this boundary.
79    pub fn execute(self) -> io::Result<SaveReceipt> {
80        let target = if self.new_name {
81            self.target.clone()
82        } else {
83            fs::canonicalize(&self.target).unwrap_or_else(|_| self.target.clone())
84        };
85        let current = match fs::metadata(&target) {
86            Ok(metadata) => Some(metadata.modified()?),
87            Err(error) if error.kind() == io::ErrorKind::NotFound => None,
88            Err(error) => return Err(error),
89        };
90        if !self.force && self.new_name && current.is_some() {
91            return Err(io::Error::new(
92                io::ErrorKind::PermissionDenied,
93                "file exists — :w! to overwrite",
94            ));
95        }
96        if !self.force && !self.new_name && current != self.baseline {
97            return Err(io::Error::new(
98                io::ErrorKind::PermissionDenied,
99                "file changed on disk — :w! to force",
100            ));
101        }
102        write_atomic(&target, &self.text, !self.new_name || self.force)?;
103        let stamp = Some(fs::metadata(&target)?.modified()?);
104        let canonical = fs::canonicalize(&target)?;
105        Ok(SaveReceipt {
106            origin: self.origin,
107            target: self.target,
108            canonical,
109            revision: self.revision,
110            stamp,
111        })
112    }
113}
114
115fn write_atomic(target: &Path, contents: &Rope, overwrite: bool) -> io::Result<()> {
116    let parent = target
117        .parent()
118        .filter(|path| !path.as_os_str().is_empty())
119        .unwrap_or_else(|| Path::new("."));
120    let mut temporary = tempfile::NamedTempFile::new_in(parent)?;
121    match fs::metadata(target) {
122        Ok(metadata) => temporary
123            .as_file()
124            .set_permissions(metadata.permissions())?,
125        Err(error) if error.kind() == io::ErrorKind::NotFound => {}
126        Err(error) => return Err(error),
127    }
128    for chunk in contents.chunks() {
129        temporary.write_all(chunk.as_bytes())?;
130    }
131    temporary.as_file().sync_all()?;
132    let result = if overwrite {
133        temporary.persist(target)
134    } else {
135        temporary.persist_noclobber(target)
136    };
137    result.map_err(|error| error.error)?;
138    #[cfg(unix)]
139    fs::File::open(parent)?.sync_all()?;
140    Ok(())
141}