Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 44 additions & 1 deletion src/common/di.rs
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,8 @@ impl AppServiceFactory {
blob_backend,
db_pool.clone(),
maintenance_pool.clone(),
),
)
.with_thumbnail_service(thumbnail_service.clone()),
);
dedup_service.initialize().await?;

Expand Down Expand Up @@ -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 {
Expand Down
23 changes: 23 additions & 0 deletions src/infrastructure/services/dedup_service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 ────────────────────────────────────────────────────────────

Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -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,
}
}

Expand Down Expand Up @@ -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],
Expand Down Expand Up @@ -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 {
Expand Down
9 changes: 9 additions & 0 deletions src/interfaces/api/handlers/webdav_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
14 changes: 13 additions & 1 deletion src/interfaces/api/handlers/wopi_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
6 changes: 6 additions & 0 deletions src/interfaces/nextcloud/uploads_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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].
Expand Down
10 changes: 10 additions & 0 deletions src/interfaces/nextcloud/webdav_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
Loading