Skip to content

Commit 2c63f5f

Browse files
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
1 parent 405721c commit 2c63f5f

6 files changed

Lines changed: 92 additions & 2 deletions

File tree

src/common/di.rs

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,8 @@ impl AppServiceFactory {
263263
blob_backend,
264264
db_pool.clone(),
265265
maintenance_pool.clone(),
266-
),
266+
)
267+
.with_thumbnail_service(thumbnail_service.clone()),
267268
);
268269
dedup_service.initialize().await?;
269270

@@ -981,6 +982,44 @@ pub struct CoreServices {
981982
pub config: AppConfig,
982983
}
983984

985+
impl CoreServices {
986+
/// Invalidate a file's moka thumbnail cache and kick off background regeneration.
987+
///
988+
/// Call this after any write that swaps the blob for an existing file.
989+
/// Safe to call for new files too (no-op on empty cache).
990+
/// Skips everything if the MIME type is not a supported image.
991+
pub async fn refresh_thumbnails_after_update(
992+
&self,
993+
file_id: String,
994+
blob_hash: String,
995+
content_type: &str,
996+
) {
997+
if !ThumbnailService::is_supported_image(content_type) {
998+
return;
999+
}
1000+
if let Err(e) = self.thumbnail_service.delete_thumbnails(&file_id).await {
1001+
tracing::warn!("Failed to invalidate thumbnail cache for {}: {}", file_id, e);
1002+
}
1003+
let ts = self.thumbnail_service.clone();
1004+
let ds = self.dedup_service.clone();
1005+
let hash = blob_hash.clone();
1006+
tokio::spawn(async move {
1007+
match ds.read_blob_bytes(&hash).await {
1008+
Ok(bytes) => {
1009+
ts.generate_all_sizes_background_from_bytes(file_id, hash, bytes);
1010+
}
1011+
Err(e) => {
1012+
tracing::warn!(
1013+
"Failed to read blob for thumbnail regeneration {}: {}",
1014+
file_id,
1015+
e
1016+
);
1017+
}
1018+
}
1019+
});
1020+
}
1021+
}
1022+
9841023
/// Container for repository services
9851024
#[derive(Clone)]
9861025
pub struct RepositoryServices {

src/infrastructure/services/dedup_service.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ use crate::application::ports::dedup_ports::{
4949
BlobMetadataDto, DedupPort, DedupResultDto, DedupStatsDto,
5050
};
5151
use crate::domain::errors::{DomainError, ErrorKind};
52+
use crate::infrastructure::services::thumbnail_service::ThumbnailService;
5253

5354
// ── CDC Constants ────────────────────────────────────────────────────────────
5455

@@ -83,6 +84,9 @@ pub struct DedupService {
8384
/// Isolated maintenance pool for long-running operations
8485
/// (verify_integrity, garbage_collect) that must never starve the primary.
8586
maintenance_pool: Arc<PgPool>,
87+
/// Optional thumbnail service — when set, blob-hash thumbnails are deleted
88+
/// from disk whenever a blob's ref_count reaches zero.
89+
thumbnail_service: Option<Arc<ThumbnailService>>,
8690
}
8791

8892
impl DedupService {
@@ -100,9 +104,17 @@ impl DedupService {
100104
backend,
101105
pool,
102106
maintenance_pool,
107+
thumbnail_service: None,
103108
}
104109
}
105110

111+
/// Attach a thumbnail service so that disk thumbnails are cleaned up when
112+
/// a blob's ref_count drops to zero.
113+
pub fn with_thumbnail_service(mut self, svc: Arc<ThumbnailService>) -> Self {
114+
self.thumbnail_service = Some(svc);
115+
self
116+
}
117+
106118
/// Creates a stub instance for testing — never hits PG or the filesystem.
107119
#[cfg(any(test, feature = "integration_tests"))]
108120
pub fn new_stub() -> Self {
@@ -117,6 +129,7 @@ impl DedupService {
117129
backend: Arc::new(LocalBlobBackend::new(Path::new("/tmp/oxicloud_stub_blobs"))),
118130
pool: stub_pool.clone(),
119131
maintenance_pool: stub_pool,
132+
thumbnail_service: None,
120133
}
121134
}
122135

@@ -772,6 +785,11 @@ impl DedupService {
772785
}
773786
}
774787

788+
// Bug 4 fix: delete disk thumbnails keyed by file_hash (last reference gone)
789+
if let Some(ts) = &self.thumbnail_service {
790+
ts.delete_blob_thumbnails(file_hash).await;
791+
}
792+
775793
tracing::info!(
776794
"MANIFEST DELETED: {} ({} chunks, {} orphan chunks removed)",
777795
&file_hash[..12],
@@ -849,6 +867,11 @@ impl DedupService {
849867
tracing::warn!("Failed to delete blob file {}: {}", hash, e);
850868
}
851869

870+
// Bug 3 fix: delete disk thumbnails keyed by hash (last reference gone)
871+
if let Some(ts) = &self.thumbnail_service {
872+
ts.delete_blob_thumbnails(hash).await;
873+
}
874+
852875
tracing::info!("BLOB DELETED: {} (no more references)", &hash[..12]);
853876
Ok(true)
854877
} else {

src/interfaces/api/handlers/webdav_handler.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -972,6 +972,12 @@ async fn handle_put(
972972
);
973973
}
974974

975+
state.core.refresh_thumbnails_after_update(
976+
file_dto.id.clone(),
977+
file_dto.etag.clone(),
978+
&content_type,
979+
).await;
980+
975981
Ok(Response::builder()
976982
.status(StatusCode::NO_CONTENT)
977983
.body(Body::empty())

src/interfaces/api/handlers/wopi_handler.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,15 @@ async fn put_file(
288288
let _ = tokio::fs::remove_file(&temp_path).await;
289289

290290
match result {
291-
Ok(_) => StatusCode::OK.into_response(),
291+
Ok(file_dto) => {
292+
state.app_state.core.refresh_thumbnails_after_update(
293+
file_dto.id.clone(),
294+
file_dto.etag.clone(),
295+
&content_type,
296+
).await;
297+
298+
StatusCode::OK.into_response()
299+
}
292300
Err(e) => {
293301
tracing::error!("WOPI PutFile failed: {}", e);
294302
StatusCode::INTERNAL_SERVER_ERROR.into_response()

src/interfaces/nextcloud/uploads_handler.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,13 @@ async fn handle_assemble(
163163
)
164164
.await
165165
.map_err(|e| AppError::internal_error(format!("Failed to update file: {}", e)))?;
166+
167+
state.core.refresh_thumbnails_after_update(
168+
dto.id.clone(),
169+
dto.etag.clone(),
170+
&content_type,
171+
).await;
172+
166173
Some(dto.etag)
167174
} else {
168175
// For new files we still need to read the temp file since create_file takes &[u8].

src/interfaces/nextcloud/webdav_handler.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -566,6 +566,13 @@ async fn handle_put(
566566
}
567567
}
568568

569+
// Bug 1 & 2 fix: invalidate stale thumbnail and regenerate from new blob.
570+
state.core.refresh_thumbnails_after_update(
571+
updated.id.clone(),
572+
updated.etag.clone(),
573+
&content_type,
574+
).await;
575+
569576
return Ok(Response::builder()
570577
.status(StatusCode::NO_CONTENT)
571578
.header(header::ETAG, format!("\"{}\"", updated.etag))

0 commit comments

Comments
 (0)