fix(linux): keep crash-recovery autosaves in $XDG_STATE_HOME, out of the config folder (#6612) (#6941)

* fix(linux): keep crash-recovery autosaves in $XDG_STATE_HOME, out of the config folder (#6612)

* test(linux): cover the XDG state recovery folder (#6612)
This commit is contained in:
Korg1ch
2026-10-19 13:32:14 -04:00
committed by GitHub
parent 4252fb7743
commit 8c2cfdba6a
3 changed files with 65 additions and 17 deletions
+58 -13
View File
@@ -10,8 +10,8 @@
//! 3. The platform convention: macOS `~/Library/Application Support/Photocraft`, Windows
//! `%APPDATA%\Photocraft`, Linux `$XDG_CONFIG_HOME/photocraft` or `~/.config/photocraft`.
//!
//! One exception: on Windows the platform convention keeps crash-recovery autosaves out of the
//! roaming profile ([`recovery_dir`]).
//! One exception: the platform convention keeps crash-recovery autosaves out of what is synced
//! with the settings: the Windows roaming profile and the Linux config folder ([`recovery_dir`]).
use std::ffi::OsString;
use std::path::{Path, PathBuf};
@@ -77,18 +77,35 @@ pub fn recovery_dir() -> Option<PathBuf> {
})
}
/// The recovery folder for `data` (#5648). Autosaves hold whole documents and are rewritten every
/// few minutes, so on Windows the platform convention puts them in the machine-local
/// `%LOCALAPPDATA%\Photocraft\Recovery`: the roaming `%APPDATA%` may be synced at sign-in and
/// sign-out or capped by policy. They stay out of `%TEMP%`, which cleanup tools may empty before
/// a crashed document is recovered. The override and portable folders keep everything in one
/// place, and other platforms already keep these folders local.
/// The recovery folder for `data`. Autosaves hold whole documents and are rewritten every few
/// minutes, so with the platform convention they stay out of what is synced with the settings:
/// - Windows (#5648): the machine-local `%LOCALAPPDATA%\Photocraft\Recovery`; the roaming
/// `%APPDATA%` may be synced at sign-in and sign-out or capped by policy.
/// - Linux and other XDG systems (#6612): `$XDG_STATE_HOME/photocraft/Recovery`, by default
/// `~/.local/state/photocraft/Recovery`, where the XDG Base Directory spec puts state that
/// should outlive a restart but isn't configuration; dotfile managers and config sync often copy
/// `~/.config` whole. As the spec asks, a relative `XDG_STATE_HOME` is ignored.
/// - macOS: `~/Library/Application Support/Photocraft/Recovery` as before. Apple's guidelines put
/// app-created data that can't be regenerated there; `~/Library/Caches` may be purged by the
/// system.
///
/// They are never in a temp or cache folder, which cleanup tools may empty before a crashed
/// document is recovered. The override and portable folders keep everything in one place.
pub fn resolve_recovery(data: &DataDir, env: impl Fn(&str) -> Option<OsString>) -> Option<PathBuf> {
if cfg!(windows)
&& data.mode == Mode::Platform
&& let Some(local) = env("LOCALAPPDATA").filter(|l| !l.is_empty())
{
return Some(PathBuf::from(local).join("Photocraft").join(RECOVERY));
if data.mode == Mode::Platform {
if cfg!(windows) {
if let Some(local) = env("LOCALAPPDATA").filter(|l| !l.is_empty()) {
return Some(PathBuf::from(local).join("Photocraft").join(RECOVERY));
}
} else if !cfg!(target_os = "macos") {
let state = env("XDG_STATE_HOME")
.map(PathBuf::from)
.filter(|s| s.is_absolute())
.or_else(|| env("HOME").filter(|h| !h.is_empty()).map(|h| PathBuf::from(h).join(".local").join("state")));
if let Some(state) = state {
return Some(state.join("photocraft").join(RECOVERY));
}
}
}
data.dir.as_ref().map(|d| d.join(RECOVERY))
}
@@ -623,6 +640,34 @@ mod tests {
let _ = std::fs::remove_dir_all(&root);
}
/// #6612: on Linux (and other XDG systems) autosaves go to `$XDG_STATE_HOME`, by default
/// `~/.local/state`, not the config folder; a relative `XDG_STATE_HOME` is ignored. Windows
/// and macOS don't read it, and an override or portable folder keeps them inside itself.
#[test]
fn autosaves_go_to_the_xdg_state_folder() {
let root = temp("recovery-xdg");
let settings = root.join("home").join(".config").join("photocraft");
let home = root.join("home");
let state = root.join("state");
let platform = DataDir { dir: Some(settings.clone()), mode: Mode::Platform, warning: None };
let xdg = !cfg!(windows) && !cfg!(target_os = "macos");
let expect = |xdg_path: PathBuf| Some(if xdg { xdg_path } else { settings.join("Recovery") });
assert_eq!(resolve_recovery(&platform, env_of(&[("XDG_STATE_HOME", &state), ("HOME", &home)])), expect(state.join("photocraft").join("Recovery")));
let default = home.join(".local").join("state").join("photocraft").join("Recovery");
assert_eq!(resolve_recovery(&platform, env_of(&[("HOME", &home)])), expect(default.clone()));
let relative = PathBuf::from("relative/state");
assert_eq!(resolve_recovery(&platform, env_of(&[("XDG_STATE_HOME", &relative), ("HOME", &home)])), expect(default.clone()));
let empty = PathBuf::new();
assert_eq!(resolve_recovery(&platform, env_of(&[("XDG_STATE_HOME", &empty), ("HOME", &home)])), expect(default));
// No home at all: beside the settings, as before.
assert_eq!(resolve_recovery(&platform, |_| None), Some(settings.join("Recovery")));
for mode in [Mode::Override, Mode::Portable] {
let d = DataDir { dir: Some(settings.clone()), mode, warning: None };
assert_eq!(resolve_recovery(&d, env_of(&[("XDG_STATE_HOME", &state), ("HOME", &home)])), Some(settings.join("Recovery")), "{mode:?}");
}
let _ = std::fs::remove_dir_all(&root);
}
/// An autosave bundle and its sidecar, as `format::Autosaver` writes them.
fn autosave(dir: &Path, key: &str) {
std::fs::create_dir_all(dir.join(format!("{key}.pcraft"))).unwrap();
+4 -3
View File
@@ -293,7 +293,7 @@ fn block_on<T>(future: impl Future<Output = T>) -> T {
/// Per-user settings directory: `PHOTOCRAFT_CONFIG_DIR`, else `<exe dir>/PhotoCraftData` in
/// portable mode, else the platform convention. Everything the app persists lives under it
/// except, on Windows, crash-recovery autosaves; see [`crate::app_dirs`].
/// except, on Windows and Linux, crash-recovery autosaves; see [`crate::app_dirs`].
pub fn config_dir() -> Option<PathBuf> {
crate::app_dirs::config_dir()
}
@@ -308,8 +308,9 @@ pub fn presets_dir() -> Option<PathBuf> {
config_dir().map(|d| d.join("Presets"))
}
/// Crash-recovery autosaves: under the settings directory, except on Windows, where they stay
/// out of the roaming profile (see [`crate::app_dirs::recovery_dir`]).
/// Crash-recovery autosaves: under the settings directory, except where that is synced with the
/// settings: the Windows roaming profile and the Linux config folder (see
/// [`crate::app_dirs::resolve_recovery`]).
fn recovery_dir() -> Option<PathBuf> {
crate::app_dirs::recovery_dir()
}
+3 -1
View File
@@ -248,7 +248,9 @@ The desktop app stores them in `preferences.json` in the platform config directo
`~/Library/Application Support/Photocraft`, Windows `%APPDATA%\Photocraft`, Linux
`$XDG_CONFIG_HOME/photocraft`; override with `PHOTOCRAFT_CONFIG_DIR`); autosaves go to its
`Recovery` folder (on Windows to `%LOCALAPPDATA%\Photocraft\Recovery`, out of the roaming
profile; autosaves left by earlier versions move there at startup), and the window size and
profile, and on Linux to `$XDG_STATE_HOME/photocraft/Recovery`, by default
`~/.local/state/photocraft/Recovery`, out of the config folder; autosaves left by earlier
versions move there at startup), and the window size and
position, the dock width and floating-panel geometry
to `ui.ron` beside it (a file holding values that would crash startup is discarded). In portable mode (a `portable.txt` or `PhotoCraft.portable` file beside the
executable, as in the Windows portable zip) that directory is `PhotoCraftData` next to the