use std::time::Duration;
use reqwest::{
multipart::{Form, Part},
Client, Response, StatusCode,
};
use url::Url;
use crate::{
error::ArxivisError,
types::{
ApiKey, CreateKeyRequest, CreateKeyResult, FileRecord, KeysEnvelope, ListOptions,
ListResult, SearchOptions, SearchResult, Stats, UploadOptions, ZipRequest,
},
};
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
#[derive(Clone)]
pub struct ArxivisClient {
base: String,
api_key: String,
http: Client,
}
impl ArxivisClient {
pub fn new(
base_url: impl Into<String>,
api_key: impl Into<String>,
) -> Result<Self, ArxivisError> {
let http = Client::builder()
.timeout(DEFAULT_TIMEOUT)
.build()
.map_err(ArxivisError::Http)?;
Ok(Self {
base: base_url.into().trim_end_matches('/').to_owned() + "/api/v1",
api_key: api_key.into(),
http,
})
}
pub fn with_timeout(mut self, timeout: Duration) -> Result<Self, ArxivisError> {
self.http = Client::builder()
.timeout(timeout)
.build()
.map_err(ArxivisError::Http)?;
Ok(self)
}
fn api_url(&self, path: &str) -> String {
format!("{}{}", self.base, path)
}
async fn check(resp: Response) -> Result<Response, ArxivisError> {
if resp.status().is_success() {
return Ok(resp);
}
let status = resp.status().as_u16();
let message = resp
.json::<serde_json::Value>()
.await
.ok()
.and_then(|v| v.get("error").and_then(|e| e.as_str()).map(String::from))
.unwrap_or_else(|| format!("HTTP {status}"));
Err(ArxivisError::Api { status, message })
}
async fn fetch<T: serde::de::DeserializeOwned>(
&self,
path: &str,
) -> Result<T, ArxivisError> {
let resp = self
.http
.get(self.api_url(path))
.header("X-API-Key", &self.api_key)
.header("Accept", "application/json")
.send()
.await?;
Ok(Self::check(resp).await?.json::<T>().await?)
}
async fn fetch_params<T: serde::de::DeserializeOwned>(
&self,
path: &str,
params: &[(&str, String)],
) -> Result<T, ArxivisError> {
let mut url = Url::parse(&self.api_url(path))
.map_err(|e| ArxivisError::Api { status: 0, message: e.to_string() })?;
for (k, v) in params {
url.query_pairs_mut().append_pair(k, v);
}
let resp = self
.http
.get(url)
.header("X-API-Key", &self.api_key)
.header("Accept", "application/json")
.send()
.await?;
Ok(Self::check(resp).await?.json::<T>().await?)
}
async fn fetch_bytes(&self, path: &str) -> Result<bytes::Bytes, ArxivisError> {
let resp = self
.http
.get(self.api_url(path))
.header("X-API-Key", &self.api_key)
.send()
.await?;
Ok(Self::check(resp).await?.bytes().await?)
}
async fn post_json<B, T>(&self, path: &str, body: &B) -> Result<T, ArxivisError>
where
B: serde::Serialize,
T: serde::de::DeserializeOwned,
{
let resp = self
.http
.post(self.api_url(path))
.header("X-API-Key", &self.api_key)
.header("Accept", "application/json")
.json(body)
.send()
.await?;
Ok(Self::check(resp).await?.json::<T>().await?)
}
async fn post_json_bytes<B>(&self, path: &str, body: &B) -> Result<bytes::Bytes, ArxivisError>
where
B: serde::Serialize,
{
let http = Client::builder()
.timeout(Duration::from_secs(600))
.build()
.map_err(ArxivisError::Http)?;
let resp = http
.post(self.api_url(path))
.header("X-API-Key", &self.api_key)
.json(body)
.send()
.await?;
Ok(Self::check(resp).await?.bytes().await?)
}
async fn do_delete(&self, path: &str) -> Result<(), ArxivisError> {
let resp = self
.http
.delete(self.api_url(path))
.header("X-API-Key", &self.api_key)
.send()
.await?;
if resp.status() == StatusCode::NO_CONTENT || resp.status().is_success() {
return Ok(());
}
let status = resp.status().as_u16();
let message = resp
.json::<serde_json::Value>()
.await
.ok()
.and_then(|v| v.get("error").and_then(|e| e.as_str()).map(String::from))
.unwrap_or_else(|| format!("HTTP {status}"));
Err(ArxivisError::Api { status, message })
}
pub async fn get_stats(&self) -> Result<Stats, ArxivisError> {
self.fetch("/stats").await
}
pub async fn upload(
&self,
data: impl Into<Vec<u8>>,
filename: impl Into<String>,
opts: UploadOptions,
) -> Result<FileRecord, ArxivisError> {
let filename = filename.into();
let mime = mime_guess::from_path(&filename)
.first_or_octet_stream()
.to_string();
let file_part = Part::bytes(data.into())
.file_name(filename.clone())
.mime_str(&mime)
.map_err(|e| ArxivisError::Http(e.into()))?;
let mut form = Form::new().part("file", file_part);
if !opts.tags.is_empty() {
form = form.text("tags", opts.tags.join(","));
}
if let Some(path) = opts.path {
form = form.text("path", path);
}
if let Some(c) = opts.compress {
form = form.text("compress", if c { "true" } else { "false" });
}
if let Some(e) = opts.encrypt {
form = form.text("encrypt", if e { "true" } else { "false" });
}
let resp = self
.http
.post(self.api_url("/files"))
.header("X-API-Key", &self.api_key)
.header("Accept", "application/json")
.multipart(form)
.send()
.await?;
Ok(Self::check(resp).await?.json::<FileRecord>().await?)
}
pub async fn list(
&self,
path: impl Into<String>,
opts: ListOptions,
) -> Result<ListResult, ArxivisError> {
let limit = if opts.limit == 0 { 50 } else { opts.limit };
self.fetch_params(
"/files",
&[
("path", path.into()),
("limit", limit.to_string()),
("offset", opts.offset.to_string()),
],
)
.await
}
pub async fn get(&self, id: &str) -> Result<FileRecord, ArxivisError> {
self.fetch(&format!("/files/{id}")).await
}
pub async fn get_by_virtual_path(&self, virtual_path: &str) -> Result<FileRecord, ArxivisError> {
let encoded = urlencoding::encode(virtual_path);
self.fetch(&format!("/files/resolve?path={encoded}")).await
}
pub async fn download(&self, id: &str) -> Result<bytes::Bytes, ArxivisError> {
self.fetch_bytes(&format!("/files/{id}/download")).await
}
pub async fn delete_file(&self, id: &str) -> Result<(), ArxivisError> {
self.do_delete(&format!("/files/{id}")).await
}
pub async fn download_zip(&self, ids: &[&str]) -> Result<bytes::Bytes, ArxivisError> {
self.post_json_bytes("/files/zip", &ZipRequest { ids }).await
}
pub async fn create_folder(&self, path: &str) -> Result<String, ArxivisError> {
let resp: serde_json::Value =
self.post_json("/folders", &serde_json::json!({ "path": path })).await?;
Ok(resp
.get("path")
.and_then(|v| v.as_str())
.unwrap_or(path)
.to_owned())
}
pub async fn search(
&self,
query: &str,
opts: SearchOptions,
) -> Result<ListResult, ArxivisError> {
let limit = if opts.limit == 0 { 50 } else { opts.limit };
self.fetch_params(
"/search",
&[
("q", query.to_owned()),
("limit", limit.to_string()),
("offset", opts.offset.to_string()),
],
)
.await
}
pub async fn semantic_search(
&self,
query: &str,
opts: SearchOptions,
) -> Result<SearchResult, ArxivisError> {
let limit = if opts.limit == 0 { 20 } else { opts.limit };
self.fetch_params(
"/search/semantic",
&[("q", query.to_owned()), ("limit", limit.to_string())],
)
.await
}
pub async fn hybrid_search(
&self,
query: &str,
opts: SearchOptions,
) -> Result<SearchResult, ArxivisError> {
let limit = if opts.limit == 0 { 30 } else { opts.limit };
self.fetch_params(
"/search/hybrid",
&[("q", query.to_owned()), ("limit", limit.to_string())],
)
.await
}
pub async fn create_key(&self, name: &str) -> Result<CreateKeyResult, ArxivisError> {
self.post_json("/keys", &CreateKeyRequest { name }).await
}
pub async fn list_keys(&self) -> Result<Vec<ApiKey>, ArxivisError> {
let envelope: KeysEnvelope = self.fetch("/keys").await?;
Ok(envelope.keys)
}
pub async fn revoke_key(&self, id: &str) -> Result<(), ArxivisError> {
self.do_delete(&format!("/keys/{id}")).await
}
pub fn download_url(&self, id: &str) -> String {
let base = self.base.trim_end_matches("/api/v1");
format!(
"{}/api/v1/files/{}/download?api_key={}",
base,
id,
urlencoding::encode(&self.api_key)
)
}
pub fn preview_url(&self, id: &str) -> String {
let base = self.base.trim_end_matches("/api/v1");
format!(
"{}/api/v1/files/{}/preview?api_key={}",
base,
id,
urlencoding::encode(&self.api_key)
)
}
pub fn path_url(&self, file_path: &str) -> String {
let base = self.base.trim_end_matches("/api/v1");
let clean = file_path.trim_start_matches('/');
format!(
"{}/f/{}?api_key={}",
base,
clean,
urlencoding::encode(&self.api_key)
)
}
}