use anyhow::{anyhow, Result}; use tracing::{debug, trace}; use upend_base::hash::{b58_encode, UpMultihash}; use upend_db::jobs::{JobContainer, JobState}; use upend_db::stores::UpStore; use std::{ collections::HashMap, fs::File, io::Write, path::{Path, PathBuf}, sync::{Arc, Mutex}, }; use self::audio::AudioPath; use self::image::ImagePath; use self::text::TextPath; use self::video::VideoPath; pub mod audio; pub mod image; pub mod text; pub mod video; pub trait Previewable { fn get_thumbnail(&self, options: HashMap) -> Result>>; } type HashWithOptions = (UpMultihash, String); pub struct PreviewStore { path: PathBuf, store: Arc>, locks: Mutex>>>, } #[cfg(feature = "previews")] impl PreviewStore { pub fn new>(path: P, store: Arc>) -> Self { PreviewStore { path: PathBuf::from(path.as_ref()), store, locks: Mutex::new(HashMap::new()), } } fn get_path( &self, hash: &UpMultihash, options: &HashMap, ) -> Arc> { let mut locks = self.locks.lock().unwrap(); let mut options_strs = options .iter() .map(|(k, v)| format!("{k}{v}")) .collect::>(); options_strs.sort(); let options_concat = options_strs.concat(); if let Some(path) = locks.get(&(hash.clone(), options_concat.clone())) { path.clone() } else { let thumbpath = self.path.join(format!( "{}{}", b58_encode(hash.to_bytes()), if options_concat.is_empty() { String::from("") } else { format!("_{options_concat}") } )); let path = Arc::new(Mutex::new(thumbpath)); locks.insert((hash.clone(), options_concat), path.clone()); path } } pub fn get( &self, hash: UpMultihash, options: HashMap, mut job_container: JobContainer, ) -> Result> { debug!("Preview for {hash} requested..."); let path_mutex = self.get_path(&hash, &options); let thumbpath = path_mutex.lock().unwrap(); if thumbpath.exists() { trace!("Preview for {hash:?} already exists, returning {thumbpath:?}"); Ok(Some(thumbpath.clone())) } else { trace!("Calculating preview for {hash:?}..."); let files = self.store.retrieve(&hash)?; if let Some(file) = files.first() { let file_path = file.get_file_path(); let mut job_handle = job_container.add_job( None, &format!("Creating preview for {:?}", file_path.file_name().unwrap()), )?; let mime_type = options.get("mime").map(|x| x.to_owned()); let mime_type: Option = if mime_type.is_some() { mime_type } else { tree_magic_mini::from_filepath(file_path).map(|m| m.into()) }; let preview = match mime_type { Some(tm) if tm.starts_with("text") => { TextPath(file_path).get_thumbnail(options) } Some(tm) if tm.starts_with("video") || tm == "application/x-matroska" => { VideoPath(file_path).get_thumbnail(options) } Some(tm) if tm.starts_with("audio") || tm == "application/x-riff" => { AudioPath(file_path).get_thumbnail(options) } Some(tm) if tm.starts_with("image") => { ImagePath(file_path).get_thumbnail(options) } Some(unknown) => Err(anyhow!("No capability for {:?} thumbnails.", unknown)), _ => Err(anyhow!("Unknown file type, or file doesn't exist.")), }; match preview { Ok(preview) => { trace!("Got preview for {hash:?}."); let _ = job_handle.update_state(JobState::Done); if let Some(data) = preview { std::fs::create_dir_all(&self.path)?; let mut file = File::create(&*thumbpath)?; file.write_all(&data)?; Ok(Some(thumbpath.clone())) } else { Ok(None) } } Err(err) => Err(err), } } else { Err(anyhow!("Object not found, or is not a file.")) } } } }