diff --git a/README.md b/README.md index a8cf3f3..40f8542 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,12 @@ Delete profile: mitractl delete-profile -i 55a3005f-f293-4168-ab70-6ab09a879679 ``` +Delete post: + +``` +mitractl delete-post -i 55a3005f-f293-4168-ab70-6ab09a879679 +``` + Generate invite code: ``` diff --git a/src/bin/mitractl.rs b/src/bin/mitractl.rs index 0a45c7c..aa524b6 100644 --- a/src/bin/mitractl.rs +++ b/src/bin/mitractl.rs @@ -7,11 +7,13 @@ use mitra::database::{create_pool, get_database_client}; use mitra::database::migrate::apply_migrations; use mitra::ethereum::utils::generate_ethereum_address; use mitra::logger::configure_logger; +use mitra::models::posts::queries::delete_post; use mitra::models::profiles::queries as profiles; use mitra::models::users::queries::{ generate_invite_code, get_invite_codes, }; +use mitra::utils::files::remove_files; /// Admin CLI tool #[derive(Clap)] @@ -23,6 +25,7 @@ struct Opts { #[derive(Clap)] enum SubCommand { DeleteProfile(DeleteProfile), + DeletePost(DeletePost), GenerateInviteCode(GenerateInviteCode), ListInviteCodes(ListInviteCodes), GenerateEthereumAddress(GenerateEthereumAddress), @@ -31,7 +34,13 @@ enum SubCommand { /// Delete profile #[derive(Clap)] struct DeleteProfile { - /// Print debug info + #[clap(short)] + id: Uuid, +} + +/// Delete post +#[derive(Clap)] +struct DeletePost { #[clap(short)] id: Uuid, } @@ -54,20 +63,25 @@ async fn main() { configure_logger(); let db_pool = create_pool(&config.database_url); apply_migrations(&db_pool).await; - let db_client = get_database_client(&db_pool).await.unwrap(); + let db_client = &mut **get_database_client(&db_pool).await.unwrap(); let opts: Opts = Opts::parse(); match opts.subcmd { SubCommand::DeleteProfile(subopts) => { - profiles::delete_profile(&**db_client, &subopts.id).await.unwrap(); + profiles::delete_profile(db_client, &subopts.id).await.unwrap(); println!("profile deleted"); }, + SubCommand::DeletePost(subopts) => { + let orphaned_files = delete_post(db_client, &subopts.id).await.unwrap(); + remove_files(orphaned_files, &config.media_dir()); + println!("post deleted"); + }, SubCommand::GenerateInviteCode(_) => { - let invite_code = generate_invite_code(&**db_client).await.unwrap(); + let invite_code = generate_invite_code(db_client).await.unwrap(); println!("generated invite code: {}", invite_code); }, SubCommand::ListInviteCodes(_) => { - let invite_codes = get_invite_codes(&**db_client).await.unwrap(); + let invite_codes = get_invite_codes(db_client).await.unwrap(); if invite_codes.len() == 0 { println!("no invite codes found"); return; diff --git a/src/lib.rs b/src/lib.rs index 19f3791..307068b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,5 +10,5 @@ pub mod mastodon_api; pub mod models; pub mod nodeinfo; pub mod scheduler; -mod utils; +pub mod utils; pub mod webfinger; diff --git a/src/models/attachments/queries.rs b/src/models/attachments/queries.rs index dc57659..be78f4e 100644 --- a/src/models/attachments/queries.rs +++ b/src/models/attachments/queries.rs @@ -22,3 +22,28 @@ pub async fn create_attachment( let db_attachment: DbMediaAttachment = inserted_row.try_get("media_attachment")?; Ok(db_attachment) } + +pub async fn find_orphaned_files( + db_client: &impl GenericClient, + files: Vec, +) -> Result, DatabaseError> { + let rows = db_client.query( + " + SELECT fname + FROM unnest($1::text[]) AS fname + WHERE + NOT EXISTS ( + SELECT 1 FROM media_attachment WHERE file_name = fname + ) + AND NOT EXISTS ( + SELECT 1 FROM actor_profile + WHERE avatar_file_name = fname OR banner_file_name = fname + ) + ", + &[&files], + ).await?; + let orphaned_files = rows.iter() + .map(|row| row.try_get("fname")) + .collect::>()?; + Ok(orphaned_files) +} diff --git a/src/models/posts/queries.rs b/src/models/posts/queries.rs index 0dfb7c6..027a357 100644 --- a/src/models/posts/queries.rs +++ b/src/models/posts/queries.rs @@ -5,6 +5,7 @@ use tokio_postgres::GenericClient; use uuid::Uuid; use crate::errors::DatabaseError; +use crate::models::attachments::queries::find_orphaned_files; use crate::models::attachments::types::DbMediaAttachment; use crate::models::profiles::queries::update_post_count; use super::types::{DbPost, Post, PostCreateData}; @@ -288,3 +289,40 @@ pub async fn is_waiting_for_token( let is_waiting: bool = row.try_get("is_waiting")?; Ok(is_waiting) } + +/// Deletes post from database and returns list of orphaned files. +pub async fn delete_post( + db_client: &mut impl GenericClient, + post_id: &Uuid, +) -> Result, DatabaseError> { + let transaction = db_client.transaction().await?; + // Get list of attached files + let attachment_rows = transaction.query( + " + SELECT file_name + FROM media_attachment WHERE post_id = $1 + ", + &[&post_id], + ).await?; + let files: Vec = attachment_rows.iter() + .map(|row| row.try_get("file_name")) + .collect::>()?; + // Delete post + let maybe_post_row = transaction.query_opt( + " + DELETE FROM post WHERE id = $1 + RETURNING post + ", + &[&post_id], + ).await?; + let post_row = maybe_post_row.ok_or(DatabaseError::NotFound("post"))?; + let db_post: DbPost = post_row.try_get("post")?; + // Update counters + if let Some(parent_id) = &db_post.in_reply_to_id { + update_reply_count(&transaction, parent_id, -1).await?; + } + update_post_count(&transaction, &db_post.author_id, -1).await?; + let orphaned_files = find_orphaned_files(&transaction, files).await?; + transaction.commit().await?; + Ok(orphaned_files) +} diff --git a/src/utils/files.rs b/src/utils/files.rs index 7701cac..eedef80 100644 --- a/src/utils/files.rs +++ b/src/utils/files.rs @@ -1,4 +1,4 @@ -use std::fs::File; +use std::fs::{remove_file, File}; use std::io::prelude::*; use std::path::PathBuf; @@ -66,3 +66,14 @@ pub fn save_validated_b64_file( pub fn get_file_url(instance_url: &str, file_name: &str) -> String { format!("{}/media/{}", instance_url, file_name) } + +pub fn remove_files(files: Vec, from_dir: &PathBuf) -> () { + for file_name in files { + let file_path = from_dir.join(&file_name); + let file_path_str = file_path.to_string_lossy(); + match remove_file(&file_path) { + Ok(_) => log::info!("removed file {}", file_path_str), + Err(_) => log::warn!("failed to remove file {}", file_path_str), + } + } +}