Compare commits

..

No commits in common. "main" and "v0.2.0" have entirely different histories.
main ... v0.2.0

15 changed files with 425 additions and 658 deletions

View file

@ -1,3 +0,0 @@
{
"rust-analyzer.cargo.features": ["bunnynet", "edge_storage"], "_":["stream", "edge_scripting", "bunny_shield"],
}

178
Cargo.lock generated
View file

@ -41,7 +41,7 @@ dependencies = [
"miniz_oxide",
"object",
"rustc-demangle",
"windows-targets",
"windows-targets 0.52.6",
]
[[package]]
@ -64,7 +64,7 @@ checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
[[package]]
name = "bunny-api-tokio"
version = "0.4.0"
version = "0.2.0"
dependencies = [
"bytes",
"log",
@ -372,28 +372,22 @@ dependencies = [
[[package]]
name = "hyper-util"
version = "0.1.14"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb"
checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2"
dependencies = [
"base64",
"bytes",
"futures-channel",
"futures-core",
"futures-util",
"http",
"http-body",
"hyper",
"ipnet",
"libc",
"percent-encoding",
"pin-project-lite",
"socket2",
"system-configuration",
"tokio",
"tower-service",
"tracing",
"windows-registry",
]
[[package]]
@ -519,16 +513,6 @@ version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
[[package]]
name = "iri-string"
version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2"
dependencies = [
"memchr",
"serde",
]
[[package]]
name = "itoa"
version = "1.0.15"
@ -736,14 +720,15 @@ checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
[[package]]
name = "reqwest"
version = "0.12.20"
version = "0.12.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eabf4c97d9130e2bf606614eb937e86edac8292eaa6f422f995d7e8de1eb1813"
checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb"
dependencies = [
"base64",
"bytes",
"encoding_rs",
"futures-core",
"futures-util",
"h2",
"http",
"http-body",
@ -752,26 +737,29 @@ dependencies = [
"hyper-rustls",
"hyper-tls",
"hyper-util",
"ipnet",
"js-sys",
"log",
"mime",
"native-tls",
"once_cell",
"percent-encoding",
"pin-project-lite",
"rustls-pki-types",
"rustls-pemfile",
"serde",
"serde_json",
"serde_urlencoded",
"sync_wrapper",
"system-configuration",
"tokio",
"tokio-native-tls",
"tower",
"tower-http",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"windows-registry",
]
[[package]]
@ -820,6 +808,15 @@ dependencies = [
"zeroize",
]
[[package]]
name = "rustls-pemfile"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "rustls-pki-types"
version = "1.12.0"
@ -1068,9 +1065,9 @@ dependencies = [
[[package]]
name = "tokio"
version = "1.45.1"
version = "1.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779"
checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165"
dependencies = [
"backtrace",
"bytes",
@ -1078,21 +1075,9 @@ dependencies = [
"mio",
"pin-project-lite",
"socket2",
"tokio-macros",
"windows-sys 0.52.0",
]
[[package]]
name = "tokio-macros"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tokio-native-tls"
version = "0.3.1"
@ -1141,24 +1126,6 @@ dependencies = [
"tower-service",
]
[[package]]
name = "tower-http"
version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2"
dependencies = [
"bitflags",
"bytes",
"futures-util",
"http",
"http-body",
"iri-string",
"pin-project-lite",
"tower",
"tower-layer",
"tower-service",
]
[[package]]
name = "tower-layer"
version = "0.3.3"
@ -1217,7 +1184,6 @@ dependencies = [
"form_urlencoded",
"idna",
"percent-encoding",
"serde",
]
[[package]]
@ -1339,19 +1305,19 @@ dependencies = [
[[package]]
name = "windows-link"
version = "0.1.3"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
[[package]]
name = "windows-registry"
version = "0.5.3"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e"
checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3"
dependencies = [
"windows-link",
"windows-result",
"windows-strings",
"windows-targets 0.53.0",
]
[[package]]
@ -1365,9 +1331,9 @@ dependencies = [
[[package]]
name = "windows-strings"
version = "0.4.2"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319"
dependencies = [
"windows-link",
]
@ -1378,7 +1344,7 @@ version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets",
"windows-targets 0.52.6",
]
[[package]]
@ -1387,7 +1353,7 @@ version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets",
"windows-targets 0.52.6",
]
[[package]]
@ -1396,14 +1362,30 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
"windows_i686_gnullvm 0.52.6",
"windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc 0.52.6",
]
[[package]]
name = "windows-targets"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b"
dependencies = [
"windows_aarch64_gnullvm 0.53.0",
"windows_aarch64_msvc 0.53.0",
"windows_i686_gnu 0.53.0",
"windows_i686_gnullvm 0.53.0",
"windows_i686_msvc 0.53.0",
"windows_x86_64_gnu 0.53.0",
"windows_x86_64_gnullvm 0.53.0",
"windows_x86_64_msvc 0.53.0",
]
[[package]]
@ -1412,48 +1394,96 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_aarch64_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnu"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_i686_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnu"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "windows_x86_64_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
[[package]]
name = "wit-bindgen-rt"
version = "0.39.0"

View file

@ -1,6 +1,6 @@
[package]
name = "bunny-api-tokio"
version = "0.4.0"
version = "0.2.0"
edition = "2024"
authors = ["Radical <radical@radical.fun>"]
license = "MIT"
@ -14,26 +14,12 @@ keywords = [
"tokio",
]
[features]
default = ["bunnynet"]
bunnynet = []
edge_storage = []
# Kept here for future use
#stream = []
#edge_scripting = []
#bunny_shield = []
[dev-dependencies]
tokio = { version = "1.45.1", features = ["fs", "rt", "rt-multi-thread", "macros"] }
[dependencies]
bytes = "1.10.1"
log = "0.4.27"
reqwest = { version = "0.12.20", features = ["json"] }
reqwest = { version = "0.12.15", features = ["json"] }
serde = { version = "1.0.219", features = ["derive"] }
thiserror = "2.0.12"
tokio = "1.45.1"
url = { version = "2.5.4", features = ["serde"] }
[package.metadata.docs.rs]
all-features = true
tokio = "1.45.0"
url = "2.5.4"

View file

@ -1,15 +1,9 @@
# bunny-api-tokio
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
[![Crates.io](https://img.shields.io/crates/v/bunny-api-tokio.svg)](https://crates.io/crates/bunny-api-tokio)
[![Visitors](https://visitor-badge.laobi.icu/badge?page_id=gorb.bunny-api-tokio)](https://git.gorb.app/gorb/bunny-api-tokio)
A Rust library providing **asynchronous access to the Bunny CDN API** using Tokio.
## Issues/PRs
Issues and PRs can be submitted on the [GitHub mirror](https://github.com/gorb-app/bunny-api-tokio)
## Features
- **Async-first**: Built with Tokio for non-blocking API calls.
- **Edge Storage API**: Supports Bunny's edge storage operations.
@ -19,5 +13,5 @@ Issues and PRs can be submitted on the [GitHub mirror](https://github.com/gorb-a
Add to your `Cargo.toml`:
```toml
[dependencies]
bunny-api-tokio = "0.4.0"
bunny-api-tokio = "0.2.0"
```

View file

@ -1,13 +0,0 @@
use serde::Deserialize;
/// API Key struct returned by list_api_keys()
#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "PascalCase")]
pub struct ApiKey {
/// API Key ID
pub id: i32,
/// API Key
pub key: String,
/// ??
pub roles: Vec<String>,
}

View file

@ -1,23 +0,0 @@
use serde::Deserialize;
use url::Url;
/// Country struct returned by get_countries() function
#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "PascalCase")]
pub struct Country {
/// Country name
pub name: String,
/// Country ISO code
pub iso_code: String,
/// Country is part of the EU
#[serde(rename = "IsEU")]
pub is_eu: bool,
/// Tax rate in percentage
pub tax_rate: f32,
/// Tax prefix
pub tax_prefix: String,
/// URL to country flag
pub flag_url: Url,
/// ??
pub pop_list: Vec<String>,
}

View file

@ -1,188 +0,0 @@
//! Contains structs, enums and implementations for the main Bunny.net API
use url::Url;
use crate::error::Error;
use reqwest::{
Client as RClient,
header::{HeaderMap, HeaderValue},
};
mod api_key;
pub use api_key::ApiKey;
mod country;
pub use country::Country;
mod pagination;
pub use pagination::Pagination;
mod region;
pub use region::Region;
/// API Client for bunny.net
#[derive(Debug, Clone)]
pub struct BunnyClient {
reqwest: RClient,
}
impl BunnyClient {
/// Creates a new Bunny.net API Client using the supplied `api_key`
///
/// ```
/// use bunny_api_tokio::{BunnyClient, error::Error};
///
/// #[tokio::main]
/// async fn main() -> Result<(), Error> {
/// // Bunny.net api key
/// let mut client = BunnyClient::new("api_key").await?;
///
/// Ok(())
/// }
/// ```
pub async fn new<T: AsRef<str>>(api_key: T) -> Result<Self, Error> {
let mut headers = HeaderMap::new();
headers.append("AccessKey", HeaderValue::from_str(api_key.as_ref())?);
headers.append("accept", HeaderValue::from_str("application/json")?);
let reqwest = RClient::builder().default_headers(headers).build()?;
Ok(Self { reqwest })
}
// TODO: Following functions could probably use better naming, the names are currently derived from the titles on the API reference
/// Returns a list of countries and tax rates
///
/// ```
/// use bunny_api_tokio::{BunnyClient, error::Error};
///
/// #[tokio::main]
/// async fn main() -> Result<(), Error> {
/// // Bunny.net api key
/// let mut client = BunnyClient::new("api_key").await?;
///
/// let countries = client.get_country_list().await?;
///
/// println!("{:#?}", countries);
/// Ok(())
/// }
/// ```
pub async fn get_country_list(&self) -> Result<Vec<Country>, Error> {
let response = self
.reqwest
.get("https://api.bunny.net/country")
.header("accept", "application/json")
.send()
.await?;
if response.status().as_u16() == 401 {
return Err(Error::Authentication(response.text().await?));
} else if response.status().as_u16() == 500 {
return Err(Error::InternalServerError(response.text().await?));
}
Ok(response.json().await?)
}
/// Returns a list of API Keys
///
/// ```
/// use bunny_api_tokio::{BunnyClient, error::Error};
///
/// #[tokio::main]
/// async fn main() -> Result<(), Error> {
/// // Bunny.net api key
/// let mut client = BunnyClient::new("api_key").await?;
///
/// let api_keys = client.list_api_keys(1, 1000).await?;
///
/// println!("{:#?}", api_keys);
/// Ok(())
/// }
/// ```
pub async fn list_api_keys(
&self,
page: i32,
per_page: i32,
) -> Result<Pagination<ApiKey>, Error> {
let response = self
.reqwest
.get("https://api.bunny.net/apikey")
.query(&[("page", page), ("perPage", per_page)])
.send()
.await?;
if response.status().as_u16() == 401 {
return Err(Error::Authentication(response.text().await?));
} else if response.status().as_u16() == 500 {
return Err(Error::InternalServerError(response.text().await?));
}
Ok(response.json().await?)
}
/// Returns a list of Regions
///
/// ```
/// use bunny_api_tokio::{BunnyClient, error::Error};
///
/// #[tokio::main]
/// async fn main() -> Result<(), Error> {
/// // Bunny.net api key
/// let mut client = BunnyClient::new("api_key").await?;
///
/// let regions = client.region_list().await?;
///
/// println!("{:#?}", regions);
/// Ok(())
/// }
/// ```
pub async fn region_list(&self) -> Result<Vec<Region>, Error> {
let response = self
.reqwest
.get("https://api.bunny.net/region")
.send()
.await?;
if response.status().as_u16() == 401 {
return Err(Error::Authentication(response.text().await?));
} else if response.status().as_u16() == 500 {
return Err(Error::InternalServerError(response.text().await?));
}
Ok(response.json().await?)
}
/// Purges a URL from the cache
///
/// ```
/// use bunny_api_tokio::{BunnyClient, error::Error};
///
/// #[tokio::main]
/// async fn main() -> Result<(), Error> {
/// // Bunny.net api key
/// let mut client = BunnyClient::new("api_key").await?;
///
/// client.purge_url("https://url_to_purge.com".parse()?, false).await?;
///
/// Ok(())
/// }
/// ```
pub async fn purge_url(&self, url: Url, asynchronous: bool) -> Result<(), Error> {
let response = self
.reqwest
.post("https://api.bunny.net/purge")
.query(&[
("url", url.to_string()),
("async", asynchronous.to_string()),
])
.send()
.await?;
if response.status().as_u16() == 401 {
return Err(Error::Authentication(response.text().await?));
} else if response.status().as_u16() == 500 {
return Err(Error::InternalServerError(response.text().await?));
}
Ok(response.json().await?)
}
}

View file

@ -1,15 +0,0 @@
use serde::Deserialize;
/// Pagination struct used by Bunny.net API
#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "PascalCase")]
pub struct Pagination<T> {
/// Vector of type T
pub items: Vec<T>,
/// Current page number
pub current_page: i32,
/// Total amount of type T
pub total_items: i32,
/// Has more items
pub has_more_items: bool,
}

View file

@ -1,25 +0,0 @@
use serde::Deserialize;
/// Region struct returned by region_list()
#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "PascalCase")]
pub struct Region {
/// Region ID
pub id: i32,
/// Name of the region
pub name: String,
/// Price per gigabyte in region
pub price_per_gigabyte: f32,
/// Region 2 letter code
pub region_code: String,
/// Continent 2 letter code
pub continent_code: String,
/// Country 2 letter code
pub country_code: String,
/// Region latitude
pub latitude: f32,
/// Region longitude
pub longitude: f32,
/// ??
pub allow_latency_routing: bool,
}

269
src/edge_storage.rs Normal file
View file

@ -0,0 +1,269 @@
//! Edge Storage API
//!
//! Contains enums, structs and functions for the Bunny Edge Storage API
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<Url> for Endpoint {
type Error = Error;
fn try_into(self) -> Result<Url, Error> {
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<Client>,
}
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").await?;
///
/// client.storage.init(Endpoint::Frankfurt, "MyStorageZone");
///
/// Ok(())
/// }
/// ```
pub fn init<T: AsRef<str>>(
&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?;
///
/// Ok(())
/// }
/// ```
pub async fn upload<T: AsRef<str>>(&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(response.text().await?));
} else if response.status().as_u16() == 400 {
return Err(Error::BadRequest(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<T: AsRef<str>>(&self, path: T) -> Result<Bytes, Error> {
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(response.text().await?));
} else if response.status().as_u16() == 404 {
return Err(Error::NotFound(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<T: AsRef<str>>(&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(response.text().await?));
} else if response.status().as_u16() == 400 {
return Err(Error::BadRequest(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<T: AsRef<str>>(&self, path: T) -> Result<Vec<ListFile>, Error> {
let response = self
.reqwest
.get(self.url.join(path.as_ref())?)
.send()
.await?;
if response.status().as_u16() == 401 {
return Err(Error::Authentication(response.text().await?));
} else if response.status().as_u16() == 400 {
return Err(Error::BadRequest(response.text().await?));
}
Ok(response.json().await?)
}
}

View file

@ -1,46 +0,0 @@
use crate::error::Error;
use url::Url;
/// Endpoints for Edge Storage API
#[derive(Debug, Clone, PartialEq, Eq)]
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<Url> for Endpoint {
type Error = Error;
fn try_into(self) -> Result<Url, Error> {
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)?),
}
}
}

View file

@ -1,37 +0,0 @@
use serde::Deserialize;
/// File information returned by list
#[derive(Deserialize, Debug, Clone)]
#[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,
}

View file

@ -1,193 +0,0 @@
//! Edge Storage API
//!
//! Contains enums, structs and functions for the Bunny Edge Storage API
use crate::error::Error;
use bytes::Bytes;
use reqwest::{
Client,
header::{HeaderMap, HeaderValue},
};
use url::Url;
mod endpoint;
pub use endpoint::Endpoint;
mod list_file;
pub use list_file::ListFile;
/// Edge Storage API for bunny
#[derive(Debug, Clone)]
pub struct EdgeStorageClient {
pub(crate) url: Url,
pub(crate) reqwest: Client,
}
impl<'a> EdgeStorageClient {
/// Creates a new EdgeStorageClient
///
/// ```
/// use bunny_api_tokio::{EdgeStorageClient, error::Error, edge_storage::Endpoint};
///
/// #[tokio::main]
/// async fn main() -> Result<(), Error> {
/// let mut client = EdgeStorageClient::new("storage_zone_api_key", Endpoint::Frankfurt, "MyStorageZone").await?;
///
/// Ok(())
/// }
/// ```
pub async fn new<T: AsRef<str>, T1: AsRef<str>>(
api_key: T,
endpoint: Endpoint,
storage_zone: T1,
) -> Result<Self, Error> {
let mut headers = HeaderMap::new();
headers.append("AccessKey", HeaderValue::from_str(api_key.as_ref())?);
headers.append("accept", HeaderValue::from_str("application/json")?);
let reqwest = Client::builder().default_headers(headers).build()?;
let endpoint: Url = endpoint.try_into()?;
let storage_zone = String::from("/") + storage_zone.as_ref() + "/";
let url = endpoint.join(&storage_zone)?;
Ok(Self { url, reqwest })
}
/// Uploads a file to the Storage Zone
///
/// ```
/// use bunny_api_tokio::{EdgeStorageClient, error::Error, edge_storage::Endpoint};
/// use tokio::fs;
///
/// #[tokio::main]
/// async fn main() -> Result<(), Error> {
/// let mut client = EdgeStorageClient::new("storage_zone_api_key", Endpoint::Frankfurt, "MyStorageZone").await?;
///
/// let file_bytes = fs::read("path/to/file.png").await.unwrap();
///
/// // Will put a file in STORAGE_ZONE/images/file.png
/// client.upload("/images/file.png", file_bytes.into()).await?;
///
/// Ok(())
/// }
/// ```
pub async fn upload<T: AsRef<str>>(&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(response.text().await?));
} else if response.status().as_u16() == 400 {
return Err(Error::BadRequest(response.text().await?));
}
Ok(())
}
/// Downloads a file from the Storage Zone
///
/// ```
/// use bunny_api_tokio::{EdgeStorageClient, error::Error, edge_storage::Endpoint};
/// use tokio::fs;
/// use tokio::io::AsyncWriteExt;
///
/// #[tokio::main]
/// async fn main() -> Result<(), Error> {
/// let mut client = EdgeStorageClient::new("storage_zone_api_key", Endpoint::Frankfurt, "MyStorageZone").await?;
///
/// // Will download the file STORAGE_ZONE/images/file.png
/// let contents = client.download("/images/file.png").await?;
///
/// let mut file = fs::File::create("file.png").await.unwrap();
/// file.write_all(&contents).await.unwrap();
///
/// Ok(())
/// }
/// ```
pub async fn download<T: AsRef<str>>(&self, path: T) -> Result<Bytes, Error> {
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(response.text().await?));
} else if response.status().as_u16() == 404 {
return Err(Error::NotFound(response.text().await?));
}
Ok(response.bytes().await?)
}
/// Deletes a file from the Storage Zone
///
/// ```
/// use bunny_api_tokio::{EdgeStorageClient, error::Error, edge_storage::Endpoint};
///
/// #[tokio::main]
/// async fn main() -> Result<(), Error> {
/// let mut client = EdgeStorageClient::new("storage_zone_api_key", Endpoint::Frankfurt, "MyStorageZone").await?;
///
/// // Will delete the file STORAGE_ZONE/images/file.png
/// client.delete("/images/file.png").await?;
///
/// Ok(())
/// }
/// ```
pub async fn delete<T: AsRef<str>>(&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(response.text().await?));
} else if response.status().as_u16() == 400 {
return Err(Error::BadRequest(response.text().await?));
}
Ok(())
}
/// Lists files on the Storage Zone
///
/// ```
/// use bunny_api_tokio::{EdgeStorageClient, error::Error, edge_storage::Endpoint};
///
/// #[tokio::main]
/// async fn main() -> Result<(), Error> {
/// let mut client = EdgeStorageClient::new("storage_zone_api_key", Endpoint::Frankfurt, "MyStorageZone").await?;
///
/// // Will list the files in STORAGE_ZONE/images/
/// let files = client.list("/images/").await?;
///
/// println!("{:#?}", files);
///
/// Ok(())
/// }
/// ```
pub async fn list<T: AsRef<str>>(&self, path: T) -> Result<Vec<ListFile>, Error> {
let response = self
.reqwest
.get(self.url.join(path.as_ref())?)
.send()
.await?;
if response.status().as_u16() == 401 {
return Err(Error::Authentication(response.text().await?));
} else if response.status().as_u16() == 400 {
return Err(Error::BadRequest(response.text().await?));
}
Ok(response.json().await?)
}
}

View file

@ -4,15 +4,15 @@
#[derive(thiserror::Error, Debug)]
pub enum Error {
/// Reqwest error
#[error(transparent)]
#[error("bleh")]
Reqwest(#[from] reqwest::Error),
/// Header contains non-ASCII characters
#[error(transparent)]
#[error("header contains non-ASCII characters")]
InvalidHeader(#[from] reqwest::header::InvalidHeaderValue),
/// URL Parse error
#[error(transparent)]
#[error("not a valid URL")]
ParseError(#[from] url::ParseError),
/// Authentication error
@ -26,8 +26,4 @@ pub enum Error {
/// Not found error
#[error("not found: {0}")]
NotFound(String),
/// Internal server error
#[error("internal server error: {0}")]
InternalServerError(String),
}

View file

@ -8,23 +8,58 @@
//! 2. Start coding
//!
//! ```
//! use bunny_api_tokio::{BunnyClient, error::Error};
//! use bunny_api_tokio::{Client, error::Error};
//!
//! #[tokio::main]
//! async fn main() -> Result<(), Error> {
//! let mut client = BunnyClient::new("api_key").await?;
//! let mut client = Client::new("api_key").await?;
//!
//! Ok(())
//! }
//! ```
#![deny(missing_docs)]
#[cfg(feature = "bunnynet")]
mod bunny;
#[cfg(feature = "bunnynet")]
pub use bunny::BunnyClient;
#[cfg(feature = "edge_storage")]
use error::Error;
use reqwest::{
Client as RClient,
header::{HeaderMap, HeaderValue},
};
use std::sync::Arc;
use url::Url;
pub mod edge_storage;
#[cfg(feature = "edge_storage")]
pub use edge_storage::EdgeStorageClient;
pub mod error;
/// API Client for bunny
pub struct Client {
/// Used to interact with the Edge Storage API
pub storage: edge_storage::Storage,
}
impl Client {
/// Creates a new Client using the supplied `api_key`
///
/// ```
/// use bunny_api_tokio::{Client, error::Error};
///
/// #[tokio::main]
/// async fn main() -> Result<(), Error> {
/// let mut client = Client::new("api_key").await?;
///
/// Ok(())
/// }
/// ```
pub async fn new<T: AsRef<str>>(api_key: T) -> Result<Self, Error> {
let mut headers = HeaderMap::new();
headers.append("AccessKey", HeaderValue::from_str(api_key.as_ref())?);
let reqwest = Arc::new(RClient::builder().default_headers(headers).build()?);
Ok(Self {
storage: edge_storage::Storage {
url: Url::parse("https://storage.bunnycdn.com").unwrap(),
reqwest,
},
})
}
}