Add storage module for presets, config, session, and history persistence

Preset save/load/list/delete/import/export, config JSON persistence,
session state save/restore, and processing history log with append/clear.
All stored as JSON under ~/.config/pixstrip/.
This commit is contained in:
2026-03-06 02:14:57 +02:00
parent 8ced89a00f
commit be7d345aa9
5 changed files with 569 additions and 2 deletions

View File

@@ -7,6 +7,7 @@ pub mod loader;
pub mod operations;
pub mod pipeline;
pub mod preset;
pub mod storage;
pub mod types;
pub fn version() -> &'static str {

View File

@@ -0,0 +1,287 @@
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use crate::config::AppConfig;
use crate::error::{PixstripError, Result};
use crate::preset::Preset;
fn default_config_dir() -> PathBuf {
dirs::config_dir()
.unwrap_or_else(|| PathBuf::from("~/.config"))
.join("pixstrip")
}
fn sanitize_filename(name: &str) -> String {
name.chars()
.map(|c| match c {
'/' | '\\' | '<' | '>' | ':' | '"' | '|' | '?' | '*' => '_',
_ => c,
})
.collect()
}
// --- Preset Store ---
pub struct PresetStore {
presets_dir: PathBuf,
}
impl PresetStore {
pub fn new() -> Self {
Self {
presets_dir: default_config_dir().join("presets"),
}
}
pub fn with_base_dir(base: &Path) -> Self {
Self {
presets_dir: base.join("presets"),
}
}
fn ensure_dir(&self) -> Result<()> {
std::fs::create_dir_all(&self.presets_dir).map_err(PixstripError::Io)
}
fn preset_path(&self, name: &str) -> PathBuf {
let filename = format!("{}.json", sanitize_filename(name));
self.presets_dir.join(filename)
}
pub fn save(&self, preset: &Preset) -> Result<()> {
self.ensure_dir()?;
let path = self.preset_path(&preset.name);
let json = serde_json::to_string_pretty(preset)
.map_err(|e| PixstripError::Preset(e.to_string()))?;
std::fs::write(&path, json).map_err(PixstripError::Io)
}
pub fn load(&self, name: &str) -> Result<Preset> {
let path = self.preset_path(name);
if !path.exists() {
return Err(PixstripError::Preset(format!(
"Preset '{}' not found",
name
)));
}
let data = std::fs::read_to_string(&path).map_err(PixstripError::Io)?;
serde_json::from_str(&data).map_err(|e| PixstripError::Preset(e.to_string()))
}
pub fn list(&self) -> Result<Vec<Preset>> {
if !self.presets_dir.exists() {
return Ok(Vec::new());
}
let mut presets = Vec::new();
for entry in std::fs::read_dir(&self.presets_dir).map_err(PixstripError::Io)? {
let entry = entry.map_err(PixstripError::Io)?;
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) == Some("json") {
let data = std::fs::read_to_string(&path).map_err(PixstripError::Io)?;
if let Ok(preset) = serde_json::from_str::<Preset>(&data) {
presets.push(preset);
}
}
}
presets.sort_by(|a, b| a.name.cmp(&b.name));
Ok(presets)
}
pub fn delete(&self, name: &str) -> Result<()> {
let path = self.preset_path(name);
if !path.exists() {
return Err(PixstripError::Preset(format!(
"Preset '{}' not found",
name
)));
}
std::fs::remove_file(&path).map_err(PixstripError::Io)
}
pub fn export_to_file(&self, preset: &Preset, path: &Path) -> Result<()> {
let json = serde_json::to_string_pretty(preset)
.map_err(|e| PixstripError::Preset(e.to_string()))?;
std::fs::write(path, json).map_err(PixstripError::Io)
}
pub fn import_from_file(&self, path: &Path) -> Result<Preset> {
let data = std::fs::read_to_string(path).map_err(PixstripError::Io)?;
let mut preset: Preset =
serde_json::from_str(&data).map_err(|e| PixstripError::Preset(e.to_string()))?;
preset.is_custom = true;
Ok(preset)
}
}
impl Default for PresetStore {
fn default() -> Self {
Self::new()
}
}
// --- Config Store ---
pub struct ConfigStore {
config_path: PathBuf,
}
impl ConfigStore {
pub fn new() -> Self {
Self {
config_path: default_config_dir().join("config.json"),
}
}
pub fn with_base_dir(base: &Path) -> Self {
Self {
config_path: base.join("config.json"),
}
}
pub fn save(&self, config: &AppConfig) -> Result<()> {
if let Some(parent) = self.config_path.parent() {
std::fs::create_dir_all(parent).map_err(PixstripError::Io)?;
}
let json = serde_json::to_string_pretty(config)
.map_err(|e| PixstripError::Config(e.to_string()))?;
std::fs::write(&self.config_path, json).map_err(PixstripError::Io)
}
pub fn load(&self) -> Result<AppConfig> {
if !self.config_path.exists() {
return Ok(AppConfig::default());
}
let data = std::fs::read_to_string(&self.config_path).map_err(PixstripError::Io)?;
serde_json::from_str(&data).map_err(|e| PixstripError::Config(e.to_string()))
}
}
impl Default for ConfigStore {
fn default() -> Self {
Self::new()
}
}
// --- Session Store ---
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SessionState {
pub last_input_dir: Option<String>,
pub last_output_dir: Option<String>,
pub last_preset_name: Option<String>,
pub current_step: u32,
}
pub struct SessionStore {
session_path: PathBuf,
}
impl SessionStore {
pub fn new() -> Self {
Self {
session_path: default_config_dir().join("session.json"),
}
}
pub fn with_base_dir(base: &Path) -> Self {
Self {
session_path: base.join("session.json"),
}
}
pub fn save(&self, state: &SessionState) -> Result<()> {
if let Some(parent) = self.session_path.parent() {
std::fs::create_dir_all(parent).map_err(PixstripError::Io)?;
}
let json = serde_json::to_string_pretty(state)
.map_err(|e| PixstripError::Config(e.to_string()))?;
std::fs::write(&self.session_path, json).map_err(PixstripError::Io)
}
pub fn load(&self) -> Result<SessionState> {
if !self.session_path.exists() {
return Ok(SessionState::default());
}
let data = std::fs::read_to_string(&self.session_path).map_err(PixstripError::Io)?;
serde_json::from_str(&data).map_err(|e| PixstripError::Config(e.to_string()))
}
}
impl Default for SessionStore {
fn default() -> Self {
Self::new()
}
}
// --- History Store ---
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HistoryEntry {
pub timestamp: String,
pub input_dir: String,
pub output_dir: String,
pub preset_name: Option<String>,
pub total: usize,
pub succeeded: usize,
pub failed: usize,
pub total_input_bytes: u64,
pub total_output_bytes: u64,
pub elapsed_ms: u64,
pub output_files: Vec<String>,
}
pub struct HistoryStore {
history_path: PathBuf,
}
impl HistoryStore {
pub fn new() -> Self {
Self {
history_path: default_config_dir().join("history.json"),
}
}
pub fn with_base_dir(base: &Path) -> Self {
Self {
history_path: base.join("history.json"),
}
}
pub fn add(&self, entry: HistoryEntry) -> Result<()> {
let mut entries = self.list()?;
entries.push(entry);
self.write_all(&entries)
}
pub fn list(&self) -> Result<Vec<HistoryEntry>> {
if !self.history_path.exists() {
return Ok(Vec::new());
}
let data = std::fs::read_to_string(&self.history_path).map_err(PixstripError::Io)?;
if data.trim().is_empty() {
return Ok(Vec::new());
}
serde_json::from_str(&data).map_err(|e| PixstripError::Config(e.to_string()))
}
pub fn clear(&self) -> Result<()> {
self.write_all(&Vec::<HistoryEntry>::new())
}
fn write_all(&self, entries: &[HistoryEntry]) -> Result<()> {
if let Some(parent) = self.history_path.parent() {
std::fs::create_dir_all(parent).map_err(PixstripError::Io)?;
}
let json = serde_json::to_string_pretty(entries)
.map_err(|e| PixstripError::Config(e.to_string()))?;
std::fs::write(&self.history_path, json).map_err(PixstripError::Io)
}
}
impl Default for HistoryStore {
fn default() -> Self {
Self::new()
}
}