use std::fs;
use std::path::Path;
use std::sync::OnceLock;
use serde::Deserialize;
use crate::api::ApiClient;
use crate::config::Config;
use crate::error::BioLibError;
const SYSTEM_SECRET_PATH: &str = "/biolib/secrets/biolib_system_secret";
#[derive(Debug, Clone, Deserialize)]
struct RuntimeJobData {
version: String,
job_uuid: String,
job_auth_token: String,
app_uri: String,
#[serde(default)]
is_environment_biolib_cloud: bool,
#[serde(default)]
job_requested_machine: Option<String>,
#[serde(default)]
job_requested_machine_spot: bool,
#[serde(default)]
job_reserved_machines: u32,
}
static JOB_DATA: OnceLock<Option<RuntimeJobData>> = OnceLock::new();
fn try_get_job_data() -> Option<&'static RuntimeJobData> {
JOB_DATA
.get_or_init(|| {
let content = fs::read_to_string(SYSTEM_SECRET_PATH).ok()?;
let data: RuntimeJobData = serde_json::from_str(&content).ok()?;
if !data.version.starts_with("1.") {
return None;
}
Some(data)
})
.as_ref()
}
fn get_job_data() -> crate::Result<&'static RuntimeJobData> {
try_get_job_data().ok_or_else(|| {
BioLibError::General("The runtime is not recognized as a BioLib app".to_string())
})
}
fn validate_secret_name(name: &str) -> crate::Result<()> {
if name
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
{
Ok(())
} else {
Err(BioLibError::Validation(
"Secret name can only contain alphanumeric characters and dashes or underscores"
.to_string(),
))
}
}
pub struct Runtime;
impl Runtime {
pub fn check_is_environment_biolib_app() -> bool {
try_get_job_data().is_some()
}
pub fn check_is_environment_biolib_cloud() -> crate::Result<bool> {
Ok(get_job_data()?.is_environment_biolib_cloud)
}
pub fn get_job_id() -> crate::Result<String> {
Ok(get_job_data()?.job_uuid.clone())
}
pub fn get_job_auth_token() -> crate::Result<String> {
Ok(get_job_data()?.job_auth_token.clone())
}
pub fn get_job_requested_machine() -> crate::Result<Option<String>> {
let data = get_job_data()?;
Ok(data.job_requested_machine.clone().filter(|m| !m.is_empty()))
}
pub fn is_spot_machine_requested() -> crate::Result<bool> {
Ok(get_job_data()?.job_requested_machine_spot)
}
pub fn get_app_uri() -> crate::Result<String> {
Ok(get_job_data()?.app_uri.clone())
}
pub fn get_max_workers() -> crate::Result<u32> {
Ok(get_job_data()?.job_reserved_machines)
}
pub fn get_secret(secret_name: &str) -> crate::Result<Vec<u8>> {
validate_secret_name(secret_name)?;
let path = format!("/biolib/secrets/{secret_name}");
fs::read(&path).map_err(|_| {
BioLibError::General(format!("Unable to get system secret: {secret_name}"))
})
}
pub fn get_temporary_client_secret(secret_name: &str) -> crate::Result<Vec<u8>> {
validate_secret_name(secret_name)?;
let path = format!("/biolib/temporary-client-secrets/{secret_name}");
fs::read(&path)
.map_err(|_| BioLibError::General(format!("Unable to get secret: {secret_name}")))
}
pub fn set_main_result_prefix(result_prefix: &str) -> crate::Result<()> {
let data = get_job_data()?;
let config = Config::load();
let mut client = ApiClient::new(&config)?;
let mut headers = std::collections::HashMap::new();
headers.insert("Job-Auth-Token".to_string(), data.job_auth_token.clone());
let body = serde_json::json!({"result_name_prefix": result_prefix});
let path = format!("/jobs/{}/main_result/", data.job_uuid);
client.patch_json_with_headers(&path, &body, &headers)?;
Ok(())
}
pub fn set_result_name_prefix(result_prefix: &str) -> crate::Result<()> {
Self::set_main_result_prefix(result_prefix)
}
pub fn set_result_name_prefix_from_fasta(path_to_fasta: &str) -> crate::Result<()> {
let file_name = Path::new(path_to_fasta)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(path_to_fasta);
let result_name = if is_biolib_generated_name(file_name) {
let content = fs::read_to_string(path_to_fasta).map_err(|err| {
BioLibError::General(format!(
"Failed to set result name from fasta file {path_to_fasta}: {err}"
))
})?;
match parse_first_fasta_id(&content) {
Some(id) => {
let truncated: String = id.replace(' ', "_").chars().take(60).collect();
truncated
}
None => return Ok(()),
}
} else {
file_name.to_string()
};
Self::set_result_name_prefix(&result_name)
}
pub fn set_result_name_from_file(path_to_file: &str) -> crate::Result<()> {
if path_to_file.to_lowercase().ends_with(".fasta") {
return Self::set_result_name_prefix_from_fasta(path_to_file);
}
let file_name = Path::new(path_to_file)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(path_to_file);
if !is_biolib_generated_name(file_name) {
let result_name: String = file_name.chars().take(60).collect();
Self::set_result_name_prefix(&result_name)?;
}
Ok(())
}
pub fn set_result_name_from_string(result_name: &str) -> crate::Result<()> {
let truncated: String = result_name.chars().take(60).collect();
Self::set_result_name_prefix(&truncated)
}
pub fn create_result_note(note: &str) -> crate::Result<()> {
let job_id = Self::get_job_id()?;
let config = Config::load();
let mut client = ApiClient::new(&config)?;
let body = serde_json::json!({"note": note});
client.post_json(&format!("/jobs/{job_id}/notes/"), &body)?;
Ok(())
}
pub fn get_oauth_access_token(scope: &str) -> crate::Result<String> {
get_job_data()?;
let config = Config::load();
let client = ApiClient::new(&config)?;
let body = serde_json::json!({"scope": scope});
let body_bytes = serde_json::to_vec(&body)?;
let url = format!("{}/api/auth/oauth-token-exchange/", client.base_url());
let http = crate::api::HttpClient::shared();
let response = http.request("POST", &url, Some(&body_bytes), None, 5, None)?;
#[derive(Deserialize)]
struct OAuthResponse {
access_token: Option<String>,
}
let oauth_response: OAuthResponse = response.json()?;
oauth_response
.access_token
.ok_or_else(|| BioLibError::General("Failed to get OAuth access token".to_string()))
}
}
fn is_biolib_generated_name(name: &str) -> bool {
let mut chars = name.chars();
if !name.starts_with("input_") {
return false;
}
for _ in 0..6 {
chars.next();
}
chars
.next()
.map(|c| c.is_ascii_alphanumeric())
.unwrap_or(false)
}
fn parse_first_fasta_id(content: &str) -> Option<String> {
for line in content.lines() {
if let Some(header) = line.strip_prefix('>') {
let id = header.split_whitespace().next().unwrap_or(header);
if !id.is_empty() {
return Some(id.to_string());
}
}
}
None
}