use std::borrow::Cow;
use async_stream::stream;
use futures_util::StreamExt;
use reqwest::multipart::{Form, Part};
use crate::{
Endpoint,
http::{
ApiClient,
errors::{ApiError, CommitError},
},
resources::FileResource,
types::{AppInfo, AppLogs, AppMetrics, RealtimeEvent, RuntimeStats},
};
pub struct AppResource {
pub id: String,
pub(crate) client: ApiClient,
}
impl AppResource {
pub fn new(http: ApiClient, id: &str) -> Self {
Self {
client: http,
id: id.to_string(),
}
}
pub fn realtime(
&self,
) -> futures_util::stream::BoxStream<
'static,
Result<RealtimeEvent, ApiError>,
> {
let client = self.client.clone();
let id = self.id.clone();
let endpoint = Endpoint::sse_realtime_app_logs(&id);
Box::pin(stream! {
let mut bytes = client
.http_client
.request(endpoint.method, client.url(&endpoint.path))
.send()
.await
.map_err(ApiError::Transport)?
.bytes_stream();
let mut buffer = Vec::<u8>::new();
let mut event_type = String::new();
let mut data = String::new();
while let Some(chunk) = bytes.next().await {
let chunk = chunk.map_err(ApiError::Transport)?;
buffer.extend_from_slice(&chunk);
while let Some(pos) =
buffer.iter().position(|&b| b == b'\n')
{
let line =
String::from_utf8_lossy(&buffer[..pos]).into_owned();
buffer.drain(..=pos);
let line = line.trim_end_matches('\r');
if line.is_empty() {
if !data.is_empty() {
let event = match event_type.as_str() {
"logs" => RealtimeEvent::Log(data.clone()),
_ => RealtimeEvent::System(data.clone()),
};
yield Ok(event);
}
event_type.clear();
data.clear();
} else if let Some(val) = line.strip_prefix("event:") {
event_type = val.trim().to_string();
} else if let Some(val) = line.strip_prefix("data:") {
if !data.is_empty() {
data.push('\n');
}
data.push_str(val.trim());
}
}
}
})
}
pub fn file(&self, path: &str) -> FileResource {
FileResource::new(self.client.clone(), path, &self.id)
}
pub async fn start(&self) -> Result<bool, ApiError> {
self.client
.request_endpoint::<()>(Endpoint::app_start(&self.id))
.await?
.into_bool_result()
}
pub async fn restart(&self) -> Result<bool, ApiError> {
self.client
.request_endpoint::<()>(Endpoint::app_restart(&self.id))
.await?
.into_bool_result()
}
pub async fn stop(&self) -> Result<bool, ApiError> {
self.client
.request_endpoint::<()>(Endpoint::app_stop(&self.id))
.await?
.into_bool_result()
}
pub async fn status(&self) -> Result<RuntimeStats, ApiError> {
self.client
.request_endpoint(Endpoint::app_status(&self.id))
.await?
.into_result_t()
}
pub async fn info(&self) -> Result<AppInfo, ApiError> {
self.client
.request_endpoint(Endpoint::app_info(&self.id))
.await?
.into_result_t()
}
pub async fn metrics(&self) -> Result<Vec<AppMetrics>, ApiError> {
self.client
.request_endpoint(Endpoint::app_metrics(&self.id))
.await?
.into_result_t()
}
pub async fn logs(&self) -> Result<String, ApiError> {
let r: AppLogs = self
.client
.request_endpoint(Endpoint::app_logs(&self.id))
.await?
.into_result_t()?;
Ok(r.logs)
}
pub async fn commit(
&self,
bytes: impl Into<Cow<'static, [u8]>>,
) -> Result<bool, CommitError> {
let endpoint = Endpoint::app_commit(&self.id);
let form = Form::new().part(
"file",
Part::bytes(bytes)
.file_name("app.zip")
.mime_str("application/zip")
.unwrap(),
);
let request = endpoint
.request_builder(&self.client.http_client, &self.client.base_url)
.multipart(form)
.build()?;
self.client
.execute_request::<()>(request)
.await?
.into_bool_result()
.map_err(|error| CommitError::Api(error))
}
pub async fn delete(&self) -> Result<bool, ApiError> {
self.client
.request_endpoint::<()>(Endpoint::app_delete(&self.id))
.await?
.into_bool_result()
}
}