Ely Office: six offline-first desktop apps in Rust and GPUI
ElyDoc, ElyTable, ElyShow (LibreOfficeKit), ElyPDF (MuPDF), ElyTree (Git), ElyMark (Milkdown and CodeMirror in CEF). ElyMail and ElyAgent follow.
This commit is contained in:
817 files changed
+143101
No files matched your search
@@ -0,0 +1,166 @@
|
||||
//! An app's kept data, and clearing it (§18.3).
|
||||
|
||||
use std::{
|
||||
io,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use crate::AppId;
|
||||
|
||||
/// Kept data the user may see and clear.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Kind {
|
||||
Log,
|
||||
/// The web content cache (ElyMark).
|
||||
Cache,
|
||||
Recovery,
|
||||
}
|
||||
|
||||
impl Kind {
|
||||
pub const ALL: [Kind; 3] = [Kind::Log, Kind::Cache, Kind::Recovery];
|
||||
|
||||
fn place(self) -> &'static str {
|
||||
match self {
|
||||
Kind::Log => "ely.log",
|
||||
Kind::Cache => "web",
|
||||
Kind::Recovery => "recovery",
|
||||
}
|
||||
}
|
||||
|
||||
/// In use while running, so cleared at the next start.
|
||||
pub fn at_next_start(self) -> bool {
|
||||
self != Kind::Log
|
||||
}
|
||||
|
||||
fn mark(self) -> String {
|
||||
format!(".clear-{}", self.place())
|
||||
}
|
||||
}
|
||||
|
||||
/// Bytes each kind holds now; a missing one holds none.
|
||||
pub fn usage(app: AppId) -> io::Result<Vec<(Kind, u64)>> {
|
||||
let dir = crate::data_dir(app).map_err(io::Error::other)?;
|
||||
Kind::ALL.into_iter().map(|kind| Ok((kind, size(&dir.join(kind.place()))?))).collect()
|
||||
}
|
||||
|
||||
fn size(path: &Path) -> io::Result<u64> {
|
||||
let meta = match std::fs::symlink_metadata(path) {
|
||||
Ok(meta) => meta,
|
||||
Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(0),
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
if !meta.is_dir() {
|
||||
return Ok(meta.len());
|
||||
}
|
||||
std::fs::read_dir(path)?.map(|entry| size(&entry?.path())).sum()
|
||||
}
|
||||
|
||||
/// Clears `kind` now, or marks it for the next start.
|
||||
pub fn clear(app: AppId, kind: Kind) -> io::Result<()> {
|
||||
let dir = crate::data_dir(app).map_err(io::Error::other)?;
|
||||
if kind.at_next_start() {
|
||||
std::fs::write(dir.join(kind.mark()), b"")?;
|
||||
log::info!("storage: {kind:?} clears at the next start");
|
||||
return Ok(());
|
||||
}
|
||||
// The logger appends; emptying keeps its handle valid.
|
||||
std::fs::OpenOptions::new().write(true).open(dir.join(kind.place()))?.set_len(0)?;
|
||||
log::info!("storage: {kind:?} cleared");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Whether `kind` waits to be cleared at the next start.
|
||||
pub fn clearing(app: AppId, kind: Kind) -> io::Result<bool> {
|
||||
let dir = crate::data_dir(app).map_err(io::Error::other)?;
|
||||
dir.join(kind.mark()).try_exists()
|
||||
}
|
||||
|
||||
/// At start, before anything opens them: the clears asked for.
|
||||
pub fn clear_marked(app: AppId) -> io::Result<()> {
|
||||
clear_marked_in(&crate::data_dir(app).map_err(io::Error::other)?)
|
||||
}
|
||||
|
||||
fn clear_marked_in(dir: &Path) -> io::Result<()> {
|
||||
for kind in Kind::ALL.into_iter().filter(|kind| kind.at_next_start()) {
|
||||
let mark = dir.join(kind.mark());
|
||||
if !mark.try_exists()? {
|
||||
continue;
|
||||
}
|
||||
let place: PathBuf = dir.join(kind.place());
|
||||
match std::fs::remove_dir_all(&place) {
|
||||
Err(error) if error.kind() != io::ErrorKind::NotFound => return Err(error),
|
||||
_ => {}
|
||||
}
|
||||
std::fs::remove_file(&mark)?;
|
||||
log::info!("storage: {kind:?} cleared as asked");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Recovery points kept per document, and space for them (§6.2).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct Limits {
|
||||
pub keep: usize,
|
||||
pub megabytes: u64,
|
||||
}
|
||||
|
||||
impl Default for Limits {
|
||||
fn default() -> Self {
|
||||
Self { keep: 20, megabytes: 64 }
|
||||
}
|
||||
}
|
||||
|
||||
const LIMITS: &str = "recovery";
|
||||
|
||||
/// `app`'s recovery limits; a bad entry is an error.
|
||||
pub fn limits(app: AppId) -> io::Result<Limits> {
|
||||
limits_of(crate::settings::Store::app(app)?.read()?.get(LIMITS))
|
||||
}
|
||||
|
||||
fn limits_of(saved: Option<&serde_json::Value>) -> io::Result<Limits> {
|
||||
let Some(saved) = saved else {
|
||||
return Ok(Limits::default());
|
||||
};
|
||||
let number = |key: &str, least: u64, most: u64| {
|
||||
saved.get(key).and_then(serde_json::Value::as_u64).filter(|n| (least..=most).contains(n)).ok_or_else(|| {
|
||||
io::Error::new(io::ErrorKind::InvalidData, format!("settings: recovery {key} cannot be {saved}"))
|
||||
})
|
||||
};
|
||||
Ok(Limits { keep: number("keep", 1, 100)? as usize, megabytes: number("megabytes", 1, 4096)? })
|
||||
}
|
||||
|
||||
/// Keeps new limits for `app` if `still` holds once locked.
|
||||
pub fn set_limits(app: AppId, limits: Limits, still: impl FnOnce() -> bool) -> io::Result<bool> {
|
||||
let saved = serde_json::json!({ "keep": limits.keep, "megabytes": limits.megabytes });
|
||||
crate::settings::Store::app(app)?.set_if(LIMITS, saved, still)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{Kind, Limits, clear_marked_in, limits_of, size};
|
||||
|
||||
#[test]
|
||||
fn limits_read_strictly() {
|
||||
assert_eq!(limits_of(None).unwrap(), Limits::default());
|
||||
let set = serde_json::json!({"keep": 10, "megabytes": 32});
|
||||
assert_eq!(limits_of(Some(&set)).unwrap(), Limits { keep: 10, megabytes: 32 });
|
||||
for bad in [serde_json::json!({"keep": 0, "megabytes": 32}), serde_json::json!({"keep": "ten"}), serde_json::json!(5)] {
|
||||
assert!(limits_of(Some(&bad)).is_err(), "{bad}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn marked_kinds_go_at_start_and_others_stay() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
std::fs::create_dir_all(dir.path().join("web/Cache")).unwrap();
|
||||
std::fs::write(dir.path().join("web/Cache/blob"), vec![0u8; 4096]).unwrap();
|
||||
std::fs::create_dir_all(dir.path().join("recovery/a")).unwrap();
|
||||
std::fs::write(dir.path().join("recovery/a/point"), b"text").unwrap();
|
||||
assert_eq!(size(&dir.path().join("web")).unwrap(), 4096);
|
||||
std::fs::write(dir.path().join(Kind::Cache.mark()), b"").unwrap();
|
||||
clear_marked_in(dir.path()).unwrap();
|
||||
assert!(!dir.path().join("web").exists(), "the cache went");
|
||||
assert!(dir.path().join("recovery/a/point").exists(), "unmarked kinds stay");
|
||||
assert!(!dir.path().join(Kind::Cache.mark()).exists(), "the mark is spent");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user