дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 19:25:54 +02:00
feat(data): move rebuildable local data with OPENIDEA_LOCAL_DATA_DIR (#2373)
- The local data directory (tools, language servers, index caches) is resolved in one place, ide_host::data_dirs::local_data_dir - The variable must be an absolute path; a missing directory is created, and an unwritable one exits with an error instead of falling back to the system default - Config and log directories still follow the system - Add docs/data-directories.md Co-authored-by: LU XIANGRUI <[email protected]>
Цей коміт міститься в:
1 джерело
16bf9b53a2
коміт
5f970d97d5
4 files changed
+126
-7
No files matched your search
@@ -1,9 +1,39 @@
|
||||
//! Storage policy and best-effort migration of rebuildable desktop data.
|
||||
|
||||
use std::ffi::OsString;
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// Moves the rebuildable local data (tools, language servers, indexes, caches);
|
||||
/// configuration and roaming data keep following the OS.
|
||||
pub const LOCAL_DATA_ENV: &str = "OPENIDEA_LOCAL_DATA_DIR";
|
||||
|
||||
/// `$OPENIDEA_LOCAL_DATA_DIR/<identifier>` when set, else the OS local data directory
|
||||
/// (Tauri's `app_local_data_dir`). The identifier keeps release and per-worktree
|
||||
/// debug builds apart under one root. A set but unusable path is an error, never a
|
||||
/// silent fallback to the OS location.
|
||||
pub fn local_data_dir(identifier: &str) -> Result<PathBuf, String> {
|
||||
resolve_local(std::env::var_os(LOCAL_DATA_ENV), dirs::data_local_dir(), identifier)
|
||||
}
|
||||
|
||||
fn resolve_local(var: Option<OsString>, os: Option<PathBuf>, identifier: &str) -> Result<PathBuf, String> {
|
||||
let Some(root) = var.filter(|v| !v.is_empty()).map(PathBuf::from) else {
|
||||
return os.map(|dir| dir.join(identifier)).ok_or_else(|| "cannot find the local data directory".into());
|
||||
};
|
||||
// The working directory depends on how the IDE was launched.
|
||||
if !root.is_absolute() {
|
||||
return Err(format!("{LOCAL_DATA_ENV} must be an absolute path: {}", root.display()));
|
||||
}
|
||||
let dir = root.join(identifier);
|
||||
let probe = dir.join(".openidea-write-check");
|
||||
fs::create_dir_all(&dir)
|
||||
.and_then(|()| fs::write(&probe, b""))
|
||||
.and_then(|()| fs::remove_file(&probe))
|
||||
.map_err(|error| format!("{LOCAL_DATA_ENV}: cannot write to {}: {error}", dir.display()))?;
|
||||
Ok(dir)
|
||||
}
|
||||
|
||||
/// Unknown entries and recovery/user content deliberately stay in roaming data.
|
||||
pub fn is_local(entry: &Path) -> bool {
|
||||
["tools", "language-server-runtimes", "index", "cache", "lsp", "notebook-runtime", "server", "logs"]
|
||||
@@ -276,6 +306,55 @@ fn relative_wrapper(text: &str, source: &Path) -> String {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn local_dir_follows_the_os_unless_the_variable_is_set() {
|
||||
let os = Some(PathBuf::from("/os-local"));
|
||||
for var in [None, Some(OsString::new())] {
|
||||
assert_eq!(
|
||||
resolve_local(var, os.clone(), "ai.l0ng.openidea").unwrap(),
|
||||
Path::new("/os-local/ai.l0ng.openidea")
|
||||
);
|
||||
}
|
||||
assert!(resolve_local(None, None, "id").is_err());
|
||||
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path().join("missing/nested");
|
||||
let dir = resolve_local(Some(root.clone().into()), os.clone(), "ai.l0ng.openidea").unwrap();
|
||||
assert_eq!(dir, root.join("ai.l0ng.openidea"));
|
||||
assert!(dir.is_dir());
|
||||
assert_eq!(fs::read_dir(&dir).unwrap().count(), 0);
|
||||
// Debug worktrees have their own identifiers and stay apart under one root.
|
||||
assert_ne!(resolve_local(Some(root.clone().into()), None, "dev-a").unwrap(), dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_dir_rejects_relative_and_unwritable_paths() {
|
||||
for relative in ["local", "./local", "../local"] {
|
||||
let error = resolve_local(Some(relative.into()), Some("/os".into()), "id").unwrap_err();
|
||||
assert!(error.contains(LOCAL_DATA_ENV) && error.contains("absolute"), "{error}");
|
||||
}
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let blocker = tmp.path().join("file");
|
||||
fs::write(&blocker, "").unwrap();
|
||||
let error = resolve_local(Some(blocker.into()), Some("/os".into()), "id").unwrap_err();
|
||||
assert!(error.contains("cannot write"), "{error}");
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn local_dir_rejects_an_existing_read_only_directory() {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let dir = tmp.path().join("id");
|
||||
fs::create_dir(&dir).unwrap();
|
||||
fs::set_permissions(&dir, fs::Permissions::from_mode(0o555)).unwrap();
|
||||
// Root ignores permission bits.
|
||||
if fs::write(dir.join("probe"), "").is_err() {
|
||||
assert!(resolve_local(Some(tmp.path().into()), None, "id").unwrap_err().contains("cannot write"));
|
||||
}
|
||||
fs::set_permissions(&dir, fs::Permissions::from_mode(0o755)).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wrappers_accept_mixed_separators_without_rewriting_neighbor_paths() {
|
||||
let text = "@echo off\r\n\"C:\\old\\tools/demo\\payload.exe\" \"C:\\old\\tools/demo-other\\x\" %*\r\n";
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# Data directories
|
||||
|
||||
OpenIDEA keeps its rebuildable data, such as downloaded tools, language servers,
|
||||
JDKs and index caches, in the operating system's local data directory:
|
||||
|
||||
- **Windows:** `%LOCALAPPDATA%\ai.l0ng.openidea`
|
||||
- **macOS:** `~/Library/Application Support/ai.l0ng.openidea`
|
||||
- **Linux:** `~/.local/share/ai.l0ng.openidea`
|
||||
|
||||
To keep this data elsewhere, for example on another drive, set
|
||||
`OPENIDEA_LOCAL_DATA_DIR` to an absolute path before starting OpenIDEA. The IDE
|
||||
then uses `<OPENIDEA_LOCAL_DATA_DIR>/ai.l0ng.openidea` and creates it when
|
||||
missing. If the path is relative or cannot be written, OpenIDEA shows an error
|
||||
and exits instead of falling back to the default location.
|
||||
|
||||
```bat
|
||||
setx OPENIDEA_LOCAL_DATA_DIR "E:\OpenIDEA"
|
||||
```
|
||||
|
||||
Settings, plugins, scratches and other user data stay in the operating system's
|
||||
data and configuration directories. Logs stay in their usual location, so they
|
||||
are still written when the variable is wrong. Existing data is not moved: quit
|
||||
OpenIDEA and move the old directory's contents to the new location, or let the
|
||||
IDE download tools and rebuild indexes again.
|
||||
@@ -89,11 +89,15 @@ pub fn run() {
|
||||
linux_webkit::configure();
|
||||
|
||||
let refresh_shell_path = ide_host::shell_env::inherit_login_shell_path(&context.config().identifier);
|
||||
let local_data_dir = ide_host::data_dirs::local_data_dir(&context.config().identifier);
|
||||
// Managed LSP/DAP installs; must run before other threads start and before PATH is cached.
|
||||
if let Some(data) = dirs::data_local_dir() {
|
||||
ide_tools::extend_process_path(&data.join(&context.config().identifier));
|
||||
ide_dap::adapter::set_tools_root(ide_tools::tools_dir(&data.join(&context.config().identifier)));
|
||||
ide_sdk::java::add_jdk_root(&ide_tools::jdk::jdks_dir(&data.join(&context.config().identifier)));
|
||||
match &local_data_dir {
|
||||
Ok(data) => {
|
||||
ide_tools::extend_process_path(data);
|
||||
ide_dap::adapter::set_tools_root(ide_tools::tools_dir(data));
|
||||
ide_sdk::java::add_jdk_root(&ide_tools::jdk::jdks_dir(data));
|
||||
}
|
||||
Err(error) => tracing::error!("{error}"),
|
||||
}
|
||||
if let Some(refresh) = refresh_shell_path {
|
||||
refresh();
|
||||
@@ -176,7 +180,20 @@ pub fn run() {
|
||||
})
|
||||
.setup(move |app| {
|
||||
builder.mount_events(app);
|
||||
let state = AppState::new(app.handle().clone())?;
|
||||
let local_data_dir = match local_data_dir {
|
||||
Ok(dir) => dir,
|
||||
Err(error) => {
|
||||
use tauri_plugin_dialog::DialogExt;
|
||||
let handle = app.handle().clone();
|
||||
app.dialog()
|
||||
.message(error)
|
||||
.title("OpenIDEA")
|
||||
.kind(tauri_plugin_dialog::MessageDialogKind::Error)
|
||||
.show(move |_| handle.exit(1));
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
let state = AppState::new(app.handle().clone(), local_data_dir)?;
|
||||
let windows = app.state::<WindowsState>();
|
||||
if let Ok(cwd) = std::env::current_dir() {
|
||||
match windows::cli_launch(&std::env::args().collect::<Vec<_>>(), &cwd) {
|
||||
|
||||
@@ -52,7 +52,7 @@ pub struct Startup {
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
pub fn new(app: AppHandle) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
pub fn new(app: AppHandle, local_data_dir: std::path::PathBuf) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
ide_workspace::process::mark_start();
|
||||
let process_to_main = ide_workspace::process::before_main();
|
||||
let process_to_setup = ide_workspace::process::process_age();
|
||||
@@ -67,7 +67,6 @@ impl AppState {
|
||||
}
|
||||
let config_dir = app.path().app_config_dir()?;
|
||||
let data_dir = app.path().app_data_dir()?;
|
||||
let local_data_dir = app.path().app_local_data_dir()?;
|
||||
std::fs::create_dir_all(&config_dir)?;
|
||||
std::fs::create_dir_all(&data_dir)?;
|
||||
if let Err(error) = std::fs::create_dir_all(&local_data_dir) {
|
||||
|
||||
Reference in new issue
Заблокувати користувача