Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 78 additions & 15 deletions desktop/src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -341,17 +341,23 @@ fn main() {
});
}

/// Returns ~/Documents/StemDeck/, creating it if needed. The Documents
/// Returns ~/Documents/StemDeck/ WITHOUT creating it. The Documents
/// *default* for the jobs folder (documents_dir_for_jobs below) and the
/// source of a pre-#403 user-data.json for one-time migration
/// (documents_store_path) -- chosen so the library is visible in
/// Finder/Explorer, eligible for iCloud/OneDrive backup, and survives app
/// reinstalls, before the user ever relocates it via Settings.
///
/// Deliberately does not mkdir: this is called on every startup just to
/// compute the *default* jobs path, even when the user has relocated their
/// library elsewhere via Settings and this default will never be used. Prior
/// to the fix for #403 (part 2) this always recreated an empty
/// ~/Documents/StemDeck/jobs, since the backend's own ensure_runtime_dirs
/// (app/core/config.py) already mkdirs whichever JOBS_DIR actually wins that
/// precedence -- this path only needs to exist when it is the one in use.
fn documents_stemdeck_dir(app: &tauri::AppHandle) -> Result<PathBuf, String> {
let documents = app.path().document_dir().map_err(|e| e.to_string())?;
let dir = documents.join("StemDeck");
fs::create_dir_all(&dir).map_err(|e| format!("failed to create ~/Documents/StemDeck: {e}"))?;
Ok(dir)
Ok(documents.join("StemDeck"))
}

/// The stems/jobs folder as it exists right now: the backend's own
Expand Down Expand Up @@ -405,24 +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.
/// 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.
/// 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) {
Ok(dir) => {
let jobs = dir.join("jobs");
let _ = fs::create_dir_all(&jobs);
jobs
let legacy_default = match documents_stemdeck_dir(app) {
Ok(dir) => dir.join("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");
}
}
Err(_) => local_data_dir()
.map(|d| d.join("jobs"))
.unwrap_or_else(|_| PathBuf::from("jobs")),
}

legacy_default
}

/// Native folder picker for the stems location. Returns None when the user
Expand Down Expand Up @@ -3912,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();
Expand Down
Loading