1 parent
83440f20e2
commit
12e0c99850
32 files changed
+729
-109
No files matched your search
+62
-19
@@ -11,7 +11,7 @@ use crate::auth::session::SessionState;
|
||||
use crate::config::AppConfig;
|
||||
use crate::db;
|
||||
use crate::error::AppError;
|
||||
use crate::storage::{BlobStorage, attachment_key};
|
||||
use crate::storage::{self, BlobStorage, attachment_key};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct UploadResponse {
|
||||
@@ -73,7 +73,13 @@ pub async fn upload_attachment(
|
||||
let chunk = match field.chunk().await {
|
||||
Ok(chunk) => chunk,
|
||||
Err(e) => {
|
||||
let _ = writer.abort().await;
|
||||
if writer.abort().await.is_err()
|
||||
&& let Err(cleanup_error) =
|
||||
storage::enqueue_unreferenced_delete(&conn, storage.as_ref(), &key)
|
||||
.await
|
||||
{
|
||||
tracing::error!(error = %cleanup_error, storage_key = %key, "Failed to queue aborted attachment cleanup");
|
||||
}
|
||||
return Err(AppError::BadRequest(format!(
|
||||
"Failed to read field data: {e}"
|
||||
)));
|
||||
@@ -82,7 +88,12 @@ pub async fn upload_attachment(
|
||||
let Some(chunk) = chunk else { break };
|
||||
size = size.saturating_add(chunk.len());
|
||||
if size > max_attachment_size {
|
||||
let _ = writer.abort().await;
|
||||
if writer.abort().await.is_err()
|
||||
&& let Err(cleanup_error) =
|
||||
storage::enqueue_unreferenced_delete(&conn, storage.as_ref(), &key).await
|
||||
{
|
||||
tracing::error!(error = %cleanup_error, storage_key = %key, "Failed to queue oversized attachment cleanup");
|
||||
}
|
||||
return Err(AppError::BadRequest(format!(
|
||||
"File '{}' exceeds maximum size of {} MiB",
|
||||
filename, config.max_attachment_size_mb
|
||||
@@ -90,18 +101,37 @@ pub async fn upload_attachment(
|
||||
}
|
||||
checksum.update(&chunk);
|
||||
if let Err(e) = writer.write(chunk).await {
|
||||
let _ = writer.abort().await;
|
||||
if writer.abort().await.is_err()
|
||||
&& let Err(cleanup_error) =
|
||||
storage::enqueue_unreferenced_delete(&conn, storage.as_ref(), &key).await
|
||||
{
|
||||
tracing::error!(error = %cleanup_error, storage_key = %key, "Failed to queue interrupted attachment cleanup");
|
||||
}
|
||||
return Err(AppError::ServiceUnavailable(format!(
|
||||
"Failed to upload attachment: {e}"
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
let stat = writer.finish().await.map_err(|e| {
|
||||
AppError::ServiceUnavailable(format!("Failed to finalize attachment upload: {e}"))
|
||||
})?;
|
||||
let stat = match writer.finish().await {
|
||||
Ok(stat) => stat,
|
||||
Err(e) => {
|
||||
if let Err(cleanup_error) =
|
||||
storage::enqueue_unreferenced_delete(&conn, storage.as_ref(), &key).await
|
||||
{
|
||||
tracing::error!(error = %cleanup_error, storage_key = %key, "Failed to queue failed-finalize attachment cleanup");
|
||||
}
|
||||
return Err(AppError::ServiceUnavailable(format!(
|
||||
"Failed to finalize attachment upload: {e}"
|
||||
)));
|
||||
}
|
||||
};
|
||||
if stat.size != size as u64 {
|
||||
let _ = storage.delete(&key).await;
|
||||
if let Err(cleanup_error) =
|
||||
storage::enqueue_unreferenced_delete(&conn, storage.as_ref(), &key).await
|
||||
{
|
||||
tracing::error!(error = %cleanup_error, storage_key = %key, "Failed to queue invalid attachment cleanup");
|
||||
}
|
||||
return Err(AppError::InternalError(format!(
|
||||
"Attachment size verification failed for '{filename}'"
|
||||
)));
|
||||
@@ -118,7 +148,11 @@ pub async fn upload_attachment(
|
||||
checksum_sha256: Some(&checksum),
|
||||
};
|
||||
if let Err(e) = db::drafts::add_draft_attachment(&conn, &attachment) {
|
||||
let _ = storage.delete(&key).await;
|
||||
if let Err(cleanup_error) =
|
||||
storage::enqueue_unreferenced_delete(&conn, storage.as_ref(), &key).await
|
||||
{
|
||||
tracing::error!(error = %cleanup_error, storage_key = %key, "Failed to queue unattached blob cleanup");
|
||||
}
|
||||
return Err(AppError::InternalError(e));
|
||||
}
|
||||
|
||||
@@ -180,21 +214,30 @@ pub async fn delete_attachment(
|
||||
let conn = db::pool::open_user_db(&config.data_dir, &session.user_hash)
|
||||
.map_err(|e| AppError::InternalError(format!("Failed to open database: {e}")))?;
|
||||
|
||||
let attachments =
|
||||
db::drafts::get_draft_attachments(&conn, &draft_id).map_err(AppError::InternalError)?;
|
||||
let attachment = attachments
|
||||
.iter()
|
||||
.find(|a| a.id == attachment_id)
|
||||
.ok_or_else(|| AppError::NotFound("Attachment not found".to_string()))?;
|
||||
|
||||
let transaction = conn.unchecked_transaction().map_err(|e| {
|
||||
AppError::InternalError(format!(
|
||||
"Failed to start attachment delete transaction: {e}"
|
||||
))
|
||||
})?;
|
||||
let attachment =
|
||||
db::drafts::get_draft_attachment_for_update(&transaction, &draft_id, &attachment_id)
|
||||
.map_err(AppError::InternalError)?
|
||||
.ok_or_else(|| AppError::NotFound("Attachment not found".to_string()))?;
|
||||
let storage_key = attachment.storage_key.clone();
|
||||
let deleted = db::drafts::delete_draft_attachment(&conn, &attachment_id)
|
||||
let cleanup_event = db::blob_delete_outbox::enqueue(&transaction, &storage_key)
|
||||
.map_err(AppError::InternalError)?;
|
||||
let deleted = db::drafts::delete_draft_attachment(&transaction, &attachment_id)
|
||||
.map_err(AppError::InternalError)?;
|
||||
if !deleted {
|
||||
return Err(AppError::NotFound("Attachment not found".to_string()));
|
||||
}
|
||||
if let Err(error) = storage.delete(&storage_key).await {
|
||||
tracing::warn!(error = %error, storage_key = %storage_key, "Failed to remove deleted attachment blob");
|
||||
transaction.commit().map_err(|e| {
|
||||
AppError::InternalError(format!("Failed to commit attachment deletion: {e}"))
|
||||
})?;
|
||||
if let Err(error) =
|
||||
storage::process_queued_delete(&conn, storage.as_ref(), &cleanup_event).await
|
||||
{
|
||||
tracing::warn!(error = %error, storage_key = %storage_key, "Failed to process queued attachment blob deletion");
|
||||
}
|
||||
|
||||
Ok(Json(DeleteResponse {
|
||||
|
||||
Reference in new issue
Block a user