fix: add R2 cleanup to every delete route
This commit is contained in:
parent
d6ef6113fd
commit
4f61077ca9
4 changed files with 148 additions and 10 deletions
|
|
@ -12,6 +12,7 @@ use crate::{
|
|||
crypto::{generate_salt, hash_password_for_storage},
|
||||
db,
|
||||
error::AppError,
|
||||
handlers::attachments,
|
||||
models::{
|
||||
cipher::CipherData,
|
||||
sync::Profile,
|
||||
|
|
@ -477,6 +478,12 @@ pub async fn delete_account(
|
|||
return Err(AppError::Unauthorized("Invalid password".to_string()));
|
||||
}
|
||||
|
||||
if attachments::attachments_enabled(env.as_ref()) {
|
||||
let bucket = attachments::require_bucket(env.as_ref())?;
|
||||
let keys = attachments::list_attachment_keys_for_user(&db, user_id).await?;
|
||||
attachments::delete_r2_objects(&bucket, &keys).await?;
|
||||
}
|
||||
|
||||
// Delete all user's ciphers
|
||||
query!(&db, "DELETE FROM ciphers WHERE user_id = ?1", user_id)
|
||||
.map_err(|_| AppError::Database)?
|
||||
|
|
|
|||
|
|
@ -81,6 +81,12 @@ pub struct AttachmentDownloadQuery {
|
|||
pub token: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct AttachmentKeyRow {
|
||||
cipher_id: String,
|
||||
id: String,
|
||||
}
|
||||
|
||||
impl NumberOrString {
|
||||
pub fn into_i64(self) -> Result<i64, AppError> {
|
||||
match self {
|
||||
|
|
@ -368,12 +374,7 @@ pub async fn delete_attachment(
|
|||
}
|
||||
|
||||
// Delete R2 object; ignore missing objects
|
||||
if let Err(err) = bucket.delete(attachment.r2_key()).await {
|
||||
let msg = err.to_string();
|
||||
if !(msg.contains("NoSuchKey") || msg.contains("404") || msg.contains("NotFound")) {
|
||||
return Err(AppError::Worker(err));
|
||||
}
|
||||
}
|
||||
delete_r2_objects(&bucket, &[attachment.r2_key()]).await?;
|
||||
|
||||
query!(&db, "DELETE FROM attachments WHERE id = ?1", attachment.id)
|
||||
.map_err(|_| AppError::Database)?
|
||||
|
|
@ -521,15 +522,111 @@ pub async fn hydrate_ciphers_attachments(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn attachments_enabled(env: &Env) -> bool {
|
||||
pub(crate) fn attachments_enabled(env: &Env) -> bool {
|
||||
env.bucket(ATTACHMENTS_BUCKET).is_ok()
|
||||
}
|
||||
|
||||
fn require_bucket(env: &Env) -> Result<Bucket, AppError> {
|
||||
pub(crate) fn require_bucket(env: &Env) -> Result<Bucket, AppError> {
|
||||
env.bucket(ATTACHMENTS_BUCKET)
|
||||
.map_err(|_| AppError::BadRequest("Attachments are not enabled".to_string()))
|
||||
}
|
||||
|
||||
fn is_not_found_error(err: &worker::Error) -> bool {
|
||||
let msg = err.to_string();
|
||||
msg.contains("NoSuchKey") || msg.contains("404") || msg.contains("NotFound")
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_r2_objects(
|
||||
bucket: &Bucket,
|
||||
keys: &[String],
|
||||
) -> Result<(), AppError> {
|
||||
for key in keys {
|
||||
if let Err(err) = bucket.delete(key).await {
|
||||
if !is_not_found_error(&err) {
|
||||
return Err(AppError::Worker(err));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn map_rows_to_keys(rows: Vec<AttachmentKeyRow>) -> Vec<String> {
|
||||
rows.into_iter()
|
||||
.map(|row| format!("{}/{}", row.cipher_id, row.id))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) async fn list_attachment_keys_for_cipher_ids(
|
||||
db: &D1Database,
|
||||
cipher_ids: &[String],
|
||||
user_id: Option<&str>,
|
||||
) -> Result<Vec<String>, AppError> {
|
||||
if cipher_ids.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let ids_json = serde_json::to_string(cipher_ids).map_err(|_| AppError::Internal)?;
|
||||
|
||||
let mut sql = "SELECT a.cipher_id, a.id FROM attachments a JOIN ciphers c ON a.cipher_id = c.id WHERE c.id IN (SELECT value FROM json_each(?1))".to_string();
|
||||
let mut params: Vec<worker::wasm_bindgen::JsValue> = vec![ids_json.into()];
|
||||
|
||||
if let Some(uid) = user_id {
|
||||
sql.push_str(" AND c.user_id = ?2");
|
||||
params.push(uid.into());
|
||||
}
|
||||
|
||||
let rows: Vec<AttachmentKeyRow> = db
|
||||
.prepare(&sql)
|
||||
.bind(¶ms)?
|
||||
.all()
|
||||
.await
|
||||
.map_err(|_| AppError::Database)?
|
||||
.results()
|
||||
.map_err(|_| AppError::Database)?;
|
||||
|
||||
Ok(map_rows_to_keys(rows))
|
||||
}
|
||||
|
||||
pub(crate) async fn list_attachment_keys_for_user(
|
||||
db: &D1Database,
|
||||
user_id: &str,
|
||||
) -> Result<Vec<String>, AppError> {
|
||||
let rows: Vec<AttachmentKeyRow> = db
|
||||
.prepare(
|
||||
"SELECT a.cipher_id, a.id FROM attachments a \
|
||||
JOIN ciphers c ON a.cipher_id = c.id \
|
||||
WHERE c.user_id = ?1",
|
||||
)
|
||||
.bind(&[user_id.into()])?
|
||||
.all()
|
||||
.await
|
||||
.map_err(|_| AppError::Database)?
|
||||
.results()
|
||||
.map_err(|_| AppError::Database)?;
|
||||
|
||||
Ok(map_rows_to_keys(rows))
|
||||
}
|
||||
|
||||
pub(crate) async fn list_attachment_keys_for_soft_deleted_before(
|
||||
db: &D1Database,
|
||||
cutoff_exclusive: &str,
|
||||
) -> Result<Vec<String>, AppError> {
|
||||
let rows: Vec<AttachmentKeyRow> = db
|
||||
.prepare(
|
||||
"SELECT a.cipher_id, a.id FROM attachments a \
|
||||
JOIN ciphers c ON a.cipher_id = c.id \
|
||||
WHERE c.deleted_at IS NOT NULL AND c.deleted_at < ?1",
|
||||
)
|
||||
.bind(&[cutoff_exclusive.into()])?
|
||||
.all()
|
||||
.await
|
||||
.map_err(|_| AppError::Database)?
|
||||
.results()
|
||||
.map_err(|_| AppError::Database)?;
|
||||
|
||||
Ok(map_rows_to_keys(rows))
|
||||
}
|
||||
|
||||
fn upload_url(cipher_id: &str, attachment_id: &str) -> String {
|
||||
format!("/api/ciphers/{cipher_id}/attachment/{attachment_id}")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -416,6 +416,12 @@ pub async fn hard_delete_cipher(
|
|||
) -> Result<Json<()>, AppError> {
|
||||
let db = db::get_db(&env)?;
|
||||
|
||||
if attachments::attachments_enabled(env.as_ref()) {
|
||||
let bucket = attachments::require_bucket(env.as_ref())?;
|
||||
let keys = attachments::list_attachment_keys_for_cipher_ids(&db, &[id.clone()], Some(&claims.sub)).await?;
|
||||
attachments::delete_r2_objects(&bucket, &keys).await?;
|
||||
}
|
||||
|
||||
query!(
|
||||
&db,
|
||||
"DELETE FROM ciphers WHERE id = ?1 AND user_id = ?2",
|
||||
|
|
@ -440,9 +446,18 @@ pub async fn hard_delete_ciphers_bulk(
|
|||
) -> Result<Json<()>, AppError> {
|
||||
let db = db::get_db(&env)?;
|
||||
let batch_size = get_batch_size(&env);
|
||||
let mut statements: Vec<D1PreparedStatement> = Vec::with_capacity(payload.ids.len());
|
||||
let ids = payload.ids;
|
||||
|
||||
for id in payload.ids {
|
||||
if attachments::attachments_enabled(env.as_ref()) {
|
||||
let bucket = attachments::require_bucket(env.as_ref())?;
|
||||
let keys =
|
||||
attachments::list_attachment_keys_for_cipher_ids(&db, &ids, Some(&claims.sub)).await?;
|
||||
attachments::delete_r2_objects(&bucket, &keys).await?;
|
||||
}
|
||||
|
||||
let mut statements: Vec<D1PreparedStatement> = Vec::with_capacity(ids.len());
|
||||
|
||||
for id in ids {
|
||||
let stmt = query!(
|
||||
&db,
|
||||
"DELETE FROM ciphers WHERE id = ?1 AND user_id = ?2",
|
||||
|
|
@ -747,6 +762,12 @@ pub async fn purge_vault(
|
|||
return Err(AppError::Unauthorized("Invalid password".to_string()));
|
||||
}
|
||||
|
||||
if attachments::attachments_enabled(env.as_ref()) {
|
||||
let bucket = attachments::require_bucket(env.as_ref())?;
|
||||
let keys = attachments::list_attachment_keys_for_user(&db, user_id).await?;
|
||||
attachments::delete_r2_objects(&bucket, &keys).await?;
|
||||
}
|
||||
|
||||
// Delete all user's ciphers (both active and soft-deleted)
|
||||
query!(&db, "DELETE FROM ciphers WHERE user_id = ?1", user_id)
|
||||
.map_err(|_| AppError::Database)?
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
//! retention period.
|
||||
|
||||
use chrono::{Duration, Utc};
|
||||
use crate::handlers::attachments;
|
||||
use std::collections::HashSet;
|
||||
use worker::{query, D1Database, Env};
|
||||
|
||||
|
|
@ -78,6 +79,18 @@ pub async fn purge_deleted_ciphers(env: &Env) -> Result<u32, worker::Error> {
|
|||
let count = count_result.map(|r| r.count).unwrap_or(0);
|
||||
|
||||
if count > 0 {
|
||||
if attachments::attachments_enabled(env) {
|
||||
let bucket = attachments::require_bucket(env)
|
||||
.map_err(|e| worker::Error::RustError(e.to_string()))?;
|
||||
let keys = attachments::list_attachment_keys_for_soft_deleted_before(&db, &cutoff_str)
|
||||
.await
|
||||
.map_err(|e| worker::Error::RustError(e.to_string()))?;
|
||||
|
||||
attachments::delete_r2_objects(&bucket, &keys)
|
||||
.await
|
||||
.map_err(|e| worker::Error::RustError(e.to_string()))?;
|
||||
}
|
||||
|
||||
// Delete the records
|
||||
query!(
|
||||
&db,
|
||||
|
|
|
|||
Loading…
Reference in a new issue