feat(storage): sftp provider over rclone rcat

This commit is contained in:
Charles GTE
2026-09-12 17:57:04 +02:00
parent c353443bdd
commit c70cde8a9c
3 changed files with 118 additions and 0 deletions
+2
View File
@@ -11,6 +11,7 @@ use providers::google_drive;
use providers::local;
use providers::rclone;
use providers::s3;
use providers::sftp;
use std::sync::Arc;
use tracing::{error, info};
@@ -40,6 +41,7 @@ pub fn get_provider(storage: &DatabaseStorage) -> Option<Box<dyn StorageProvider
google_cloud_storage::GoogleCloudStorageProvider {},
)),
"rclone" => Some(Box::new(rclone::RcloneProvider {})),
"sftp" => Some(Box::new(sftp::SftpProvider {})),
_ => {
error!("Unknown storage provider: {}", storage.provider);
None
+1
View File
@@ -4,3 +4,4 @@ pub mod google_drive;
pub mod local;
pub mod rclone;
pub mod s3;
pub mod sftp;
+115
View File
@@ -0,0 +1,115 @@
pub mod helpers;
pub mod models;
use crate::core::context::Context;
use crate::services::api::models::agent::status::DatabaseStorage;
use crate::services::backup::models::{BackupResult, UploadResult};
use crate::services::storage::StorageProvider;
use crate::services::storage::providers::rclone::helpers::{rcat, remote_target, write_config};
use crate::services::storage::providers::sftp::helpers::build_sftp_config;
use crate::services::storage::providers::sftp::models::SftpProviderConfig;
use crate::utils::common::BackupMethod;
use crate::utils::file::{full_file_name, full_file_path};
use crate::utils::stream::build_stream;
use async_trait::async_trait;
use std::sync::Arc;
use tokio::fs;
use tracing::{error, info};
pub struct SftpProvider {}
fn failed(storage_id: &str, error: impl ToString, total_size: Option<u64>) -> UploadResult {
UploadResult {
storage_id: storage_id.to_string(),
success: false,
error: Some(error.to_string()),
remote_file_path: None,
total_size,
}
}
#[async_trait]
impl StorageProvider for SftpProvider {
async fn upload(
&self,
ctx: Arc<Context>,
result: BackupResult,
_method: BackupMethod,
storage: &DatabaseStorage,
encrypt: Option<bool>,
_backup_storage_id: &str,
) -> UploadResult {
let storage_id = storage.id.clone();
let Some(file_path) = result.backup_file else {
return failed(&storage_id, "Missing backup file path", None);
};
let total_size = match fs::metadata(&file_path).await {
Ok(meta) => meta.len(),
Err(e) => {
error!("Failed to get file size: {}", e);
return failed(&storage_id, e, None);
}
};
let config: SftpProviderConfig = match storage.clone().config.try_into() {
Ok(c) => c,
Err(e) => {
error!("sftp config deserialization failed: {}", e);
return failed(&storage_id, e, Some(total_size));
}
};
// `_key_file` stays in scope so the temp key survives the whole upload.
let (config_text, _key_file) = match build_sftp_config(&config) {
Ok(v) => v,
Err(e) => {
error!("sftp config build failed: {}", e);
return failed(&storage_id, e, Some(total_size));
}
};
let encrypt = encrypt.unwrap_or(false);
let upload = match build_stream(&file_path, encrypt, &ctx.edge_key.master_key_b64).await {
Ok(u) => u,
Err(e) => {
error!("Stream build failed: {}", e);
return failed(&storage_id, e, Some(total_size));
}
};
let file_name = full_file_name(encrypt);
let remote_file_path = full_file_path(&file_name, storage.folder_name.as_deref());
let config_file = match write_config(&config_text) {
Ok(f) => f,
Err(e) => {
error!("sftp config write failed: {}", e);
return failed(&storage_id, e, Some(total_size));
}
};
let target = remote_target("sftp", &config.remote_path, &remote_file_path);
info!("Starting sftp (rclone) upload to {}", target);
match rcat(config_file.path(), &target, upload.stream).await {
Ok(()) => {
info!("sftp upload successful: {}", remote_file_path);
UploadResult {
storage_id,
success: true,
error: None,
remote_file_path: Some(remote_file_path),
total_size: Some(total_size),
}
}
Err(e) => {
error!("sftp upload failed: {:?}", e);
failed(&storage_id, e, Some(total_size))
}
}
}
}