summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorD. Scott Boggs <scott@tams.tech>2022-12-26 11:19:54 -0500
committerD. Scott Boggs <scott@tams.tech>2022-12-26 11:19:54 -0500
commit6e5c93d997f61635e11c9ad4afce710cf0bc4a5d (patch)
tree2cdcceed9168b3510ff57e1e32548e44742b1569
parent88bfa67f5ae78437f64076d259f282c55d6ae1dc (diff)
Fix bug in media upload, add optional detectoin of mime-/file-typefix/media-upload
-rw-r--r--Cargo.toml9
-rw-r--r--examples/upload_photo.rs8
-rw-r--r--src/errors.rs55
-rw-r--r--src/macros.rs99
-rw-r--r--src/mastodon.rs92
5 files changed, 153 insertions, 110 deletions
diff --git a/Cargo.toml b/Cargo.toml
index 8449762..a272cf6 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -26,6 +26,10 @@ url = "1"
hyper-old-types = "0.11.0"
futures-util = "0.3.25"
+[dependencies.magic]
+version = "0.13.0"
+optional = true
+
[dependencies.uuid]
version = "1.2.2"
features = ["v4"]
@@ -80,8 +84,9 @@ html2text = "0.4.4"
version = "0.13"
[features]
-all = ["toml", "json", "env"]
-default = ["reqwest/default-tls"]
+all = ["toml", "json", "env", "magic"]
+# default = ["reqwest/default-tls"]
+default = ["reqwest/default-tls", "magic"]
env = ["envy"]
json = []
rustls-tls = ["reqwest/rustls-tls"]
diff --git a/examples/upload_photo.rs b/examples/upload_photo.rs
index b7806ef..62c9904 100644
--- a/examples/upload_photo.rs
+++ b/examples/upload_photo.rs
@@ -30,8 +30,14 @@ async fn main() -> Result<()> {
femme::with_level(femme::LevelFilter::Trace);
let mastodon = register::get_mastodon_data().await?;
let input = register::read_line("Enter the path to the photo you'd like to post: ")?;
+ let description = register::read_line("describe the media? ")?;
+ let description = if description.trim().is_empty() {
+ None
+ } else {
+ Some(description)
+ };
- let media = mastodon.media(input).await?;
+ let media = mastodon.media(input, description).await?;
let status = StatusBuilder::new()
.status("Mastodon-async photo upload example/demo (automated post)")
.media_ids([media.id])
diff --git a/src/errors.rs b/src/errors.rs
index f04e545..eb547ba 100644
--- a/src/errors.rs
+++ b/src/errors.rs
@@ -3,6 +3,8 @@ use std::{error, fmt, io::Error as IoError, num::TryFromIntError};
#[cfg(feature = "env")]
use envy::Error as EnvyError;
use hyper_old_types::Error as HeaderParseError;
+#[cfg(feature = "magic")]
+use magic::MagicError;
use reqwest::{header::ToStrError as HeaderStrError, Error as HttpError, StatusCode};
use serde::Deserialize;
use serde_json::Error as SerdeError;
@@ -68,6 +70,9 @@ pub enum Error {
/// At the time of writing, this can only be triggered when a file is
/// larger than the system's usize allows.
IntConversion(TryFromIntError),
+ #[cfg(feature = "magic")]
+ /// An error received from the magic crate
+ Magic(MagicError),
/// Other errors
Other(String),
}
@@ -80,30 +85,33 @@ impl fmt::Display for Error {
impl error::Error for Error {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
+ use Error::*;
match *self {
- Error::Serde(ref e) => Some(e),
- Error::UrlEncoded(ref e) => Some(e),
- Error::Http(ref e) => Some(e),
- Error::Io(ref e) => Some(e),
- Error::Url(ref e) => Some(e),
+ Serde(ref e) => Some(e),
+ UrlEncoded(ref e) => Some(e),
+ Http(ref e) => Some(e),
+ Io(ref e) => Some(e),
+ Url(ref e) => Some(e),
#[cfg(feature = "toml")]
- Error::TomlSer(ref e) => Some(e),
+ TomlSer(ref e) => Some(e),
#[cfg(feature = "toml")]
- Error::TomlDe(ref e) => Some(e),
- Error::HeaderStrError(ref e) => Some(e),
- Error::HeaderParseError(ref e) => Some(e),
+ TomlDe(ref e) => Some(e),
+ HeaderStrError(ref e) => Some(e),
+ HeaderParseError(ref e) => Some(e),
#[cfg(feature = "env")]
- Error::Envy(ref e) => Some(e),
- Error::SerdeQs(ref e) => Some(e),
- Error::IntConversion(ref e) => Some(e),
- Error::Api {
+ Envy(ref e) => Some(e),
+ SerdeQs(ref e) => Some(e),
+ IntConversion(ref e) => Some(e),
+ #[cfg(feature = "magic")]
+ Magic(ref e) => Some(e),
+ Api {
..
}
- | Error::ClientIdRequired
- | Error::ClientSecretRequired
- | Error::AccessTokenRequired
- | Error::MissingField(_)
- | Error::Other(..) => None,
+ | ClientIdRequired
+ | ClientSecretRequired
+ | AccessTokenRequired
+ | MissingField(_)
+ | Other(..) => None,
}
}
}
@@ -145,14 +153,19 @@ from! {
SerdeError => Serde,
UrlEncodedError => UrlEncoded,
UrlError => Url,
- #[cfg(feature = "toml")] TomlSerError => TomlSer,
- #[cfg(feature = "toml")] TomlDeError => TomlDe,
+ #[cfg(feature = "toml")]
+ TomlSerError => TomlSer,
+ #[cfg(feature = "toml")]
+ TomlDeError => TomlDe,
HeaderStrError => HeaderStrError,
HeaderParseError => HeaderParseError,
- #[cfg(feature = "env")] EnvyError => Envy,
+ #[cfg(feature = "env")]
+ EnvyError => Envy,
SerdeQsError => SerdeQs,
String => Other,
TryFromIntError => IntConversion,
+ #[cfg(feature = "magic")]
+ MagicError => Magic,
}
#[macro_export]
diff --git a/src/macros.rs b/src/macros.rs
index 00d1cfc..4b8eee3 100644
--- a/src/macros.rs
+++ b/src/macros.rs
@@ -163,33 +163,15 @@ macro_rules! route_v2 {
"`, with a description/alt-text.",
"\n# Errors\nIf `access_token` is not set."),
pub async fn $name(&self $(, $param: $typ)*, description: Option<String>) -> Result<$ret> {
- use reqwest::multipart::{Form, Part};
- use std::io::Read;
- use log::{debug, error, as_debug};
+ use reqwest::multipart::Form;
+ use log::{debug, as_debug};
use uuid::Uuid;
let call_id = Uuid::new_v4();
let form_data = Form::new()
$(
- .part(stringify!($param), {
- let path = $param.as_ref();
- match std::fs::File::open(path) {
- Ok(mut file) => {
- let mut data = if let Ok(metadata) = file.metadata() {
- Vec::with_capacity(metadata.len().try_into()?)
- } else {
- vec![]
- };
- file.read_to_end(&mut data)?;
- Part::bytes(data)
- }
- Err(err) => {
- error!(path = as_debug!(path), error = as_debug!(err); "error reading file contents for multipart form");
- return Err(err.into());
- }
- }
- })
+ .part(stringify!($param), self.get_form_part($param)?)
)*;
let form_data = if let Some(description) = description {
@@ -224,33 +206,16 @@ macro_rules! route_v2 {
$url,
"`\n# Errors\nIf `access_token` is not set."),
pub async fn $name(&self, $($param: $typ,)*) -> Result<$ret> {
- use reqwest::multipart::{Form, Part};
- use std::io::Read;
- use log::{debug, error, as_debug};
+ use reqwest::multipart::Form;
+ use log::{debug, as_debug};
use uuid::Uuid;
+
let call_id = Uuid::new_v4();
let form_data = Form::new()
$(
- .part(stringify!($param), {
- let path = $param.as_ref();
- match std::fs::File::open(path) {
- Ok(mut file) => {
- let mut data = if let Ok(metadata) = file.metadata() {
- Vec::with_capacity(metadata.len().try_into()?)
- } else {
- vec![]
- };
- file.read_to_end(&mut data)?;
- Part::bytes(data)
- }
- Err(err) => {
- error!(path = as_debug!(path), error = as_debug!(err); "error reading file contents for multipart form");
- return Err(err.into());
- }
- }
- })
+ .part(stringify!($param), self.get_form_part($param)?)
)*;
let url = &self.route(concat!("/api/v2/", $url));
@@ -285,33 +250,16 @@ macro_rules! route {
$url,
"`\n# Errors\nIf `access_token` is not set."),
pub async fn $name(&self, $($param: $typ,)*) -> Result<$ret> {
- use reqwest::multipart::{Form, Part};
- use std::io::Read;
- use log::{debug, error, as_debug};
+ use reqwest::multipart::Form;
+ use log::{debug, as_debug};
use uuid::Uuid;
+
let call_id = Uuid::new_v4();
let form_data = Form::new()
$(
- .part(stringify!($param), {
- let path = $param.as_ref();
- match std::fs::File::open(path) {
- Ok(mut file) => {
- let mut data = if let Ok(metadata) = file.metadata() {
- Vec::with_capacity(metadata.len().try_into()?)
- } else {
- vec![]
- };
- file.read_to_end(&mut data)?;
- Part::bytes(data)
- }
- Err(err) => {
- error!(path = as_debug!(path), error = as_debug!(err); "error reading file contents for multipart form");
- return Err(err.into());
- }
- }
- })
+ .part(stringify!($param), self.get_form_part($param)?)
)*;
let url = &self.route(concat!("/api/v1/", $url));
@@ -343,33 +291,16 @@ macro_rules! route {
"`, with a description/alt-text.",
"\n# Errors\nIf `access_token` is not set."),
pub async fn $name(&self $(, $param: $typ)*, description: Option<String>) -> Result<$ret> {
- use reqwest::multipart::{Form, Part};
- use std::io::Read;
- use log::{debug, error, as_debug};
+ use reqwest::multipart::Form;
+ use log::{debug, as_debug};
use uuid::Uuid;
+
let call_id = Uuid::new_v4();
let form_data = Form::new()
$(
- .part(stringify!($param), {
- let path = $param.as_ref();
- match std::fs::File::open(path) {
- Ok(mut file) => {
- let mut data = if let Ok(metadata) = file.metadata() {
- Vec::with_capacity(metadata.len().try_into()?)
- } else {
- vec![]
- };
- file.read_to_end(&mut data)?;
- Part::bytes(data)
- }
- Err(err) => {
- error!(path = as_debug!(path), error = as_debug!(err); "error reading file contents for multipart form");
- return Err(err.into());
- }
- }
- })
+ .part(stringify!($param), self.get_form_part($param)?)
)*;
let form_data = if let Some(description) = description {
diff --git a/src/mastodon.rs b/src/mastodon.rs
index dfe1aaa..dc585e2 100644
--- a/src/mastodon.rs
+++ b/src/mastodon.rs
@@ -23,16 +23,21 @@ use crate::{
};
use futures::TryStream;
use log::{as_debug, as_serde, debug, error, trace};
-use reqwest::{Client, RequestBuilder};
+#[cfg(feature = "magic")]
+use magic::CookieFlags;
+use reqwest::{multipart::Part, Client, RequestBuilder};
use url::Url;
use uuid::Uuid;
/// The Mastodon client is a smart pointer to this struct
-#[derive(Clone, Debug)]
+#[derive(Debug)]
pub struct MastodonClient {
pub(crate) client: Client,
/// Raw data about your mastodon instance.
pub data: Data,
+ /// A handle to access libmagic for mime-types.
+ #[cfg(feature = "magic")]
+ magic: magic::Cookie,
}
/// Your mastodon application client, handles all requests to and from Mastodon.
@@ -49,6 +54,18 @@ pub struct MastodonUnauthenticated {
impl From<Data> for Mastodon {
/// Creates a mastodon instance from the data struct.
+ #[cfg(feature = "magic")]
+ fn from(data: Data) -> Mastodon {
+ MastodonClient {
+ client: Client::new(),
+ data,
+ magic: Self::default_magic().expect("failed to open magic cookie or load database"),
+ }
+ .into()
+ }
+
+ /// Creates a mastodon instance from the data struct.
+ #[cfg(not(feature = "magic"))]
fn from(data: Data) -> Mastodon {
MastodonClient {
client: Client::new(),
@@ -156,7 +173,41 @@ impl Mastodon {
stream_direct@"direct",
}
+ /// Return a magic cookie, loaded with the default mime
+ #[cfg(feature = "magic")]
+ fn default_magic() -> Result<magic::Cookie> {
+ let magic = magic::Cookie::open(Default::default())?;
+ magic.load::<&str>(&[])?;
+ magic.set_flags(CookieFlags::MIME)?;
+ Ok(magic)
+ }
+
+ /// Create a new Mastodon Client
+ #[cfg(feature = "magic")]
+ pub fn new(client: Client, data: Data) -> Self {
+ Self::new_with_magic(
+ client,
+ data,
+ Self::default_magic().expect("failed to open magic cookie or load database"),
+ )
+ }
+
+ /// Create a new Mastodon Client, passing in a pre-constructed magic
+ /// cookie.
+ ///
+ /// This is mainly here so you have a wait to construct the client which
+ /// won't panic.
+ #[cfg(feature = "magic")]
+ pub fn new_with_magic(client: Client, data: Data, magic: magic::Cookie) -> Self {
+ Mastodon(Arc::new(MastodonClient {
+ client,
+ data,
+ magic,
+ }))
+ }
+
/// Create a new Mastodon Client
+ #[cfg(not(feature = "magic"))]
pub fn new(client: Client, data: Data) -> Self {
Mastodon(Arc::new(MastodonClient {
client,
@@ -342,6 +393,43 @@ impl Mastodon {
fn authenticated(&self, request: RequestBuilder) -> RequestBuilder {
request.bearer_auth(&self.data.token)
}
+
+ /// Return a part for a multipart form submission from a file, including
+ /// the name of the file, and, if the "magic" feature is enabled, the mime-
+ /// type.
+ fn get_form_part(&self, path: impl AsRef<Path>) -> Result<Part> {
+ use std::io::Read;
+
+ let path = path.as_ref();
+ let mime = if cfg!(feature = "magic") {
+ self.magic.file(path).ok()
+ // if it doesn't work, it's no big deal. The server will look at
+ // the filepath if this isn't here and things should still work.
+ } else {
+ None
+ };
+ match std::fs::File::open(path) {
+ Ok(mut file) => {
+ let mut data = if let Ok(metadata) = file.metadata() {
+ Vec::with_capacity(metadata.len().try_into()?)
+ } else {
+ vec![]
+ };
+ file.read_to_end(&mut data)?;
+ let part =
+ Part::bytes(data).file_name(Cow::Owned(path.to_string_lossy().to_string()));
+ Ok(if let Some(mime) = mime {
+ part.mime_str(&mime)?
+ } else {
+ part
+ })
+ },
+ Err(err) => {
+ error!(path = as_debug!(path), error = as_debug!(err); "error reading file contents for multipart form");
+ return Err(err.into());
+ },
+ }
+ }
}
impl MastodonUnauthenticated {