diff --git a/Cargo.lock b/Cargo.lock index d8a78128798..a70fea61dc4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -924,6 +924,16 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" +[[package]] +name = "colored" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8" +dependencies = [ + "lazy_static", + "windows-sys 0.48.0", +] + [[package]] name = "comrak" version = "0.29.0" @@ -1023,6 +1033,7 @@ dependencies = [ "chrono", "claims", "clap", + "colored", "cookie", "crates_io_cdn_logs", "crates_io_database", diff --git a/Cargo.toml b/Cargo.toml index 2441b04e2e4..487534745a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,7 @@ base64 = "=0.22.1" bigdecimal = { version = "=0.4.5", features = ["serde"] } bon = "=2.3.0" cargo-manifest = "=0.15.2" +colored = "=2.1.0" crates_io_cdn_logs = { path = "crates/crates_io_cdn_logs" } crates_io_database = { path = "crates/crates_io_database" } crates_io_database_dump = { path = "crates/crates_io_database_dump" } diff --git a/src/admin/delete_crate.rs b/src/admin/delete_crate.rs index f86773410bc..47a31ac7f18 100644 --- a/src/admin/delete_crate.rs +++ b/src/admin/delete_crate.rs @@ -1,13 +1,16 @@ -use crate::schema::{crate_owners, teams, users}; +use crate::schema::crate_downloads; use crate::worker::jobs; use crate::{admin::dialoguer, db, schema::crates}; use anyhow::Context; +use colored::Colorize; use crates_io_worker::BackgroundJob; use diesel::dsl::sql; -use diesel::sql_types::Text; -use diesel::{ExpressionMethods, JoinOnDsl, QueryDsl}; +use diesel::sql_types::{Array, BigInt, Text}; +use diesel::{ExpressionMethods, QueryDsl}; use diesel_async::RunQueryDsl; +use futures_util::TryStreamExt; use std::collections::HashMap; +use std::fmt::Display; #[derive(clap::Parser, Debug)] #[command( @@ -33,39 +36,60 @@ pub async fn run(opts: Opts) -> anyhow::Result<()> { let mut crate_names = opts.crate_names; crate_names.sort(); - let query_result = crates::table + let existing_crates = crates::table + .inner_join(crate_downloads::table) + .filter(crates::name.eq_any(&crate_names)) .select(( crates::name, crates::id, - sql::( - "CASE WHEN crate_owners.owner_kind = 1 THEN teams.login ELSE users.gh_login END", + crate_downloads::downloads, + sql::>( + r#" + ARRAY( + SELECT + CASE WHEN crate_owners.owner_kind = 1 THEN + teams.login + ELSE + users.gh_login + END + FROM crate_owners + LEFT JOIN teams ON teams.id = crate_owners.owner_id + LEFT JOIN users ON users.id = crate_owners.owner_id + WHERE crate_owners.crate_id = crates.id + ) + "#, + ), + sql::( + // This is an incorrect reverse dependencies query, since it + // includes the `dependencies` rows for all versions, not just + // the "default version" per crate. However, it's good enough + // for our purposes here. + r#" + ( + SELECT COUNT(*) + FROM dependencies + WHERE dependencies.crate_id = crates.id + ) + "#, ), )) - .left_join(crate_owners::table.on(crate_owners::crate_id.eq(crates::id))) - .left_join(teams::table.on(teams::id.eq(crate_owners::owner_id))) - .left_join(users::table.on(users::id.eq(crate_owners::owner_id))) - .filter(crates::name.eq_any(&crate_names)) - .load::<(String, i32, String)>(&mut conn) + .load_stream::<(String, i32, i64, Vec, i64)>(&mut conn) .await - .context("Failed to look up crate name from the database")?; - - let mut existing_crates: HashMap)> = HashMap::new(); - for (name, id, login) in query_result { - let entry = existing_crates - .entry(name) - .or_insert_with(|| (id, Vec::new())); - - entry.1.push(login); - } + .context("Failed to look up crate name from the database")? + .try_fold( + HashMap::new(), + |mut map, (name, id, downloads, owners, rev_deps)| { + map.insert(name, CrateInfo::new(id, downloads, owners, rev_deps)); + futures_util::future::ready(Ok(map)) + }, + ) + .await?; println!("Deleting the following crates:"); println!(); for name in &crate_names { match existing_crates.get(name) { - Some((id, owners)) => { - let owners = owners.join(", "); - println!(" - {name} (id={id}, owners={owners})"); - } + Some(info) => println!(" - {} ({info})", name.bold()), None => println!(" - {name} (⚠️ crate not found)"), } } @@ -78,7 +102,9 @@ pub async fn run(opts: Opts) -> anyhow::Result<()> { } for name in &crate_names { - if let Some((id, _)) = existing_crates.get(name) { + if let Some(crate_info) = existing_crates.get(name) { + let id = crate_info.id; + info!("{name}: Deleting crate from the database…"); if let Err(error) = diesel::delete(crates::table.find(id)) .execute(&mut conn) @@ -110,3 +136,41 @@ pub async fn run(opts: Opts) -> anyhow::Result<()> { Ok(()) } + +#[derive(Debug, Clone)] +struct CrateInfo { + id: i32, + downloads: i64, + owners: Vec, + rev_deps: i64, +} + +impl CrateInfo { + pub fn new(id: i32, downloads: i64, owners: Vec, rev_deps: i64) -> Self { + Self { + id, + downloads, + owners, + rev_deps, + } + } +} + +impl Display for CrateInfo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let id = self.id; + let owners = self.owners.join(", "); + + write!(f, "id={id}, owners={owners}")?; + if self.downloads > 5000 { + let downloads = format!("downloads={}", self.downloads).bright_red().bold(); + write!(f, ", {downloads}")?; + } + if self.rev_deps > 0 { + let rev_deps = format!("rev_deps={}", self.rev_deps).bright_red().bold(); + write!(f, ", {rev_deps}")?; + } + + Ok(()) + } +}