Add delete-post CLI command

This commit is contained in:
silverpill 2021-09-25 20:46:19 +00:00
parent c605185bb5
commit 283c426c49
6 changed files with 101 additions and 7 deletions

View file

@ -96,6 +96,12 @@ Delete profile:
mitractl delete-profile -i 55a3005f-f293-4168-ab70-6ab09a879679 mitractl delete-profile -i 55a3005f-f293-4168-ab70-6ab09a879679
``` ```
Delete post:
```
mitractl delete-post -i 55a3005f-f293-4168-ab70-6ab09a879679
```
Generate invite code: Generate invite code:
``` ```

View file

@ -7,11 +7,13 @@ use mitra::database::{create_pool, get_database_client};
use mitra::database::migrate::apply_migrations; use mitra::database::migrate::apply_migrations;
use mitra::ethereum::utils::generate_ethereum_address; use mitra::ethereum::utils::generate_ethereum_address;
use mitra::logger::configure_logger; use mitra::logger::configure_logger;
use mitra::models::posts::queries::delete_post;
use mitra::models::profiles::queries as profiles; use mitra::models::profiles::queries as profiles;
use mitra::models::users::queries::{ use mitra::models::users::queries::{
generate_invite_code, generate_invite_code,
get_invite_codes, get_invite_codes,
}; };
use mitra::utils::files::remove_files;
/// Admin CLI tool /// Admin CLI tool
#[derive(Clap)] #[derive(Clap)]
@ -23,6 +25,7 @@ struct Opts {
#[derive(Clap)] #[derive(Clap)]
enum SubCommand { enum SubCommand {
DeleteProfile(DeleteProfile), DeleteProfile(DeleteProfile),
DeletePost(DeletePost),
GenerateInviteCode(GenerateInviteCode), GenerateInviteCode(GenerateInviteCode),
ListInviteCodes(ListInviteCodes), ListInviteCodes(ListInviteCodes),
GenerateEthereumAddress(GenerateEthereumAddress), GenerateEthereumAddress(GenerateEthereumAddress),
@ -31,7 +34,13 @@ enum SubCommand {
/// Delete profile /// Delete profile
#[derive(Clap)] #[derive(Clap)]
struct DeleteProfile { struct DeleteProfile {
/// Print debug info #[clap(short)]
id: Uuid,
}
/// Delete post
#[derive(Clap)]
struct DeletePost {
#[clap(short)] #[clap(short)]
id: Uuid, id: Uuid,
} }
@ -54,20 +63,25 @@ async fn main() {
configure_logger(); configure_logger();
let db_pool = create_pool(&config.database_url); let db_pool = create_pool(&config.database_url);
apply_migrations(&db_pool).await; 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(); let opts: Opts = Opts::parse();
match opts.subcmd { match opts.subcmd {
SubCommand::DeleteProfile(subopts) => { SubCommand::DeleteProfile(subopts) => {
profiles::delete_profile(&**db_client, &subopts.id).await.unwrap(); profiles::delete_profile(db_client, &subopts.id).await.unwrap();
println!("profile deleted"); 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(_) => { 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); println!("generated invite code: {}", invite_code);
}, },
SubCommand::ListInviteCodes(_) => { 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 { if invite_codes.len() == 0 {
println!("no invite codes found"); println!("no invite codes found");
return; return;

View file

@ -10,5 +10,5 @@ pub mod mastodon_api;
pub mod models; pub mod models;
pub mod nodeinfo; pub mod nodeinfo;
pub mod scheduler; pub mod scheduler;
mod utils; pub mod utils;
pub mod webfinger; pub mod webfinger;

View file

@ -22,3 +22,28 @@ pub async fn create_attachment(
let db_attachment: DbMediaAttachment = inserted_row.try_get("media_attachment")?; let db_attachment: DbMediaAttachment = inserted_row.try_get("media_attachment")?;
Ok(db_attachment) Ok(db_attachment)
} }
pub async fn find_orphaned_files(
db_client: &impl GenericClient,
files: Vec<String>,
) -> Result<Vec<String>, 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::<Result<_, _>>()?;
Ok(orphaned_files)
}

View file

@ -5,6 +5,7 @@ use tokio_postgres::GenericClient;
use uuid::Uuid; use uuid::Uuid;
use crate::errors::DatabaseError; use crate::errors::DatabaseError;
use crate::models::attachments::queries::find_orphaned_files;
use crate::models::attachments::types::DbMediaAttachment; use crate::models::attachments::types::DbMediaAttachment;
use crate::models::profiles::queries::update_post_count; use crate::models::profiles::queries::update_post_count;
use super::types::{DbPost, Post, PostCreateData}; 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")?; let is_waiting: bool = row.try_get("is_waiting")?;
Ok(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<Vec<String>, 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<String> = attachment_rows.iter()
.map(|row| row.try_get("file_name"))
.collect::<Result<_, _>>()?;
// 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)
}

View file

@ -1,4 +1,4 @@
use std::fs::File; use std::fs::{remove_file, File};
use std::io::prelude::*; use std::io::prelude::*;
use std::path::PathBuf; 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 { pub fn get_file_url(instance_url: &str, file_name: &str) -> String {
format!("{}/media/{}", instance_url, file_name) format!("{}/media/{}", instance_url, file_name)
} }
pub fn remove_files(files: Vec<String>, 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),
}
}
}