1
This commit is contained in:
126
src-tauri/src/oss.rs
Normal file
126
src-tauri/src/oss.rs
Normal file
@@ -0,0 +1,126 @@
|
||||
//! Minimal Aliyun OSS PUT-Object client.
|
||||
//!
|
||||
//! Only the subset required by the Douyin pipeline is implemented:
|
||||
//! - PUT a single local file under `audio/<uuid>.<ext>`
|
||||
//! - Sign with HMAC-SHA1 using AccessKey ID + Secret
|
||||
//! - Return the public https URL
|
||||
//!
|
||||
//! No external `aliyun-oss` crate is used so that the dependency footprint
|
||||
//! stays small.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use base64::{engine::general_purpose::STANDARD as B64, Engine as _};
|
||||
use chrono::Utc;
|
||||
use hmac::{Hmac, Mac};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha1::Sha1;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum OssError {
|
||||
#[error("missing field: {0}")]
|
||||
MissingField(&'static str),
|
||||
#[error("io: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("http: {0}")]
|
||||
Http(#[from] reqwest::Error),
|
||||
#[error("oss returned status={status}, body={body}")]
|
||||
BadStatus { status: u16, body: String },
|
||||
#[error("hmac error")]
|
||||
Hmac,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct OssConfig {
|
||||
#[serde(rename = "accessKeyId")]
|
||||
pub access_key_id: String,
|
||||
#[serde(rename = "accessKeySecret")]
|
||||
pub access_key_secret: String,
|
||||
pub bucket: String,
|
||||
pub region: String,
|
||||
#[serde(default)]
|
||||
pub endpoint: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct OssUploadResult {
|
||||
pub success: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub url: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
type HmacSha1 = Hmac<Sha1>;
|
||||
|
||||
fn rfc1123_now() -> String {
|
||||
Utc::now().format("%a, %d %b %Y %H:%M:%S GMT").to_string()
|
||||
}
|
||||
|
||||
fn sign(secret: &str, str_to_sign: &str) -> Result<String, OssError> {
|
||||
let mut mac = HmacSha1::new_from_slice(secret.as_bytes()).map_err(|_| OssError::Hmac)?;
|
||||
mac.update(str_to_sign.as_bytes());
|
||||
Ok(B64.encode(mac.finalize().into_bytes()))
|
||||
}
|
||||
|
||||
fn require<'a>(v: &'a str, name: &'static str) -> Result<&'a str, OssError> {
|
||||
if v.trim().is_empty() {
|
||||
Err(OssError::MissingField(name))
|
||||
} else {
|
||||
Ok(v)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn put_object(
|
||||
cfg: &OssConfig,
|
||||
local_path: &Path,
|
||||
object_key: &str,
|
||||
content_type: &str,
|
||||
) -> Result<String, OssError> {
|
||||
let ak = require(&cfg.access_key_id, "accessKeyId")?;
|
||||
let sk = require(&cfg.access_key_secret, "accessKeySecret")?;
|
||||
let bucket = require(&cfg.bucket, "bucket")?;
|
||||
let region = require(&cfg.region, "region")?;
|
||||
|
||||
let host = format!("{bucket}.{region}.aliyuncs.com");
|
||||
let endpoint = cfg
|
||||
.endpoint
|
||||
.clone()
|
||||
.unwrap_or_else(|| format!("https://{host}"));
|
||||
let url = format!("{}/{}", endpoint.trim_end_matches('/'), object_key);
|
||||
|
||||
let body = tokio::fs::read(local_path).await?;
|
||||
let date = rfc1123_now();
|
||||
|
||||
// String-to-sign for OSS V1:
|
||||
// VERB\nContent-MD5\nContent-Type\nDate\nCanonicalizedOSSHeaders+CanonicalizedResource
|
||||
let canonicalized_resource = format!("/{bucket}/{object_key}");
|
||||
let str_to_sign = format!(
|
||||
"PUT\n\n{ct}\n{date}\n{res}",
|
||||
ct = content_type,
|
||||
date = date,
|
||||
res = canonicalized_resource
|
||||
);
|
||||
let signature = sign(sk, &str_to_sign)?;
|
||||
let authorization = format!("OSS {ak}:{signature}");
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.build()
|
||||
.map_err(OssError::Http)?;
|
||||
let resp = client
|
||||
.put(&url)
|
||||
.header("Date", &date)
|
||||
.header("Content-Type", content_type)
|
||||
.header("Authorization", authorization)
|
||||
.body(body)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let status = resp.status().as_u16();
|
||||
if !(200..300).contains(&status) {
|
||||
let body = resp.text().await.unwrap_or_default();
|
||||
return Err(OssError::BadStatus { status, body });
|
||||
}
|
||||
Ok(url)
|
||||
}
|
||||
Reference in New Issue
Block a user