Files
cursor-byok/server/src/config.rs
T

179 lines
5.6 KiB
Rust

use std::{env, fs, net::SocketAddr, path::PathBuf, time::Duration};
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use crate::{Error, Result};
const DATA_DIR_NAME: &str = ".cursor-byok-v3";
const DATABASE_FILE_NAME: &str = "cursor-byok.db";
const V0049_DATA_DIR_NAME: &str = ".cursor-local-assistant-v2";
const V0049_CONFIG_FILE_NAME: &str = "config.yaml";
const DEFAULT_PROVIDER_REQUEST_TIMEOUT: Duration = Duration::from_secs(3000);
pub fn managed_data_dir() -> Result<PathBuf> {
let home_dir = dirs::home_dir()
.ok_or_else(|| Error::Config("cannot resolve user home directory".into()))?;
let data_dir = home_dir.join(DATA_DIR_NAME);
fs::create_dir_all(&data_dir)?;
#[cfg(unix)]
fs::set_permissions(&data_dir, fs::Permissions::from_mode(0o700))?;
Ok(data_dir)
}
pub fn v0049_config_path() -> Result<PathBuf> {
let home_dir = dirs::home_dir()
.ok_or_else(|| Error::Config("cannot resolve user home directory".into()))?;
Ok(home_dir
.join(V0049_DATA_DIR_NAME)
.join(V0049_CONFIG_FILE_NAME))
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ProviderKind {
OpenAiChat,
OpenAiResponses,
Anthropic,
}
#[derive(Clone)]
pub struct ProviderConfig {
pub kind: ProviderKind,
pub request_url: String,
pub api_key: String,
pub custom_headers: reqwest::header::HeaderMap,
pub max_output_tokens: Option<u64>,
pub request_timeout: Duration,
}
#[derive(Clone)]
pub struct Config {
pub listen_addr: SocketAddr,
pub database_url: String,
pub provider_request_timeout: Duration,
pub console: Option<ConsoleSource>,
pub use_persisted_ports: bool,
}
#[derive(Clone)]
pub enum ConsoleSource {
Directory(PathBuf),
Proxy(url::Url),
}
impl Config {
pub fn from_env() -> Result<Self> {
let listen_addr = env::var("CURSOR_LISTEN_ADDR")
.unwrap_or_else(|_| "127.0.0.1:3000".into())
.parse()
.map_err(|error| Error::Config(format!("invalid CURSOR_LISTEN_ADDR: {error}")))?;
let request_timeout = match env::var("CURSOR_PROVIDER_TIMEOUT_SECONDS") {
Ok(value) => Duration::from_secs(value.parse().map_err(|error| {
Error::Config(format!("invalid CURSOR_PROVIDER_TIMEOUT_SECONDS: {error}"))
})?),
Err(env::VarError::NotPresent) => DEFAULT_PROVIDER_REQUEST_TIMEOUT,
Err(error) => {
return Err(Error::Config(format!(
"invalid CURSOR_PROVIDER_TIMEOUT_SECONDS: {error}"
)))
}
};
let console_dir = env::var_os("CURSOR_CONSOLE_DIR").map(PathBuf::from);
let console_proxy = env::var("CURSOR_CONSOLE_PROXY")
.ok()
.map(|value| {
value.parse().map_err(|error| {
Error::Config(format!("invalid CURSOR_CONSOLE_PROXY: {error}"))
})
})
.transpose()?;
let console = match (console_dir, console_proxy) {
(Some(_), Some(_)) => {
return Err(Error::Config(
"CURSOR_CONSOLE_DIR and CURSOR_CONSOLE_PROXY cannot both be set".into(),
))
}
(Some(directory), None) => Some(ConsoleSource::Directory(directory)),
(None, Some(proxy)) => Some(ConsoleSource::Proxy(proxy)),
(None, None) => None,
};
Ok(Self {
listen_addr,
database_url: database_url_from_env()?,
provider_request_timeout: request_timeout,
console,
use_persisted_ports: false,
})
}
pub fn desktop() -> Result<Self> {
Ok(Self {
listen_addr: "127.0.0.1:0"
.parse()
.expect("desktop listen address is static"),
database_url: default_database_url()?,
provider_request_timeout: DEFAULT_PROVIDER_REQUEST_TIMEOUT,
console: None,
use_persisted_ports: true,
})
}
}
fn database_url_from_env() -> Result<String> {
match env::var("CURSOR_DATABASE_URL") {
Ok(database_url) => Ok(database_url),
Err(env::VarError::NotPresent) => default_database_url(),
Err(error) => Err(Error::Config(format!(
"invalid CURSOR_DATABASE_URL: {error}"
))),
}
}
fn default_database_url() -> Result<String> {
let data_dir = managed_data_dir()?;
database_url_for_dir(&data_dir)
}
#[cfg(test)]
fn database_url_in(home_dir: &std::path::Path) -> Result<String> {
let data_dir = home_dir.join(DATA_DIR_NAME);
fs::create_dir_all(&data_dir)?;
#[cfg(unix)]
fs::set_permissions(&data_dir, fs::Permissions::from_mode(0o700))?;
database_url_for_dir(&data_dir)
}
fn database_url_for_dir(data_dir: &std::path::Path) -> Result<String> {
let database_path = data_dir.join(DATABASE_FILE_NAME);
let database_path = database_path
.to_str()
.ok_or_else(|| Error::Config("database path is not valid UTF-8".into()))?;
Ok(format!("sqlite://{database_path}"))
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn managed_database_supports_home_paths_with_spaces() {
let directory = tempfile::tempdir().unwrap();
let home_dir = directory.path().join("home with spaces");
let database_url = database_url_in(&home_dir).unwrap();
let store = crate::store::Store::connect(&database_url).await.unwrap();
drop(store);
let data_dir = home_dir.join(DATA_DIR_NAME);
assert!(data_dir.join(DATABASE_FILE_NAME).is_file());
#[cfg(unix)]
assert_eq!(
fs::metadata(data_dir).unwrap().permissions().mode() & 0o777,
0o700
);
}
}