From b14a8f0af72dc380ce51d1240c1f0b6e86418bbd Mon Sep 17 00:00:00 2001 From: Nathan Miguel Date: Wed, 30 Jul 2025 18:06:40 -0300 Subject: [PATCH 1/5] Added support to commit apps - Added `ReadFile` to `Error`; - Enabled multipart support from reqwest to send files over http protocol; On branch dev Your branch is up to date with 'origin/dev'. Changes to be committed: modified: Cargo.toml modified: src/app.rs modified: src/app/manage.rs new file: src/app/manage/commit.rs modified: src/client.rs modified: src/error.rs modified: src/lib.rs modified: src/util.rs --- Cargo.toml | 2 +- src/app.rs | 5 ++-- src/app/manage.rs | 2 ++ src/app/manage/commit.rs | 9 +++++++ src/client.rs | 26 +++++++++++++++--- src/error.rs | 2 ++ src/lib.rs | 2 +- src/util.rs | 57 ++++++++++++++++++++++++++++++++++++++++ 8 files changed, 97 insertions(+), 8 deletions(-) create mode 100644 src/app/manage/commit.rs 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..daf86f7 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, @@ -76,8 +75,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..96eae76 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, @@ -18,7 +19,7 @@ use crate::{ }; use tracing::{debug, trace}; - +use crate::util::make_request_with_file; use super::error::Error; #[derive(Clone)] @@ -284,8 +285,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> { 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..7f69b1e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,4 +21,4 @@ pub use app::{ pub use client::Discloud; pub use error::Error; pub use team_manager::{APITeamMember, TeamManager, TeamMember, TeamPerms}; -pub use user::{Locale, User}; +pub use user::{Locale, User}; \ No newline at end of file diff --git a/src/util.rs b/src/util.rs index 67a8a80..571eddf 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::fmt::Debug; use reqwest::{Client, Method}; @@ -112,3 +113,59 @@ 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).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) +} From 29556f5743383a4005deb619a3eb54d85ef7f0d3 Mon Sep 17 00:00:00 2001 From: Nathan Miguel Date: Thu, 31 Jul 2025 12:53:21 -0300 Subject: [PATCH 2/5] Added read timeout of 100 secs --- src/util.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/util.rs b/src/util.rs index 571eddf..f3ae50e 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::{collections::HashMap, time::Duration}; use std::fmt::Debug; use reqwest::{Client, Method}; @@ -123,7 +123,10 @@ pub async fn make_request_with_file( let url = default_url(path); debug!("Creating request client"); - let client = Client::builder().user_agent(&config.user_agent).build()?; + let client = Client::builder() + .user_agent(&config.user_agent) + .read_timeout(Duration::from_secs(100)) + .build()?; debug!(url = url, "Making request"); From efd96c018eff088e15fedbe6a38fd9dbce54db5f Mon Sep 17 00:00:00 2001 From: Nathan Miguel Date: Sat, 2 Aug 2025 15:13:36 -0300 Subject: [PATCH 3/5] Adicionado suporte para rota team e upload On branch dev Your branch is up to date with 'origin/dev'. Changes to be committed: modified: src/client.rs modified: src/lib.rs new file: src/team.rs new file: src/upload.rs --- src/client.rs | 215 ++++++++++++++++++++++++++++++++++++++++++++++++-- src/lib.rs | 24 +++++- src/team.rs | 58 ++++++++++++++ src/upload.rs | 26 ++++++ 4 files changed, 316 insertions(+), 7 deletions(-) create mode 100644 src/team.rs create mode 100644 src/upload.rs diff --git a/src/client.rs b/src/client.rs index 96eae76..81c02f6 100644 --- a/src/client.rs +++ b/src/client.rs @@ -8,14 +8,10 @@ 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}; @@ -335,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) -> Result, Error> { + 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/lib.rs b/src/lib.rs index 7f69b1e..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; @@ -21,4 +23,24 @@ pub use app::{ pub use client::Discloud; pub use error::Error; pub use team_manager::{APITeamMember, TeamManager, TeamMember, TeamPerms}; -pub use user::{Locale, User}; \ No newline at end of file +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 From a35e329743248a99083ef2b53c3c0d748b8ec421 Mon Sep 17 00:00:00 2001 From: Nathan Miguel Date: Sat, 2 Aug 2025 15:15:28 -0300 Subject: [PATCH 4/5] Removido o return type de get_team_mods --- src/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.rs b/src/client.rs index 81c02f6..b80eef2 100644 --- a/src/client.rs +++ b/src/client.rs @@ -364,7 +364,7 @@ impl Discloud { Ok(res.apps) } - pub async fn get_team_mods(&self, owner_id: &str) -> Result, Error> { + pub async fn get_team_mods(&self, owner_id: &str) { todo!() } From 1c47c7cf5a005eef9496f5a600248aa76922e41d Mon Sep 17 00:00:00 2001 From: Nathan Miguel Date: Fri, 19 Sep 2025 15:28:59 -0300 Subject: [PATCH 5/5] updated App --- src/app.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/app.rs b/src/app.rs index daf86f7..0deeb24 100644 --- a/src/app.rs +++ b/src/app.rs @@ -27,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 { @@ -34,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, @@ -43,6 +62,7 @@ pub struct App { pub online: bool, pub ram: i32, pub ram_killed: bool, + pub sync_git: Option, pub r#type: i32, }