use std::sync::Arc; use super::Extractor; use crate::{ addressing::Address, database::{ constants, entry::{Entry, EntryValue}, stores::{fs::FILE_MIME_KEY, UpStore}, UpEndConnection, }, util::jobs::{JobContainer, JobState}, }; use anyhow::{anyhow, Result}; pub struct ExifExtractor; // TODO: EXIF metadata is oftentimes a constant/enum value. What's the proper // model for enum-like values in UpEnd? impl Extractor for ExifExtractor { fn get( &self, address: &Address, _connection: &UpEndConnection, store: Arc>, mut job_container: JobContainer, ) -> Result> { if let Address::Hash(hash) = address { let files = store.retrieve(hash)?; if let Some(file) = files.get(0) { let file_path = file.get_file_path(); let mut job_handle = job_container.add_job( None, &format!( r#"Getting EXIF info from "{:}""#, file_path .components() .last() .unwrap() .as_os_str() .to_string_lossy() ), )?; let file = std::fs::File::open(file_path)?; let mut bufreader = std::io::BufReader::new(&file); let exifreader = exif::Reader::new(); let exif = exifreader.read_from_container(&mut bufreader)?; let result: Vec = exif .fields() .filter(|field| !matches!(field.value, exif::Value::Undefined(..))) .flat_map(|field| { if let Some(tag_description) = field.tag.description() { let attribute = format!("EXIF_{}", field.tag.1); vec![ Entry { entity: address.clone(), attribute: attribute.clone(), value: match field.tag { exif::Tag::ExifVersion => { EntryValue::String(format!("{}", field.display_value())) } _ => EntryValue::guess_from(format!( "{}", field.display_value() )), }, provenance: "SYSTEM EXTRACTOR".to_string(), timestamp: chrono::Utc::now().naive_utc(), }, Entry { entity: Address::Attribute(attribute), attribute: constants::LABEL_ATTR.into(), value: format!("EXIF: {}", tag_description).into(), provenance: "SYSTEM EXTRACTOR".to_string(), timestamp: chrono::Utc::now().naive_utc(), }, ] } else { vec![] } }) .collect(); let _ = job_handle.update_state(JobState::Done); Ok(result) } else { Err(anyhow!("Couldn't find file for {hash:?}!")) } } else { Ok(vec![]) } } fn is_needed(&self, address: &Address, connection: &UpEndConnection) -> Result { let is_photo = connection.retrieve_object(address)?.iter().any(|e| { if e.attribute == FILE_MIME_KEY { if let EntryValue::String(mime) = &e.value { return mime.starts_with("image"); } } false }); if !is_photo { return Ok(false); } let is_extracted = !connection .query(format!("(matches @{} (contains \"EXIF\") ?)", address).parse()?)? .is_empty(); if is_extracted { return Ok(false); } Ok(true) } }