soma_tauri_shell/
persistence.rs1use std::{
9 fs, io,
10 path::{Path, PathBuf},
11};
12
13use serde::{de::DeserializeOwned, Serialize};
14use tauri::{AppHandle, Manager};
15
16use crate::command::CommandResult;
17
18pub fn app_data_path(app: &AppHandle, file_name: &str) -> CommandResult<PathBuf> {
22 app.path()
23 .app_config_dir()
24 .map(|dir| dir.join(file_name))
25 .map_err(|err| format!("failed to resolve app config directory: {err}"))
26}
27
28pub fn read_json_or_default<T: DeserializeOwned + Default>(path: &Path) -> CommandResult<T> {
31 let contents = match fs::read_to_string(path) {
32 Ok(contents) => contents,
33 Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(T::default()),
34 Err(err) => {
35 return Err(format!("failed to read {}: {err}", path.display()));
36 }
37 };
38 parse_json(&contents, path)
39}
40
41pub fn parse_json<T: DeserializeOwned>(contents: &str, path: &Path) -> CommandResult<T> {
45 serde_json::from_str(contents)
46 .map_err(|err| format!("failed to parse {}: {err}", path.display()))
47}
48
49pub fn write_json_atomic<T: Serialize>(path: &Path, value: &T) -> CommandResult<()> {
52 if let Some(parent) = path.parent() {
53 fs::create_dir_all(parent)
54 .map_err(|err| format!("failed to create directory {}: {err}", parent.display()))?;
55 }
56 let body =
57 serde_json::to_string_pretty(value).map_err(|err| format!("failed to serialize: {err}"))?;
58 atomic_write(path, body.as_bytes())
59}
60
61pub fn atomic_write(path: &Path, data: &[u8]) -> CommandResult<()> {
73 let tmp = path.with_extension(format!("tmp-{}", uuid::Uuid::new_v4()));
74 let write = || -> CommandResult<()> {
75 {
76 let mut opts = fs::OpenOptions::new();
77 opts.write(true).create(true).truncate(true);
78
79 #[cfg(unix)]
80 {
81 use std::os::unix::fs::OpenOptionsExt;
82 opts.mode(0o600);
83 }
84
85 let mut file = opts
86 .open(&tmp)
87 .map_err(|err| format!("failed to open {}: {err}", tmp.display()))?;
88
89 use std::io::Write;
90 file.write_all(data)
91 .map_err(|err| format!("failed to write {}: {err}", tmp.display()))?;
92 file.sync_all()
93 .map_err(|err| format!("failed to sync {}: {err}", tmp.display()))?;
94 }
95 fs::rename(&tmp, path)
96 .map_err(|err| format!("failed to rename into {}: {err}", path.display()))
97 };
98 write().inspect_err(|_| {
99 let _ = fs::remove_file(&tmp);
100 })
101}
102
103pub fn env_var_or_none(key: &str) -> Option<String> {
108 std::env::var(key)
109 .ok()
110 .filter(|value| !value.trim().is_empty())
111}
112
113#[cfg(test)]
114#[path = "persistence_tests.rs"]
115mod tests;