Graphite/frontend/iced/src/persist.rs

144 lines
4.3 KiB
Rust

use graphite_editor::messages::frontend::utility_types::PersistedState;
use graphite_editor::messages::prelude::DocumentId;
use std::path::PathBuf;
const APP_DIRECTORY_NAME: &str = if cfg!(target_os = "linux") { "graphite" } else { "Graphite" };
const STATE_FILE_NAME: &str = "state.json";
const PREFERENCES_FILE_NAME: &str = "preferences.json";
const DOCUMENTS_DIRECTORY_NAME: &str = "documents";
const DOCUMENT_FILE_EXTENSION: &str = "graphite";
fn root_dir() -> Option<PathBuf> {
let base = dirs::config_local_dir().or_else(dirs::data_local_dir)?;
let path = base.join(APP_DIRECTORY_NAME);
if let Err(e) = std::fs::create_dir_all(&path) {
tracing::warn!("failed to create graphite config directory at {path:?}: {e}");
return None;
}
Some(path)
}
fn documents_dir() -> Option<PathBuf> {
let path = root_dir()?.join(DOCUMENTS_DIRECTORY_NAME);
if let Err(e) = std::fs::create_dir_all(&path) {
tracing::warn!("failed to create documents directory at {path:?}: {e}");
return None;
}
Some(path)
}
fn state_path() -> Option<PathBuf> {
Some(root_dir()?.join(STATE_FILE_NAME))
}
fn preferences_path() -> Option<PathBuf> {
Some(root_dir()?.join(PREFERENCES_FILE_NAME))
}
fn document_path(id: DocumentId) -> Option<PathBuf> {
Some(documents_dir()?.join(format!("{:x}.{}", id.0, DOCUMENT_FILE_EXTENSION)))
}
pub fn read_state() -> Option<PersistedState> {
let path = state_path()?;
let raw = match std::fs::read_to_string(&path) {
Ok(raw) => raw,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return None,
Err(e) => {
tracing::warn!("failed to read persisted state from {path:?}: {e}");
return None;
}
};
match serde_json::from_str::<PersistedState>(&raw) {
Ok(state) => Some(state),
Err(e) => {
tracing::warn!("failed to parse persisted state at {path:?}: {e}");
None
}
}
}
pub fn write_state(state: &PersistedState) {
let Some(path) = state_path() else { return };
let serialized = match serde_json::to_string_pretty(state) {
Ok(s) => s,
Err(e) => {
tracing::warn!("failed to serialize persisted state: {e}");
return;
}
};
if let Err(e) = std::fs::write(&path, serialized) {
tracing::warn!("failed to write persisted state to {path:?}: {e}");
return;
}
garbage_collect_documents(state);
}
pub fn read_document(document_id: DocumentId) -> Option<String> {
let path = document_path(document_id)?;
match std::fs::read_to_string(&path) {
Ok(content) => Some(content),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => None,
Err(e) => {
tracing::warn!("failed to read document {document_id:?} from {path:?}: {e}");
None
}
}
}
pub fn write_document(document_id: DocumentId, content: &str) {
let Some(path) = document_path(document_id) else { return };
if let Err(e) = std::fs::write(&path, content) {
tracing::warn!("failed to write document {document_id:?} to {path:?}: {e}");
}
}
pub fn delete_document(document_id: DocumentId) {
let Some(path) = document_path(document_id) else { return };
match std::fs::remove_file(&path) {
Ok(()) => {}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
Err(e) => tracing::warn!("failed to delete document {document_id:?} at {path:?}: {e}"),
}
}
pub fn read_preferences() -> Option<String> {
let path = preferences_path()?;
match std::fs::read_to_string(&path) {
Ok(raw) => Some(raw),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => None,
Err(e) => {
tracing::warn!("failed to read preferences from {path:?}: {e}");
None
}
}
}
pub fn write_preferences(json: &str) {
let Some(path) = preferences_path() else { return };
if let Err(e) = std::fs::write(&path, json) {
tracing::warn!("failed to write preferences to {path:?}: {e}");
}
}
fn garbage_collect_documents(state: &PersistedState) {
let Some(dir) = documents_dir() else { return };
let valid: std::collections::HashSet<PathBuf> = state.documents.iter().filter_map(|doc| document_path(doc.id)).collect();
let entries = match std::fs::read_dir(&dir) {
Ok(entries) => entries,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return,
Err(e) => {
tracing::warn!("failed to scan documents directory at {dir:?}: {e}");
return;
}
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_file() && !valid.contains(&path) {
if let Err(e) = std::fs::remove_file(&path) {
tracing::warn!("failed to remove orphaned document at {path:?}: {e}");
}
}
}
}