feat: enhance media management with EXIF data extraction, metadata filtering, and storage path generation

refactor: update configuration handling to use environment variables and improve code organization
This commit is contained in:
2025-11-14 11:22:51 +01:00
parent 70dc0a7131
commit 3c3b51a2a7
24 changed files with 393 additions and 181 deletions

View File

@@ -1,16 +1,9 @@
use std::{path::PathBuf, sync::Arc};
use std::{path::{Path, PathBuf}, sync::Arc};
use async_trait::async_trait;
use chrono::Datelike;
use futures::stream::StreamExt;
use libertas_core::{
authz,
config::Config,
error::{CoreError, CoreResult},
models::Media,
repositories::{AlbumShareRepository, MediaRepository, UserRepository},
schema::{ListMediaOptions, UploadMediaData},
services::MediaService,
authz, config::AppConfig, error::{CoreError, CoreResult}, media_utils::{ExtractedExif, extract_exif_data_from_bytes, get_storage_path_and_date}, models::{Media, MediaBundle, MediaMetadata}, repositories::{AlbumShareRepository, MediaMetadataRepository, MediaRepository, UserRepository}, schema::{ListMediaOptions, UploadMediaData}, services::MediaService
};
use serde_json::json;
use sha2::{Digest, Sha256};
@@ -21,7 +14,8 @@ pub struct MediaServiceImpl {
repo: Arc<dyn MediaRepository>,
user_repo: Arc<dyn UserRepository>,
album_share_repo: Arc<dyn AlbumShareRepository>,
config: Config,
metadata_repo: Arc<dyn MediaMetadataRepository>,
config: AppConfig,
nats_client: async_nats::Client,
}
@@ -30,13 +24,15 @@ impl MediaServiceImpl {
repo: Arc<dyn MediaRepository>,
user_repo: Arc<dyn UserRepository>,
album_share_repo: Arc<dyn AlbumShareRepository>,
config: Config,
metadata_repo: Arc<dyn MediaMetadataRepository>,
config: AppConfig,
nats_client: async_nats::Client,
) -> Self {
Self {
repo,
user_repo,
album_share_repo,
metadata_repo,
config,
nats_client,
}
@@ -55,10 +51,22 @@ impl MediaService for MediaServiceImpl {
self.check_upload_prerequisites(owner_id, file_size, &hash)
.await?;
let storage_path = self.persist_media_file(&file_bytes, &filename).await?;
let file_bytes_clone = file_bytes.clone();
let extracted_data = tokio::task::spawn_blocking(move || {
extract_exif_data_from_bytes(&file_bytes_clone)
})
.await
.unwrap()?;
let (storage_path_buf, _date_taken) =
get_storage_path_and_date(&extracted_data, &filename);
let storage_path_str = self
.persist_media_file(&file_bytes, &storage_path_buf)
.await?;
let media = self
.persist_media_metadata(owner_id, filename, mime_type, storage_path, hash, file_size)
.persist_media_metadata(owner_id, filename, mime_type, storage_path_str, hash, file_size, extracted_data)
.await?;
self.publish_new_media_job(media.id).await?;
@@ -66,7 +74,7 @@ impl MediaService for MediaServiceImpl {
Ok(media)
}
async fn get_media_details(&self, id: Uuid, user_id: Uuid) -> CoreResult<Media> {
async fn get_media_details(&self, id: Uuid, user_id: Uuid) -> CoreResult<MediaBundle> {
let media = self
.repo
.find_by_id(id)
@@ -79,20 +87,23 @@ impl MediaService for MediaServiceImpl {
.await?
.ok_or(CoreError::NotFound("User".to_string(), user_id))?;
if authz::is_owner(user_id, &media) || authz::is_admin(&user) {
return Ok(media);
}
let is_shared = self
if !authz::is_owner(user_id, &media) && !authz::is_admin(&user) {
let is_shared = self
.album_share_repo
.is_media_in_shared_album(id, user_id)
.await?;
if is_shared {
return Ok(media);
tracing::warn!("User {} attempted to access media {} without permission, media owner is: {}", user_id, id, media.owner_id);
if !is_shared {
tracing::warn!("User {} attempted to access media {} without permission, media owner is: {}", user_id, id, media.owner_id);
return Err(CoreError::Auth("Access denied".to_string()));
}
}
Err(CoreError::Auth("Access denied".to_string()))
let metadata = self.metadata_repo.find_by_media_id(id).await?;
Ok(MediaBundle { media, metadata })
}
async fn list_user_media(&self, user_id: Uuid, options: ListMediaOptions) -> CoreResult<Vec<Media>> {
@@ -216,27 +227,18 @@ impl MediaServiceImpl {
Ok(())
}
async fn persist_media_file(&self, file_bytes: &[u8], filename: &str) -> CoreResult<String> {
let now = chrono::Utc::now();
let year = now.year().to_string();
let month = format!("{:02}", now.month());
async fn persist_media_file(&self, file_bytes: &[u8], storage_path: &Path) -> CoreResult<String> {
let mut dest_path = PathBuf::from(&self.config.media_library_path);
dest_path.push(year.clone());
dest_path.push(month.clone());
dest_path.push(storage_path);
fs::create_dir_all(&dest_path).await?;
dest_path.push(filename);
let storage_path_str = PathBuf::from(&year)
.join(&month)
.join(filename)
.to_string_lossy()
.to_string();
if let Some(parent) = dest_path.parent() {
fs::create_dir_all(parent).await?;
}
let mut file = fs::File::create(&dest_path).await?;
file.write_all(&file_bytes).await?;
Ok(storage_path_str)
Ok(storage_path.to_string_lossy().to_string())
}
async fn persist_media_metadata(
@@ -247,6 +249,7 @@ impl MediaServiceImpl {
storage_path: String,
hash: String,
file_size: i64,
extracted_data: ExtractedExif,
) -> CoreResult<Media> {
let media_model = Media {
id: Uuid::new_v4(),
@@ -260,6 +263,22 @@ impl MediaServiceImpl {
};
self.repo.create(&media_model).await?;
let mut metadata_models = Vec::new();
for (source, tag_name, tag_value) in extracted_data.all_tags {
metadata_models.push(MediaMetadata {
id: Uuid::new_v4(),
media_id: media_model.id,
source,
tag_name,
tag_value,
});
}
if !metadata_models.is_empty() {
self.metadata_repo.create_batch(&metadata_models).await?;
}
self.user_repo
.update_storage_used(owner_id, file_size)
.await?;

View File

@@ -2,7 +2,7 @@ use std::sync::Arc;
use async_trait::async_trait;
use libertas_core::{
config::Config,
config::AppConfig,
error::{CoreError, CoreResult},
models::{Role, User},
repositories::UserRepository,
@@ -17,7 +17,7 @@ pub struct UserServiceImpl {
repo: Arc<dyn UserRepository>,
hasher: Arc<dyn PasswordHasher>,
tokenizer: Arc<dyn TokenGenerator>,
config: Arc<Config>,
config: Arc<AppConfig>,
}
impl UserServiceImpl {
@@ -25,7 +25,7 @@ impl UserServiceImpl {
repo: Arc<dyn UserRepository>,
hasher: Arc<dyn PasswordHasher>,
tokenizer: Arc<dyn TokenGenerator>,
config: Arc<Config>,
config: Arc<AppConfig>,
) -> Self {
Self {
repo,