Files
ElyOffice/crates/ely-platform/src/storage.rs
T
ZacharyZhang-NYandClaude Opus 5.5 8a942e6d80 Remove CEF: the web content crate, ElyMark's page, their packaging, licences and tests (C14.5)
PRD and agents.md name the native route; ElyMail's body will not use
CEF or Electron. ElyMark's package carries the KaTeX fonts' OFL.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 03:15:30 -04:00

163 lines
5.5 KiB
Rust

//! 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,
Recovery,
}
impl Kind {
pub const ALL: [Kind; 2] = [Kind::Log, Kind::Recovery];
fn place(self) -> &'static str {
match self {
Kind::Log => "ely.log",
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("recovery/a")).unwrap();
std::fs::write(dir.path().join("recovery/a/point"), vec![0u8; 4096]).unwrap();
std::fs::write(dir.path().join("ely.log"), b"line").unwrap();
assert_eq!(size(&dir.path().join("recovery")).unwrap(), 4096);
std::fs::write(dir.path().join(Kind::Recovery.mark()), b"").unwrap();
clear_marked_in(dir.path()).unwrap();
assert!(!dir.path().join("recovery").exists(), "the copies went");
assert!(dir.path().join("ely.log").exists(), "the log stays");
assert!(!dir.path().join(Kind::Recovery.mark()).exists(), "the mark is spent");
}
}