Skip to content

Commit d6aea52

Browse files
committed
Add an option to delete a version of a crate
- Test for authors in delete_version - Enforce that table_name and column_name are hard-coded
1 parent 7903abc commit d6aea52

File tree

3 files changed

+180
-40
lines changed

3 files changed

+180
-40
lines changed

src/bin/cratesfyi.rs

+31-9
Original file line numberDiff line numberDiff line change
@@ -432,11 +432,10 @@ enum DatabaseSubcommand {
432432
/// Updates monthly release activity chart
433433
UpdateReleaseActivity,
434434

435-
/// Removes a whole crate from the database
436-
DeleteCrate {
437-
/// Name of the crate to delete
438-
#[structopt(name = "CRATE_NAME")]
439-
crate_name: String,
435+
/// Remove documentation from the database
436+
Delete {
437+
#[structopt(subcommand)]
438+
command: DeleteSubcommand,
440439
},
441440

442441
/// Blacklist operations
@@ -467,10 +466,13 @@ impl DatabaseSubcommand {
467466
Self::UpdateReleaseActivity => cratesfyi::utils::update_release_activity(&*ctx.conn()?)
468467
.expect("Failed to update release activity"),
469468

470-
Self::DeleteCrate { crate_name } => {
471-
db::delete_crate(&*ctx.conn()?, &crate_name).expect("failed to delete the crate");
472-
}
473-
469+
Self::Delete {
470+
command: DeleteSubcommand::Version { name, version },
471+
} => db::delete_version(&*ctx.conn()?, &name, &version)
472+
.expect("failed to delete the crate"),
473+
Self::Delete {
474+
command: DeleteSubcommand::Crate { name },
475+
} => db::delete_crate(&*ctx.conn()?, &name).expect("failed to delete the crate"),
474476
Self::Blacklist { command } => command.handle_args(ctx)?,
475477
}
476478
Ok(())
@@ -518,6 +520,26 @@ impl BlacklistSubcommand {
518520
}
519521
}
520522

523+
#[derive(Debug, Clone, PartialEq, Eq, StructOpt)]
524+
enum DeleteSubcommand {
525+
/// Delete a whole crate
526+
Crate {
527+
/// Name of the crate to delete
528+
#[structopt(name = "CRATE_NAME")]
529+
name: String,
530+
},
531+
/// Delete a single version of a crate (which may include multiple builds)
532+
Version {
533+
/// Name of the crate to delete
534+
#[structopt(name = "CRATE_NAME")]
535+
name: String,
536+
537+
/// The version of the crate to delete
538+
#[structopt(name = "VERSION")]
539+
version: String,
540+
},
541+
}
542+
521543
struct Context {
522544
build_queue: OnceCell<Arc<BuildQueue>>,
523545
config: OnceCell<Arc<Config>>,

src/db/delete_crate.rs

+148-30
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use crate::storage::s3::{s3_client, S3_BUCKET_NAME};
22
use failure::{Error, Fail};
3-
use postgres::Connection;
3+
use postgres::{transaction::Transaction, Connection};
44
use rusoto_s3::{DeleteObjectsRequest, ListObjectsV2Request, ObjectIdentifier, S3Client, S3};
55

66
/// List of directories in docs.rs's underlying storage (either the database or S3) containing a
@@ -14,22 +14,89 @@ enum CrateDeletionError {
1414
}
1515

1616
pub fn delete_crate(conn: &Connection, name: &str) -> Result<(), Error> {
17-
let crate_id_res = conn.query("SELECT id FROM crates WHERE name = $1", &[&name])?;
18-
let crate_id = if crate_id_res.is_empty() {
19-
return Err(CrateDeletionError::MissingCrate(name.into()).into());
20-
} else {
21-
crate_id_res.get(0).get("id")
22-
};
17+
let crate_id = get_id(conn, name)?;
18+
delete_crate_from_database(conn, name, crate_id)?;
19+
if let Some(s3) = s3_client() {
20+
for prefix in STORAGE_PATHS_TO_DELETE {
21+
delete_prefix_from_s3(&s3, &format!("{}/{}/", prefix, name))?;
22+
}
23+
}
24+
Ok(())
25+
}
26+
27+
pub fn delete_version(conn: &Connection, name: &str, version: &str) -> Result<(), Error> {
28+
delete_version_from_database(conn, name, version)?;
2329

24-
delete_from_database(conn, name, crate_id)?;
2530
if let Some(s3) = s3_client() {
26-
delete_from_s3(&s3, name)?;
31+
for prefix in STORAGE_PATHS_TO_DELETE {
32+
delete_prefix_from_s3(&s3, &format!("{}/{}/", prefix, name))?;
33+
}
34+
}
35+
36+
Ok(())
37+
}
38+
39+
fn get_id(conn: &Connection, name: &str) -> Result<i32, Error> {
40+
let crate_id_res = conn.query("SELECT id FROM crates WHERE name = $1", &[&name])?;
41+
if let Some(row) = crate_id_res.into_iter().next() {
42+
Ok(row.get("id"))
43+
} else {
44+
Err(CrateDeletionError::MissingCrate(name.into()).into())
2745
}
46+
}
2847

48+
// metaprogramming!
49+
// NOTE: it is _crucial_ that table_name and column_name be hard-coded, i.e. NOT user input
50+
fn delete_metadata(
51+
transaction: &Transaction,
52+
crate_id: i32,
53+
version: &str,
54+
table_name: &'static str,
55+
column_name: &'static str,
56+
) -> Result<(), Error> {
57+
transaction.execute(
58+
&format!("DELETE FROM {} WHERE {} IN (SELECT id FROM releases WHERE crate_id = $1 AND version = $2)", table_name, column_name),
59+
&[&crate_id, &version],
60+
)?;
2961
Ok(())
3062
}
3163

32-
fn delete_from_database(conn: &Connection, name: &str, crate_id: i32) -> Result<(), Error> {
64+
fn delete_version_from_database(conn: &Connection, name: &str, version: &str) -> Result<(), Error> {
65+
let crate_id = get_id(conn, name)?;
66+
let transaction = conn.transaction()?;
67+
let metadata = [
68+
("author_rels", "rid"),
69+
("owner_rels", "cid"),
70+
("keyword_rels", "rid"),
71+
("builds", "rid"),
72+
];
73+
for &(table, column) in &metadata {
74+
delete_metadata(&transaction, crate_id, version, table, column)?;
75+
}
76+
transaction.execute(
77+
"DELETE FROM releases WHERE crate_id = $1 AND version = $2",
78+
&[&crate_id, &version],
79+
)?;
80+
transaction.execute(
81+
"UPDATE crates SET latest_version_id = (
82+
SELECT id FROM releases WHERE release_time = (
83+
SELECT MAX(release_time) FROM releases WHERE crate_id = $1
84+
)
85+
) WHERE id = $1",
86+
&[&crate_id],
87+
)?;
88+
89+
for prefix in STORAGE_PATHS_TO_DELETE {
90+
transaction.execute(
91+
"DELETE FROM files WHERE path LIKE $1;",
92+
&[&format!("{}/{}/{}/%", prefix, name, version)],
93+
)?;
94+
}
95+
96+
transaction.commit().map_err(Into::into)
97+
}
98+
99+
fn delete_crate_from_database(conn: &Connection, name: &str, crate_id: i32) -> Result<(), Error> {
33100
let transaction = conn.transaction()?;
34101

35102
transaction.execute(
@@ -68,13 +135,6 @@ fn delete_from_database(conn: &Connection, name: &str, crate_id: i32) -> Result<
68135
Ok(())
69136
}
70137

71-
fn delete_from_s3(s3: &S3Client, name: &str) -> Result<(), Error> {
72-
for prefix in STORAGE_PATHS_TO_DELETE {
73-
delete_prefix_from_s3(s3, &format!("{}/{}/", prefix, name))?;
74-
}
75-
Ok(())
76-
}
77-
78138
fn delete_prefix_from_s3(s3: &S3Client, name: &str) -> Result<(), Error> {
79139
let mut continuation_token = None;
80140
loop {
@@ -124,23 +184,25 @@ fn delete_prefix_from_s3(s3: &S3Client, name: &str) -> Result<(), Error> {
124184
#[cfg(test)]
125185
mod tests {
126186
use super::*;
187+
use crate::test::wrapper;
127188
use failure::Error;
128189
use postgres::Connection;
129190

191+
fn crate_exists(conn: &Connection, name: &str) -> Result<bool, Error> {
192+
Ok(!conn
193+
.query("SELECT * FROM crates WHERE name = $1;", &[&name])?
194+
.is_empty())
195+
}
196+
197+
fn release_exists(conn: &Connection, id: i32) -> Result<bool, Error> {
198+
Ok(!conn
199+
.query("SELECT * FROM releases WHERE id = $1;", &[&id])?
200+
.is_empty())
201+
}
202+
130203
#[test]
131204
fn test_delete_from_database() {
132-
fn crate_exists(conn: &Connection, name: &str) -> Result<bool, Error> {
133-
Ok(!conn
134-
.query("SELECT * FROM crates WHERE name = $1;", &[&name])?
135-
.is_empty())
136-
}
137-
fn release_exists(conn: &Connection, id: i32) -> Result<bool, Error> {
138-
Ok(!conn
139-
.query("SELECT * FROM releases WHERE id = $1;", &[&id])?
140-
.is_empty())
141-
}
142-
143-
crate::test::wrapper(|env| {
205+
wrapper(|env| {
144206
let db = env.db();
145207

146208
// Create fake packages in the database
@@ -168,7 +230,7 @@ mod tests {
168230
.get(0)
169231
.get("id");
170232

171-
delete_from_database(&db.conn(), "package-1", *pkg1_id)?;
233+
delete_crate_from_database(&db.conn(), "package-1", *pkg1_id)?;
172234

173235
assert!(!crate_exists(&db.conn(), "package-1")?);
174236
assert!(crate_exists(&db.conn(), "package-2")?);
@@ -179,4 +241,60 @@ mod tests {
179241
Ok(())
180242
});
181243
}
244+
245+
#[test]
246+
fn test_delete_version() {
247+
wrapper(|env| {
248+
fn authors(conn: &Connection, crate_id: i32) -> Result<Vec<String>, Error> {
249+
Ok(conn
250+
.query(
251+
"SELECT name FROM authors
252+
INNER JOIN author_rels ON authors.id = author_rels.aid
253+
INNER JOIN releases ON author_rels.rid = releases.id
254+
WHERE releases.crate_id = $1",
255+
&[&crate_id],
256+
)?
257+
.into_iter()
258+
.map(|row| row.get(0))
259+
.collect())
260+
}
261+
262+
let db = env.db();
263+
let v1 = db
264+
.fake_release()
265+
.name("a")
266+
.version("1.0.0")
267+
.author("malicious actor")
268+
.create()?;
269+
let v2 = db
270+
.fake_release()
271+
.name("a")
272+
.version("2.0.0")
273+
.author("Peter Rabbit")
274+
.create()?;
275+
assert!(release_exists(&db.conn(), v1)?);
276+
assert!(release_exists(&db.conn(), v2)?);
277+
let crate_id = db
278+
.conn()
279+
.query("SELECT crate_id FROM releases WHERE id = $1", &[&v1])?
280+
.into_iter()
281+
.next()
282+
.unwrap()
283+
.get(0);
284+
assert_eq!(
285+
authors(&db.conn(), crate_id)?,
286+
vec!["malicious actor".to_string(), "Peter Rabbit".to_string()]
287+
);
288+
289+
delete_version(&db.conn(), "a", "1.0.0")?;
290+
assert!(!release_exists(&db.conn(), v1)?);
291+
assert!(release_exists(&db.conn(), v2)?);
292+
assert_eq!(
293+
authors(&db.conn(), crate_id)?,
294+
vec!["Peter Rabbit".to_string()]
295+
);
296+
297+
Ok(())
298+
})
299+
}
182300
}

src/db/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
33
pub(crate) use self::add_package::add_build_into_database;
44
pub(crate) use self::add_package::add_package_into_database;
5-
pub use self::delete_crate::delete_crate;
5+
pub use self::delete_crate::{delete_crate, delete_version};
66
pub use self::file::add_path_into_database;
77
pub use self::migrate::migrate;
88
pub use self::pool::{Pool, PoolError};

0 commit comments

Comments
 (0)