144 lines
4.3 KiB
Rust
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}");
|
|
}
|
|
}
|
|
}
|
|
}
|