use crate::errors::WOWSQLError;
use crate::models::{FileUploadResult, StorageFile, StorageQuota};
use reqwest::Client;
use serde_json::Value;
use std::time::Duration;
pub struct WOWSQLStorage {
base_url: String,
api_key: String,
client: Client,
auto_check_quota: bool,
project_slug: String,
}
impl WOWSQLStorage {
pub fn new(project_url: &str, api_key: &str) -> Result<Self, WOWSQLError> {
let mut url = project_url.trim().to_string();
if url.ends_with('/') {
url.pop();
}
let project_slug =
if let Some(host) = url.split("://").nth(1).and_then(|s| s.split('/').next()) {
if host.contains('.') {
host.split('.').next().unwrap_or(host).to_string()
} else {
host.to_string()
}
} else {
url.clone()
};
let client = Client::builder()
.timeout(Duration::from_secs(60))
.build()
.map_err(|e| WOWSQLError::Network(format!("Failed to create HTTP client: {}", e)))?;
Ok(Self {
base_url: "https://api.wowsql.com".to_string(),
api_key: api_key.to_string(),
client,
auto_check_quota: true,
project_slug,
})
}
pub async fn get_quota(&self, _force_refresh: bool) -> Result<StorageQuota, WOWSQLError> {
let url = format!(
"{}/api/v1/storage/s3/projects/{}/quota",
self.base_url, self.project_slug
);
self.execute_request(&url, "GET", None).await
}
pub async fn check_upload_allowed(
&self,
file_size_bytes: i64,
) -> Result<(bool, String), WOWSQLError> {
let quota = self.get_quota(true).await?;
let file_size_gb = file_size_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
if file_size_gb > quota.storage_available_gb {
return Ok((false, format!(
"Storage limit exceeded! File size: {:.4} GB, Available: {:.4} GB. Upgrade your plan to get more storage.",
file_size_gb, quota.storage_available_gb
)));
}
Ok((
true,
format!(
"Upload allowed. {:.4} GB available.",
quota.storage_available_gb
),
))
}
pub async fn upload_from_path(
&self,
file_path: &str,
file_key: Option<&str>,
folder: Option<&str>,
content_type: Option<&str>,
check_quota: Option<bool>,
) -> Result<FileUploadResult, WOWSQLError> {
use std::fs;
let bytes = fs::read(file_path)
.map_err(|e| WOWSQLError::Storage(format!("Failed to read file: {}", e)))?;
let key = file_key.unwrap_or_else(|| {
std::path::Path::new(file_path)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("file")
});
self.upload_bytes(&bytes, key, folder, content_type, check_quota)
.await
}
pub async fn upload_bytes(
&self,
bytes: &[u8],
key: &str,
folder: Option<&str>,
content_type: Option<&str>,
check_quota: Option<bool>,
) -> Result<FileUploadResult, WOWSQLError> {
let should_check = check_quota.unwrap_or(self.auto_check_quota);
if should_check {
let (allowed, message) = self.check_upload_allowed(bytes.len() as i64).await?;
if !allowed {
let quota = self.get_quota(false).await?;
return Err(WOWSQLError::StorageLimitExceeded {
message,
required_bytes: bytes.len() as i64,
available_bytes: quota.storage_available_bytes(),
});
}
}
let mut url = format!(
"{}/api/v1/storage/s3/projects/{}/upload",
self.base_url, self.project_slug
);
if let Some(folder) = folder {
use urlencoding::encode;
url = format!("{}?folder={}", url, encode(folder));
}
let file_name = key.split('/').next_back().unwrap_or("file").to_string();
let mut form = reqwest::multipart::Form::new()
.text("key", key.to_string())
.part(
"file",
reqwest::multipart::Part::bytes(bytes.to_vec()).file_name(file_name),
);
if let Some(ct) = content_type {
form = form.text("content_type", ct.to_string());
}
let response = self
.client
.post(&url)
.header("Authorization", format!("Bearer {}", self.api_key))
.multipart(form)
.send()
.await
.map_err(|e| WOWSQLError::Network(format!("Upload failed: {}", e)))?;
let status = response.status();
let text = response
.text()
.await
.map_err(|e| WOWSQLError::Network(format!("Failed to read response: {}", e)))?;
if !status.is_success() {
return Err(self.handle_error(status.as_u16(), &text));
}
serde_json::from_str(&text)
.map_err(|e| WOWSQLError::Storage(format!("Failed to parse upload response: {}", e)))
}
pub async fn list_files(
&self,
prefix: Option<&str>,
max_keys: Option<usize>,
) -> Result<Vec<StorageFile>, WOWSQLError> {
use urlencoding::encode;
let max_keys = max_keys.unwrap_or(1000);
let mut url = format!(
"{}/api/v1/storage/s3/projects/{}/files?max_keys={}",
self.base_url, self.project_slug, max_keys
);
if let Some(prefix) = prefix {
url = format!("{}&prefix={}", url, encode(prefix));
}
let response: Value = self.execute_request(&url, "GET", None).await?;
if let Some(files_array) = response.get("files").and_then(|v| v.as_array()) {
let mut files = Vec::new();
for file_value in files_array {
if let Ok(file) = serde_json::from_value(file_value.clone()) {
files.push(file);
}
}
return Ok(files);
}
Ok(vec![])
}
pub async fn get_file_url(
&self,
key: &str,
expires_in: Option<u64>,
) -> Result<Value, WOWSQLError> {
use urlencoding::encode;
let expires_in = expires_in.unwrap_or(3600);
let url = format!(
"{}/api/v1/storage/s3/projects/{}/files/{}/url?expires_in={}",
self.base_url,
self.project_slug,
encode(key),
expires_in
);
self.execute_request(&url, "GET", None).await
}
pub async fn get_presigned_url(
&self,
key: &str,
expires_in: Option<u64>,
operation: Option<&str>,
) -> Result<String, WOWSQLError> {
let url = format!(
"{}/api/v1/storage/s3/projects/{}/presigned-url",
self.base_url, self.project_slug
);
let body = serde_json::json!({
"file_key": key,
"expires_in": expires_in.unwrap_or(3600),
"operation": operation.unwrap_or("get_object")
});
let response: Value = self.execute_request(&url, "POST", Some(body)).await?;
response
.get("url")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.ok_or_else(|| WOWSQLError::Storage("Invalid presigned URL response".to_string()))
}
pub async fn download(
&self,
key: &str,
expires_in: Option<u64>,
) -> Result<String, WOWSQLError> {
self.get_presigned_url(key, expires_in, Some("get_object"))
.await
}
pub async fn get_storage_info(&self) -> Result<Value, WOWSQLError> {
let url = format!(
"{}/api/v1/storage/s3/projects/{}/info",
self.base_url, self.project_slug
);
self.execute_request(&url, "GET", None).await
}
pub async fn provision_storage(&self, region: Option<&str>) -> Result<Value, WOWSQLError> {
let url = format!(
"{}/api/v1/storage/s3/projects/{}/provision",
self.base_url, self.project_slug
);
let body = serde_json::json!({ "region": region.unwrap_or("us-east-1") });
self.execute_request(&url, "POST", Some(body)).await
}
pub async fn get_available_regions(&self) -> Result<Vec<Value>, WOWSQLError> {
let url = format!("{}/api/v1/storage/s3/regions", self.base_url);
let response: Value = self.execute_request(&url, "GET", None).await?;
if let Some(regions_array) = response.get("regions").and_then(|v| v.as_array()) {
return Ok(regions_array.clone());
}
Ok(vec![])
}
pub async fn delete_file(&self, key: &str) -> Result<(), WOWSQLError> {
use urlencoding::encode;
let url = format!(
"{}/api/v1/storage/s3/projects/{}/files/{}",
self.base_url,
self.project_slug,
encode(key)
);
self.execute_request::<Value>(&url, "DELETE", None).await?;
Ok(())
}
async fn execute_request<T: serde::de::DeserializeOwned>(
&self,
url: &str,
method: &str,
body: Option<Value>,
) -> Result<T, WOWSQLError> {
let mut request = self
.client
.request(
method
.parse()
.map_err(|_| WOWSQLError::General("Invalid method".to_string()))?,
url,
)
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.header("Authorization", format!("Bearer {}", self.api_key));
if let Some(body) = body {
request = request.json(&body);
}
let response = request
.send()
.await
.map_err(|e| WOWSQLError::Network(format!("Request failed: {}", e)))?;
let status = response.status();
let text = response
.text()
.await
.map_err(|e| WOWSQLError::Network(format!("Failed to read response: {}", e)))?;
if !status.is_success() {
return Err(self.handle_error(status.as_u16(), &text));
}
serde_json::from_str(&text)
.map_err(|e| WOWSQLError::General(format!("Failed to parse response: {}", e)))
}
fn handle_error(&self, status_code: u16, body: &str) -> WOWSQLError {
let error_response: Value = serde_json::from_str(body).unwrap_or(Value::Null);
let message = error_response
.get("error")
.and_then(|v| v.as_str())
.or_else(|| error_response.get("message").and_then(|v| v.as_str()))
.or_else(|| error_response.get("detail").and_then(|v| v.as_str()))
.unwrap_or(&format!("Request failed with status {}", status_code))
.to_string();
match status_code {
401 | 403 => WOWSQLError::Authentication(message),
404 => WOWSQLError::NotFound(message),
413 => WOWSQLError::StorageLimitExceeded {
message,
required_bytes: 0,
available_bytes: 0,
},
429 => WOWSQLError::RateLimit(message),
_ => WOWSQLError::Storage(message),
}
}
}