1c5a47a0 by Jabis Sevón

initial commit

0 parents
1 MATTERMOST_DATA_DIRECTORY="/opt/mattermost/data/"
2 DATABASE_NAME="mattermost"
3 DATABASE_USER="mattermost"
4 PGPASSWORD=""
5 DATABASE_HOST="127.0.0.1"
6 DATABASE_PORT="5432"
7 RETENTION_DAYS="730"
8 FILE_BATCH_SIZE="500"
1 /data
2 /target
3 .env
4 mmjab
5 *.sql
6 *.csv
7 *.log
...\ No newline at end of file ...\ No newline at end of file
This diff is collapsed. Click to expand it.
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
1 ### Mattermost Team version Jabis'd cleanup utilities
...\ No newline at end of file ...\ No newline at end of file
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 }
1 #!/bin/bash
2 set -a; source .env; set +a
3
4 ./mmjab
Styling with Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!