use std::collections::HashMap;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use crate::channel::ControlChannel;
use crate::code::Code;
use crate::commands::{run_command, Commands, RunOptions};
use crate::error::SolariError;
use crate::files::Files;
use crate::git::{Git, GitRunner};
use crate::http::{encode_uri_component, new_idempotency_key, HttpOptions, HttpTransport};
use crate::types::{
CommandResult, CreateSandboxRequest, CreateSandboxResponse, Lifecycle, ResumeResponse,
SandboxView, VolumeAttachment,
};
#[derive(Debug, Clone)]
pub struct ClientOptions {
pub api_key: String,
pub base_url: String,
pub call_timeout_ms: Option<u64>,
}
impl ClientOptions {
pub fn new(api_key: impl Into<String>, base_url: impl Into<String>) -> Self {
ClientOptions {
api_key: api_key.into(),
base_url: base_url.into(),
call_timeout_ms: None,
}
}
}
#[derive(Default, Clone)]
pub struct CreateOptions {
pub template: Option<String>,
pub cpu: Option<u32>,
pub mem_mb: Option<u32>,
pub disk_gb: Option<u32>,
pub envs: Option<HashMap<String, String>>,
pub metadata: Option<HashMap<String, String>>,
pub timeout_ms: Option<u64>,
pub from_snapshot: Option<String>,
pub lifecycle: Option<Lifecycle>,
pub resolution: Option<String>,
pub record: Option<bool>,
pub volumes: Option<Vec<VolumeAttachment>>,
}
impl CreateOptions {
pub(crate) fn into_request(self, kind: &str) -> CreateSandboxRequest {
CreateSandboxRequest {
template: self.template,
kind: Some(kind.to_string()),
cpu: self.cpu,
mem_mb: self.mem_mb,
disk_gb: self.disk_gb,
envs: self.envs,
metadata: self.metadata,
timeout_ms: self.timeout_ms,
from_snapshot: self.from_snapshot,
lifecycle: self.lifecycle,
resolution: self.resolution,
record: self.record,
volumes: self.volumes,
}
}
}
impl From<CreateOptions> for CreateSandboxRequest {
fn from(o: CreateOptions) -> Self {
o.into_request("sandbox")
}
}
pub struct Client {
http: Arc<HttpTransport>,
call_timeout_ms: Option<u64>,
}
impl Client {
pub fn new(opts: ClientOptions) -> Result<Self, SolariError> {
let http = HttpTransport::new(HttpOptions::new(opts.api_key, opts.base_url))?;
Ok(Client {
http: Arc::new(http),
call_timeout_ms: opts.call_timeout_ms,
})
}
pub async fn create(&self, opts: CreateOptions) -> Result<Sandbox, SolariError> {
self.create_with_kind(opts, "sandbox").await
}
pub async fn create_desktop(&self, opts: CreateOptions) -> Result<Sandbox, SolariError> {
self.create_with_kind(opts, "desktop").await
}
async fn create_with_kind(
&self,
opts: CreateOptions,
kind: &str,
) -> Result<Sandbox, SolariError> {
let req = opts.into_request(kind);
let body = serde_json::to_value(&req).unwrap();
let resp: CreateSandboxResponse = self
.http
.request("POST", "/sandboxes", Some(body), Some(new_idempotency_key()))
.await?;
Ok(Sandbox::from_response(self.http.clone(), resp, self.call_timeout_ms))
}
pub async fn connect(&self, sandbox_id: &str) -> Result<Sandbox, SolariError> {
let view = self.get(sandbox_id).await?;
let control_url = view.control_url.clone().unwrap_or_else(|| {
format!(
"{}/control/{}",
self.http.ws_origin(),
encode_uri_component(sandbox_id)
)
});
let stream_url = if view.kind == "desktop" {
Some(format!(
"{}/stream/{}",
self.http.ws_origin(),
encode_uri_component(sandbox_id)
))
} else {
None
};
let resp = CreateSandboxResponse {
sandbox_id: view.sandbox_id,
kind: view.kind,
control_url,
expires_at: view.expires_at,
stream_url,
};
Ok(Sandbox::from_response(self.http.clone(), resp, self.call_timeout_ms))
}
pub async fn get(&self, sandbox_id: &str) -> Result<SandboxView, SolariError> {
self.http
.request(
"GET",
&format!("/sandboxes/{}", encode_uri_component(sandbox_id)),
None,
None,
)
.await
}
pub async fn kill(&self, sandbox_id: &str) -> Result<(), SolariError> {
let _: serde_json::Value = self
.http
.request(
"DELETE",
&format!("/sandboxes/{}", encode_uri_component(sandbox_id)),
None,
None,
)
.await?;
Ok(())
}
pub async fn pause(&self, sandbox_id: &str) -> Result<(), SolariError> {
let _: serde_json::Value = self
.http
.request(
"POST",
&format!("/sandboxes/{}/pause", encode_uri_component(sandbox_id)),
None,
None,
)
.await?;
Ok(())
}
pub async fn resume(&self, sandbox_id: &str) -> Result<String, SolariError> {
let r: ResumeResponse = self
.http
.request(
"POST",
&format!("/sandboxes/{}/resume", encode_uri_component(sandbox_id)),
None,
None,
)
.await?;
Ok(r.control_url.unwrap_or_else(|| {
format!(
"{}/control/{}",
self.http.ws_origin(),
encode_uri_component(sandbox_id)
)
}))
}
}
pub struct Sandbox {
id: String,
kind: String,
control_url: String,
stream_url: Option<String>,
expires_at: String,
http: Arc<HttpTransport>,
channel: Arc<ControlChannel>,
}
impl Sandbox {
pub(crate) fn from_response(
http: Arc<HttpTransport>,
resp: CreateSandboxResponse,
call_timeout_ms: Option<u64>,
) -> Self {
let channel = ControlChannel::new(
resp.control_url.clone(),
http.auth_headers(),
call_timeout_ms,
);
Sandbox {
id: resp.sandbox_id,
kind: resp.kind,
control_url: resp.control_url,
stream_url: resp.stream_url,
expires_at: resp.expires_at,
http,
channel: Arc::new(channel),
}
}
pub fn id(&self) -> &str {
&self.id
}
pub fn kind(&self) -> &str {
&self.kind
}
pub fn control_url(&self) -> &str {
&self.control_url
}
pub fn stream_url(&self) -> Option<&str> {
self.stream_url.as_deref()
}
pub fn expires_at(&self) -> &str {
&self.expires_at
}
pub fn connected(&self) -> bool {
self.channel.connected()
}
pub async fn connect(&self) -> Result<(), SolariError> {
self.channel.connect().await
}
pub fn close(&self) {
self.channel.close();
}
pub async fn pause(&self) -> Result<(), SolariError> {
let _: serde_json::Value = self
.http
.request(
"POST",
&format!("/sandboxes/{}/pause", encode_uri_component(&self.id)),
None,
None,
)
.await?;
self.close();
Ok(())
}
pub async fn resume(&mut self) -> Result<(), SolariError> {
let r: ResumeResponse = self
.http
.request(
"POST",
&format!("/sandboxes/{}/resume", encode_uri_component(&self.id)),
None,
None,
)
.await?;
let control_url = r.control_url.unwrap_or_else(|| {
format!(
"{}/control/{}",
self.http.ws_origin(),
encode_uri_component(&self.id)
)
});
self.control_url = control_url.clone();
self.channel.set_control_url(control_url);
self.channel.reconnect().await
}
pub async fn kill(&self) -> Result<(), SolariError> {
let _: serde_json::Value = self
.http
.request(
"DELETE",
&format!("/sandboxes/{}", encode_uri_component(&self.id)),
None,
None,
)
.await?;
self.close();
Ok(())
}
pub fn commands(&self) -> Commands<'_> {
Commands { sb: self }
}
pub fn files(&self) -> Files<'_> {
Files { sb: self }
}
pub fn code(&self) -> Code<'_> {
Code { sb: self }
}
pub fn git(&self) -> Git<'_> {
Git::new(self)
}
pub(crate) fn channel(&self) -> &ControlChannel {
&self.channel
}
pub(crate) fn channel_arc(&self) -> Arc<ControlChannel> {
self.channel.clone()
}
pub(crate) fn http(&self) -> &HttpTransport {
&self.http
}
}
impl GitRunner for Sandbox {
fn run_git<'a>(
&'a self,
args: Vec<String>,
cwd: Option<String>,
) -> Pin<Box<dyn Future<Output = Result<CommandResult, SolariError>> + Send + 'a>> {
Box::pin(async move {
let opts = RunOptions {
args: Some(args),
cwd,
..Default::default()
};
run_command(self, "git", &opts).await
})
}
}