fix: fix services accessig directly to localstorage
Prevent services accessing directly to localstorage and prefer using an astraction layer to expose full blob. The abstraction layer (dedup services) will cover backend storage election (local, s3, ...), encryption, etc This change permit audio_metadata_service, media_metadaa_service, face_indexing_service to handle blobs without worring of the backend. note: prefered way to handle blob is the streamed way. Some services may not have this possibility
This commit is contained in:
@@ -8,6 +8,7 @@ use uuid::Uuid;
|
||||
|
||||
use crate::application::ports::file_lifecycle::FileLifecycleHook;
|
||||
use crate::common::errors::DomainError;
|
||||
use crate::infrastructure::services::dedup_service::DedupService;
|
||||
|
||||
#[derive(Debug, FromRow)]
|
||||
pub struct AudioFileRow {
|
||||
@@ -17,22 +18,32 @@ pub struct AudioFileRow {
|
||||
|
||||
pub struct AudioMetadataService {
|
||||
pool: Arc<PgPool>,
|
||||
blob_root: PathBuf,
|
||||
/// CDC-aware blob reader. Same abstraction `thumbnail_service` uses —
|
||||
/// hides both the chunk-manifest concatenation and the underlying
|
||||
/// `BlobStorageBackend` wrapper stack.
|
||||
dedup: Arc<DedupService>,
|
||||
/// Tier-1 scratch directory for `stream_blob_to_tempfile`. Pulled
|
||||
/// from `AppConfig::temp_dir` (env `OXICLOUD_TEMP_DIR`) at DI time.
|
||||
temp_dir: PathBuf,
|
||||
}
|
||||
|
||||
impl AudioMetadataService {
|
||||
pub fn new(pool: Arc<PgPool>, blob_root: PathBuf) -> Self {
|
||||
Self { pool, blob_root }
|
||||
pub fn new(pool: Arc<PgPool>, dedup: Arc<DedupService>, temp_dir: PathBuf) -> Self {
|
||||
Self {
|
||||
pool,
|
||||
dedup,
|
||||
temp_dir,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_audio_file(mime_type: &str) -> bool {
|
||||
mime_type.starts_with("audio/")
|
||||
}
|
||||
|
||||
pub fn spawn_extraction_background(service: Arc<Self>, file_id: Uuid, file_path: PathBuf) {
|
||||
pub fn spawn_extraction_background(service: Arc<Self>, file_id: Uuid, blob_hash: String) {
|
||||
tokio::spawn(async move {
|
||||
tracing::info!("🎵 Extracting audio metadata for: {}", file_id);
|
||||
if let Err(e) = service.extract_and_save(&file_id, &file_path).await {
|
||||
if let Err(e) = service.extract_and_save(&file_id, &blob_hash).await {
|
||||
tracing::warn!("Failed to extract audio metadata: {}", e);
|
||||
}
|
||||
});
|
||||
@@ -41,22 +52,17 @@ impl AudioMetadataService {
|
||||
pub fn spawn_extraction_with_delete_background(
|
||||
service: Arc<Self>,
|
||||
file_id: Uuid,
|
||||
file_path: PathBuf,
|
||||
blob_hash: String,
|
||||
) {
|
||||
tokio::spawn(async move {
|
||||
tracing::info!("🎵 Updating audio metadata for: {}", file_id);
|
||||
let _ = service.delete_metadata(&file_id).await;
|
||||
if let Err(e) = service.extract_and_save(&file_id, &file_path).await {
|
||||
if let Err(e) = service.extract_and_save(&file_id, &blob_hash).await {
|
||||
tracing::warn!("Failed to update audio metadata: {}", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn blob_path(&self, hash: &str) -> PathBuf {
|
||||
let prefix = &hash[0..2];
|
||||
self.blob_root.join(prefix).join(format!("{}.blob", hash))
|
||||
}
|
||||
|
||||
/// Extract ID3 tag and MP3 duration from a file.
|
||||
///
|
||||
/// All I/O is synchronous (id3 + mp3_duration crates), so this MUST
|
||||
@@ -104,15 +110,26 @@ impl AudioMetadataService {
|
||||
pub async fn extract_and_save(
|
||||
&self,
|
||||
file_id: &Uuid,
|
||||
file_path: &Path,
|
||||
blob_hash: &str,
|
||||
) -> Result<(), DomainError> {
|
||||
info!(
|
||||
"AudioMetadataService: blob_root={:?}, file_id={}, file_path={:?}",
|
||||
self.blob_root, file_id, file_path,
|
||||
"AudioMetadataService: extracting file_id={}, blob_hash={}",
|
||||
file_id, blob_hash,
|
||||
);
|
||||
|
||||
// Stream the blob (CDC-aware — chunks concatenated on the fly for
|
||||
// chunked files; wrapper stack handles encryption + retry + cache)
|
||||
// to a tempfile in the configured tier-1 temp dir, then hand its
|
||||
// `.path()` to the id3 + mp3_duration crates which only expose
|
||||
// `from_path` APIs. Peak process-heap = one chunk (~1 MiB)
|
||||
// regardless of MP3 size. Guard drops → tempfile auto-removed.
|
||||
let named = self
|
||||
.dedup
|
||||
.stream_blob_to_tempfile(blob_hash, &self.temp_dir, ".mp3")
|
||||
.await?;
|
||||
let path = named.path().to_path_buf();
|
||||
|
||||
// ── Sync I/O on the blocking thread pool (never stalls Tokio workers) ──
|
||||
let path = file_path.to_path_buf();
|
||||
let metadata = tokio::task::spawn_blocking(move || Self::extract_metadata_blocking(&path))
|
||||
.await
|
||||
.map_err(|e| {
|
||||
@@ -121,6 +138,10 @@ impl AudioMetadataService {
|
||||
format!("spawn_blocking join error: {e}"),
|
||||
)
|
||||
})?;
|
||||
// Explicitly hold `named` alive until after the extraction — the
|
||||
// `spawn_blocking` closure only borrows the raw `path`, so the
|
||||
// guard must not drop while the extractor is running.
|
||||
drop(named);
|
||||
|
||||
let Some(m) = metadata else {
|
||||
return Ok(());
|
||||
@@ -208,8 +229,10 @@ impl AudioMetadataService {
|
||||
let audio_file = row.map_err(|e| {
|
||||
DomainError::database_error(format!("Failed to fetch audio file row: {}", e))
|
||||
})?;
|
||||
let file_path = self.blob_path(&audio_file.blob_hash);
|
||||
match self.extract_and_save(&audio_file.file_id, &file_path).await {
|
||||
match self
|
||||
.extract_and_save(&audio_file.file_id, &audio_file.blob_hash)
|
||||
.await
|
||||
{
|
||||
Ok(()) => processed += 1,
|
||||
Err(e) => {
|
||||
warn!(
|
||||
@@ -313,8 +336,7 @@ impl AudioMetadataService {
|
||||
}
|
||||
Ok(_) => {
|
||||
// No existing metadata found — original not yet processed; fall back.
|
||||
let file_path = service.blob_path(&blob_hash);
|
||||
if let Err(e) = service.extract_and_save(&new_file_id, &file_path).await {
|
||||
if let Err(e) = service.extract_and_save(&new_file_id, &blob_hash).await {
|
||||
warn!(
|
||||
"Failed to extract audio metadata for {}: {}",
|
||||
new_file_id, e
|
||||
@@ -368,10 +390,11 @@ impl FileLifecycleHook for AudioMetadataService {
|
||||
};
|
||||
let service = Arc::new(Self {
|
||||
pool: self.pool.clone(),
|
||||
blob_root: self.blob_root.clone(),
|
||||
dedup: self.dedup.clone(),
|
||||
temp_dir: self.temp_dir.clone(),
|
||||
});
|
||||
if is_new_blob {
|
||||
Self::spawn_extraction_background(service, uuid, self.blob_path(blob_hash));
|
||||
Self::spawn_extraction_background(service, uuid, blob_hash.to_string());
|
||||
} else {
|
||||
Self::clone_or_extract_background(service, uuid, blob_hash.to_string());
|
||||
}
|
||||
@@ -400,7 +423,8 @@ impl FileLifecycleHook for AudioMetadataService {
|
||||
};
|
||||
let service = Arc::new(Self {
|
||||
pool: self.pool.clone(),
|
||||
blob_root: self.blob_root.clone(),
|
||||
dedup: self.dedup.clone(),
|
||||
temp_dir: self.temp_dir.clone(),
|
||||
});
|
||||
Self::clone_from_source_background(service, uuid, source_uuid, blob_hash.to_string());
|
||||
}
|
||||
@@ -415,9 +439,10 @@ impl FileLifecycleHook for AudioMetadataService {
|
||||
};
|
||||
let service = Arc::new(Self {
|
||||
pool: self.pool.clone(),
|
||||
blob_root: self.blob_root.clone(),
|
||||
dedup: self.dedup.clone(),
|
||||
temp_dir: self.temp_dir.clone(),
|
||||
});
|
||||
Self::spawn_extraction_with_delete_background(service, uuid, self.blob_path(blob_hash));
|
||||
Self::spawn_extraction_with_delete_background(service, uuid, blob_hash.to_string());
|
||||
}
|
||||
|
||||
fn on_file_deleted(&self, _file_id: &str) {
|
||||
|
||||
Reference in New Issue
Block a user