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:
@@ -1,6 +1,9 @@
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::error::{CoreError, CoreResult};
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum DatabaseType {
|
||||
Postgres,
|
||||
Sqlite,
|
||||
@@ -30,6 +33,48 @@ pub struct ThumbnailConfig {
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
pub struct Config {
|
||||
#[serde(rename = "DATABASE_URL")]
|
||||
pub database_url: String,
|
||||
|
||||
#[serde(rename = "DATABASE_DB_TYPE")]
|
||||
pub database_db_type: DatabaseType,
|
||||
|
||||
#[serde(rename = "SERVER_ADDRESS")]
|
||||
pub server_address: String,
|
||||
|
||||
#[serde(rename = "JWT_SECRET")]
|
||||
pub jwt_secret: String,
|
||||
|
||||
#[serde(rename = "MEDIA_LIBRARY_PATH")]
|
||||
pub media_library_path: String,
|
||||
|
||||
#[serde(rename = "BROKER_URL")]
|
||||
pub broker_url: String,
|
||||
|
||||
#[serde(default = "default_max_upload_size")]
|
||||
#[serde(rename = "MAX_UPLOAD_SIZE_MB")]
|
||||
pub max_upload_size_mb: u32,
|
||||
|
||||
#[serde(default = "default_storage_quota")]
|
||||
#[serde(rename = "DEFAULT_STORAGE_QUOTA_GB")]
|
||||
pub default_storage_quota_gb: u64,
|
||||
|
||||
#[serde(default = "default_allowed_sort_columns")]
|
||||
#[serde(rename = "ALLOWED_SORT_COLUMNS")]
|
||||
pub allowed_sort_columns: Vec<String>,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub thumbnail_config: Option<ThumbnailConfig>,
|
||||
}
|
||||
|
||||
fn default_max_upload_size() -> u32 { 100 }
|
||||
fn default_storage_quota() -> u64 { 10 }
|
||||
fn default_allowed_sort_columns() -> Vec<String> {
|
||||
vec!["created_at".to_string(), "original_filename".to_string()]
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppConfig {
|
||||
pub database: DatabaseConfig,
|
||||
pub server_address: String,
|
||||
pub jwt_secret: String,
|
||||
@@ -40,3 +85,29 @@ pub struct Config {
|
||||
pub allowed_sort_columns: Option<Vec<String>>,
|
||||
pub thumbnail_config: Option<ThumbnailConfig>,
|
||||
}
|
||||
|
||||
pub fn load_config() -> CoreResult<AppConfig> {
|
||||
// Load the .env file at that specific path
|
||||
let env_path = dotenvy::dotenv()
|
||||
.map_err(|e| CoreError::Config(format!("Failed to load .env file: {}", e)))?;
|
||||
|
||||
println!("Loaded config from {}", env_path.display());
|
||||
|
||||
let config = envy::from_env::<Config>()
|
||||
.map_err(|e| CoreError::Config(format!("Failed to load config from env: {}", e)))?;
|
||||
|
||||
Ok(AppConfig {
|
||||
database: DatabaseConfig {
|
||||
db_type: config.database_db_type,
|
||||
url: config.database_url,
|
||||
},
|
||||
server_address: config.server_address,
|
||||
jwt_secret: config.jwt_secret,
|
||||
media_library_path: config.media_library_path,
|
||||
broker_url: config.broker_url,
|
||||
max_upload_size_mb: Some(config.max_upload_size_mb),
|
||||
default_storage_quota_gb: Some(config.default_storage_quota_gb),
|
||||
allowed_sort_columns: Some(config.allowed_sort_columns),
|
||||
thumbnail_config: config.thumbnail_config,
|
||||
})
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::path::Path;
|
||||
use std::{io::Cursor, path::{Path, PathBuf}};
|
||||
|
||||
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||
use nom_exif::{AsyncMediaParser, AsyncMediaSource, ExifIter, TrackInfo};
|
||||
use chrono::{DateTime, Datelike, NaiveDateTime, Utc};
|
||||
use nom_exif::{AsyncMediaParser, AsyncMediaSource, ExifIter, MediaParser, MediaSource, TrackInfo};
|
||||
|
||||
use crate::{error::{CoreError, CoreResult}, models::MediaMetadataSource};
|
||||
|
||||
@@ -10,10 +10,76 @@ pub struct ExtractedExif {
|
||||
pub all_tags: Vec<(MediaMetadataSource, String, String)>,
|
||||
}
|
||||
|
||||
const EXIF_DATE_FORMATS: &[&str] = &[
|
||||
"%Y:%m:%d %H:%M:%S",
|
||||
"%Y-%m-%d %H:%M:%S",
|
||||
"%Y/%m/%d %H:%M:%S",
|
||||
"%Y-%m-%dT%H:%M:%S",
|
||||
];
|
||||
|
||||
pub fn parse_exif_datetime(s: &str) -> Option<DateTime<Utc>> {
|
||||
NaiveDateTime::parse_from_str(s, "%Y:%m:%d %H:%M:%S")
|
||||
.ok()
|
||||
.map(|ndt| ndt.and_local_timezone(Utc).unwrap())
|
||||
for format in EXIF_DATE_FORMATS {
|
||||
if let Ok(ndt) = NaiveDateTime::parse_from_str(s, format) {
|
||||
return Some(ndt.and_local_timezone(Utc).unwrap());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn extract_exif_data_from_bytes(
|
||||
bytes: &[u8],
|
||||
) -> CoreResult<ExtractedExif> {
|
||||
let ms = MediaSource::seekable(Cursor::new(bytes))
|
||||
.map_err(|e| CoreError::Unknown(format!("Failed to open bytes for EXIF: {}", e)))?;
|
||||
|
||||
let mut parser = MediaParser::new();
|
||||
|
||||
let all_tags = if ms.has_exif() {
|
||||
let iter: ExifIter = match parser.parse(ms) {
|
||||
Ok(iter) => iter,
|
||||
Err(e) => {
|
||||
println!("Could not parse EXIF: {}", e);
|
||||
return Ok(ExtractedExif::default());
|
||||
}
|
||||
};
|
||||
iter.into_iter()
|
||||
.filter_map(|mut x| {
|
||||
let res = x.take_result();
|
||||
match res {
|
||||
Ok(v) => Some((
|
||||
MediaMetadataSource::Exif,
|
||||
x.tag()
|
||||
.map(|t| t.to_string())
|
||||
.unwrap_or_else(|| format!("Unknown(0x{:04x})", x.tag_code())),
|
||||
v.to_string(),
|
||||
)),
|
||||
Err(e) => {
|
||||
println!("!! EXIF parsing error for tag 0x{:04x}: {}", x.tag_code(), e);
|
||||
None
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
} else {
|
||||
match parser.parse::<_, _, TrackInfo>(ms) {
|
||||
Ok(info) => info
|
||||
.into_iter()
|
||||
.map(|x| {
|
||||
(
|
||||
MediaMetadataSource::TrackInfo,
|
||||
x.0.to_string(),
|
||||
x.1.to_string(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
Err(e) => {
|
||||
println!("Could not parse TrackInfo: {}", e);
|
||||
return Ok(ExtractedExif::default());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(ExtractedExif { all_tags })
|
||||
}
|
||||
|
||||
pub async fn extract_exif_data(file_path: &Path) -> CoreResult<ExtractedExif> {
|
||||
@@ -74,4 +140,26 @@ pub async fn extract_exif_data(file_path: &Path) -> CoreResult<ExtractedExif> {
|
||||
};
|
||||
|
||||
Ok(ExtractedExif { all_tags })
|
||||
}
|
||||
|
||||
pub fn get_storage_path_and_date(
|
||||
extracted_data: &ExtractedExif,
|
||||
filename: &str,
|
||||
) -> (PathBuf, Option<DateTime<Utc>>) {
|
||||
let date_taken_str = extracted_data.all_tags.iter()
|
||||
.find(|(source, tag_name, _)| {
|
||||
*source == MediaMetadataSource::Exif &&
|
||||
(tag_name == "DateTimeOriginal" || tag_name == "ModifyDate")
|
||||
})
|
||||
.map(|(_, _, tag_value)| tag_value);
|
||||
|
||||
let date_taken = date_taken_str.and_then(|s| parse_exif_datetime(s));
|
||||
let file_date = date_taken.clone().unwrap_or_else(chrono::Utc::now);
|
||||
|
||||
let year = file_date.year().to_string();
|
||||
let month = format!("{:02}", file_date.month());
|
||||
|
||||
let storage_path = PathBuf::from(&year).join(&month).join(filename);
|
||||
|
||||
(storage_path, date_taken)
|
||||
}
|
||||
@@ -145,3 +145,9 @@ pub struct AlbumShare {
|
||||
pub user_id: uuid::Uuid,
|
||||
pub permission: AlbumPermission,
|
||||
}
|
||||
|
||||
|
||||
pub struct MediaBundle {
|
||||
pub media: Media,
|
||||
pub metadata: Vec<MediaMetadata>,
|
||||
}
|
||||
@@ -3,7 +3,7 @@ use std::sync::Arc;
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::{
|
||||
config::Config, error::CoreResult, models::Media, repositories::{AlbumRepository, MediaMetadataRepository, MediaRepository, UserRepository}
|
||||
config::AppConfig, error::CoreResult, models::Media, repositories::{AlbumRepository, MediaMetadataRepository, MediaRepository, UserRepository}
|
||||
};
|
||||
|
||||
pub struct PluginData {
|
||||
@@ -16,7 +16,7 @@ pub struct PluginContext {
|
||||
pub user_repo: Arc<dyn UserRepository>,
|
||||
pub metadata_repo: Arc<dyn MediaMetadataRepository>,
|
||||
pub media_library_path: String,
|
||||
pub config: Arc<Config>,
|
||||
pub config: Arc<AppConfig>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
|
||||
@@ -57,8 +57,9 @@ pub struct SortParams {
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct FilterParams {
|
||||
// In the future, you can add fields like:
|
||||
// pub mime_type: Option<String>,
|
||||
pub mime_type: Option<String>,
|
||||
pub metadata_filters: Option<Vec<MetadataFilter>>,
|
||||
// In the future, we can add fields like:
|
||||
// pub date_range: Option<(chrono::DateTime<chrono::Utc>, chrono::DateTime<chrono::Utc>)>,
|
||||
}
|
||||
|
||||
@@ -67,4 +68,10 @@ pub struct ListMediaOptions {
|
||||
pub sort: Option<SortParams>,
|
||||
pub filter: Option<FilterParams>,
|
||||
// pub pagination: Option<PaginationParams>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MetadataFilter {
|
||||
pub tag_name: String,
|
||||
pub tag_value: String,
|
||||
}
|
||||
@@ -3,7 +3,7 @@ use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
error::CoreResult,
|
||||
models::{Album, Media, User},
|
||||
models::{Album, Media, MediaBundle, User},
|
||||
schema::{
|
||||
AddMediaToAlbumData, CreateAlbumData, CreateUserData, ListMediaOptions, LoginUserData, ShareAlbumData, UpdateAlbumData, UploadMediaData
|
||||
},
|
||||
@@ -12,7 +12,7 @@ use crate::{
|
||||
#[async_trait]
|
||||
pub trait MediaService: Send + Sync {
|
||||
async fn upload_media(&self, data: UploadMediaData<'_>) -> CoreResult<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>;
|
||||
async fn list_user_media(&self, user_id: Uuid, options: ListMediaOptions) -> CoreResult<Vec<Media>>;
|
||||
async fn get_media_filepath(&self, id: Uuid, user_id: Uuid) -> CoreResult<String>;
|
||||
async fn delete_media(&self, id: Uuid, user_id: Uuid) -> CoreResult<()>;
|
||||
|
||||
Reference in New Issue
Block a user