From c70cde8a9c625a8477da6835138416dfa5f6555b Mon Sep 17 00:00:00 2001 From: Charles GTE Date: Sat, 12 Sep 2026 17:57:04 +0200 Subject: [PATCH] feat(storage): sftp provider over rclone rcat --- src/services/storage/mod.rs | 2 + src/services/storage/providers/mod.rs | 1 + src/services/storage/providers/sftp/mod.rs | 115 +++++++++++++++++++++ 3 files changed, 118 insertions(+) create mode 100644 src/services/storage/providers/sftp/mod.rs diff --git a/src/services/storage/mod.rs b/src/services/storage/mod.rs index 7c130b0..a9ba6f1 100644 --- a/src/services/storage/mod.rs +++ b/src/services/storage/mod.rs @@ -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 Some(Box::new(rclone::RcloneProvider {})), + "sftp" => Some(Box::new(sftp::SftpProvider {})), _ => { error!("Unknown storage provider: {}", storage.provider); None diff --git a/src/services/storage/providers/mod.rs b/src/services/storage/providers/mod.rs index ddf6a82..064ef4e 100644 --- a/src/services/storage/providers/mod.rs +++ b/src/services/storage/providers/mod.rs @@ -4,3 +4,4 @@ pub mod google_drive; pub mod local; pub mod rclone; pub mod s3; +pub mod sftp; diff --git a/src/services/storage/providers/sftp/mod.rs b/src/services/storage/providers/sftp/mod.rs new file mode 100644 index 0000000..34e0df4 --- /dev/null +++ b/src/services/storage/providers/sftp/mod.rs @@ -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) -> 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, + result: BackupResult, + _method: BackupMethod, + storage: &DatabaseStorage, + encrypt: Option, + _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)) + } + } + } +}