From 6ec03184078cc9d5c07194b9b1352ad58ccb01e9 Mon Sep 17 00:00:00 2001 From: Radical Date: Tue, 20 May 2025 01:39:31 +0200 Subject: [PATCH] feat: implement edge storage api --- Cargo.lock | 17 --- Cargo.toml | 2 +- src/edge_storage.rs | 251 ++++++++++++++++++++++++++++++++++++++++++++ src/error.rs | 12 +++ src/lib.rs | 115 +------------------- 5 files changed, 268 insertions(+), 129 deletions(-) create mode 100644 src/edge_storage.rs diff --git a/Cargo.lock b/Cargo.lock index be7d5c0..1f914d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -566,16 +566,6 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" -[[package]] -name = "mime_guess" -version = "2.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" -dependencies = [ - "mime", - "unicase", -] - [[package]] name = "miniz_oxide" version = "0.8.8" @@ -752,7 +742,6 @@ dependencies = [ "js-sys", "log", "mime", - "mime_guess", "native-tls", "once_cell", "percent-encoding", @@ -1175,12 +1164,6 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" -[[package]] -name = "unicase" -version = "2.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" - [[package]] name = "unicode-ident" version = "1.0.18" diff --git a/Cargo.toml b/Cargo.toml index 3219f29..ae0745e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ license = "MIT" [dependencies] bytes = "1.10.1" log = "0.4.27" -reqwest = { version = "0.12.15", features = ["json", "multipart"] } +reqwest = { version = "0.12.15", features = ["json"] } serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0.140" thiserror = "2.0.12" diff --git a/src/edge_storage.rs b/src/edge_storage.rs new file mode 100644 index 0000000..b11514a --- /dev/null +++ b/src/edge_storage.rs @@ -0,0 +1,251 @@ +//! B + +use std::sync::Arc; + +use crate::Error; +use bytes::Bytes; +use reqwest::Client; +use serde::Deserialize; +use url::Url; + +/// Endpoints for Edge Storage API +pub enum Endpoint { + /// Uses https://storage.bunnycdn.com as endpoint + Frankfurt, + /// Uses https://uk.storage.bunnycdn.com as endpoint + London, + /// Uses https://ny.storage.bunnycdn.com as endpoint + NewYork, + /// Uses https://la.storage.bunnycdn.com as endpoint + LosAngeles, + /// Uses https://sg.storage.bunnycdn.com as endpoint + Singapore, + /// Uses https://se.storage.bunnycdn.com as endpoint + Stockholm, + /// Uses https://br.storage.bunnycdn.com as endpoint + SaoPaulo, + /// Uses https://jh.storage.bunnycdn.com as endpoint + Johannesburg, + /// Uses https://syd.storage.bunnycdn.com as endpoint + Sydney, + /// Lets you input a custom endpoint, in case bunny adds a new one and this crate isnt up-to-date, has to be a valid URL with http(s) in front + Custom(String), +} + +impl TryInto for Endpoint { + type Error = Error; + + fn try_into(self) -> Result { + match self { + Endpoint::Frankfurt => Ok(Url::parse("https://storage.bunnycdn.com")?), + Endpoint::London => Ok(Url::parse("https://uk.storage.bunnycdn.com")?), + Endpoint::NewYork => Ok(Url::parse("https://ny.storage.bunnycdn.com")?), + Endpoint::LosAngeles => Ok(Url::parse("https://la.storage.bunnycdn.com")?), + Endpoint::Singapore => Ok(Url::parse("https://sg.storage.bunnycdn.com")?), + Endpoint::Stockholm => Ok(Url::parse("https://se.storage.bunnycdn.com")?), + Endpoint::SaoPaulo => Ok(Url::parse("https://br.storage.bunnycdn.com")?), + Endpoint::Johannesburg => Ok(Url::parse("https://jh.storage.bunnycdn.com")?), + Endpoint::Sydney => Ok(Url::parse("https://syd.storage.bunnycdn.com")?), + Endpoint::Custom(url) => Ok(Url::parse(&url)?), + } + } +} + +/// File information returned by list +#[derive(Deserialize, Debug)] +#[serde(rename_all = "PascalCase")] +pub struct ListFile { + /// ?? + pub guid: String, + /// Name of the storage zone the object is in + pub storage_zone_name: String, + /// Path to object + pub path: String, + /// Object name + pub object_name: String, + /// Length of the object in bytes + pub length: u32, + /// When the object was last modified + pub last_changed: String, + /// ?? + pub server_id: u32, + /// ?? + pub array_number: u32, + /// If the object is a directory + pub is_directory: bool, + /// ?? + pub user_id: String, + /// Object content type + pub content_type: String, + /// When the object was created + pub date_created: String, + /// ID of the storage zone the object is in + pub storage_zone_id: u32, + /// File checksum on server + pub checksum: String, + /// Zones the object is replicated to + pub replicated_zones: String, +} + +/// Edge Storage API for bunny +pub struct Storage { + pub(crate) url: Url, + pub(crate) reqwest: Arc, +} + +impl<'a> Storage { + /// Sets endpoint and storage zone used by Edge Storage API + /// + /// ``` + /// use bunny_api_tokio::{Client, error::Error, edge_storage::Endpoint}; + /// + /// #[tokio::main] + /// async fn main() -> Result<(), Error> { + /// let mut client = Client::new("api_key")?; + /// + /// client.storage.init(Endpoint::Frankfurt, "MyStorageZone"); + /// } + /// ``` + pub fn init>(&mut self, endpoint: Endpoint, storage_zone: T) -> Result<(), Error> { + let endpoint: Url = endpoint.try_into()?; + let storage_zone = String::from("/") + storage_zone.as_ref() + "/"; + + self.url = endpoint.join(&storage_zone)?; + Ok(()) + } + + /// Uploads a file to the Storage Zone + /// + /// ``` + /// use bunny_api_tokio::{Client, error::Error, edge_storage::Endpoint}; + /// use tokio::fs; + /// + /// #[tokio::main] + /// async fn main() -> Result<(), Error> { + /// let mut client = Client::new("api_key").await?; + /// + /// client.storage.init(Endpoint::Frankfurt, "MyStorageZone"); + /// + /// let file_bytes = fs::read("path/to/file.png").await?; + /// + /// // Will put a file in STORAGE_ZONE/images/file.png + /// client.storage.upload("/images/file.png", file_bytes).await?; + /// } + /// ``` + pub async fn upload>(&self, path: T, file: Bytes) -> Result<(), Error> { + let response = self.reqwest.put(self.url.join(path.as_ref())?) + .header("Content-Type", "application/octet-stream") + .body(file) + .send() + .await?; + + if response.status().as_u16() == 401 { + return Err(Error::Authentication(String::from(response.text().await?))) + } else if response.status().as_u16() == 400 { + return Err(Error::BadRequest(String::from(response.text().await?))) + } + + Ok(()) + } + + /// Downloads a file from the Storage Zone + /// + /// ``` + /// use bunny_api_tokio::{Client, error::Error, edge_storage::Endpoint}; + /// use tokio::fs; + /// use tokio::io::AsyncWriteExt; + /// + /// #[tokio::main] + /// async fn main() -> Result<(), Error> { + /// let mut client = Client::new("api_key").await?; + /// + /// client.storage.init(Endpoint::Frankfurt, "MyStorageZone"); + /// + /// // Will download the file STORAGE_ZONE/images/file.png + /// let contents = client.storage.download("/images/file.png").await?; + /// + /// let mut file = fs::File::create("file.png").await?; + /// file.write_all(contents).await?; + /// + /// Ok(()) + /// } + /// ``` + pub async fn download>(&self, path: T) -> Result { + let response = self.reqwest.get(self.url.join(path.as_ref())?) + .header("accept", "*/*") + .send() + .await?; + + if response.status().as_u16() == 401 { + return Err(Error::Authentication(String::from(response.text().await?))) + } else if response.status().as_u16() == 404 { + return Err(Error::NotFound(String::from(response.text().await?))) + } + + Ok(response.bytes().await?) + } + + /// Deletes a file from the Storage Zone + /// + /// ``` + /// use bunny_api_tokio::{Client, error::Error, edge_storage::Endpoint}; + /// + /// #[tokio::main] + /// async fn main() -> Result<(), Error> { + /// let mut client = Client::new("api_key").await?; + /// + /// client.storage.init(Endpoint::Frankfurt, "MyStorageZone"); + /// + /// // Will delete the file STORAGE_ZONE/images/file.png + /// client.storage.delete("/images/file.png").await?; + /// + /// Ok(()) + /// } + /// ``` + pub async fn delete>(&self, path: T) -> Result<(), Error> { + let response = self.reqwest.delete(self.url.join(path.as_ref())?) + .send() + .await?; + + if response.status().as_u16() == 401 { + return Err(Error::Authentication(String::from(response.text().await?))) + } else if response.status().as_u16() == 400 { + return Err(Error::BadRequest(String::from(response.text().await?))) + } + + Ok(()) + } + + /// Lists files on the Storage Zone + /// + /// ``` + /// use bunny_api_tokio::{Client, error::Error, edge_storage::Endpoint}; + /// + /// #[tokio::main] + /// async fn main() -> Result<(), Error> { + /// let mut client = Client::new("api_key").await?; + /// + /// client.storage.init(Endpoint::Frankfurt, "MyStorageZone"); + /// + /// // Will list the files in STORAGE_ZONE/images/ + /// let files = client.storage.list("/images/").await?; + /// + /// println!("{:#?}", files) + /// + /// Ok(()) + /// } + /// ``` + pub async fn list>(&self, path: T) -> Result, Error> { + let response = self.reqwest.get(self.url.join(path.as_ref())?) + .send() + .await?; + + if response.status().as_u16() == 401 { + return Err(Error::Authentication(String::from(response.text().await?))) + } else if response.status().as_u16() == 400 { + return Err(Error::BadRequest(String::from(response.text().await?))) + } + + Ok(response.json().await?) + } +} diff --git a/src/error.rs b/src/error.rs index 3f95651..4b012bd 100644 --- a/src/error.rs +++ b/src/error.rs @@ -14,4 +14,16 @@ pub enum Error { /// URL Parse error #[error("not a valid URL")] ParseError(#[from] url::ParseError), + + /// Authentication error + #[error("authentication error: {0}")] + Authentication(String), + + /// Bad request error + #[error("bad request: {0}")] + BadRequest(String), + + /// Not found error + #[error("not found: {0}")] + NotFound(String), } diff --git a/src/lib.rs b/src/lib.rs index 76dedc5..38e89f7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,124 +1,18 @@ //! This library provides access to the Bunny API asynchronously using tokio, it's not fully implemented but PRs are welcome. #![deny(missing_docs)] -use std::{sync::Arc}; -use bytes::Bytes; -use log::debug; +use std::sync::Arc; use reqwest::{header::{HeaderMap, HeaderValue}, Client as RClient}; use error::Error; use url::Url; -pub use reqwest::multipart; pub mod error; - -/// Endpoints for Edge Storage API -pub enum StorageEndpoint { - /// Uses https://storage.bunnycdn.com as endpoint - Frankfurt, - /// Uses https://uk.storage.bunnycdn.com as endpoint - London, - /// Uses https://ny.storage.bunnycdn.com as endpoint - NewYork, - /// Uses https://la.storage.bunnycdn.com as endpoint - LosAngeles, - /// Uses https://sg.storage.bunnycdn.com as endpoint - Singapore, - /// Uses https://se.storage.bunnycdn.com as endpoint - Stockholm, - /// Uses https://br.storage.bunnycdn.com as endpoint - SaoPaulo, - /// Uses https://jh.storage.bunnycdn.com as endpoint - Johannesburg, - /// Uses https://syd.storage.bunnycdn.com as endpoint - Sydney, - /// Lets you input a custom endpoint, in case bunny adds a new one and this crate isnt up-to-date, has to be a valid URL with http(s) in front - Custom(String), -} - -impl TryInto for StorageEndpoint { - type Error = Error; - - fn try_into(self) -> Result { - match self { - StorageEndpoint::Frankfurt => Ok(Url::parse("https://storage.bunnycdn.com")?), - StorageEndpoint::London => Ok(Url::parse("https://uk.storage.bunnycdn.com")?), - StorageEndpoint::NewYork => Ok(Url::parse("https://ny.storage.bunnycdn.com")?), - StorageEndpoint::LosAngeles => Ok(Url::parse("https://la.storage.bunnycdn.com")?), - StorageEndpoint::Singapore => Ok(Url::parse("https://sg.storage.bunnycdn.com")?), - StorageEndpoint::Stockholm => Ok(Url::parse("https://se.storage.bunnycdn.com")?), - StorageEndpoint::SaoPaulo => Ok(Url::parse("https://br.storage.bunnycdn.com")?), - StorageEndpoint::Johannesburg => Ok(Url::parse("https://jh.storage.bunnycdn.com")?), - StorageEndpoint::Sydney => Ok(Url::parse("https://syd.storage.bunnycdn.com")?), - StorageEndpoint::Custom(url) => Ok(Url::parse(&url)?), - } - } -} - -/// Edge Storage API for bunny -pub struct Storage { - url: Url, - reqwest: Arc, -} - -impl<'a> Storage { - /// Sets endpoint and storage zone used by Edge Storage API - /// - /// ``` - /// use bunny_api_tokio::{Client, error::Error, StorageEndpoint}; - /// - /// #[tokio::main] - /// async fn main() -> Result<(), Error> { - /// let mut client = Client::new("api_key")?; - /// - /// client.storage.init(StorageEndpoint::Frankfurt, "MyStorageZone"); - /// } - /// ``` - pub fn init>(&mut self, endpoint: StorageEndpoint, storage_zone: T) -> Result<(), Error> { - let endpoint: Url = endpoint.try_into()?; - let storage_zone = String::from("/") + storage_zone.as_ref() + "/"; - - self.url = endpoint.join(&storage_zone)?; - Ok(()) - } - - /// Uploads a file to the Storage Zone - /// - /// ``` - /// use bunny_api_tokio::{Client, error::Error, StorageEndpoint}; - /// use tokio::fs; - /// - /// #[tokio::main] - /// async fn main() -> Result<(), Error> { - /// let mut client = Client::new("api_key")?; - /// - /// client.storage.init(StorageEndpoint::Frankfurt, "MyStorageZone"); - /// - /// let file_bytes = fs::read("path/to/file.png").await?; - /// - /// // Will put a file in STORAGE_ZONE/images/file.png - /// client.storage.upload("/images/file.png", file_bytes); - /// } - /// ``` - pub async fn upload>(&self, path: T, file: Bytes) -> Result<(), Error> { - let response = self.reqwest.put(self.url.join(path.as_ref())?) - .header("Content-Type", "application/octet-stream") - .body(file) - .send() - .await? - .text() - .await?; - - debug!("{}", response); - - Ok(()) - } -} +pub mod edge_storage; /// API Client for bunny pub struct Client { - reqwest: Arc, /// Used to interact with the Edge Storage API - pub storage: Storage + pub storage: edge_storage::Storage } impl Client { @@ -141,8 +35,7 @@ impl Client { .build()?); Ok(Self { - reqwest: reqwest.clone(), - storage: Storage { + storage: edge_storage::Storage { url: Url::parse("https://storage.bunnycdn.com").unwrap(), reqwest, },