Files
DarkRoom/platform/dr-plat/src/state.rs
T
dtourolle ef1154af94 Resolve every base directory in one place, and on Windows
Five sites each read XDG_*_HOME and fell back to $HOME/.local/… on
their own, which is fine on Linux and wrong everywhere else: Windows
sets neither variable, so every one of them degraded to a path
relative to the working directory — for a Start Menu launch,
C:\Windows\System32. The models lookup walked XDG_DATA_DIRS the same
way.

dr_plat::dirs now holds the rule per platform: XDG on Unix, the known
folders on Windows — %APPDATA% for config, which roams, and
%LOCALAPPDATA% for data and state, which do not — and the executable's
own directory as the system data dir, which is where the installer
puts the models. The Android overrides stay where they were; only the
fallback behind them moved. Both rule sets are unit-tested on either
host, and the Windows one was confirmed by running the application
under Wine: its log landed in AppData\Local\darkroom\state and nothing
was written anywhere else.
2026-09-12 07:34:05 +02:00

92 lines
4.2 KiB
Rust

//! TRACES: NFR-OPS-1
//! Where this platform lets the application keep notes about itself.
//!
//! Not the catalog, not the photographs, not the user's configuration — those
//! three already have homes (`dr_sync::account::config_dir`,
//! `dr_ui::library::data_root`, and the sidecars beside the images). This is
//! the fourth thing: the diagnostic residue an application leaves so that a
//! failure yesterday can be read about today. A log, and — when the crash
//! path lands — a crash record.
//!
//! # Why it is its own directory and not one of the other three
//!
//! XDG separates them for a reason that is not tidiness. `$XDG_CONFIG_HOME`
//! is what the user has chosen and would miss; `$XDG_DATA_HOME` is what the
//! application built and would have to rebuild; `$XDG_STATE_HOME` is
//! "state that should persist between restarts but is not important or
//! portable enough for the data directory" — which is exactly a log file. It
//! is also the directory nobody backs up, and a log is the one file here we
//! actively want a user to be able to delete without consequence.
//!
//! # Android has none of those variables, so the platform must say
//!
//! There is no `$HOME` on Android and no XDG anything (ARCH §6.9), so the
//! guesses below resolve to a path the app cannot write. `dr_sync::account`
//! learned this the expensive way — the session list went to a doomed path,
//! nothing failed loudly, and backgrounding the app lost the sign-in — and the
//! shape of the fix is copied here deliberately: the platform entry point
//! declares the directory once, before anything opens a file in it.
//!
//! **Which Android directory is a diagnostics decision, and it belongs to the
//! caller.** `AndroidApp` offers two, and they differ in precisely the way
//! that matters here:
//!
//! * `internal_data_path` — `/data/data/<pkg>/files`. Private, durable, and
//! unreachable: pulling a file out of it needs `run-as` against a debuggable
//! build, or root.
//! * `external_data_path` — `/sdcard/Android/data/<pkg>/files`. Same lifetime
//! (the system deletes it with the app, not under storage pressure — that is
//! the *cache* directory), needs no permission since API 19, and `adb pull`
//! reads it from any build.
//!
//! A log nobody can retrieve is not a diagnostic, so `darkroom-android` points
//! this at the external one. That choice has a consequence, and it is the
//! reason [`crate::diagnostics`] redacts at the sink rather than trusting call
//! sites: everything written here is readable by anyone holding the device.
use std::path::PathBuf;
use std::sync::OnceLock;
/// Declared once by the platform entry point; a guess otherwise.
static STATE_DIR: OnceLock<PathBuf> = OnceLock::new();
/// Declare where this platform keeps application state.
///
/// Call it before anything opens a file — installing the logger is normally
/// the very next line — because a later call is *ignored* rather than obeyed.
/// That is deliberate: two callers disagreeing about the directory would
/// otherwise split the log across two files depending on which ran first, and
/// a silently-ignored second call leaves one log rather than two halves.
///
/// Desktop needs no call. The XDG resolution below is correct there.
pub fn set_state_dir(dir: PathBuf) {
let _ = STATE_DIR.set(dir);
}
/// The directory this application's state belongs in.
///
/// It is not created here. Whoever writes into it creates it, so that merely
/// asking the question leaves nothing behind on a machine that never logs.
pub fn state_dir() -> PathBuf {
if let Some(dir) = STATE_DIR.get() {
return dir.clone();
}
crate::dirs::base_dir(crate::dirs::Base::State)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_declared_directory_is_declared_once() {
// The property the entry points rely on: two callers cannot split the
// log in half. Exercised on a fresh `OnceLock` rather than the global
// one, which any other test in this binary may already have set.
let cell: OnceLock<PathBuf> = OnceLock::new();
assert!(cell.set(PathBuf::from("/first")).is_ok());
assert!(cell.set(PathBuf::from("/second")).is_err());
assert_eq!(cell.get(), Some(&PathBuf::from("/first")));
}
}