feat: default new portable installs' jobs folder inside the package

Windows portable builds already keep cache/models/settings.json next to
StemDeck.exe (local_data_dir(), #399); the jobs default never plugged into
that and always resolved to ~/Documents/StemDeck/jobs regardless of
portable-ness.

documents_dir_for_jobs() now defaults new portable installs to
local_data_dir()/jobs instead. Existing installs are unaffected: if
~/Documents/StemDeck/jobs already has anything in it, that stays the
resolved default (checked against disk content directly, since there is no
explicit jobs_dir in settings.json to record an implicit default -- and
only the backend writes that file). Non-portable installs (installer
builds, macOS, Linux) are untouched either way.
This commit is contained in:
Thales
2026-08-21 22:04:14 +01:00
parent 67ecf3dde6
commit e5870e5782
+66 -7
View File
@@ -411,22 +411,62 @@ fn documents_store_path(app: &tauri::AppHandle) -> Result<PathBuf, String> {
Ok(new_path)
}
/// The DEFAULT stems folder: ~/Documents/StemDeck/jobs/. Falls back to
/// data_dir/jobs if document_dir is unavailable. Does NOT create it -- see
/// documents_stemdeck_dir for why.
/// True if `path` exists and contains at least one entry. Used to tell an
/// already-in-use default folder apart from one nothing has ever written to.
fn directory_has_entries(path: &Path) -> bool {
fs::read_dir(path)
.map(|mut entries| entries.next().is_some())
.unwrap_or(false)
}
/// The DEFAULT stems folder. Does NOT create it -- see documents_stemdeck_dir
/// for why.
///
/// Handed to the backend as STEMDECK_DEFAULT_JOBS_DIR, not STEMDECK_JOBS_DIR:
/// the latter means "this deployment pins the location" and would override the
/// folder the user picked in Settings (#354). The backend owns that choice; it
/// is the one that has to move the library when it changes, including
/// creating whichever path wins (app/core/config.py's ensure_runtime_dirs).
///
/// Two candidates, resolved in this order:
///
/// 1. ~/Documents/StemDeck/jobs, if it already has anything in it. Every
/// install before this default existed used this path, so an existing
/// user's real library lives there without any explicit `jobs_dir` in
/// settings.json to record it -- it was simply "the default." Checking
/// disk content directly (rather than writing a one-time migration flag
/// into settings.json, which only the backend otherwise writes) keeps this
/// self-contained: nothing to persist, no other-process race, and it stays
/// correct on every future launch for as long as that folder holds data.
/// 2. Otherwise, for the Windows portable package, local_data_dir()/jobs --
/// i.e. next to data/cache and data/models inside the package itself,
/// rather than leaving a footprint in Documents. Non-portable installs
/// (installer builds, macOS, Linux) keep candidate 1 either way: the
/// original Documents rationale (visible in Finder/Explorer, eligible for
/// OneDrive/iCloud backup, survives reinstalls) still applies to them.
fn documents_dir_for_jobs(app: &tauri::AppHandle) -> PathBuf {
match documents_stemdeck_dir(app) {
let legacy_default = match documents_stemdeck_dir(app) {
Ok(dir) => dir.join("jobs"),
Err(_) => local_data_dir()
.map(|d| d.join("jobs"))
.unwrap_or_else(|_| PathBuf::from("jobs")),
Err(_) => {
return local_data_dir()
.map(|d| d.join("jobs"))
.unwrap_or_else(|_| PathBuf::from("jobs"));
}
};
if directory_has_entries(&legacy_default) {
return legacy_default;
}
if let Ok(root) = app_root() {
if is_portable_package(&root) {
if let Ok(data_dir) = local_data_dir() {
return data_dir.join("jobs");
}
}
}
legacy_default
}
/// Native folder picker for the stems location. Returns None when the user
@@ -3916,6 +3956,25 @@ b6052160df96b31c9b1e33854a4dcda3d4b57641b880270f31736fb9f445d384 ffmpeg-n7.1-la
assert!(super::is_portable_package(root.path()));
}
#[test]
fn directory_has_entries_true_for_a_populated_dir() {
let dir = make_tmp();
fs::write(dir.path().join("registry.json"), "{}").unwrap();
assert!(super::directory_has_entries(dir.path()));
}
#[test]
fn directory_has_entries_false_for_an_empty_dir() {
let dir = make_tmp();
assert!(!super::directory_has_entries(dir.path()));
}
#[test]
fn directory_has_entries_false_for_a_missing_dir() {
let dir = make_tmp();
assert!(!super::directory_has_entries(&dir.path().join("does-not-exist")));
}
#[test]
fn stale_data_dir_marker_is_removed_for_gpu_builds() {
let root = make_tmp();