Akarshan d5ffc6a476
feat: Migrate Jan's API server to llamacpp-extension
Things to ponder:
- Now, the v1/models endpoint of the API server will return an empty
  list if no models are loaded
- Streaming v1/chat/completion routing works as well as v1/models; needs
  further testing
2025-07-07 20:52:00 +05:30

387 lines
11 KiB
Rust

use serde::{Deserialize, Serialize};
use std::{fs, io, path::PathBuf};
use tauri::{AppHandle, Manager, Runtime, State};
use super::{server, setup, state::AppState};
const CONFIGURATION_FILE_NAME: &str = "settings.json";
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct AppConfiguration {
pub data_folder: String,
// Add other fields as needed
}
impl AppConfiguration {
pub fn default() -> Self {
Self {
data_folder: String::from("./data"), // Set a default value for the data_folder
// Add other fields with default values as needed
}
}
}
#[tauri::command]
pub fn get_app_configurations<R: Runtime>(app_handle: tauri::AppHandle<R>) -> AppConfiguration {
let mut app_default_configuration = AppConfiguration::default();
if std::env::var("CI").unwrap_or_default() == "e2e" {
return app_default_configuration;
}
let configuration_file = get_configuration_file_path(app_handle.clone());
let default_data_folder = default_data_folder_path(app_handle.clone());
if !configuration_file.exists() {
log::info!(
"App config not found, creating default config at {:?}",
configuration_file
);
app_default_configuration.data_folder = default_data_folder;
if let Err(err) = fs::write(
&configuration_file,
serde_json::to_string(&app_default_configuration).unwrap(),
) {
log::error!("Failed to create default config: {}", err);
}
return app_default_configuration;
}
match fs::read_to_string(&configuration_file) {
Ok(content) => match serde_json::from_str::<AppConfiguration>(&content) {
Ok(app_configurations) => app_configurations,
Err(err) => {
log::error!(
"Failed to parse app config, returning default config instead. Error: {}",
err
);
app_default_configuration
}
},
Err(err) => {
log::error!(
"Failed to read app config, returning default config instead. Error: {}",
err
);
app_default_configuration
}
}
}
#[tauri::command]
pub fn update_app_configuration(
app_handle: tauri::AppHandle,
configuration: AppConfiguration,
) -> Result<(), String> {
let configuration_file = get_configuration_file_path(app_handle);
log::info!(
"update_app_configuration, configuration_file: {:?}",
configuration_file
);
fs::write(
configuration_file,
serde_json::to_string(&configuration).map_err(|e| e.to_string())?,
)
.map_err(|e| e.to_string())
}
#[tauri::command]
pub fn get_jan_data_folder_path<R: Runtime>(app_handle: tauri::AppHandle<R>) -> PathBuf {
if cfg!(test) {
return PathBuf::from("./data");
}
let app_configurations = get_app_configurations(app_handle);
PathBuf::from(app_configurations.data_folder)
}
#[tauri::command]
pub fn get_jan_extensions_path(app_handle: tauri::AppHandle) -> PathBuf {
get_jan_data_folder_path(app_handle).join("extensions")
}
#[tauri::command]
pub fn get_configuration_file_path<R: Runtime>(app_handle: tauri::AppHandle<R>) -> PathBuf {
let app_path = app_handle.path().app_data_dir().unwrap_or_else(|err| {
log::error!(
"Failed to get app data directory: {}. Using home directory instead.",
err
);
let home_dir = std::env::var(if cfg!(target_os = "windows") {
"USERPROFILE"
} else {
"HOME"
})
.expect("Failed to determine the home directory");
PathBuf::from(home_dir)
});
let package_name = env!("CARGO_PKG_NAME");
#[cfg(target_os = "linux")]
let old_data_dir = {
if let Some(config_path) = dirs::config_dir() {
config_path.join(package_name)
} else {
log::debug!("Could not determine config directory");
app_path
.parent()
.unwrap_or(&app_path.join("../"))
.join(package_name)
}
};
#[cfg(not(target_os = "linux"))]
let old_data_dir = app_path
.parent()
.unwrap_or(&app_path.join("../"))
.join(package_name);
if old_data_dir.exists() {
return old_data_dir.join(CONFIGURATION_FILE_NAME);
} else {
return app_path.join(CONFIGURATION_FILE_NAME);
}
}
#[tauri::command]
pub fn default_data_folder_path<R: Runtime>(app_handle: tauri::AppHandle<R>) -> String {
let mut path = app_handle.path().data_dir().unwrap();
let app_name = std::env::var("APP_NAME")
.unwrap_or_else(|_| app_handle.config().product_name.clone().unwrap());
path.push(app_name);
path.push("data");
let mut path_str = path.to_str().unwrap().to_string();
if let Some(stripped) = path.to_str().unwrap().to_string().strip_suffix(".ai.app") {
path_str = stripped.to_string();
}
path_str
}
#[tauri::command]
pub fn relaunch(app: AppHandle) {
app.restart()
}
#[tauri::command]
pub fn open_app_directory(app: AppHandle) {
let app_path = app.path().app_data_dir().unwrap();
if cfg!(target_os = "windows") {
std::process::Command::new("explorer")
.arg(app_path)
.spawn()
.expect("Failed to open app directory");
} else if cfg!(target_os = "macos") {
std::process::Command::new("open")
.arg(app_path)
.spawn()
.expect("Failed to open app directory");
} else {
std::process::Command::new("xdg-open")
.arg(app_path)
.spawn()
.expect("Failed to open app directory");
}
}
#[tauri::command]
pub fn open_file_explorer(path: String) {
let path = PathBuf::from(path);
if cfg!(target_os = "windows") {
std::process::Command::new("explorer")
.arg(path)
.spawn()
.expect("Failed to open file explorer");
} else if cfg!(target_os = "macos") {
std::process::Command::new("open")
.arg(path)
.spawn()
.expect("Failed to open file explorer");
} else {
std::process::Command::new("xdg-open")
.arg(path)
.spawn()
.expect("Failed to open file explorer");
}
}
#[tauri::command]
pub fn install_extensions(app: AppHandle) {
if let Err(err) = setup::install_extensions(app, true) {
log::error!("Failed to install extensions: {}", err);
}
}
#[tauri::command]
pub fn get_active_extensions(app: AppHandle) -> Vec<serde_json::Value> {
let mut path = get_jan_extensions_path(app);
path.push("extensions.json");
log::info!("get jan extensions, path: {:?}", path);
let contents = fs::read_to_string(path);
let contents: Vec<serde_json::Value> = match contents {
Ok(data) => match serde_json::from_str::<Vec<serde_json::Value>>(&data) {
Ok(exts) => exts
.into_iter()
.map(|ext| {
serde_json::json!({
"url": ext["url"],
"name": ext["name"],
"productName": ext["productName"],
"active": ext["_active"],
"description": ext["description"],
"version": ext["version"]
})
})
.collect(),
Err(error) => {
log::error!("Failed to parse extensions.json: {}", error);
vec![]
}
},
Err(error) => {
log::error!("Failed to read extensions.json: {}", error);
vec![]
}
};
return contents;
}
#[tauri::command]
pub fn get_user_home_path(app: AppHandle) -> String {
return get_app_configurations(app.clone()).data_folder;
}
/// Recursively copy a directory from src to dst
fn copy_dir_recursive(src: &PathBuf, dst: &PathBuf) -> Result<(), io::Error> {
if !dst.exists() {
fs::create_dir_all(dst)?;
}
for entry in fs::read_dir(src)? {
let entry = entry?;
let file_type = entry.file_type()?;
let src_path = entry.path();
let dst_path = dst.join(entry.file_name());
if file_type.is_dir() {
copy_dir_recursive(&src_path, &dst_path)?;
} else {
fs::copy(&src_path, &dst_path)?;
}
}
Ok(())
}
#[tauri::command]
pub fn change_app_data_folder(
app_handle: tauri::AppHandle,
new_data_folder: String,
) -> Result<(), String> {
// Get current data folder path
let current_data_folder = get_jan_data_folder_path(app_handle.clone());
let new_data_folder_path = PathBuf::from(&new_data_folder);
// Create the new data folder if it doesn't exist
if !new_data_folder_path.exists() {
fs::create_dir_all(&new_data_folder_path)
.map_err(|e| format!("Failed to create new data folder: {}", e))?;
}
// Copy all files from the old folder to the new one
if current_data_folder.exists() {
log::info!(
"Copying data from {:?} to {:?}",
current_data_folder,
new_data_folder_path
);
// Check if this is a parent directory to avoid infinite recursion
if new_data_folder_path.starts_with(&current_data_folder) {
return Err(
"New data folder cannot be a subdirectory of the current data folder".to_string(),
);
}
copy_dir_recursive(&current_data_folder, &new_data_folder_path)
.map_err(|e| format!("Failed to copy data to new folder: {}", e))?;
} else {
log::info!("Current data folder does not exist, nothing to copy");
}
// Update the configuration to point to the new folder
let mut configuration = get_app_configurations(app_handle.clone());
configuration.data_folder = new_data_folder;
// Save the updated configuration
update_app_configuration(app_handle, configuration)
}
#[tauri::command]
pub fn app_token(state: State<'_, AppState>) -> Option<String> {
state.app_token.clone()
}
#[tauri::command]
pub async fn start_server(
state: State<'_, AppState>,
host: String,
port: u16,
prefix: String,
api_key: String,
trusted_hosts: Vec<String>,
) -> Result<bool, String> {
let server_handle = state.server_handle.clone();
let sessions = state.llama_server_process.clone();
server::start_server(
server_handle,
sessions,
host,
port,
prefix,
api_key,
vec![trusted_hosts],
)
.await
.map_err(|e| e.to_string())?;
Ok(true)
}
#[tauri::command]
pub async fn stop_server(state: State<'_, AppState>) -> Result<(), String> {
let server_handle = state.server_handle.clone();
server::stop_server(server_handle)
.await
.map_err(|e| e.to_string())?;
Ok(())
}
#[tauri::command]
pub async fn get_server_status(state: State<'_, AppState>) -> Result<bool, String> {
let server_handle = state.server_handle.clone();
Ok(server::is_server_running(server_handle).await)
}
#[tauri::command]
pub async fn read_logs(app: AppHandle) -> Result<String, String> {
let log_path = get_jan_data_folder_path(app).join("logs").join("app.log");
if log_path.exists() {
let content = fs::read_to_string(log_path).map_err(|e| e.to_string())?;
Ok(content)
} else {
Err(format!("Log file not found"))
}
}