initial commit
0 parents
Showing
7 changed files
with
356 additions
and
0 deletions
.env.example
0 → 100644
.gitignore
0 → 100644
Cargo.lock
0 → 100644
This diff is collapsed.
Click to expand it.
Cargo.toml
0 → 100644
1 | [package] | ||
2 | name = "mmjab" | ||
3 | version = "0.1.0" | ||
4 | edition = "2021" | ||
5 | |||
6 | [dependencies] | ||
7 | chrono = "0.4.26" | ||
8 | tokio = { version = "*", features = ["macros", "signal", "rt-multi-thread"] } | ||
9 | tokio-util = { version = "0.7.10", features = ["io"] } | ||
10 | tokio-postgres = { version = "0.7.12", features= ["runtime","array-impls","js","with-bit-vec-0_6", "with-chrono-0_4"] } | ||
11 | log = {version = "0.4.25", features = ["std","serde"] } | ||
12 | anyhow = "1.0.95" | ||
13 | clap = { version = "4.5.27", features = ["env"] } | ||
14 | env_logger = "0.11.6" | ||
15 | dotenv = { version = "0.15.0", features = ["clap", "cli"] } | ||
16 | |||
17 | [profile.release] | ||
18 | lto = true | ||
19 | opt-level = 3 | ||
20 | codegen-units = 1 |
README.md
0 → 100644
1 | ### Mattermost Team version Jabis'd cleanup utilities | ||
... | \ No newline at end of file | ... | \ No newline at end of file |
src/main.rs
0 → 100644
1 | use anyhow::{Result, Context}; | ||
2 | use tokio_postgres::{Client, NoTls}; | ||
3 | use log::{error, info, warn, trace}; | ||
4 | use std::{fs,env}; | ||
5 | use std::path::Path; | ||
6 | use chrono::Utc; | ||
7 | use clap::{Arg, Command}; | ||
8 | use dotenv::dotenv; | ||
9 | |||
10 | #[tokio::main] | ||
11 | async fn main() { | ||
12 | if env::var("RUST_LOG").is_err() { | ||
13 | env::set_var("RUST_LOG", "info") | ||
14 | } | ||
15 | env_logger::init(); | ||
16 | dotenv().ok(); | ||
17 | let matches = Command::new("Clean Utility") | ||
18 | .version("1.0") | ||
19 | .about("Cleans old files and database rows based on retention policies") | ||
20 | .arg( | ||
21 | Arg::new("data_dir") | ||
22 | .long("data-dir") | ||
23 | .env("MATTERMOST_DATA_DIRECTORY") | ||
24 | .help("Path to the Mattermost data directory") | ||
25 | .required(true) | ||
26 | ) | ||
27 | .arg( | ||
28 | Arg::new("db_name") | ||
29 | .short('n') | ||
30 | .long("db-name") | ||
31 | .env("DATABASE_NAME") | ||
32 | .help("Database name") | ||
33 | .required(true) | ||
34 | ) | ||
35 | .arg( | ||
36 | Arg::new("db_user") | ||
37 | .short('u') | ||
38 | .long("db-user") | ||
39 | .env("DATABASE_USER") | ||
40 | .help("Database user") | ||
41 | .required(true) | ||
42 | ) | ||
43 | .arg( | ||
44 | Arg::new("db_password") | ||
45 | .short('p') | ||
46 | .long("db-password") | ||
47 | .env("PGPASSWORD") | ||
48 | .help("Database password") | ||
49 | .required(true), | ||
50 | ) | ||
51 | .arg( | ||
52 | Arg::new("db_host") | ||
53 | .short('h') | ||
54 | .long("db-host") | ||
55 | .env("DATABASE_HOST") | ||
56 | .help("Database host") | ||
57 | .required(true) | ||
58 | ) | ||
59 | .arg( | ||
60 | Arg::new("db_port") | ||
61 | .short('P') | ||
62 | .long("db-port") | ||
63 | .env("DATABASE_PORT") | ||
64 | .help("Database port") | ||
65 | .required(true) | ||
66 | ) | ||
67 | .arg( | ||
68 | Arg::new("retention_days") | ||
69 | .short('D') | ||
70 | .long("retention-days") | ||
71 | .env("RETENTION_DAYS") | ||
72 | .help("Number of days to retain data") | ||
73 | .required(true) | ||
74 | ) | ||
75 | .arg( | ||
76 | Arg::new("file_batch_size") | ||
77 | .short('b') | ||
78 | .long("file-batch-size") | ||
79 | .env("FILE_BATCH_SIZE") | ||
80 | .help("Batch size for file deletion") | ||
81 | .required(true), | ||
82 | ) | ||
83 | .arg( | ||
84 | Arg::new("remove_posts") | ||
85 | .long("remove-posts") | ||
86 | .help("Wipe posts older than timestamp") | ||
87 | .required(false) | ||
88 | ) | ||
89 | .arg( | ||
90 | Arg::new("dry_run") | ||
91 | .long("dry-run") | ||
92 | .help("Perform a dry run without making any changes") | ||
93 | .required(false) | ||
94 | ) | ||
95 | .get_matches(); | ||
96 | |||
97 | let mattermost_data_directory = matches.get_one::<String>("data_dir").unwrap(); | ||
98 | let database_name = matches.get_one::<String>("db_name").unwrap(); | ||
99 | let database_user = matches.get_one::<String>("db_user").unwrap(); | ||
100 | let database_password = matches.get_one::<String>("db_password").unwrap(); | ||
101 | let database_host = matches.get_one::<String>("db_host").unwrap(); | ||
102 | let database_port = matches.get_one::<String>("db_port").unwrap(); | ||
103 | let retention_days = matches.get_one::<String>("retention_days").unwrap(); | ||
104 | let file_batch_size = matches.get_one::<String>("file_batch_size").unwrap(); | ||
105 | let remove_posts = matches.contains_id("remove_posts"); | ||
106 | let dry_run = matches.contains_id("dry_run"); | ||
107 | |||
108 | let retention_days = retention_days.parse::<i64>().expect("fucking hell retention"); | ||
109 | let file_batch_size = file_batch_size.parse::<usize>().expect("fucking hell retention"); | ||
110 | //let file_batch_size = file_batch_size.parse::<usize>().expect("fucking hell retention"); | ||
111 | if let Err(err) = clean( | ||
112 | mattermost_data_directory, | ||
113 | database_name, | ||
114 | database_user, | ||
115 | database_password, | ||
116 | database_host, | ||
117 | database_port, | ||
118 | retention_days, | ||
119 | file_batch_size, | ||
120 | remove_posts, | ||
121 | dry_run, | ||
122 | ).await { | ||
123 | error!("Cleaning operation failed: {}", err); | ||
124 | } else { | ||
125 | info!("Cleaning operation completed successfully."); | ||
126 | } | ||
127 | } | ||
128 | |||
129 | pub async fn clean( | ||
130 | mattermost_data_directory: &str, | ||
131 | database_name: &str, | ||
132 | database_user: &str, | ||
133 | database_password: &str, | ||
134 | database_host: &str, | ||
135 | database_port: &str, | ||
136 | retention_days: i64, | ||
137 | file_batch_size: usize, | ||
138 | remove_posts: bool, | ||
139 | dry_run: bool, | ||
140 | ) -> Result<()> { | ||
141 | validate( | ||
142 | mattermost_data_directory, | ||
143 | database_name, | ||
144 | database_user, | ||
145 | database_host, | ||
146 | retention_days, | ||
147 | file_batch_size, | ||
148 | )?; | ||
149 | |||
150 | let connection_string = format!( | ||
151 | "postgres://{}:{}@{}:{}/{}?sslmode=disable", | ||
152 | database_user, database_password, database_host, database_port, database_name | ||
153 | ); | ||
154 | trace!("Connection string: {}", &connection_string); | ||
155 | let (client, connection) = tokio_postgres::connect(&connection_string, NoTls).await.context("Failed to connect to the database")?; | ||
156 | |||
157 | tokio::spawn(async move { | ||
158 | if let Err(e) = connection.await { | ||
159 | warn!("error happened at spawn {e}"); | ||
160 | eprintln!("connection error: {}", e); | ||
161 | } | ||
162 | }); | ||
163 | info!("Connection established: OK"); | ||
164 | let millisecond_epoch = (Utc::now() - chrono::Duration::days(retention_days)).timestamp_millis(); | ||
165 | |||
166 | clean_files(&client, millisecond_epoch, mattermost_data_directory, file_batch_size, dry_run).await?; | ||
167 | delete_file_info_rows(&client, millisecond_epoch, dry_run).await?; | ||
168 | if remove_posts { | ||
169 | delete_post_rows(&client, millisecond_epoch, dry_run).await?; | ||
170 | } else { | ||
171 | info!("Skipping posts removal") | ||
172 | } | ||
173 | |||
174 | Ok(()) | ||
175 | } | ||
176 | |||
177 | async fn clean_files( | ||
178 | client: &Client, | ||
179 | millisecond_epoch: i64, | ||
180 | mattermost_data_directory: &str, | ||
181 | file_batch_size: usize, | ||
182 | dry_run: bool, | ||
183 | ) -> Result<()> { | ||
184 | let mut batch = 0; | ||
185 | let mut more_results = true; | ||
186 | |||
187 | while more_results { | ||
188 | more_results = clean_files_batch( | ||
189 | client, | ||
190 | millisecond_epoch, | ||
191 | mattermost_data_directory, | ||
192 | file_batch_size, | ||
193 | batch, | ||
194 | dry_run, | ||
195 | ).await?; | ||
196 | batch += 1; | ||
197 | } | ||
198 | |||
199 | Ok(()) | ||
200 | } | ||
201 | |||
202 | async fn clean_files_batch( | ||
203 | client: &Client, | ||
204 | millisecond_epoch: i64, | ||
205 | mattermost_data_directory: &str, | ||
206 | file_batch_size: usize, | ||
207 | batch: usize, | ||
208 | dry_run: bool, | ||
209 | ) -> Result<bool> { | ||
210 | let query = " | ||
211 | SELECT path, thumbnailpath, previewpath | ||
212 | FROM fileinfo | ||
213 | WHERE createat < $1 | ||
214 | OFFSET $2 | ||
215 | LIMIT $3; | ||
216 | "; | ||
217 | trace!("Querying: {}",&query); | ||
218 | let offset = (batch * file_batch_size) as i64; | ||
219 | let limit = file_batch_size as i64; | ||
220 | trace!("params: {} {} {}",&millisecond_epoch, &offset, &limit); | ||
221 | let rows = client | ||
222 | .query(query, &[&millisecond_epoch, &offset, &limit]) | ||
223 | .await.context("Failed to fetch file info rows")?; | ||
224 | |||
225 | let mut more_results = false; | ||
226 | |||
227 | for row in rows { | ||
228 | more_results = true; | ||
229 | let path: String = row.get("path"); | ||
230 | let thumbnail_path: String = row.get("thumbnailpath"); | ||
231 | let preview_path: String = row.get("previewpath"); | ||
232 | |||
233 | if dry_run { | ||
234 | info!("[DRY RUN] Would remove: {:?}, {:?}, {:?}", path, thumbnail_path, preview_path); | ||
235 | } else { | ||
236 | remove_files(mattermost_data_directory, &path, &thumbnail_path, &preview_path).context("Failed to remove files")?; | ||
237 | } | ||
238 | } | ||
239 | |||
240 | Ok(more_results) | ||
241 | } | ||
242 | |||
243 | fn remove_files(base_dir: &str, path: &str, thumbnail_path: &str, preview_path: &str) -> Result<()> { | ||
244 | let files = [path, thumbnail_path, preview_path]; | ||
245 | let mut num_deleted = 0; | ||
246 | for file in files { | ||
247 | if !file.is_empty() { | ||
248 | let full_path = Path::new(base_dir).join(file); | ||
249 | if full_path.exists() { | ||
250 | fs::remove_file(full_path.clone()).context(format!("Failed to delete file: {:?}", &full_path))?; | ||
251 | trace!("Removed: {:#?} ", &full_path); | ||
252 | num_deleted += 1; | ||
253 | } else { | ||
254 | trace!("Path does not exist: {:#?} ", &full_path); | ||
255 | } | ||
256 | } | ||
257 | } | ||
258 | if num_deleted > 0 { | ||
259 | info!("Deleted: {} files. Main file: {}",num_deleted,path); | ||
260 | } else { | ||
261 | trace!("No files to be deleted"); | ||
262 | } | ||
263 | Ok(()) | ||
264 | } | ||
265 | |||
266 | async fn delete_file_info_rows(client: &Client, millisecond_epoch: i64, dry_run: bool) -> Result<()> { | ||
267 | let query = " | ||
268 | DELETE FROM fileinfo | ||
269 | WHERE createat < $1; | ||
270 | "; | ||
271 | trace!("Querying: {}",&query); | ||
272 | trace!("Params: {:#?}",&millisecond_epoch); | ||
273 | if dry_run { | ||
274 | info!("[DRY RUN] Would delete file info rows older than {}", millisecond_epoch); | ||
275 | return Ok(()); | ||
276 | } | ||
277 | let result = client.execute(query, &[&millisecond_epoch]).await.context("Failed to delete file info rows")?; | ||
278 | info!("Removed {} file information rows", result); | ||
279 | Ok(()) | ||
280 | } | ||
281 | |||
282 | async fn delete_post_rows(client: &Client, millisecond_epoch: i64, dry_run: bool) -> Result<()> { | ||
283 | let query = " | ||
284 | DELETE FROM posts | ||
285 | WHERE createat < $1; | ||
286 | "; | ||
287 | trace!("Querying: {}",&query); | ||
288 | trace!("Params: {:#?}",&millisecond_epoch); | ||
289 | if dry_run { | ||
290 | info!("[DRY RUN] Would delete post rows older than {}", millisecond_epoch); | ||
291 | return Ok(()); | ||
292 | } | ||
293 | let result = client.execute(query, &[&millisecond_epoch]).await.context("Failed to delete post rows")?; | ||
294 | info!("Removed {} post rows", result); | ||
295 | Ok(()) | ||
296 | } | ||
297 | |||
298 | fn validate( | ||
299 | mattermost_data_directory: &str, | ||
300 | database_name: &str, | ||
301 | database_user: &str, | ||
302 | database_host: &str, | ||
303 | retention_days: i64, | ||
304 | file_batch_size: usize, | ||
305 | ) -> Result<()> { | ||
306 | if mattermost_data_directory.is_empty() | ||
307 | || database_name.is_empty() | ||
308 | || database_user.is_empty() | ||
309 | || database_host.is_empty() | ||
310 | || retention_days <= 0 | ||
311 | || file_batch_size == 0 | ||
312 | { | ||
313 | anyhow::bail!("Invalid input parameters"); | ||
314 | } | ||
315 | Ok(()) | ||
316 | } |
-
Please register or sign in to post a comment