torrust_tracker_deployer_lib/application/services/rendering/
backup.rs1use std::path::PathBuf;
7use std::sync::Arc;
8
9use chrono::{DateTime, Utc};
10use tracing::{info, instrument};
11
12use crate::domain::backup::BackupConfig;
13use crate::domain::tracker::DatabaseConfig;
14use crate::domain::TemplateManager;
15use crate::infrastructure::templating::backup::template::wrapper::backup_config::context::{
16 BackupContext, BackupDatabaseConfig,
17};
18use crate::infrastructure::templating::backup::{
19 BackupProjectGenerator, BackupProjectGeneratorError,
20};
21use crate::infrastructure::templating::TemplateMetadata;
22
23pub struct BackupTemplateRenderingService {
31 templates_dir: PathBuf,
32 build_dir: PathBuf,
33}
34
35impl BackupTemplateRenderingService {
36 #[must_use]
43 pub fn from_paths(templates_dir: PathBuf, build_dir: PathBuf) -> Self {
44 Self {
45 templates_dir,
46 build_dir,
47 }
48 }
49
50 #[instrument(
71 name = "backup_rendering_service",
72 skip_all,
73 fields(
74 templates_dir = %self.templates_dir.display(),
75 build_dir = %self.build_dir.display()
76 )
77 )]
78 pub async fn render(
79 &self,
80 backup_config: Option<&BackupConfig>,
81 database_config: &DatabaseConfig,
82 created_at: DateTime<Utc>,
83 ) -> Result<Option<PathBuf>, BackupTemplateRenderingServiceError> {
84 let Some(backup_config) = backup_config else {
86 info!(
87 reason = "backup_not_configured",
88 "Skipping backup template rendering - backup not configured"
89 );
90 return Ok(None);
91 };
92
93 info!(
94 templates_dir = %self.templates_dir.display(),
95 build_dir = %self.build_dir.display(),
96 "Rendering backup configuration templates"
97 );
98
99 let template_manager = Arc::new(TemplateManager::new(self.templates_dir.clone()));
100 let generator = BackupProjectGenerator::new(self.build_dir.clone(), template_manager);
101
102 let backup_database_config = convert_database_config_to_backup(database_config);
103 let metadata = TemplateMetadata::new(created_at);
104 let context = BackupContext::from_config(metadata, backup_config, backup_database_config);
105
106 generator
107 .render(&context, backup_config.schedule())
108 .await
109 .map_err(BackupTemplateRenderingServiceError::RenderingFailed)?;
110
111 let backup_dir_path = self.build_dir.join("backup/etc");
112
113 info!(
114 backup_dir_path = %backup_dir_path.display(),
115 "Backup templates rendered successfully"
116 );
117
118 Ok(Some(backup_dir_path))
119 }
120}
121
122fn convert_database_config_to_backup(config: &DatabaseConfig) -> BackupDatabaseConfig {
127 match config {
128 DatabaseConfig::Sqlite(sqlite_config) => BackupDatabaseConfig::Sqlite {
129 path: format!(
130 "/data/storage/tracker/lib/database/{}",
131 sqlite_config.database_name()
132 ),
133 },
134 DatabaseConfig::Mysql(mysql_config) => BackupDatabaseConfig::Mysql {
135 host: mysql_config.host().to_string(),
136 port: mysql_config.port(),
137 database: mysql_config.database_name().to_string(),
138 user: mysql_config.username().to_string(),
139 password: mysql_config.password().expose_secret().to_string(),
140 },
141 }
142}
143
144#[derive(Debug, thiserror::Error)]
146pub enum BackupTemplateRenderingServiceError {
147 #[error("Backup template rendering failed: {0}")]
149 RenderingFailed(#[from] BackupProjectGeneratorError),
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155 use tempfile::TempDir;
156
157 use crate::domain::backup::BackupConfig;
158 use crate::domain::tracker::{DatabaseConfig, SqliteConfig};
159
160 #[tokio::test]
161 async fn it_should_create_service_with_from_paths() {
162 let templates_dir = TempDir::new().expect("Failed to create temp dir");
163 let build_dir = TempDir::new().expect("Failed to create temp dir");
164
165 let service = BackupTemplateRenderingService::from_paths(
166 templates_dir.path().to_path_buf(),
167 build_dir.path().to_path_buf(),
168 );
169
170 assert_eq!(service.templates_dir, templates_dir.path());
171 assert_eq!(service.build_dir, build_dir.path());
172 }
173
174 #[tokio::test]
175 async fn it_should_return_none_when_backup_not_configured() {
176 let templates_dir = TempDir::new().expect("Failed to create temp dir");
177 let build_dir = TempDir::new().expect("Failed to create temp dir");
178
179 let service = BackupTemplateRenderingService::from_paths(
180 templates_dir.path().to_path_buf(),
181 build_dir.path().to_path_buf(),
182 );
183
184 let database_config = DatabaseConfig::Sqlite(SqliteConfig::new("tracker.db").unwrap());
185 let created_at = Utc::now();
186
187 let result = service.render(None, &database_config, created_at).await;
188
189 assert!(result.is_ok());
190 assert!(result.unwrap().is_none());
191 }
192
193 #[tokio::test]
194 async fn it_should_render_backup_templates_when_backup_is_configured() {
195 let templates_dir = TempDir::new().expect("Failed to create temp dir");
196 let build_dir = TempDir::new().expect("Failed to create temp dir");
197
198 let service = BackupTemplateRenderingService::from_paths(
199 templates_dir.path().to_path_buf(),
200 build_dir.path().to_path_buf(),
201 );
202
203 let backup_config = BackupConfig::default();
204 let database_config = DatabaseConfig::Sqlite(SqliteConfig::new("tracker.db").unwrap());
205 let created_at = Utc::now();
206
207 let result = service
208 .render(Some(&backup_config), &database_config, created_at)
209 .await;
210
211 assert!(result.is_ok());
212 let backup_dir = result.unwrap();
213 assert!(backup_dir.is_some());
214 let backup_dir = backup_dir.unwrap();
215 assert!(backup_dir.to_string_lossy().contains("backup/etc"));
216 }
217
218 #[test]
219 fn it_should_convert_sqlite_config_to_backup_format() {
220 let sqlite_config = SqliteConfig::new("tracker.db").unwrap();
221 let database_config = DatabaseConfig::Sqlite(sqlite_config.clone());
222
223 let backup_config = convert_database_config_to_backup(&database_config);
224
225 match backup_config {
226 BackupDatabaseConfig::Sqlite { path } => {
227 assert!(path.contains(sqlite_config.database_name()));
228 }
229 BackupDatabaseConfig::Mysql { .. } => {
230 panic!("Expected Sqlite config");
231 }
232 }
233 }
234}