use std::collections::HashMap;
use crate::auth::clients::auth_fetch::AuthFetch;
use crate::auth::error::AuthError;
use crate::primitives::hash::sha256;
use crate::services::ServicesError;
use crate::wallet::interfaces::WalletInterface;
use super::storage_utils::get_url_for_hash;
#[derive(Debug, Clone)]
pub struct StorageUploaderConfig {
pub base_url: String,
pub default_retention_period: Option<u64>,
}
impl Default for StorageUploaderConfig {
fn default() -> Self {
StorageUploaderConfig {
base_url: "https://storage.bsvb.tech".into(),
default_retention_period: None,
}
}
}
#[derive(Debug, Clone)]
pub struct UploadFileResult {
pub published: bool,
pub uhrp_url: String,
}
#[cfg(feature = "network")]
#[derive(serde::Deserialize)]
struct UploadInfoResponse {
status: Option<String>,
#[serde(rename = "uploadURL")]
upload_url: Option<String>,
#[serde(rename = "requiredHeaders")]
required_headers: Option<HashMap<String, String>>,
#[allow(dead_code)]
amount: Option<u64>,
}
pub struct StorageUploader<W: WalletInterface + Clone + 'static> {
auth_fetch: AuthFetch<W>,
config: StorageUploaderConfig,
}
impl<W: WalletInterface + Clone + 'static> StorageUploader<W> {
pub fn new(wallet: W, config: StorageUploaderConfig) -> Self {
StorageUploader {
auth_fetch: AuthFetch::new(wallet),
config,
}
}
#[cfg(feature = "network")]
pub async fn upload(
&mut self,
data: &[u8],
content_type: &str,
retention_period: Option<u64>,
) -> Result<UploadFileResult, ServicesError> {
let retention = retention_period
.or(self.config.default_retention_period)
.unwrap_or(525600);
let body = serde_json::json!({
"fileSize": data.len(),
"retentionPeriod": retention,
});
let body_bytes =
serde_json::to_vec(&body).map_err(|e| ServicesError::Serialization(e.to_string()))?;
let mut headers = HashMap::new();
headers.insert("Content-Type".to_string(), "application/json".to_string());
let url = format!("{}/upload", self.config.base_url);
let response = self
.auth_fetch
.fetch(&url, "POST", Some(body_bytes), Some(headers))
.await
.map_err(|e: AuthError| ServicesError::Auth(e))?;
if response.status >= 400 {
return Err(ServicesError::Storage(format!(
"upload info request failed: HTTP {}",
response.status
)));
}
let info: UploadInfoResponse = serde_json::from_slice(&response.body)
.map_err(|e| ServicesError::Serialization(e.to_string()))?;
if info.status.as_deref() == Some("error") {
return Err(ServicesError::Storage(
"upload route returned an error".into(),
));
}
let upload_url = info
.upload_url
.ok_or_else(|| ServicesError::Storage("upload response missing uploadURL".into()))?;
let required_headers = info.required_headers.unwrap_or_default();
let client = reqwest::Client::new();
let mut req = client
.put(&upload_url)
.header("Content-Type", content_type)
.body(data.to_vec());
for (k, v) in &required_headers {
req = req.header(k, v);
}
let put_response = req
.send()
.await
.map_err(|e| ServicesError::Http(e.to_string()))?;
if !put_response.status().is_success() {
return Err(ServicesError::Storage(format!(
"file upload failed: HTTP {}",
put_response.status()
)));
}
let hash = sha256(data);
let uhrp_url = get_url_for_hash(&hash);
Ok(UploadFileResult {
published: true,
uhrp_url,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_uhrp_url_from_upload_data() {
let data = b"test file content";
let hash = sha256(data);
let url = get_url_for_hash(&hash);
assert!(super::super::storage_utils::is_valid_url(&url));
let recovered = super::super::storage_utils::get_hash_from_url(&url).unwrap();
assert_eq!(hash, recovered);
}
#[test]
fn test_default_config() {
let config = StorageUploaderConfig::default();
assert!(config.base_url.contains("storage"));
assert!(config.default_retention_period.is_none());
}
}