pub mod providers;
pub mod rate_limit;
pub mod config;
pub use config::{ProviderRootsConfig, ProviderCredentialsConfig};
pub use cloud_sync_core::traits;
pub use cloud_sync_core::state;
pub use cloud_sync_core::path;
pub use cloud_sync_std::ignore;
pub use cloud_sync_std::checksum;
pub use providers::{OAuthCredentials, WebDAVCredentials, S3Credentials, SFTPCredentials, NextcloudCredentials, MegaCredentials, AzureBlobCredentials, GCSCredentials, B2Credentials, PCloudCredentials, IPFSCredentials, SimulatedFallback, local_sim::LocalSimulation, CommonProviderSettings, ProviderConfig, EncryptedBackend, SyncMode, BackendCredentials, BackendRegistry, OAuthTokenManager};
pub use cloud_sync_core::{SyncState, FileState, ConflictPolicy};
pub use cloud_sync_std::SyncIgnore;
pub use providers::utils::{RetryConfig, set_global_retry_config, get_global_retry_config};
pub fn create_backend(
creds: BackendCredentials,
sim_root: std::path::PathBuf,
) -> std::sync::Arc<dyn traits::StorageBackend> {
BackendRegistry::create_backend(creds, sim_root)
}
#[cfg(feature = "google_drive")]
pub use providers::{GoogleDriveProvider, GoogleDriveProviderBuilder};
#[cfg(feature = "dropbox")]
pub use providers::{DropboxProvider, DropboxProviderBuilder};
#[cfg(feature = "onedrive")]
pub use providers::{OneDriveProvider, OneDriveProviderBuilder};
#[cfg(feature = "webdav")]
pub use providers::{WebDAVProvider, WebDAVProviderBuilder};
#[cfg(feature = "s3")]
pub use providers::{S3Provider, S3ProviderBuilder};
#[cfg(feature = "sftp")]
pub use providers::{SFTPProvider, SFTPProviderBuilder};
#[cfg(feature = "nextcloud")]
pub use providers::{NextcloudProvider, NextcloudProviderBuilder};
#[cfg(feature = "box")]
pub use providers::{BoxProvider, BoxProviderBuilder};
#[cfg(feature = "mega")]
pub use providers::{MegaProvider, MegaProviderBuilder};
#[cfg(feature = "azure_blob")]
pub use providers::{AzureBlobProvider, AzureBlobProviderBuilder};
#[cfg(feature = "gcs")]
pub use providers::{GCSProvider, GCSProviderBuilder};
#[cfg(feature = "b2")]
pub use providers::{B2Provider, B2ProviderBuilder};
#[cfg(feature = "pcloud")]
pub use providers::{PCloudProvider, PCloudProviderBuilder};
#[cfg(feature = "ipfs")]
pub use providers::{IPFSProvider, IPFSProviderBuilder};
pub use cloud_sync_core::{StorageBackend, StorageError, StorageItem, SyncPolicy};
#[cfg(test)]
mod tests {
use super::*;
use std::fs::File;
use std::io::Write;
use tempfile::tempdir;
#[allow(dead_code)]
async fn mount_oauth_mock(server: &wiremock::MockServer, token_val: &str) {
use wiremock::matchers::{method, path};
use wiremock::{Mock, ResponseTemplate};
Mock::given(method("POST"))
.and(path("/oauth"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"access_token": token_val
})))
.mount(server)
.await;
}
#[allow(dead_code)]
async fn run_simulated_flow_test<B>(provider_name: &str)
where
B: StorageBackend + 'static,
{
let temp_dir = tempdir().unwrap();
let safe_name = provider_name.to_lowercase().replace(' ', "_");
let provider_root = temp_dir.path().join(format!("{}_root", safe_name));
let local_sim = LocalSimulation::new(provider_root.clone(), provider_name.to_string());
let provider = SimulatedFallback::<B>::new(None, local_sim, provider_name, SyncMode::TwoWay);
let local_file_path = temp_dir.path().join("test.txt");
let mut file = File::create(&local_file_path).unwrap();
writeln!(file, "Hello simulated cloud storage!").unwrap();
provider.upload(&local_file_path, "hello.txt").await.unwrap();
let remote_file = provider_root.join("hello.txt");
assert!(remote_file.exists());
assert_eq!(std::fs::read_to_string(remote_file).unwrap().trim(), "Hello simulated cloud storage!");
let items = provider.list("").await.unwrap();
assert_eq!(items.len(), 1);
assert_eq!(items[0].path.to_string_lossy(), "hello.txt");
let download_path = temp_dir.path().join("downloaded.txt");
provider.download("hello.txt", &download_path).await.unwrap();
assert!(download_path.exists());
assert_eq!(std::fs::read_to_string(download_path).unwrap().trim(), "Hello simulated cloud storage!");
provider.delete("hello.txt").await.unwrap();
assert!(!provider_root.join("hello.txt").exists());
}
#[tokio::test]
#[cfg(feature = "google_drive")]
async fn test_google_drive_provider_flow() {
run_simulated_flow_test::<GoogleDriveProvider>("Google Drive").await;
}
#[tokio::test]
#[cfg(feature = "google_drive")]
async fn test_google_drive_mock_http_flow() {
use wiremock::matchers::{method, path, query_param};
use wiremock::{Mock, MockServer, ResponseTemplate};
let server = MockServer::start().await;
mount_oauth_mock(&server, "mocked-token-123").await;
Mock::given(method("GET"))
.and(path("/files"))
.and(query_param("q", "name = 'hello.txt' and 'root' in parents and mimeType != 'application/vnd.google-apps.folder' and trashed = false"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"files": [
{
"id": "hello-file-id",
"mimeType": "text/plain"
}
]
})))
.mount(&server)
.await;
Mock::given(method("GET"))
.and(path("/files"))
.and(query_param("q", "'root' in parents and trashed = false"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"files": [
{
"id": "hello-file-id",
"name": "hello.txt",
"size": "32",
"mimeType": "text/plain"
}
]
})))
.mount(&server)
.await;
Mock::given(method("PATCH"))
.and(path("/upload/hello-file-id"))
.respond_with(ResponseTemplate::new(200))
.mount(&server)
.await;
Mock::given(method("GET"))
.and(path("/files/hello-file-id"))
.and(query_param("alt", "media"))
.respond_with(ResponseTemplate::new(200).set_body_string("Hello simulated cloud storage!"))
.mount(&server)
.await;
Mock::given(method("DELETE"))
.and(path("/files/hello-file-id"))
.respond_with(ResponseTemplate::new(200))
.mount(&server)
.await;
let temp_dir = tempdir().unwrap();
let provider_root = temp_dir.path().join("gdrive_root");
let creds = OAuthCredentials {
client_id: "mock_client".to_string(),
client_secret: "mock_secret".to_string(),
refresh_token: "mock_refresh".to_string(),
common: CommonProviderSettings::default(),
};
let inner = GoogleDriveProvider::new(creds)
.with_endpoints(
format!("{}/oauth", server.uri()),
format!("{}/files", server.uri()),
format!("{}/upload", server.uri()),
);
let local_sim = LocalSimulation::new(provider_root.clone(), "Google Drive".to_string());
let provider = SimulatedFallback::new(Some(inner), local_sim, "Google Drive", SyncMode::TwoWay);
let local_file_path = temp_dir.path().join("test.txt");
let mut file = File::create(&local_file_path).unwrap();
writeln!(file, "Hello simulated cloud storage!").unwrap();
provider.upload(&local_file_path, "hello.txt").await.unwrap();
let items = provider.list("").await.unwrap();
assert_eq!(items.len(), 1);
assert_eq!(items[0].path.to_string_lossy(), "hello.txt");
let download_path = temp_dir.path().join("downloaded.txt");
provider.download("hello.txt", &download_path).await.unwrap();
assert!(download_path.exists());
assert_eq!(std::fs::read_to_string(download_path).unwrap().trim(), "Hello simulated cloud storage!");
provider.delete("hello.txt").await.unwrap();
}
#[tokio::test]
#[cfg(feature = "google_drive")]
async fn test_google_drive_real_flow() {
let mut config_path = std::path::Path::new("../private_config.toml");
if !config_path.exists() {
config_path = std::path::Path::new("../config.toml");
}
if !config_path.exists() {
println!("Skipping real Google Drive test: configuration file not found.");
return;
}
let content = match std::fs::read_to_string(config_path) {
Ok(c) => c,
Err(_) => {
println!("Skipping real Google Drive test: failed to read config file");
return;
}
};
#[derive(serde::Deserialize)]
struct TestConfig {
google_credentials: Option<OAuthCredentials>,
}
let config: TestConfig = match toml::from_str(&content) {
Ok(cfg) => cfg,
Err(e) => {
println!("Skipping real Google Drive test: failed to parse config file ({:?})", e);
return;
}
};
let credentials = match config.google_credentials {
Some(creds) => {
if creds.client_secret.contains('*')
|| creds.client_secret.contains("PLACEHOLDER")
|| creds.client_id.contains("PLACEHOLDER")
|| creds.client_id.is_empty()
{
println!("Skipping real Google Drive test: Credentials contain placeholder or masked secret.");
return;
}
creds
}
None => {
println!("Skipping real Google Drive test: No google_credentials found in config file");
return;
}
};
println!("Running real Google Drive integration test...");
let temp_dir = tempdir().unwrap();
let provider_root = temp_dir.path().join("gdrive_root");
let inner = GoogleDriveProvider::new(credentials);
let local_sim = LocalSimulation::new(provider_root.clone(), "Google Drive".to_string());
let provider = SimulatedFallback::new(Some(inner), local_sim, "Google Drive", SyncMode::TwoWay);
let file_name = format!("test_real_{}.txt", std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs());
let local_file_path = temp_dir.path().join(&file_name);
let mut file = File::create(&local_file_path).unwrap();
writeln!(file, "Hello real Google Drive!").unwrap();
let run_test = || async {
provider.upload(&local_file_path, &file_name).await?;
let items = provider.list("").await?;
let found = items.iter().any(|item| item.path.to_string_lossy() == file_name);
assert!(found, "Uploaded file was not found in the file listing");
let download_path = temp_dir.path().join("downloaded_real.txt");
provider.download(&file_name, &download_path).await?;
assert!(download_path.exists());
assert_eq!(std::fs::read_to_string(download_path).unwrap().trim(), "Hello real Google Drive!");
provider.delete(&file_name).await?;
let items_after = provider.list("").await?;
let found_after = items_after.iter().any(|item| item.path.to_string_lossy() == file_name);
assert!(!found_after, "File was not successfully deleted from Google Drive");
Ok::<(), StorageError>(())
};
if let Err(e) = run_test().await {
println!("Skipping real Google Drive test due to API/connection error: {:?}", e);
}
}
#[tokio::test]
#[cfg(feature = "dropbox")]
async fn test_dropbox_provider_simulated_flow() {
run_simulated_flow_test::<DropboxProvider>("Dropbox").await;
}
#[tokio::test]
#[cfg(feature = "dropbox")]
async fn test_dropbox_mock_http_flow() {
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
let server = MockServer::start().await;
mount_oauth_mock(&server, "mocked-dropbox-token-123").await;
Mock::given(method("POST"))
.and(path("/content/upload"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"name": "hello.txt",
"id": "id:12345"
})))
.mount(&server)
.await;
Mock::given(method("POST"))
.and(path("/content/download"))
.respond_with(ResponseTemplate::new(200).set_body_string("Hello simulated cloud storage!"))
.mount(&server)
.await;
Mock::given(method("POST"))
.and(path("/files/list_folder"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"entries": [
{
".tag": "file",
"name": "hello.txt",
"size": 32
}
],
"cursor": "ZtkX...",
"has_more": false
})))
.mount(&server)
.await;
Mock::given(method("POST"))
.and(path("/files/delete_v2"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"metadata": {
".tag": "file",
"name": "hello.txt"
}
})))
.mount(&server)
.await;
let temp_dir = tempdir().unwrap();
let provider_root = temp_dir.path().join("dropbox_root");
let creds = OAuthCredentials {
client_id: "mock_client".to_string(),
client_secret: "mock_secret".to_string(),
refresh_token: "mock_refresh".to_string(),
common: CommonProviderSettings::default(),
};
let inner = DropboxProvider::new(creds)
.with_endpoints(
format!("{}/oauth", server.uri()),
format!("{}/files", server.uri()),
format!("{}/content", server.uri()),
);
let local_sim = LocalSimulation::new(provider_root.clone(), "Dropbox".to_string());
let provider = SimulatedFallback::new(Some(inner), local_sim, "Dropbox", SyncMode::TwoWay);
let local_file_path = temp_dir.path().join("test.txt");
let mut file = File::create(&local_file_path).unwrap();
writeln!(file, "Hello simulated cloud storage!").unwrap();
provider.upload(&local_file_path, "hello.txt").await.unwrap();
let items = provider.list("").await.unwrap();
assert_eq!(items.len(), 1);
assert_eq!(items[0].path.to_string_lossy(), "hello.txt");
let download_path = temp_dir.path().join("downloaded.txt");
provider.download("hello.txt", &download_path).await.unwrap();
assert!(download_path.exists());
assert_eq!(std::fs::read_to_string(download_path).unwrap().trim(), "Hello simulated cloud storage!");
provider.delete("hello.txt").await.unwrap();
}
#[tokio::test]
#[cfg(feature = "dropbox")]
async fn test_dropbox_real_flow() {
let mut config_path = std::path::Path::new("../private_config.toml");
if !config_path.exists() {
config_path = std::path::Path::new("../config.toml");
}
if !config_path.exists() {
println!("Skipping real Dropbox test: configuration file not found.");
return;
}
let content = match std::fs::read_to_string(config_path) {
Ok(c) => c,
Err(_) => {
println!("Skipping real Dropbox test: failed to read config file");
return;
}
};
#[derive(serde::Deserialize)]
struct TestConfig {
dropbox_credentials: Option<OAuthCredentials>,
}
let config: TestConfig = match toml::from_str(&content) {
Ok(cfg) => cfg,
Err(e) => {
println!("Skipping real Dropbox test: failed to parse config file ({:?})", e);
return;
}
};
let credentials = match config.dropbox_credentials {
Some(creds) => {
if creds.client_secret.contains('*')
|| creds.client_secret.contains("PLACEHOLDER")
|| creds.client_id.contains("PLACEHOLDER")
|| creds.client_id.is_empty()
{
println!("Skipping real Dropbox test: Credentials contain placeholder or masked secret.");
return;
}
creds
}
None => {
println!("Skipping real Dropbox test: No dropbox_credentials found in config file");
return;
}
};
println!("Running real Dropbox integration test...");
let temp_dir = tempdir().unwrap();
let provider_root = temp_dir.path().join("dropbox_root");
let inner = DropboxProvider::new(credentials);
let local_sim = LocalSimulation::new(provider_root.clone(), "Dropbox".to_string());
let provider = SimulatedFallback::new(Some(inner), local_sim, "Dropbox", SyncMode::TwoWay);
let file_name = format!("test_real_{}.txt", std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs());
let local_file_path = temp_dir.path().join(&file_name);
let mut file = File::create(&local_file_path).unwrap();
writeln!(file, "Hello real Dropbox!").unwrap();
let run_test = || async {
provider.upload(&local_file_path, &file_name).await?;
let items = provider.list("").await?;
let found = items.iter().any(|item| item.path.to_string_lossy() == file_name);
assert!(found, "Uploaded file was not found in the file listing");
let download_path = temp_dir.path().join("downloaded_real.txt");
provider.download(&file_name, &download_path).await?;
assert!(download_path.exists());
assert_eq!(std::fs::read_to_string(download_path).unwrap().trim(), "Hello real Dropbox!");
provider.delete(&file_name).await?;
let items_after = provider.list("").await?;
let found_after = items_after.iter().any(|item| item.path.to_string_lossy() == file_name);
assert!(!found_after, "File was not successfully deleted from Dropbox");
Ok::<(), StorageError>(())
};
if let Err(e) = run_test().await {
println!("Skipping real Dropbox test due to API/connection error: {:?}", e);
}
}
#[tokio::test]
#[cfg(feature = "onedrive")]
async fn test_onedrive_provider_simulated_flow() {
run_simulated_flow_test::<OneDriveProvider>("OneDrive").await;
}
#[tokio::test]
#[cfg(feature = "onedrive")]
async fn test_onedrive_mock_http_flow() {
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
let server = MockServer::start().await;
mount_oauth_mock(&server, "mocked-onedrive-token-123").await;
Mock::given(method("PUT"))
.and(path("/me/drive/root:/hello.txt:/content"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"name": "hello.txt"
})))
.mount(&server)
.await;
Mock::given(method("GET"))
.and(path("/me/drive/root:/hello.txt:/content"))
.respond_with(ResponseTemplate::new(200).set_body_string("Hello simulated cloud storage!"))
.mount(&server)
.await;
Mock::given(method("GET"))
.and(path("/me/drive/root/children"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"value": [
{
"name": "hello.txt",
"size": 32
}
]
})))
.mount(&server)
.await;
Mock::given(method("DELETE"))
.and(path("/me/drive/root:/hello.txt"))
.respond_with(ResponseTemplate::new(200))
.mount(&server)
.await;
let temp_dir = tempdir().unwrap();
let provider_root = temp_dir.path().join("onedrive_root");
let creds = OAuthCredentials {
client_id: "mock_client".to_string(),
client_secret: "mock_secret".to_string(),
refresh_token: "mock_refresh".to_string(),
common: CommonProviderSettings::default(),
};
let inner = OneDriveProvider::new(creds)
.with_endpoints(
format!("{}/oauth", server.uri()),
server.uri(),
);
let local_sim = LocalSimulation::new(provider_root.clone(), "OneDrive".to_string());
let provider = SimulatedFallback::new(Some(inner), local_sim, "OneDrive", SyncMode::TwoWay);
let local_file_path = temp_dir.path().join("test.txt");
let mut file = File::create(&local_file_path).unwrap();
writeln!(file, "Hello simulated cloud storage!").unwrap();
provider.upload(&local_file_path, "hello.txt").await.unwrap();
let items = provider.list("").await.unwrap();
assert_eq!(items.len(), 1);
assert_eq!(items[0].path.to_string_lossy(), "hello.txt");
let download_path = temp_dir.path().join("downloaded.txt");
provider.download("hello.txt", &download_path).await.unwrap();
assert!(download_path.exists());
assert_eq!(std::fs::read_to_string(download_path).unwrap().trim(), "Hello simulated cloud storage!");
provider.delete("hello.txt").await.unwrap();
}
#[tokio::test]
#[cfg(feature = "onedrive")]
async fn test_onedrive_real_flow() {
let mut config_path = std::path::Path::new("../private_config.toml");
if !config_path.exists() {
config_path = std::path::Path::new("../config.toml");
}
if !config_path.exists() {
println!("Skipping real OneDrive test: configuration file not found.");
return;
}
let content = match std::fs::read_to_string(config_path) {
Ok(c) => c,
Err(_) => {
println!("Skipping real OneDrive test: failed to read config file");
return;
}
};
#[derive(serde::Deserialize)]
struct TestConfig {
onedrive_credentials: Option<OAuthCredentials>,
}
let config: TestConfig = match toml::from_str(&content) {
Ok(cfg) => cfg,
Err(e) => {
println!("Skipping real OneDrive test: failed to parse config file ({:?})", e);
return;
}
};
let credentials = match config.onedrive_credentials {
Some(creds) => {
if creds.client_secret.contains('*')
|| creds.client_secret.contains("PLACEHOLDER")
|| creds.client_id.contains("PLACEHOLDER")
|| creds.client_id.is_empty()
{
println!("Skipping real OneDrive test: Credentials contain placeholder or masked secret.");
return;
}
creds
}
None => {
println!("Skipping real OneDrive test: No onedrive_credentials found in config file");
return;
}
};
println!("Running real OneDrive integration test...");
let temp_dir = tempdir().unwrap();
let provider_root = temp_dir.path().join("onedrive_root");
let inner = OneDriveProvider::new(credentials);
let local_sim = LocalSimulation::new(provider_root.clone(), "OneDrive".to_string());
let provider = SimulatedFallback::new(Some(inner), local_sim, "OneDrive", SyncMode::TwoWay);
let file_name = format!("test_real_{}.txt", std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs());
let local_file_path = temp_dir.path().join(&file_name);
let mut file = File::create(&local_file_path).unwrap();
writeln!(file, "Hello real OneDrive!").unwrap();
let run_test = || async {
provider.upload(&local_file_path, &file_name).await?;
let items = provider.list("").await?;
let found = items.iter().any(|item| item.path.to_string_lossy() == file_name);
assert!(found, "Uploaded file was not found in the file listing");
let download_path = temp_dir.path().join("downloaded_real.txt");
provider.download(&file_name, &download_path).await?;
assert!(download_path.exists());
assert_eq!(std::fs::read_to_string(download_path).unwrap().trim(), "Hello real OneDrive!");
provider.delete(&file_name).await?;
let items_after = provider.list("").await?;
let found_after = items_after.iter().any(|item| item.path.to_string_lossy() == file_name);
assert!(!found_after, "File was not successfully deleted from OneDrive");
Ok::<(), StorageError>(())
};
if let Err(e) = run_test().await {
println!("Skipping real OneDrive test due to API/connection error: {:?}", e);
}
}
#[tokio::test]
#[cfg(feature = "webdav")]
async fn test_webdav_provider_simulated_flow() {
run_simulated_flow_test::<WebDAVProvider>("WebDAV").await;
}
#[tokio::test]
#[cfg(feature = "webdav")]
async fn test_webdav_mock_http_flow() {
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
let server = MockServer::start().await;
Mock::given(method("MKCOL"))
.and(path("/MySyncFolder"))
.respond_with(ResponseTemplate::new(201))
.mount(&server)
.await;
Mock::given(method("PUT"))
.and(path("/MySyncFolder/hello.txt"))
.respond_with(ResponseTemplate::new(201))
.mount(&server)
.await;
Mock::given(method("GET"))
.and(path("/MySyncFolder/hello.txt"))
.respond_with(ResponseTemplate::new(200).set_body_string("Hello simulated WebDAV storage!"))
.mount(&server)
.await;
let propfind_xml = r#"<?xml version="1.0" encoding="utf-8"?>
<d:multistatus xmlns:d="DAV:">
<d:response>
<d:href>/MySyncFolder</d:href>
<d:propstat>
<d:prop>
<d:resourcetype><d:collection/></d:resourcetype>
</d:prop>
<d:status>HTTP/1.1 200 OK</d:status>
</d:propstat>
</d:response>
<d:response>
<d:href>/MySyncFolder/hello.txt</d:href>
<d:propstat>
<d:prop>
<d:getcontentlength>32</d:getcontentlength>
<d:resourcetype/>
</d:prop>
<d:status>HTTP/1.1 200 OK</d:status>
</d:propstat>
</d:response>
</d:multistatus>"#;
Mock::given(method("PROPFIND"))
.and(path("/MySyncFolder/"))
.respond_with(ResponseTemplate::new(207).set_body_string(propfind_xml))
.mount(&server)
.await;
Mock::given(method("DELETE"))
.and(path("/MySyncFolder/hello.txt"))
.respond_with(ResponseTemplate::new(204))
.mount(&server)
.await;
let temp_dir = tempdir().unwrap();
let provider_root = temp_dir.path().join("webdav_root");
let creds = WebDAVCredentials {
url: server.uri(),
username: "username".to_string(),
password: "password".to_string(),
common: CommonProviderSettings {
destination_folder: Some("MySyncFolder".to_string()),
enabled: None,
sync_mode: None,
encryption_password: None,
max_upload_rate: None,
max_download_rate: None,
selective_sync: None,
},
};
let inner = WebDAVProvider::new(creds).with_endpoints(server.uri());
let local_sim = LocalSimulation::new(provider_root.clone(), "WebDAV".to_string());
let provider = SimulatedFallback::new(Some(inner), local_sim, "WebDAV", SyncMode::TwoWay);
let local_file_path = temp_dir.path().join("test.txt");
let mut file = File::create(&local_file_path).unwrap();
writeln!(file, "Hello simulated WebDAV storage!").unwrap();
provider.upload(&local_file_path, "hello.txt").await.unwrap();
let items = provider.list("").await.unwrap();
assert_eq!(items.len(), 1);
assert_eq!(items[0].path.to_string_lossy(), "hello.txt");
let download_path = temp_dir.path().join("downloaded.txt");
provider.download("hello.txt", &download_path).await.unwrap();
assert!(download_path.exists());
assert_eq!(std::fs::read_to_string(download_path).unwrap().trim(), "Hello simulated WebDAV storage!");
provider.delete("hello.txt").await.unwrap();
}
#[tokio::test]
#[cfg(feature = "webdav")]
async fn test_webdav_real_flow() {
let mut config_path = std::path::Path::new("../private_config.toml");
if !config_path.exists() {
config_path = std::path::Path::new("../config.toml");
}
if !config_path.exists() {
println!("Skipping real WebDAV test: configuration file not found.");
return;
}
let content = match std::fs::read_to_string(config_path) {
Ok(c) => c,
Err(_) => {
println!("Skipping real WebDAV test: failed to read config file");
return;
}
};
#[derive(serde::Deserialize)]
struct TestConfig {
webdav_credentials: Option<WebDAVCredentials>,
}
let config: TestConfig = match toml::from_str(&content) {
Ok(cfg) => cfg,
Err(e) => {
println!("Skipping real WebDAV test: failed to parse config file ({:?})", e);
return;
}
};
let credentials = match config.webdav_credentials {
Some(creds) => {
if creds.username.contains("PLACEHOLDER")
|| creds.username.is_empty()
{
println!("Skipping real WebDAV test: Credentials contain placeholder or empty username.");
return;
}
creds
}
None => {
println!("Skipping real WebDAV test: No webdav_credentials found in config file");
return;
}
};
println!("Running real WebDAV integration test...");
let temp_dir = tempdir().unwrap();
let provider_root = temp_dir.path().join("webdav_root");
let inner = WebDAVProvider::new(credentials);
let local_sim = LocalSimulation::new(provider_root.clone(), "WebDAV".to_string());
let provider = SimulatedFallback::new(Some(inner), local_sim, "WebDAV", SyncMode::TwoWay);
let file_name = format!("test_real_{}.txt", std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs());
let local_file_path = temp_dir.path().join(&file_name);
let mut file = File::create(&local_file_path).unwrap();
writeln!(file, "Hello real WebDAV!").unwrap();
let run_test = || async {
provider.upload(&local_file_path, &file_name).await?;
let items = provider.list("").await?;
let found = items.iter().any(|item| item.path.to_string_lossy() == file_name);
assert!(found, "Uploaded file was not found in the file listing");
let download_path = temp_dir.path().join("downloaded_real.txt");
provider.download(&file_name, &download_path).await?;
assert!(download_path.exists());
assert_eq!(std::fs::read_to_string(download_path).unwrap().trim(), "Hello real WebDAV!");
provider.delete(&file_name).await?;
let items_after = provider.list("").await?;
let found_after = items_after.iter().any(|item| item.path.to_string_lossy() == file_name);
assert!(!found_after, "File was not successfully deleted from WebDAV");
Ok::<(), StorageError>(())
};
if let Err(e) = run_test().await {
println!("Skipping real WebDAV test due to API/connection error: {:?}", e);
}
}
#[tokio::test]
#[cfg(feature = "s3")]
async fn test_s3_provider_simulated_flow() {
run_simulated_flow_test::<S3Provider>("S3").await;
}
#[tokio::test]
#[cfg(feature = "s3")]
async fn test_s3_mock_http_flow() {
use wiremock::matchers::{method, path, path_regex};
use wiremock::{Mock, MockServer, ResponseTemplate};
let server = MockServer::start().await;
Mock::given(method("PUT"))
.and(path("/test-bucket/MySyncFolder/hello.txt"))
.respond_with(ResponseTemplate::new(200))
.mount(&server)
.await;
Mock::given(method("GET"))
.and(path("/test-bucket/MySyncFolder/hello.txt"))
.respond_with(ResponseTemplate::new(200).set_body_string("Hello simulated S3 storage!"))
.mount(&server)
.await;
let list_xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<ListBucketResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<Name>test-bucket</Name>
<Prefix>MySyncFolder/</Prefix>
<KeyCount>1</KeyCount>
<MaxKeys>1000</MaxKeys>
<IsTruncated>false</IsTruncated>
<Contents>
<Key>MySyncFolder/hello.txt</Key>
<LastModified>2026-06-26T12:00:00.000Z</LastModified>
<ETag>"3a3f"</ETag>
<Size>32</Size>
<StorageClass>STANDARD</StorageClass>
</Contents>
</ListBucketResult>"#;
Mock::given(method("GET"))
.and(path_regex(r"^/test-bucket/?$"))
.respond_with(ResponseTemplate::new(200).set_body_string(list_xml))
.mount(&server)
.await;
Mock::given(method("DELETE"))
.and(path("/test-bucket/MySyncFolder/hello.txt"))
.respond_with(ResponseTemplate::new(204))
.mount(&server)
.await;
let temp_dir = tempdir().unwrap();
let provider_root = temp_dir.path().join("s3_root");
let creds = S3Credentials {
bucket: "test-bucket".to_string(),
region: "us-east-1".to_string(),
access_key_id: "access_key".to_string(),
secret_access_key: "secret_key".to_string(),
endpoint: Some(server.uri()),
common: CommonProviderSettings {
destination_folder: Some("MySyncFolder".to_string()),
enabled: None,
sync_mode: None,
encryption_password: None,
max_upload_rate: None,
max_download_rate: None,
selective_sync: None,
},
};
let inner = S3Provider::new(creds).with_endpoints(server.uri());
let local_sim = LocalSimulation::new(provider_root.clone(), "S3".to_string());
let provider = SimulatedFallback::new(Some(inner), local_sim, "S3", SyncMode::TwoWay);
let local_file_path = temp_dir.path().join("test.txt");
let mut file = File::create(&local_file_path).unwrap();
writeln!(file, "Hello simulated S3 storage!").unwrap();
provider.upload(&local_file_path, "hello.txt").await.unwrap();
let items = provider.list("").await.unwrap();
assert_eq!(items.len(), 1);
assert_eq!(items[0].path.to_string_lossy(), "hello.txt");
let download_path = temp_dir.path().join("downloaded.txt");
provider.download("hello.txt", &download_path).await.unwrap();
assert!(download_path.exists());
assert_eq!(std::fs::read_to_string(download_path).unwrap().trim(), "Hello simulated S3 storage!");
provider.delete("hello.txt").await.unwrap();
}
#[tokio::test]
#[cfg(feature = "s3")]
async fn test_s3_real_flow() {
let mut config_path = std::path::Path::new("../private_config.toml");
if !config_path.exists() {
config_path = std::path::Path::new("../config.toml");
}
if !config_path.exists() {
println!("Skipping real S3 test: configuration file not found.");
return;
}
let content = match std::fs::read_to_string(config_path) {
Ok(c) => c,
Err(_) => {
println!("Skipping real S3 test: failed to read config file");
return;
}
};
#[derive(serde::Deserialize)]
struct TestConfig {
s3_credentials: Option<S3Credentials>,
}
let config: TestConfig = match toml::from_str(&content) {
Ok(cfg) => cfg,
Err(e) => {
println!("Skipping real S3 test: failed to parse config file ({:?})", e);
return;
}
};
let credentials = match config.s3_credentials {
Some(creds) => {
if creds.access_key_id.contains("PLACEHOLDER")
|| creds.access_key_id.is_empty()
{
println!("Skipping real S3 test: Credentials contain placeholder or empty key.");
return;
}
creds
}
None => {
println!("Skipping real S3 test: No s3_credentials found in config file");
return;
}
};
println!("Running real S3 integration test...");
let temp_dir = tempdir().unwrap();
let provider_root = temp_dir.path().join("s3_root");
let inner = S3Provider::new(credentials);
let local_sim = LocalSimulation::new(provider_root.clone(), "S3".to_string());
let provider = SimulatedFallback::new(Some(inner), local_sim, "S3", SyncMode::TwoWay);
let file_name = format!("test_real_{}.txt", std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs());
let local_file_path = temp_dir.path().join(&file_name);
let mut file = File::create(&local_file_path).unwrap();
writeln!(file, "Hello real S3!").unwrap();
let run_test = || async {
provider.upload(&local_file_path, &file_name).await?;
let items = provider.list("").await?;
let found = items.iter().any(|item| item.path.to_string_lossy() == file_name);
assert!(found, "Uploaded file was not found in the file listing");
let download_path = temp_dir.path().join("downloaded_real.txt");
provider.download(&file_name, &download_path).await?;
assert!(download_path.exists());
assert_eq!(std::fs::read_to_string(download_path).unwrap().trim(), "Hello real S3!");
provider.delete(&file_name).await?;
let items_after = provider.list("").await?;
let found_after = items_after.iter().any(|item| item.path.to_string_lossy() == file_name);
assert!(!found_after, "File was not successfully deleted from S3");
Ok::<(), StorageError>(())
};
if let Err(e) = run_test().await {
println!("Skipping real S3 test due to API/connection error: {:?}", e);
}
}
#[tokio::test]
#[cfg(feature = "sftp")]
async fn test_sftp_provider_simulated_flow() {
run_simulated_flow_test::<SFTPProvider>("SFTP").await;
}
#[tokio::test]
#[cfg(feature = "nextcloud")]
async fn test_nextcloud_provider_simulated_flow() {
run_simulated_flow_test::<NextcloudProvider>("Nextcloud").await;
}
#[tokio::test]
#[cfg(feature = "box")]
async fn test_box_provider_simulated_flow() {
run_simulated_flow_test::<BoxProvider>("Box").await;
}
#[tokio::test]
#[cfg(feature = "mega")]
async fn test_mega_provider_simulated_flow() {
run_simulated_flow_test::<MegaProvider>("MEGA").await;
}
#[tokio::test]
#[cfg(feature = "azure_blob")]
async fn test_azure_blob_provider_simulated_flow() {
run_simulated_flow_test::<AzureBlobProvider>("Azure Blob").await;
}
#[tokio::test]
#[cfg(feature = "gcs")]
async fn test_gcs_provider_simulated_flow() {
run_simulated_flow_test::<GCSProvider>("GCS").await;
}
#[tokio::test]
#[cfg(feature = "b2")]
async fn test_b2_provider_simulated_flow() {
run_simulated_flow_test::<B2Provider>("B2").await;
}
#[tokio::test]
#[cfg(feature = "pcloud")]
async fn test_pcloud_provider_simulated_flow() {
run_simulated_flow_test::<PCloudProvider>("pCloud").await;
}
#[tokio::test]
#[cfg(feature = "ipfs")]
async fn test_ipfs_provider_simulated_flow() {
run_simulated_flow_test::<IPFSProvider>("IPFS").await;
}
}