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.
92 lines
4.2 KiB
Rust
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")));
|
|
}
|
|
}
|