feat: delete all files when workspace is deleted
This commit is contained in:
parent
5ed6a14943
commit
f8b9f623cf
|
|
@ -164,8 +164,10 @@ async fn delete_workspace_handler(
|
||||||
workspace_id: web::Path<Uuid>,
|
workspace_id: web::Path<Uuid>,
|
||||||
state: Data<AppState>,
|
state: Data<AppState>,
|
||||||
) -> Result<Json<AppResponse<()>>> {
|
) -> Result<Json<AppResponse<()>>> {
|
||||||
|
let bucket_storage = &state.bucket_storage;
|
||||||
|
|
||||||
// TODO: add permission for workspace deletion
|
// TODO: add permission for workspace deletion
|
||||||
workspace::ops::delete_workspace_for_user(&state.pg_pool, &workspace_id).await?;
|
workspace::ops::delete_workspace_for_user(&state.pg_pool, &workspace_id, bucket_storage).await?;
|
||||||
Ok(AppResponse::Ok().into())
|
Ok(AppResponse::Ok().into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use app_error::AppError;
|
use app_error::AppError;
|
||||||
use database::collab::upsert_collab_member_with_txn;
|
use database::collab::upsert_collab_member_with_txn;
|
||||||
|
use database::file::bucket_s3_impl::BucketClientS3Impl;
|
||||||
|
use database::file::BucketStorage;
|
||||||
use database::pg_row::{AFWorkspaceMemberRow, AFWorkspaceRow};
|
use database::pg_row::{AFWorkspaceMemberRow, AFWorkspaceRow};
|
||||||
|
use database::resource_usage::get_all_workspace_blob_metadata;
|
||||||
use database::user::select_uid_from_email;
|
use database::user::select_uid_from_email;
|
||||||
use database::workspace::{
|
use database::workspace::{
|
||||||
change_workspace_icon, delete_from_workspace, delete_workspace_members, insert_user_workspace,
|
change_workspace_icon, delete_from_workspace, delete_workspace_members, insert_user_workspace,
|
||||||
|
|
@ -14,14 +17,34 @@ use shared_entity::response::AppResponseError;
|
||||||
use sqlx::{types::uuid, PgPool};
|
use sqlx::{types::uuid, PgPool};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::ops::DerefMut;
|
use std::ops::DerefMut;
|
||||||
|
use std::sync::Arc;
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
pub async fn delete_workspace_for_user(
|
pub async fn delete_workspace_for_user(
|
||||||
pg_pool: &PgPool,
|
pg_pool: &PgPool,
|
||||||
workspace_id: &Uuid,
|
workspace_id: &Uuid,
|
||||||
|
bucket_storage: &Arc<BucketStorage<BucketClientS3Impl>>,
|
||||||
) -> Result<(), AppResponseError> {
|
) -> Result<(), AppResponseError> {
|
||||||
|
// remove files from s3
|
||||||
|
|
||||||
|
let blob_metadatas = get_all_workspace_blob_metadata(pg_pool, workspace_id)
|
||||||
|
.await
|
||||||
|
.context("Get all workspace blob metadata")?;
|
||||||
|
|
||||||
|
for blob_metadata in blob_metadatas {
|
||||||
|
bucket_storage
|
||||||
|
.delete_blob(workspace_id, blob_metadata.file_id.as_str())
|
||||||
|
.await
|
||||||
|
.context("Delete blob from s3")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove from postgres
|
||||||
delete_from_workspace(pg_pool, workspace_id).await?;
|
delete_from_workspace(pg_pool, workspace_id).await?;
|
||||||
|
|
||||||
|
// TODO: There can be a rare case where user uploads while workspace is being deleted.
|
||||||
|
// We need some routine job to clean up these orphaned files.
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,82 @@
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
mod put_and_get;
|
mod put_and_get;
|
||||||
mod usage;
|
mod usage;
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use tracing::warn;
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
pub static ref LOCALHOST_MINIO_URL: Cow<'static, str> =
|
||||||
|
get_env_var("LOCALHOST_MINIO_URL", "http://localhost:9000");
|
||||||
|
pub static ref LOCALHOST_MINIO_ACCESS_KEY: Cow<'static, str> =
|
||||||
|
get_env_var("LOCALHOST_MINIO_ACCESS_KEY", "minioadmin");
|
||||||
|
pub static ref LOCALHOST_MINIO_SECRET_KEY: Cow<'static, str> =
|
||||||
|
get_env_var("LOCALHOST_MINIO_SECRET_KEY", "minioadmin");
|
||||||
|
pub static ref LOCALHOST_MINIO_BUCKET_NAME: Cow<'static, str> =
|
||||||
|
get_env_var("LOCALHOST_MINIO_BUCKET_NAME", "appflowy");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TestBucket(pub s3::Bucket);
|
||||||
|
|
||||||
|
impl TestBucket {
|
||||||
|
pub async fn new() -> Self {
|
||||||
|
let region = s3::Region::Custom {
|
||||||
|
region: "".to_owned(),
|
||||||
|
endpoint: LOCALHOST_MINIO_URL.to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let cred = s3::creds::Credentials {
|
||||||
|
access_key: Some(LOCALHOST_MINIO_ACCESS_KEY.to_string()),
|
||||||
|
secret_key: Some(LOCALHOST_MINIO_SECRET_KEY.to_string()),
|
||||||
|
security_token: None,
|
||||||
|
session_token: None,
|
||||||
|
expiration: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
match s3::Bucket::create_with_path_style(
|
||||||
|
&LOCALHOST_MINIO_BUCKET_NAME,
|
||||||
|
region.clone(),
|
||||||
|
cred.clone(),
|
||||||
|
s3::BucketConfiguration::default(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => {},
|
||||||
|
Err(e) => match e {
|
||||||
|
s3::error::S3Error::Http(409, _) => {},
|
||||||
|
_ => panic!("could not create bucket: {}", e),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
Self(
|
||||||
|
s3::Bucket::new(&LOCALHOST_MINIO_BUCKET_NAME, region.clone(), cred.clone())
|
||||||
|
.unwrap()
|
||||||
|
.with_path_style(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_object(&self, workspace_id: &str, file_id: &str) -> Option<bytes::Bytes> {
|
||||||
|
let object_key = format!("{}/{}", workspace_id, file_id);
|
||||||
|
match self.0.get_object(&object_key).await {
|
||||||
|
Ok(resp) => {
|
||||||
|
assert!(resp.status_code() == 200);
|
||||||
|
Some(resp.bytes().to_owned())
|
||||||
|
},
|
||||||
|
Err(err) => match err {
|
||||||
|
s3::error::S3Error::Http(404, _) => None,
|
||||||
|
_ => panic!("could not get object: {}", err),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_env_var<'default>(key: &str, default: &'default str) -> Cow<'default, str> {
|
||||||
|
dotenvy::dotenv().ok();
|
||||||
|
match std::env::var(key) {
|
||||||
|
Ok(value) => Cow::Owned(value),
|
||||||
|
Err(_) => {
|
||||||
|
warn!("could not read env var {}: using default: {}", key, default);
|
||||||
|
Cow::Borrowed(default)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
use super::TestBucket;
|
||||||
use app_error::ErrorCode;
|
use app_error::ErrorCode;
|
||||||
use client_api_test_util::{generate_unique_registered_user_client, workspace_id_from_client};
|
use client_api_test_util::{generate_unique_registered_user_client, workspace_id_from_client};
|
||||||
|
|
||||||
|
|
@ -96,3 +97,40 @@ async fn put_delete_get() {
|
||||||
let err = c1.get_blob(&url).await.unwrap_err();
|
let err = c1.get_blob(&url).await.unwrap_err();
|
||||||
assert_eq!(err.code, ErrorCode::RecordNotFound);
|
assert_eq!(err.code, ErrorCode::RecordNotFound);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn put_and_delete_workspace() {
|
||||||
|
let test_bucket = TestBucket::new().await;
|
||||||
|
|
||||||
|
let (c1, _user1) = generate_unique_registered_user_client().await;
|
||||||
|
let workspace_id = workspace_id_from_client(&c1).await;
|
||||||
|
let file_id = uuid::Uuid::new_v4().to_string();
|
||||||
|
let blob_to_put = "some contents 1";
|
||||||
|
{
|
||||||
|
// put blob
|
||||||
|
let mime = mime::TEXT_PLAIN_UTF_8;
|
||||||
|
let url = c1.get_blob_url(&workspace_id, &file_id);
|
||||||
|
c1.put_blob(&url, blob_to_put, &mime).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// blob exists in the bucket
|
||||||
|
let raw_data = test_bucket
|
||||||
|
.get_object(&workspace_id, &file_id)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(blob_to_put, String::from_utf8_lossy(&raw_data));
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete workspace
|
||||||
|
c1.delete_workspace(&workspace_id).await.unwrap();
|
||||||
|
|
||||||
|
{
|
||||||
|
// blob does not exist in the bucket
|
||||||
|
let is_none = test_bucket
|
||||||
|
.get_object(&workspace_id, &file_id)
|
||||||
|
.await
|
||||||
|
.is_none();
|
||||||
|
assert!(is_none);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue