use crate::traits::{StorageBackend, StorageError, StorageItem};
use crate::providers::PCloudCredentials;
use crate::providers::utils::translate_http_error;
use async_trait::async_trait;
use std::path::Path;
use std::time::{SystemTime, Duration};
use tokio::fs;
use tracing::info;
use serde::Deserialize;
#[derive(Deserialize, Debug)]
struct PCloudMetadata {
name: String,
size: Option<u64>,
modified: Option<u64>,
isfolder: bool,
}
#[derive(Deserialize, Debug)]
struct PCloudContents {
contents: Option<Vec<PCloudMetadata>>,
}
#[derive(Deserialize, Debug)]
struct PCloudListResponse {
metadata: Option<PCloudContents>,
}
#[derive(Deserialize, Debug)]
struct PCloudFileLinkResponse {
hosts: Vec<String>,
path: String,
}
pub struct PCloudProvider {
client: reqwest::Client,
credentials: PCloudCredentials,
api_url: String,
}
crate::impl_provider_builder!(PCloudProvider, PCloudProviderBuilder, PCloudCredentials, absolute);
impl PCloudProvider {
pub fn with_client_options(
credentials: PCloudCredentials,
timeout: Option<std::time::Duration>,
custom_headers: Option<reqwest::header::HeaderMap>,
) -> Self {
let api_url = if let Some(ref ep) = credentials.endpoint {
ep.trim_end_matches('/').to_string()
} else {
"https://api.pcloud.com".to_string()
};
Self {
client: super::utils::build_http_client(timeout, custom_headers),
credentials,
api_url,
}
}
}
#[async_trait]
impl StorageBackend for PCloudProvider {
fn name(&self) -> &str {
"pCloud"
}
async fn upload(&self, local_path: &Path, remote_path: &str) -> Result<(), StorageError> {
super::utils::execute_with_retry(self.name(), "upload", || async {
let clean_path = self.format_path(remote_path);
info!("[{}] Real upload starting for '{}'", self.name(), clean_path);
let file_content = fs::read(local_path).await?;
let parent_dir = Path::new(clean_path.as_ref())
.parent()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|| "/".to_string());
let file_name = Path::new(clean_path.as_ref())
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("")
.to_string();
let upload_url = format!("{}/uploadfile", self.api_url);
let form = reqwest::multipart::Form::new()
.part("file", reqwest::multipart::Part::bytes(file_content).file_name(file_name));
let res = super::utils::apply_bearer_auth(self.client.post(&upload_url), &self.credentials.access_token)
.query(&[("path", &parent_dir)])
.multipart(form)
.send()
.await?;
if !res.status().is_success() {
return Err(translate_http_error(res, self.name(), "upload").await);
}
Ok(())
}).await
}
async fn download(&self, remote_path: &str, local_path: &Path) -> Result<(), StorageError> {
super::utils::execute_with_retry(self.name(), "download", || async {
let clean_path = self.format_path(remote_path);
let link_url = format!("{}/getfilelink", self.api_url);
let res = super::utils::apply_bearer_auth(self.client.get(&link_url), &self.credentials.access_token)
.query(&[("path", &clean_path)])
.send()
.await?;
if !res.status().is_success() {
return Err(translate_http_error(res, self.name(), "getfilelink").await);
}
let link_info: PCloudFileLinkResponse = res.json().await?;
let host = link_info.hosts.first()
.ok_or_else(|| StorageError::NotFound("No download hosts returned by pCloud".to_string()))?;
let download_url = format!("https://{}{}", host, link_info.path);
let dl_res = self.client.get(&download_url).send().await?;
if !dl_res.status().is_success() {
return Err(translate_http_error(dl_res, self.name(), "download").await);
}
if let Some(parent) = local_path.parent() {
fs::create_dir_all(parent).await?;
}
let bytes = dl_res.bytes().await?;
fs::write(local_path, bytes).await?;
Ok(())
}).await
}
async fn delete(&self, remote_path: &str) -> Result<(), StorageError> {
super::utils::execute_with_retry(self.name(), "delete", || async {
let clean_path = self.format_path(remote_path);
let delete_url = format!("{}/deletefile", self.api_url);
let res = super::utils::apply_bearer_auth(self.client.get(&delete_url), &self.credentials.access_token)
.query(&[("path", &clean_path)])
.send()
.await?;
if !res.status().is_success() {
return Err(translate_http_error(res, self.name(), "delete").await);
}
Ok(())
}).await
}
async fn list(&self, remote_path: &str) -> Result<Vec<StorageItem>, StorageError> {
super::utils::execute_with_retry(self.name(), "list", || async {
let clean_path = self.format_path(remote_path);
let list_url = format!("{}/listfolder", self.api_url);
let res = super::utils::apply_bearer_auth(self.client.get(&list_url), &self.credentials.access_token)
.query(&[("path", &clean_path)])
.send()
.await?;
if !res.status().is_success() {
return Err(translate_http_error(res, self.name(), "list").await);
}
let list_response: PCloudListResponse = res.json().await?;
let mut items = Vec::new();
if let Some(meta) = list_response.metadata {
if let Some(contents) = meta.contents {
for entry in contents {
if entry.isfolder {
continue;
}
let item_path = super::utils::strip_destination_prefix(Path::new(&entry.name), self.credentials.common.destination_folder.as_deref());
let modified = entry.modified
.map(|m| SystemTime::UNIX_EPOCH + Duration::from_secs(m))
.unwrap_or(SystemTime::now());
items.push(StorageItem {
path: item_path,
size: entry.size.unwrap_or(0),
modified,
is_dir: false,
checksum: None,
permissions: None,
});
}
}
}
Ok(items)
}).await
}
}
pub struct PCloudProviderBuilder {
pub credentials: PCloudCredentials,
pub timeout: Option<std::time::Duration>,
pub custom_headers: Option<reqwest::header::HeaderMap>,
}
impl PCloudProviderBuilder {
pub fn new(credentials: PCloudCredentials) -> Self {
Self {
credentials,
timeout: None,
custom_headers: None,
}
}
pub fn build(self) -> PCloudProvider {
PCloudProvider::with_client_options(self.credentials, self.timeout, self.custom_headers)
}
}