bugfix/thumbnails on update
Bug 1 & 2 (webdav_handler.rs handle_put() update branch):
- After a successful file update via WebDAV PUT, if the content type is a supported image:
a. delete_thumbnails(file_id) — evicts the stale moka cache entry
b. Spawns a background task to read the new blob bytes and call generate_all_sizes_background_from_bytes
Bug 3 & 4 (dedup_service.rs):
- Added thumbnail_service: Option<Arc<ThumbnailService>> field with a with_thumbnail_service() builder
- In remove_legacy_reference(): calls delete_blob_thumbnails(hash) when ref_count hits 0
- In remove_manifest_reference(): calls delete_blob_thumbnails(file_hash) when manifest's last ref is dropped
- Wired in di.rs — the thumbnail service is created before dedup service so the ordering works cleanly
This commit is contained in:
+44
-1
@@ -263,7 +263,8 @@ impl AppServiceFactory {
|
||||
blob_backend,
|
||||
db_pool.clone(),
|
||||
maintenance_pool.clone(),
|
||||
),
|
||||
)
|
||||
.with_thumbnail_service(thumbnail_service.clone()),
|
||||
);
|
||||
dedup_service.initialize().await?;
|
||||
|
||||
@@ -981,6 +982,48 @@ pub struct CoreServices {
|
||||
pub config: AppConfig,
|
||||
}
|
||||
|
||||
impl CoreServices {
|
||||
/// Invalidate a file's moka thumbnail cache and kick off background regeneration.
|
||||
///
|
||||
/// Call this after any write that swaps the blob for an existing file.
|
||||
/// Safe to call for new files too (no-op on empty cache).
|
||||
/// Skips everything if the MIME type is not a supported image.
|
||||
pub async fn refresh_thumbnails_after_update(
|
||||
&self,
|
||||
file_id: String,
|
||||
blob_hash: String,
|
||||
content_type: &str,
|
||||
) {
|
||||
if !ThumbnailService::is_supported_image(content_type) {
|
||||
return;
|
||||
}
|
||||
if let Err(e) = self.thumbnail_service.delete_thumbnails(&file_id).await {
|
||||
tracing::warn!(
|
||||
"Failed to invalidate thumbnail cache for {}: {}",
|
||||
file_id,
|
||||
e
|
||||
);
|
||||
}
|
||||
let ts = self.thumbnail_service.clone();
|
||||
let ds = self.dedup_service.clone();
|
||||
let hash = blob_hash.clone();
|
||||
tokio::spawn(async move {
|
||||
match ds.read_blob_bytes(&hash).await {
|
||||
Ok(bytes) => {
|
||||
ts.generate_all_sizes_background_from_bytes(file_id, hash, bytes);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
"Failed to read blob for thumbnail regeneration {}: {}",
|
||||
file_id,
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Container for repository services
|
||||
#[derive(Clone)]
|
||||
pub struct RepositoryServices {
|
||||
|
||||
@@ -49,6 +49,7 @@ use crate::application::ports::dedup_ports::{
|
||||
BlobMetadataDto, DedupPort, DedupResultDto, DedupStatsDto,
|
||||
};
|
||||
use crate::domain::errors::{DomainError, ErrorKind};
|
||||
use crate::infrastructure::services::thumbnail_service::ThumbnailService;
|
||||
|
||||
// ── CDC Constants ────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -83,6 +84,9 @@ pub struct DedupService {
|
||||
/// Isolated maintenance pool for long-running operations
|
||||
/// (verify_integrity, garbage_collect) that must never starve the primary.
|
||||
maintenance_pool: Arc<PgPool>,
|
||||
/// Optional thumbnail service — when set, blob-hash thumbnails are deleted
|
||||
/// from disk whenever a blob's ref_count reaches zero.
|
||||
thumbnail_service: Option<Arc<ThumbnailService>>,
|
||||
}
|
||||
|
||||
impl DedupService {
|
||||
@@ -100,9 +104,17 @@ impl DedupService {
|
||||
backend,
|
||||
pool,
|
||||
maintenance_pool,
|
||||
thumbnail_service: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Attach a thumbnail service so that disk thumbnails are cleaned up when
|
||||
/// a blob's ref_count drops to zero.
|
||||
pub fn with_thumbnail_service(mut self, svc: Arc<ThumbnailService>) -> Self {
|
||||
self.thumbnail_service = Some(svc);
|
||||
self
|
||||
}
|
||||
|
||||
/// Creates a stub instance for testing — never hits PG or the filesystem.
|
||||
#[cfg(any(test, feature = "integration_tests"))]
|
||||
pub fn new_stub() -> Self {
|
||||
@@ -117,6 +129,7 @@ impl DedupService {
|
||||
backend: Arc::new(LocalBlobBackend::new(Path::new("/tmp/oxicloud_stub_blobs"))),
|
||||
pool: stub_pool.clone(),
|
||||
maintenance_pool: stub_pool,
|
||||
thumbnail_service: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -772,6 +785,11 @@ impl DedupService {
|
||||
}
|
||||
}
|
||||
|
||||
// Bug 4 fix: delete disk thumbnails keyed by file_hash (last reference gone)
|
||||
if let Some(ts) = &self.thumbnail_service {
|
||||
ts.delete_blob_thumbnails(file_hash).await;
|
||||
}
|
||||
|
||||
tracing::info!(
|
||||
"MANIFEST DELETED: {} ({} chunks, {} orphan chunks removed)",
|
||||
&file_hash[..12],
|
||||
@@ -849,6 +867,11 @@ impl DedupService {
|
||||
tracing::warn!("Failed to delete blob file {}: {}", hash, e);
|
||||
}
|
||||
|
||||
// Bug 3 fix: delete disk thumbnails keyed by hash (last reference gone)
|
||||
if let Some(ts) = &self.thumbnail_service {
|
||||
ts.delete_blob_thumbnails(hash).await;
|
||||
}
|
||||
|
||||
tracing::info!("BLOB DELETED: {} (no more references)", &hash[..12]);
|
||||
Ok(true)
|
||||
} else {
|
||||
|
||||
@@ -972,6 +972,15 @@ async fn handle_put(
|
||||
);
|
||||
}
|
||||
|
||||
state
|
||||
.core
|
||||
.refresh_thumbnails_after_update(
|
||||
file_dto.id.clone(),
|
||||
file_dto.etag.clone(),
|
||||
&content_type,
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(Response::builder()
|
||||
.status(StatusCode::NO_CONTENT)
|
||||
.body(Body::empty())
|
||||
|
||||
@@ -288,7 +288,19 @@ async fn put_file(
|
||||
let _ = tokio::fs::remove_file(&temp_path).await;
|
||||
|
||||
match result {
|
||||
Ok(_) => StatusCode::OK.into_response(),
|
||||
Ok(file_dto) => {
|
||||
state
|
||||
.app_state
|
||||
.core
|
||||
.refresh_thumbnails_after_update(
|
||||
file_dto.id.clone(),
|
||||
file_dto.etag.clone(),
|
||||
&content_type,
|
||||
)
|
||||
.await;
|
||||
|
||||
StatusCode::OK.into_response()
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("WOPI PutFile failed: {}", e);
|
||||
StatusCode::INTERNAL_SERVER_ERROR.into_response()
|
||||
|
||||
@@ -163,6 +163,12 @@ async fn handle_assemble(
|
||||
)
|
||||
.await
|
||||
.map_err(|e| AppError::internal_error(format!("Failed to update file: {}", e)))?;
|
||||
|
||||
state
|
||||
.core
|
||||
.refresh_thumbnails_after_update(dto.id.clone(), dto.etag.clone(), &content_type)
|
||||
.await;
|
||||
|
||||
Some(dto.etag)
|
||||
} else {
|
||||
// For new files we still need to read the temp file since create_file takes &[u8].
|
||||
|
||||
@@ -566,6 +566,16 @@ async fn handle_put(
|
||||
}
|
||||
}
|
||||
|
||||
// Bug 1 & 2 fix: invalidate stale thumbnail and regenerate from new blob.
|
||||
state
|
||||
.core
|
||||
.refresh_thumbnails_after_update(
|
||||
updated.id.clone(),
|
||||
updated.etag.clone(),
|
||||
&content_type,
|
||||
)
|
||||
.await;
|
||||
|
||||
return Ok(Response::builder()
|
||||
.status(StatusCode::NO_CONTENT)
|
||||
.header(header::ETAG, format!("\"{}\"", updated.etag))
|
||||
|
||||
Reference in New Issue
Block a user