torrust_tracker_deployer_lib/application/command_handlers/purge/handler.rs
1//! Purge command handler implementation
2
3use std::path::PathBuf;
4use std::sync::Arc;
5
6use tracing::{info, instrument, warn};
7
8use super::errors::PurgeCommandHandlerError;
9use crate::domain::environment::repository::EnvironmentRepository;
10use crate::domain::EnvironmentName;
11
12/// `PurgeCommandHandler` orchestrates the removal of all local environment data
13///
14/// This command handler removes all local files associated with an environment:
15/// 1. Removes the `data/{env-name}/` directory (environment state, configs, etc.)
16/// 2. Removes the `build/{env-name}/` directory (generated templates, artifacts)
17/// 3. Removes the environment entry from the repository
18///
19/// # State Management
20///
21/// Unlike other commands, purge **does not transition environment state**:
22/// - Works on environments in any state
23/// - Removes all local data regardless of current state
24/// - Does not persist state after purge (the environment data is removed)
25///
26/// # Idempotency
27///
28/// The purge operation is idempotent. Running it multiple times on the same
29/// environment will:
30/// - Succeed if the directories are already removed
31/// - Not fail due to missing resources
32/// - Report appropriate status to the user
33///
34/// # Important Notes
35///
36/// - **Does NOT destroy infrastructure**: Only removes local files
37/// - **Irreversible operation**: All local environment data is permanently deleted
38/// - **Works in any state**: Can purge environments that are Created, Provisioned, Running, etc.
39pub struct PurgeCommandHandler {
40 repository: Arc<dyn EnvironmentRepository + Send + Sync>,
41 working_dir: PathBuf,
42}
43
44impl PurgeCommandHandler {
45 /// Create a new `PurgeCommandHandler`
46 ///
47 /// # Arguments
48 ///
49 /// * `repository` - Repository for accessing environment data
50 /// * `working_dir` - Root directory containing `data/` and `build/` subdirectories
51 #[must_use]
52 pub fn new(
53 repository: Arc<dyn EnvironmentRepository + Send + Sync>,
54 working_dir: PathBuf,
55 ) -> Self {
56 Self {
57 repository,
58 working_dir,
59 }
60 }
61
62 /// Execute the complete purge workflow
63 ///
64 /// # Arguments
65 ///
66 /// * `env_name` - The name of the environment to purge
67 ///
68 /// # Errors
69 ///
70 /// Returns an error if:
71 /// * Environment not found in repository
72 /// * Unable to remove data directory due to permissions or I/O errors
73 /// * Unable to remove build directory due to permissions or I/O errors
74 /// * Unable to remove environment from repository
75 ///
76 /// If directories are already removed, the operation succeeds (idempotent).
77 #[instrument(
78 name = "purge_command",
79 skip_all,
80 fields(
81 command_type = "purge",
82 environment = %env_name
83 )
84 )]
85 pub fn execute(&self, env_name: &EnvironmentName) -> Result<(), PurgeCommandHandlerError> {
86 // Verify environment exists
87 self.verify_environment_exists(env_name)?;
88
89 // Remove data directory
90 self.remove_data_directory(env_name)?;
91
92 // Remove build directory
93 self.remove_build_directory(env_name)?;
94
95 // Remove from repository (this also removes the environment.json file)
96 self.remove_from_repository(env_name)?;
97
98 info!(
99 command = "purge",
100 environment = %env_name,
101 "Environment purged successfully"
102 );
103
104 Ok(())
105 }
106
107 /// Verify environment exists in repository
108 fn verify_environment_exists(
109 &self,
110 env_name: &EnvironmentName,
111 ) -> Result<(), PurgeCommandHandlerError> {
112 match self.repository.exists(env_name) {
113 Ok(true) => Ok(()),
114 Ok(false) => Err(PurgeCommandHandlerError::EnvironmentNotFound {
115 name: env_name.to_string(),
116 }),
117 Err(e) => {
118 warn!(
119 command = "purge",
120 environment = %env_name,
121 error = %e,
122 "Failed to check if environment exists, proceeding anyway"
123 );
124 // Don't fail the purge if we can't check existence
125 // The user may be trying to clean up a corrupted environment
126 Ok(())
127 }
128 }
129 }
130
131 /// Remove the data directory for the environment
132 fn remove_data_directory(
133 &self,
134 env_name: &EnvironmentName,
135 ) -> Result<(), PurgeCommandHandlerError> {
136 let data_dir = self.working_dir.join("data").join(env_name.as_str());
137
138 if !data_dir.exists() {
139 info!(
140 command = "purge",
141 environment = %env_name,
142 path = %data_dir.display(),
143 "Data directory does not exist, skipping removal"
144 );
145 return Ok(());
146 }
147
148 info!(
149 command = "purge",
150 environment = %env_name,
151 path = %data_dir.display(),
152 "Removing data directory"
153 );
154
155 std::fs::remove_dir_all(&data_dir).map_err(|source| {
156 PurgeCommandHandlerError::DataDirectoryRemovalFailed {
157 path: data_dir,
158 source,
159 }
160 })?;
161
162 Ok(())
163 }
164
165 /// Remove the build directory for the environment
166 fn remove_build_directory(
167 &self,
168 env_name: &EnvironmentName,
169 ) -> Result<(), PurgeCommandHandlerError> {
170 let build_dir = self.working_dir.join("build").join(env_name.as_str());
171
172 if !build_dir.exists() {
173 info!(
174 command = "purge",
175 environment = %env_name,
176 path = %build_dir.display(),
177 "Build directory does not exist, skipping removal"
178 );
179 return Ok(());
180 }
181
182 info!(
183 command = "purge",
184 environment = %env_name,
185 path = %build_dir.display(),
186 "Removing build directory"
187 );
188
189 std::fs::remove_dir_all(&build_dir).map_err(|source| {
190 PurgeCommandHandlerError::BuildDirectoryRemovalFailed {
191 path: build_dir,
192 source,
193 }
194 })?;
195
196 Ok(())
197 }
198
199 /// Remove environment from repository
200 fn remove_from_repository(
201 &self,
202 env_name: &EnvironmentName,
203 ) -> Result<(), PurgeCommandHandlerError> {
204 info!(
205 command = "purge",
206 environment = %env_name,
207 "Removing environment from repository"
208 );
209
210 self.repository.delete(env_name)?;
211
212 Ok(())
213 }
214}