diff --git a/Cargo.toml b/Cargo.toml index bdb0785..33e9a55 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ edition = "2024" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -reqwest = { version = "0.12", features = ["json"] } +reqwest = { version = "0.12", features = ["json", "multipart"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" tracing = "0.1" diff --git a/src/app.rs b/src/app.rs index 25fde22..0deeb24 100644 --- a/src/app.rs +++ b/src/app.rs @@ -6,7 +6,6 @@ pub mod manage; pub mod status; use crate::{Discloud, Error, TeamMember, TeamPerms}; - use self::{ backup::AppBackup, logs::AppLogs, @@ -28,6 +27,22 @@ pub struct AppResponseUnique { pub status: String, } +#[derive(Deserialize, Debug, Clone)] +pub struct LastCommit { + pub message: String, + pub sha: String, +} + +#[derive(Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct SyncGit { + pub auto_deploy: bool, + #[serde(rename = "repositoryID")] + pub repository_id: u64, + pub branch_name: String, + pub last_commit: LastCommit +} + #[derive(Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct App { @@ -35,7 +50,10 @@ pub struct App { pub auto_restart: bool, #[serde(rename = "avatarURL")] pub avatar_url: String, + pub added_at_timestamp: u64, + pub cluster_name: String, pub exit_code: Option, + pub apts: Vec, pub id: String, pub lang: String, pub main_file: String, @@ -44,6 +62,7 @@ pub struct App { pub online: bool, pub ram: i32, pub ram_killed: bool, + pub sync_git: Option, pub r#type: i32, } @@ -76,8 +95,8 @@ impl App { client.set_app_ram(&self.id, quantity).await } - pub async fn commit(&self) { - todo!() + pub async fn commit(&self, client: &Discloud, id: &str, filepath: &str) -> Result<(), Error> { + client.commit_app(&self.id, filepath).await } pub async fn delete(&self, client: &Discloud) -> Result<(), Error> { diff --git a/src/app/manage.rs b/src/app/manage.rs index 56f1ceb..f952da5 100644 --- a/src/app/manage.rs +++ b/src/app/manage.rs @@ -1,9 +1,11 @@ +pub mod commit; pub mod delete; pub mod ram; pub mod restart; pub mod start; pub mod stop; +pub use commit::*; pub use delete::*; pub use ram::*; pub use restart::*; diff --git a/src/app/manage/commit.rs b/src/app/manage/commit.rs new file mode 100644 index 0000000..b9b8ec3 --- /dev/null +++ b/src/app/manage/commit.rs @@ -0,0 +1,9 @@ +use serde::Deserialize; + +#[derive(Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct AppCommitResponseUnique { + pub status: String, + pub status_code: i32, + pub message: String, +} \ No newline at end of file diff --git a/src/client.rs b/src/client.rs index ab03029..b80eef2 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,5 +1,6 @@ #![allow(unused)] +use std::fs; use reqwest::{ header::{HeaderMap, HeaderName}, Client, Method, Request, StatusCode, @@ -7,18 +8,14 @@ use reqwest::{ use serde::de::DeserializeOwned; use crate::{ - app::{backup::*, logs::*, manage::*, status::*, App, AppResponseAll, AppResponseUnique}, - config::Config, - team_manager::{ + app::{backup::*, logs::*, manage::*, status::*, App, AppResponseAll, AppResponseUnique}, config::Config, team::{TeamApp, TeamAppsResponseUnique}, team_manager::{ APITeamMember, AddTeamMemberResponse, GetTeamManagerResponse, TeamMember, TeamMemberBody, TeamPerms, - }, - user::{Locale, LocaleResponse, User, UserResponse}, - util::{make_request, make_request_with_body, DiscloudDefaultResponse}, + }, upload::{UploadApp, UploadOk}, user::{Locale, LocaleResponse, User, UserResponse}, util::{make_request, make_request_with_body, DiscloudDefaultResponse} }; use tracing::{debug, trace}; - +use crate::util::make_request_with_file; use super::error::Error; #[derive(Clone)] @@ -284,8 +281,27 @@ impl Discloud { Ok(()) } - pub async fn commit_app(&self) { - todo!() + pub async fn commit_app(&self, id: &str, filepath: &str) -> Result<(), Error> { + if id == "all" { + return Err(Error::InvalidRequest( + "Don't use all with that function.", + )); + } + + let file = match fs::read(filepath) { + Ok(buf) => buf, + Err(err) => { + return Err(Error::ReadFile(err)) + } + }; + + let res: AppCommitResponseUnique = make_request_with_file(&self.config, Method::PUT, &format!("app/{id}/commit"), file).await?; + + if res.status == "error" { + return Err(Error::Unknown); + } + + Ok(()) } pub async fn delete_app(&self, id: &str) -> Result<(), Error> { @@ -315,4 +331,211 @@ impl Discloud { Ok(res.apps) } + + // Upload + + pub async fn upload_app(&self, filepath: &str) -> Result { + let file = match fs::read(filepath) { + Ok(buf) => buf, + Err(err) => { + return Err(Error::ReadFile(err)) + } + }; + + let res: UploadOk = make_request_with_file(&self.config, Method::POST, &format!("upload"), file).await?; + + if res.status == "error" { + return Err(Error::Unknown); + } + + Ok(res.app) + } + + // Team + + pub async fn get_team_apps(&self) -> Result, Error> { + let res: TeamAppsResponseUnique = + make_request(&self.config, Method::GET, &format!("team")).await?; + + if res.status == "error" { + return Err(Error::Unknown); + } + + Ok(res.apps) + } + + pub async fn get_team_mods(&self, owner_id: &str) { + todo!() + } + + pub async fn start_team_app(&self, id: &str) -> Result { + if id == "all" { + return Err(AppStartError::Other(Error::InvalidRequest( + "Don't use all with that function. Use start_all_team_apps method instead.", + ))); + } + + let res: AppStartResponseUnique = + make_request(&self.config, Method::PUT, &format!("team/{id}/start")).await?; + + if res.status == "error" { + return Err(AppStartError::AlreadyOnline); + } + + res.app_status.ok_or(AppStartError::Other(Error::Unknown)) + } + + pub async fn start_all_team_apps(&self) -> Result { + let res: AppStartResponseAll = + make_request(&self.config, Method::PUT, "team/all/start").await?; + + Ok(res.apps) + } + + pub async fn restart_team_app(&self, id: &str) -> Result<(), Error> { + if id == "all" { + return Err(Error::InvalidRequest( + "Don't use all with that function. Use restart_all_team_apps method instead.", + )); + } + + let res: AppRestartResponseUnique = + make_request(&self.config, Method::PUT, &format!("team/{id}/restart")).await?; + + if res.status == "error" { + return Err(Error::Unknown); + } + + Ok(()) + } + + pub async fn restart_all_team_apps(&self) -> Result { + let res: AppRestartResponseAll = + make_request(&self.config, Method::PUT, "team/all/restart").await?; + + Ok(res.apps) + } + + pub async fn stop_team_app(&self, id: &str) -> Result<(), AppStopError> { + if id == "all" { + return Err(AppStopError::Other(Error::InvalidRequest( + "Don't use all with that function. Use stop_all_team_apps method instead.", + ))); + } + + let res: AppStartResponseUnique = + make_request(&self.config, Method::PUT, &format!("team/{id}/stop")).await?; + + if res.status == "error" { + return Err(AppStopError::AlreadyOffline); + } + + Ok(()) + } + + pub async fn stop_all_team_apps(&self) -> Result { + let res: AppStopResponseAll = + make_request(&self.config, Method::PUT, "team/all/stop").await?; + + Ok(res.apps) + } + + pub async fn commit_team_app(&self, id: &str, filepath: &str) -> Result<(), Error> { + if id == "all" { + return Err(Error::InvalidRequest( + "Don't use all with that function.", + )); + } + + let file = match fs::read(filepath) { + Ok(buf) => buf, + Err(err) => { + return Err(Error::ReadFile(err)) + } + }; + + let res: AppCommitResponseUnique = make_request_with_file(&self.config, Method::PUT, &format!("team/{id}/commit"), file).await?; + + if res.status == "error" { + return Err(Error::Unknown); + } + + Ok(()) + } + + pub async fn get_team_app_backup(&self, id: &str) -> Result { + if id == "all" { + return Err(Error::InvalidRequest( + "Don't use all with that function. Use get_all_team_apps_backup method instead.", + )); + } + + let res: AppBackupResponseUnique = + make_request(&self.config, Method::GET, &format!("team/{id}/backup")).await?; + + Ok(res.backups) + } + + pub async fn get_team_all_apps_backup(&self) -> Result, Error> { + let res: AppBackupResponseAll = + make_request(&self.config, Method::GET, "team/all/backup").await?; + + Ok(res.backups) + } + + pub async fn get_team_app_logs(&self, id: &str) -> Result { + if id == "all" { + return Err(Error::InvalidRequest( + "Don't use all with that function. Use get_all_team_apps_logs method instead.", + )); + } + + let res: AppLogsResponseUnique = + make_request(&self.config, Method::GET, &format!("team/{id}/logs")).await?; + + Ok(res.apps) + } + + pub async fn get_all_team_apps_logs(&self) -> Result, Error> { + let res: AppLogsResponseAll = + make_request(&self.config, Method::GET, "team/all/logs").await?; + + Ok(res.apps) + } + + pub async fn set_team_app_ram(&self, id: &str, quantity: u32) -> Result<(), AppRamError> { + let res: AppRamResponse = make_request_with_body( + &self.config, + Method::PUT, + &format!("team/{id}/ram"), + AppRamBody { ram: quantity }, + ) + .await?; + + if res.status == "error" { + return Err(AppRamError::ForbiddenQuantity(res.message)); + } + + Ok(()) + } + + pub async fn get_all_team_apps_status(&self) -> Result, Error> { + let res: AppStatusResponseAll = + make_request(&self.config, Method::GET, "team/all/status").await?; + + Ok(res.apps) + } + + pub async fn get_team_app_status(&self, id: &str) -> Result { + if id == "all" { + return Err(Error::InvalidRequest( + "Don't use all with that function. Use get_all_team_apps_status method instead.", + )); + } + + let res: AppStatusResponseUnique = + make_request(&self.config, Method::GET, &format!("team/{id}/status")).await?; + + Ok(res.apps) + } } diff --git a/src/error.rs b/src/error.rs index 8c6ba15..be5a6b1 100644 --- a/src/error.rs +++ b/src/error.rs @@ -8,6 +8,7 @@ pub enum Error { ServerError, Forbidden, InvalidRequest(&'static str), + ReadFile(std::io::Error), NotFound(&'static str), Conflict(&'static str), Unknown, @@ -20,6 +21,7 @@ impl fmt::Display for Error { Error::InvalidToken => write!(f, "Invalid API token provided"), Error::Ratelimited => write!(f, "You have been ratelimited"), Error::InvalidRequest(r) => write!(f, "Invalid request: {r}"), + Error::ReadFile(ref e) => write!(f, "Error trying to read file: {e}"), Error::ServerError => write!(f, "Server error"), Error::Forbidden => write!(f, "Forbidden access"), Error::NotFound(r) => write!(f, "Not found: {r}"), diff --git a/src/lib.rs b/src/lib.rs index 001b4bf..0385116 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,6 +5,8 @@ mod client; mod config; mod error; mod team_manager; +mod team; +mod upload; mod user; mod util; @@ -22,3 +24,23 @@ pub use client::Discloud; pub use error::Error; pub use team_manager::{APITeamMember, TeamManager, TeamMember, TeamPerms}; pub use user::{Locale, User}; + +#[cfg(test)] +mod test { + use std::env; + + use dotenvy::dotenv; + use tokio::test; + + use crate::Discloud; + + #[tokio::test] + async fn test() { + dotenv().ok(); + let client = Discloud::new(&env::var("DISCLOUD_TOKEN").expect("Discloud token is required")); + if let Err(err) = client.commit_app("1753838917006", "App.zip").await { + panic!("{:?}", err); + } + println!("Ok"); + } +} \ No newline at end of file diff --git a/src/team.rs b/src/team.rs new file mode 100644 index 0000000..cce9e9c --- /dev/null +++ b/src/team.rs @@ -0,0 +1,58 @@ +use serde::Deserialize; + +use crate::{AppBackup, AppLogs, AppRamError, AppStartError, AppStartStatus, AppStatus, AppStopError, Discloud, Error}; + +#[derive(Deserialize, Debug, Clone)] +pub struct TeamAppsResponseUnique { + pub status: String, + pub message: String, + pub apps: Vec +} + +#[derive(Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct TeamApp { + pub id: String, + pub name: String, + pub online: bool, + pub ram: u32, + pub ram_killed: bool, + pub exit_code: i32, + pub lang: String, + pub perms: Vec, + pub r#type: i32 +} + +impl TeamApp { + pub async fn start(&self, client: &Discloud) -> Result { + client.start_team_app(&self.id).await + } + + pub async fn restart(&self, client: &Discloud) -> Result<(), Error> { + client.restart_team_app(&self.id).await + } + + pub async fn stop(&self, client: &Discloud) -> Result<(), AppStopError> { + client.stop_team_app(&self.id).await + } + + pub async fn commit(&self, filepath: &str, client: &Discloud) -> Result<(), Error> { + client.commit_team_app(&self.id, filepath).await + } + + pub async fn get_backup(&self, client: &Discloud) -> Result { + client.get_team_app_backup(&self.id).await + } + + pub async fn get_logs(&self, client: &Discloud) -> Result { + client.get_team_app_logs(&self.id).await + } + + pub async fn set_ram(&self, quantity: u32, client: &Discloud) -> Result<(), AppRamError> { + client.set_team_app_ram(&self.id, quantity).await + } + + pub async fn get_status(&self, client: &Discloud) -> Result { + client.get_team_app_status(&self.id).await + } +} \ No newline at end of file diff --git a/src/upload.rs b/src/upload.rs new file mode 100644 index 0000000..1c5a68b --- /dev/null +++ b/src/upload.rs @@ -0,0 +1,26 @@ +use serde::Deserialize; + +#[derive(Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct UploadOk { + pub status: String, + pub status_code: i32, + pub message: String, + pub app: UploadApp, + pub logs: Option, +} + +#[derive(Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct UploadApp { + pub added_at_timestamp: u64, + pub auto_restart: bool, + pub avatar_url: String, + pub id: String, + pub lang: String, + pub main_file: String, + pub name: String, + pub ram: u32, + pub r#type: i32, + pub version: String, +} \ No newline at end of file diff --git a/src/util.rs b/src/util.rs index 67a8a80..f3ae50e 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,3 +1,4 @@ +use std::{collections::HashMap, time::Duration}; use std::fmt::Debug; use reqwest::{Client, Method}; @@ -112,3 +113,62 @@ pub async fn make_request_with_body( + config: &Config, + method: Method, + path: &str, + file: Vec, +) -> Result { + let url = default_url(path); + + debug!("Creating request client"); + let client = Client::builder() + .user_agent(&config.user_agent) + .read_timeout(Duration::from_secs(100)) + .build()?; + + debug!(url = url, "Making request"); + + let file_multipart = reqwest::multipart::Part::bytes(file) + .file_name("App.zip") + .mime_str("application/x-zip-compressed")?; + + let form = reqwest::multipart::Form::new().part("file", file_multipart); + + let response = client + .request(method, url) + .multipart(form) + .header("api-token", &config.token) + .header("Content-Type", "multipart/form-data") + .send() + .await?; + + let response_status = response.status(); + + if !response_status.is_success() { + let response_body = response.text().await?; + debug!( + status = response_status.as_u16(), + response_body, "Request failed" + ); + if response_status.is_server_error() { + return Err(Error::ServerError); + } + let response: DiscloudDefaultResponse = + serde_json::from_str(&response_body).unwrap_or_default(); + + return Err(match response_status.as_u16() { + 401 => Error::InvalidToken, + 403 => Error::Forbidden, + 429 => Error::Ratelimited, + 404 => Error::NotFound(response.message.leak()), + 409 => Error::Conflict(response.message.leak()), + _ => Error::Unknown, + }); + } + + let body = response.json::().await?; + debug!(response_body = format!("{body:?}"), "Request succeed"); + Ok(body) +}