use std::fmt::Debug;
use std::fmt::Formatter;
use base64::Engine;
use hmac::Hmac;
use hmac::Mac;
use http::header;
use http::HeaderMap;
use http::Request;
use http::Response;
use md5::Digest;
use serde::Deserialize;
use sha1::Sha1;
use self::constants::*;
use crate::raw::*;
use crate::*;
pub(super) mod constants {
pub const X_UPYUN_FILE_TYPE: &str = "x-upyun-file-type";
pub const X_UPYUN_FILE_SIZE: &str = "x-upyun-file-size";
pub const X_UPYUN_CACHE_CONTROL: &str = "x-upyun-meta-cache-control";
pub const X_UPYUN_CONTENT_DISPOSITION: &str = "x-upyun-meta-content-disposition";
pub const X_UPYUN_MULTI_STAGE: &str = "X-Upyun-Multi-Stage";
pub const X_UPYUN_MULTI_TYPE: &str = "X-Upyun-Multi-Type";
pub const X_UPYUN_MULTI_DISORDER: &str = "X-Upyun-Multi-Disorder";
pub const X_UPYUN_MULTI_UUID: &str = "X-Upyun-Multi-Uuid";
pub const X_UPYUN_PART_ID: &str = "X-Upyun-Part-Id";
pub const X_UPYUN_FOLDER: &str = "x-upyun-folder";
pub const X_UPYUN_MOVE_SOURCE: &str = "X-Upyun-Move-Source";
pub const X_UPYUN_COPY_SOURCE: &str = "X-Upyun-Copy-Source";
pub const X_UPYUN_METADATA_DIRECTIVE: &str = "X-Upyun-Metadata-Directive";
pub const X_UPYUN_LIST_ITER: &str = "x-list-iter";
pub const X_UPYUN_LIST_LIMIT: &str = "X-List-Limit";
pub const X_UPYUN_LIST_MAX_LIMIT: usize = 4096;
pub const X_UPYUN_LIST_DEFAULT_LIMIT: usize = 256;
}
#[derive(Clone)]
pub struct UpyunCore {
pub root: String,
pub operator: String,
pub bucket: String,
pub signer: UpyunSigner,
pub client: HttpClient,
}
impl Debug for UpyunCore {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Backend")
.field("root", &self.root)
.field("bucket", &self.bucket)
.field("operator", &self.operator)
.finish_non_exhaustive()
}
}
impl UpyunCore {
#[inline]
pub async fn send(&self, req: Request<Buffer>) -> Result<Response<Buffer>> {
self.client.send(req).await
}
pub fn sign(&self, req: &mut Request<Buffer>) -> Result<()> {
let date = chrono::Utc::now()
.format("%a, %d %b %Y %H:%M:%S GMT")
.to_string();
let authorization =
self.signer
.authorization(&date, req.method().as_str(), req.uri().path());
req.headers_mut()
.insert("Authorization", authorization.parse().unwrap());
req.headers_mut().insert("Date", date.parse().unwrap());
Ok(())
}
}
impl UpyunCore {
pub async fn download_file(&self, path: &str, range: BytesRange) -> Result<Response<HttpBody>> {
let path = build_abs_path(&self.root, path);
let url = format!(
"https://v0.api.upyun.com/{}/{}",
self.bucket,
percent_encode_path(&path)
);
let req = Request::get(url);
let mut req = req
.header(header::RANGE, range.to_header())
.body(Buffer::new())
.map_err(new_request_build_error)?;
self.sign(&mut req)?;
self.client.fetch(req).await
}
pub async fn info(&self, path: &str) -> Result<Response<Buffer>> {
let path = build_abs_path(&self.root, path);
let url = format!(
"https://v0.api.upyun.com/{}/{}",
self.bucket,
percent_encode_path(&path)
);
let req = Request::head(url);
let mut req = req.body(Buffer::new()).map_err(new_request_build_error)?;
self.sign(&mut req)?;
self.send(req).await
}
pub fn upload(
&self,
path: &str,
size: Option<u64>,
args: &OpWrite,
body: Buffer,
) -> Result<Request<Buffer>> {
let p = build_abs_path(&self.root, path);
let url = format!(
"https://v0.api.upyun.com/{}/{}",
self.bucket,
percent_encode_path(&p)
);
let mut req = Request::put(&url);
if let Some(size) = size {
req = req.header(header::CONTENT_LENGTH, size.to_string())
}
if let Some(mime) = args.content_type() {
req = req.header(header::CONTENT_TYPE, mime)
}
if let Some(pos) = args.content_disposition() {
req = req.header(X_UPYUN_CONTENT_DISPOSITION, pos)
}
if let Some(cache_control) = args.cache_control() {
req = req.header(X_UPYUN_CACHE_CONTROL, cache_control)
}
let mut req = req.body(body).map_err(new_request_build_error)?;
self.sign(&mut req)?;
Ok(req)
}
pub async fn delete(&self, path: &str) -> Result<Response<Buffer>> {
let path = build_abs_path(&self.root, path);
let url = format!(
"https://v0.api.upyun.com/{}/{}",
self.bucket,
percent_encode_path(&path)
);
let req = Request::delete(url);
let mut req = req.body(Buffer::new()).map_err(new_request_build_error)?;
self.sign(&mut req)?;
self.send(req).await
}
pub async fn copy(&self, from: &str, to: &str) -> Result<Response<Buffer>> {
let from = format!("/{}/{}", self.bucket, build_abs_path(&self.root, from));
let to = build_abs_path(&self.root, to);
let url = format!(
"https://v0.api.upyun.com/{}/{}",
self.bucket,
percent_encode_path(&to)
);
let mut req = Request::put(url);
req = req.header(header::CONTENT_LENGTH, "0");
req = req.header(X_UPYUN_COPY_SOURCE, from);
req = req.header(X_UPYUN_METADATA_DIRECTIVE, "copy");
let mut req = req.body(Buffer::new()).map_err(new_request_build_error)?;
self.sign(&mut req)?;
self.send(req).await
}
pub async fn move_object(&self, from: &str, to: &str) -> Result<Response<Buffer>> {
let from = format!("/{}/{}", self.bucket, build_abs_path(&self.root, from));
let to = build_abs_path(&self.root, to);
let url = format!(
"https://v0.api.upyun.com/{}/{}",
self.bucket,
percent_encode_path(&to)
);
let mut req = Request::put(url);
req = req.header(header::CONTENT_LENGTH, "0");
req = req.header(X_UPYUN_MOVE_SOURCE, from);
req = req.header(X_UPYUN_METADATA_DIRECTIVE, "copy");
let mut req = req.body(Buffer::new()).map_err(new_request_build_error)?;
self.sign(&mut req)?;
self.send(req).await
}
pub async fn create_dir(&self, path: &str) -> Result<Response<Buffer>> {
let path = build_abs_path(&self.root, path);
let path = path[..path.len() - 1].to_string();
let url = format!(
"https://v0.api.upyun.com/{}/{}",
self.bucket,
percent_encode_path(&path)
);
let mut req = Request::post(url);
req = req.header("folder", "true");
req = req.header(X_UPYUN_FOLDER, "true");
let mut req = req.body(Buffer::new()).map_err(new_request_build_error)?;
self.sign(&mut req)?;
self.send(req).await
}
pub async fn initiate_multipart_upload(
&self,
path: &str,
args: &OpWrite,
) -> Result<Response<Buffer>> {
let path = build_abs_path(&self.root, path);
let url = format!(
"https://v0.api.upyun.com/{}/{}",
self.bucket,
percent_encode_path(&path)
);
let mut req = Request::put(url);
req = req.header(X_UPYUN_MULTI_STAGE, "initiate");
req = req.header(X_UPYUN_MULTI_DISORDER, "true");
if let Some(content_type) = args.content_type() {
req = req.header(X_UPYUN_MULTI_TYPE, content_type);
}
if let Some(content_disposition) = args.content_disposition() {
req = req.header(X_UPYUN_CONTENT_DISPOSITION, content_disposition)
}
if let Some(cache_control) = args.cache_control() {
req = req.header(X_UPYUN_CACHE_CONTROL, cache_control)
}
let mut req = req.body(Buffer::new()).map_err(new_request_build_error)?;
self.sign(&mut req)?;
self.send(req).await
}
pub fn upload_part(
&self,
path: &str,
upload_id: &str,
part_number: usize,
size: u64,
body: Buffer,
) -> Result<Request<Buffer>> {
let p = build_abs_path(&self.root, path);
let url = format!(
"https://v0.api.upyun.com/{}/{}",
self.bucket,
percent_encode_path(&p),
);
let mut req = Request::put(&url);
req = req.header(header::CONTENT_LENGTH, size);
req = req.header(X_UPYUN_MULTI_STAGE, "upload");
req = req.header(X_UPYUN_MULTI_UUID, upload_id);
req = req.header(X_UPYUN_PART_ID, part_number);
let mut req = req.body(body).map_err(new_request_build_error)?;
self.sign(&mut req)?;
Ok(req)
}
pub async fn complete_multipart_upload(
&self,
path: &str,
upload_id: &str,
) -> Result<Response<Buffer>> {
let p = build_abs_path(&self.root, path);
let url = format!(
"https://v0.api.upyun.com/{}/{}",
self.bucket,
percent_encode_path(&p),
);
let mut req = Request::put(url);
req = req.header(X_UPYUN_MULTI_STAGE, "complete");
req = req.header(X_UPYUN_MULTI_UUID, upload_id);
let mut req = req.body(Buffer::new()).map_err(new_request_build_error)?;
self.sign(&mut req)?;
self.send(req).await
}
pub async fn list_objects(
&self,
path: &str,
iter: &str,
limit: Option<usize>,
) -> Result<Response<Buffer>> {
let path = build_abs_path(&self.root, path);
let url = format!(
"https://v0.api.upyun.com/{}/{}",
self.bucket,
percent_encode_path(&path),
);
let mut req = Request::get(url.clone());
req = req.header(header::ACCEPT, "application/json");
if !iter.is_empty() {
req = req.header(X_UPYUN_LIST_ITER, iter);
}
if let Some(mut limit) = limit {
if limit > X_UPYUN_LIST_MAX_LIMIT {
limit = X_UPYUN_LIST_DEFAULT_LIMIT;
}
req = req.header(X_UPYUN_LIST_LIMIT, limit);
}
let mut req = req.body(Buffer::new()).map_err(new_request_build_error)?;
self.sign(&mut req)?;
self.send(req).await
}
}
#[derive(Clone, Default)]
pub struct UpyunSigner {
pub operator: String,
pub password: String,
}
type HmacSha1 = Hmac<Sha1>;
impl UpyunSigner {
pub fn authorization(&self, date: &str, method: &str, uri: &str) -> String {
let sign = vec![method, uri, date];
let sign = sign
.into_iter()
.filter(|s| !s.is_empty())
.collect::<Vec<&str>>()
.join("&");
let mut mac = HmacSha1::new_from_slice(format_md5(self.password.as_bytes()).as_bytes())
.expect("HMAC can take key of any size");
mac.update(sign.as_bytes());
let sign_str = mac.finalize().into_bytes();
let sign = base64::engine::general_purpose::STANDARD.encode(sign_str.as_slice());
format!("UPYUN {}:{}", self.operator, sign)
}
}
pub(super) fn parse_info(headers: &HeaderMap) -> Result<Metadata> {
let mode = if parse_header_to_str(headers, X_UPYUN_FILE_TYPE)? == Some("file") {
EntryMode::FILE
} else {
EntryMode::DIR
};
let mut m = Metadata::new(mode);
if let Some(v) = parse_header_to_str(headers, X_UPYUN_FILE_SIZE)? {
let size = v.parse::<u64>().map_err(|e| {
Error::new(ErrorKind::Unexpected, "header value is not valid integer")
.with_operation("parse_info")
.set_source(e)
})?;
m.set_content_length(size);
}
if let Some(v) = parse_content_type(headers)? {
m.set_content_type(v);
}
if let Some(v) = parse_content_md5(headers)? {
m.set_content_md5(v);
}
if let Some(v) = parse_header_to_str(headers, X_UPYUN_CACHE_CONTROL)? {
m.set_cache_control(v);
}
if let Some(v) = parse_header_to_str(headers, X_UPYUN_CONTENT_DISPOSITION)? {
m.set_content_disposition(v);
}
Ok(m)
}
pub fn format_md5(bs: &[u8]) -> String {
let mut hasher = md5::Md5::new();
hasher.update(bs);
format!("{:x}", hasher.finalize())
}
#[derive(Debug, Deserialize)]
pub(super) struct File {
#[serde(rename = "type")]
pub type_field: String,
pub name: String,
pub length: u64,
pub last_modified: i64,
}
#[derive(Debug, Deserialize)]
pub(super) struct ListObjectsResponse {
pub iter: String,
pub files: Vec<File>,
}