Some checks failed
Desktop Release / Bootstrap updater signing key (push) Has been cancelled
Desktop Release / Windows x64 NSIS (push) Has been cancelled
Desktop Release / Linux x64 DEB + AppImage (push) Has been cancelled
Desktop Release / Publish public desktop release (push) Has been cancelled
163 lines
4.8 KiB
Rust
163 lines
4.8 KiB
Rust
use std::{
|
|
collections::VecDeque,
|
|
fs,
|
|
path::{Path, PathBuf},
|
|
sync::Arc,
|
|
};
|
|
|
|
use anyhow::{Context, Result};
|
|
use parking_lot::Mutex;
|
|
use serde::{Deserialize, Serialize};
|
|
use serde_json::json;
|
|
use tauri::{AppHandle, Emitter};
|
|
|
|
use crate::model::{AppConfig, AuthState, JobSummary, ScanEvent};
|
|
|
|
const MAX_JOBS: usize = 200;
|
|
|
|
#[derive(Clone)]
|
|
pub struct SharedState {
|
|
pub config: Arc<Mutex<AppConfig>>,
|
|
pub jobs: Arc<Mutex<VecDeque<JobSummary>>>,
|
|
pub pending: Arc<Mutex<Vec<ScanEvent>>>,
|
|
pub auth: Arc<Mutex<AuthState>>,
|
|
pub data_dir: PathBuf,
|
|
}
|
|
|
|
impl SharedState {
|
|
pub fn load(data_dir: PathBuf) -> Result<Self> {
|
|
fs::create_dir_all(&data_dir).context("create FieldLens Print data directory")?;
|
|
|
|
let mut config = read_json::<AppConfig>(&data_dir.join("config.json")).unwrap_or_default();
|
|
if config.spool_dir.trim().is_empty() {
|
|
config.spool_dir = data_dir.join("spool").to_string_lossy().into_owned();
|
|
}
|
|
if config.client_id == "fieldlens-print" {
|
|
config.client_id = "fieldlens-print-desktop".into();
|
|
}
|
|
fs::create_dir_all(&config.spool_dir).ok();
|
|
|
|
let pending = read_json::<Vec<ScanEvent>>(&data_dir.join("pending.json")).unwrap_or_default();
|
|
let persisted_auth =
|
|
read_json::<PersistedAuth>(&data_dir.join("auth.json")).unwrap_or_default();
|
|
let auth = AuthState {
|
|
refresh_token: persisted_auth.refresh_token,
|
|
..AuthState::default()
|
|
};
|
|
|
|
let state = Self {
|
|
config: Arc::new(Mutex::new(config)),
|
|
jobs: Arc::new(Mutex::new(VecDeque::new())),
|
|
pending: Arc::new(Mutex::new(pending)),
|
|
auth: Arc::new(Mutex::new(auth)),
|
|
data_dir,
|
|
};
|
|
state.persist_config()?;
|
|
Ok(state)
|
|
}
|
|
|
|
pub fn persist_config(&self) -> Result<()> {
|
|
write_json_atomic(&self.data_dir.join("config.json"), &*self.config.lock())
|
|
}
|
|
|
|
pub fn persist_pending(&self) -> Result<()> {
|
|
write_json_atomic(&self.data_dir.join("pending.json"), &*self.pending.lock())
|
|
}
|
|
|
|
pub fn persist_auth(&self) -> Result<()> {
|
|
let path = self.data_dir.join("auth.json");
|
|
let persisted = PersistedAuth {
|
|
refresh_token: self.auth.lock().refresh_token.clone(),
|
|
};
|
|
write_json_atomic(&path, &persisted)?;
|
|
|
|
#[cfg(unix)]
|
|
{
|
|
use std::os::unix::fs::PermissionsExt;
|
|
fs::set_permissions(&path, fs::Permissions::from_mode(0o600))
|
|
.context("secure FieldLens auth state")?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn clear_auth(&self) -> Result<()> {
|
|
*self.auth.lock() = AuthState::default();
|
|
self.persist_auth()
|
|
}
|
|
|
|
pub fn replace_config(&self, next: AppConfig) -> Result<AppConfig> {
|
|
fs::create_dir_all(&next.spool_dir).ok();
|
|
*self.config.lock() = next.clone();
|
|
self.persist_config()?;
|
|
Ok(next)
|
|
}
|
|
|
|
pub fn push_job(&self, app: &AppHandle, job: JobSummary) {
|
|
{
|
|
let mut jobs = self.jobs.lock();
|
|
if let Some(index) = jobs.iter().position(|existing| existing.id == job.id) {
|
|
jobs.remove(index);
|
|
}
|
|
jobs.push_front(job.clone());
|
|
while jobs.len() > MAX_JOBS {
|
|
jobs.pop_back();
|
|
}
|
|
}
|
|
let _ = app.emit("capture-job", &job);
|
|
}
|
|
|
|
pub fn queue_scan(
|
|
&self,
|
|
raw_value: String,
|
|
format: String,
|
|
source: String,
|
|
job_id: &str,
|
|
label: &str,
|
|
) -> ScanEvent {
|
|
let mut pending = self.pending.lock();
|
|
let duplicate = pending.iter().any(|event| event.raw_value == raw_value);
|
|
let event = ScanEvent {
|
|
id: uuid::Uuid::new_v4().to_string(),
|
|
raw_value,
|
|
format,
|
|
duplicate,
|
|
captured_at: chrono::Utc::now().to_rfc3339(),
|
|
source,
|
|
source_metadata: json!({
|
|
"jobId": job_id,
|
|
"label": label,
|
|
}),
|
|
};
|
|
pending.push(event.clone());
|
|
drop(pending);
|
|
let _ = self.persist_pending();
|
|
event
|
|
}
|
|
|
|
pub fn pending_count(&self) -> usize {
|
|
self.pending.lock().len()
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Default, Serialize, Deserialize)]
|
|
struct PersistedAuth {
|
|
refresh_token: Option<String>,
|
|
}
|
|
|
|
fn read_json<T: serde::de::DeserializeOwned>(path: &Path) -> Option<T> {
|
|
let bytes = fs::read(path).ok()?;
|
|
serde_json::from_slice(&bytes).ok()
|
|
}
|
|
|
|
fn write_json_atomic<T: Serialize + ?Sized>(path: &Path, value: &T) -> Result<()> {
|
|
let bytes = serde_json::to_vec_pretty(value)?;
|
|
let temp = path.with_extension("tmp");
|
|
fs::write(&temp, bytes)?;
|
|
if path.exists() {
|
|
fs::remove_file(path)?;
|
|
}
|
|
fs::rename(&temp, path)?;
|
|
Ok(())
|
|
}
|