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 { 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 { 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 { Some(root_dir()?.join(STATE_FILE_NAME)) } fn preferences_path() -> Option { Some(root_dir()?.join(PREFERENCES_FILE_NAME)) } fn document_path(id: DocumentId) -> Option { Some(documents_dir()?.join(format!("{:x}.{}", id.0, DOCUMENT_FILE_EXTENSION))) } pub fn read_state() -> Option { 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::(&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 { 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 { 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 = 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}"); } } } }