use std::fmt; use std::io::Read; use std::path::{Path, PathBuf}; #[derive(Debug)] /// A builder pattern struct for preparing a single attachment for upload. /// /// For more details, see [`new_media()`](struct.Mastodon.html#method.new_media). pub struct MediaBuilder { /// The media attachment itself pub data: MediaBuilderData, /// The filename to send to the server pub filename: Option, /// Mimetype to send to the server, identifying what is in the attachment. /// /// The string should be a valid mimetype. pub mimetype: Option, /// Plain text description of the attached piece of media, for accessibility pub description: Option, /// (x, y) focus point, used by clients to determine how to crop an image pub focus: Option<(f64, f64)>, } /// Enum representing possible sources of attachments to upload pub enum MediaBuilderData { /// An arbitrary reader. It is useful for reading from media already in memory. Reader(Box), /// Variant represening a file path of the file to attach. File(PathBuf), } impl fmt::Debug for MediaBuilderData { fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { match self { MediaBuilderData::File(f) => fmt.debug_tuple("File").field(&f).finish(), MediaBuilderData::Reader(_) => fmt.debug_tuple("Reader").field(&format_args!("...")).finish(), } } } impl MediaBuilder { /// Create a new MediaBuilder from a reader `data` pub fn from_reader(data: R) -> MediaBuilder { MediaBuilder { data: MediaBuilderData::Reader(Box::from(data)), filename: None, mimetype: None, description: None, focus: None, } } /// Create a new MediaBuilder from a file under `path` /// /// This function will not check whether the file exists or if it can be read. If the path is /// not valid, [`add_media()`](trait.MastodonClient.html#method.add_media) will return an error when called with the `MediaBuilder`. pub fn from_file(path: impl AsRef) -> MediaBuilder { let pb = path.as_ref().to_owned(); let filename = pb.file_name().expect("file name").to_string_lossy().to_string(); let mimetype = match pb.extension().map(|s| s.to_str()).flatten() { Some("jpg") | Some("jpeg") => Some("image/jpeg".to_string()), Some("png") => Some("image/png".to_string()), Some("gif") => Some("image/gif".to_string()), Some("txt") => Some("text/plain".to_string()), // ... _ => None, }; MediaBuilder { data: MediaBuilderData::File(pb), filename: Some(filename), mimetype, description: None, focus: None, } } /// Set filename pub fn filename(&mut self, filename: impl ToString) { self.filename = Some(filename.to_string()); } /// Set custom mime type pub fn mimetype(&mut self, mimetype: impl ToString) { self.mimetype = Some(mimetype.to_string()); } /// Set an alt text description for the attachment. pub fn description(&mut self, description: impl ToString) { self.description = Some(description.to_string()); } /// Set a focus point for an image attachment. pub fn focus(&mut self, f1: f64, f2: f64) { self.focus = Some((f1, f2)); } } #[cfg(test)] mod tests { use super::*; use std::io::Cursor; #[test] fn test_from_reader() { let source = vec![0u8, 1, 2]; let builder = MediaBuilder::from_reader(Cursor::new(source.clone())); assert_eq!(builder.filename, None); assert_eq!(builder.mimetype, None); assert_eq!(builder.description, None); assert_eq!(builder.focus, None); if let MediaBuilderData::Reader(r) = builder.data { assert_eq!(r.bytes().map(|b| b.unwrap()).collect::>(), source); } else { panic!("Unable to destructure MediaBuilder.data into a reader"); } } #[test] fn test_from_file() { let builder = MediaBuilder::from_file("/fake/file/path.png".into()); assert_eq!(builder.filename, None); assert_eq!(builder.mimetype, None); assert_eq!(builder.description, None); assert_eq!(builder.focus, None); if let MediaBuilderData::File(f) = builder.data { assert_eq!(f, "/fake/file/path.png"); } else { panic!("Unable to destructure MediaBuilder.data into a filepath"); } } }