use reqwest::{
multipart::{Form, Part},
Body, RequestBuilder,
};
use serde::{Deserialize, Serialize};
use thiserror::Error;
const API_URL: &str = "https://neocities.org/api/";
enum Auth {
Login { username: String, password: String },
Key(String),
}
pub struct Neocities {
auth: Auth,
client: reqwest::Client,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(untagged)]
pub enum ListEntry {
File {
path: String,
size: i64,
updated_at: String,
sha1_hash: String,
},
Directory {
path: String,
updated_at: String,
},
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Info {
#[serde(rename = "sitename")]
pub site_name: String,
pub hits: i64,
pub created_at: String,
pub last_updated: String,
pub domain: Option<String>,
pub tags: Vec<String>,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "result")]
enum ApiResult<T> {
#[serde(rename = "error")]
Error {
#[serde(default)]
error_type: String,
#[serde(default)]
message: String,
},
#[serde(rename = "success")]
Success {
#[serde(alias = "info")]
#[serde(alias = "files")]
#[serde(alias = "api_key")]
#[serde(alias = "message")]
data: T,
},
}
impl<T> ApiResult<T> {
fn to_result(self) -> Result<T, NeocitiesError> {
match self {
ApiResult::Success { data } => Ok(data),
ApiResult::Error {
error_type,
message,
} => Err(NeocitiesError::ApiErr(
error_type.to_string(),
message.to_string(),
)),
}
}
}
impl Neocities {
pub fn new(key: String) -> Self {
let client = reqwest::Client::new();
Self {
auth: Auth::Key(key),
client,
}
}
pub fn login(username: String, password: String) -> Self {
let client = reqwest::Client::new();
let auth = Auth::Login { username, password };
Self { client, auth }
}
pub async fn list<T: AsRef<str>>(&self, path: T) -> Result<Vec<ListEntry>, NeocitiesError> {
let mut request = self.client.get(API_URL.to_string() + "list");
request = add_authorization_header(request, &self.auth);
if !path.as_ref().is_empty() {
request = request.form(&[("path", path.as_ref())]);
}
let response = request.send().await?.error_for_status()?;
response
.json::<ApiResult<Vec<ListEntry>>>()
.await?
.to_result()
}
pub async fn info<T: AsRef<str>>(&self, site_name: T) -> Result<Info, NeocitiesError> {
let mut request = self.client.get(API_URL.to_string() + "info");
request = add_authorization_header(request, &self.auth);
if !site_name.as_ref().is_empty() {
request = request.form(&[("sitename", site_name.as_ref())]);
}
let response = request.send().await?.error_for_status()?;
response.json::<ApiResult<Info>>().await?.to_result()
}
pub async fn key(&self) -> Result<String, NeocitiesError> {
let mut request = self.client.get(API_URL.to_string() + "key");
request = add_authorization_header(request, &self.auth);
let response = request.send().await?.error_for_status()?;
response.json::<ApiResult<String>>().await?.to_result()
}
pub async fn upload<T: Into<Body>>(
&self,
file_path: String,
file: T,
) -> Result<String, NeocitiesError> {
let part = Part::stream(file).file_name(file_path.clone());
let form = Form::new().part(file_path, part);
let mut request = self.client.post(API_URL.to_string() + "upload");
request = add_authorization_header(request, &self.auth);
request = request.multipart(form);
let response = request.send().await?;
response.json::<ApiResult<String>>().await?.to_result()
}
pub async fn delete<T: AsRef<[String]>>(
&self,
file_paths: T,
) -> Result<String, NeocitiesError> {
let mut request = self.client.post(API_URL.to_string() + "delete");
request = add_authorization_header(request, &self.auth);
for path in file_paths.as_ref() {
request = request.query(&[("filenames[]", path.as_str())]);
}
request
.send()
.await?
.json::<ApiResult<String>>()
.await?
.to_result()
}
}
fn add_authorization_header(request: RequestBuilder, auth: &Auth) -> RequestBuilder {
match auth {
Auth::Login { username, password } => request.basic_auth(username, Some(password)),
Auth::Key(key) => request.bearer_auth(key),
}
}
#[derive(Error, Debug)]
pub enum NeocitiesError {
#[error("API returned error `{0}` with message `{1}`")]
ApiErr(String, String),
#[error(transparent)]
ReqwestErr(#[from] reqwest::Error),
}