Fix bug in media upload, add optional detectoin of mime-/file-type
This commit is contained in:
parent
88bfa67f5a
commit
6e5c93d997
|
@ -26,6 +26,10 @@ url = "1"
|
||||||
hyper-old-types = "0.11.0"
|
hyper-old-types = "0.11.0"
|
||||||
futures-util = "0.3.25"
|
futures-util = "0.3.25"
|
||||||
|
|
||||||
|
[dependencies.magic]
|
||||||
|
version = "0.13.0"
|
||||||
|
optional = true
|
||||||
|
|
||||||
[dependencies.uuid]
|
[dependencies.uuid]
|
||||||
version = "1.2.2"
|
version = "1.2.2"
|
||||||
features = ["v4"]
|
features = ["v4"]
|
||||||
|
@ -80,8 +84,9 @@ html2text = "0.4.4"
|
||||||
version = "0.13"
|
version = "0.13"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
all = ["toml", "json", "env"]
|
all = ["toml", "json", "env", "magic"]
|
||||||
default = ["reqwest/default-tls"]
|
# default = ["reqwest/default-tls"]
|
||||||
|
default = ["reqwest/default-tls", "magic"]
|
||||||
env = ["envy"]
|
env = ["envy"]
|
||||||
json = []
|
json = []
|
||||||
rustls-tls = ["reqwest/rustls-tls"]
|
rustls-tls = ["reqwest/rustls-tls"]
|
||||||
|
|
|
@ -30,8 +30,14 @@ async fn main() -> Result<()> {
|
||||||
femme::with_level(femme::LevelFilter::Trace);
|
femme::with_level(femme::LevelFilter::Trace);
|
||||||
let mastodon = register::get_mastodon_data().await?;
|
let mastodon = register::get_mastodon_data().await?;
|
||||||
let input = register::read_line("Enter the path to the photo you'd like to post: ")?;
|
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()
|
let status = StatusBuilder::new()
|
||||||
.status("Mastodon-async photo upload example/demo (automated post)")
|
.status("Mastodon-async photo upload example/demo (automated post)")
|
||||||
.media_ids([media.id])
|
.media_ids([media.id])
|
||||||
|
|
|
@ -3,6 +3,8 @@ use std::{error, fmt, io::Error as IoError, num::TryFromIntError};
|
||||||
#[cfg(feature = "env")]
|
#[cfg(feature = "env")]
|
||||||
use envy::Error as EnvyError;
|
use envy::Error as EnvyError;
|
||||||
use hyper_old_types::Error as HeaderParseError;
|
use hyper_old_types::Error as HeaderParseError;
|
||||||
|
#[cfg(feature = "magic")]
|
||||||
|
use magic::MagicError;
|
||||||
use reqwest::{header::ToStrError as HeaderStrError, Error as HttpError, StatusCode};
|
use reqwest::{header::ToStrError as HeaderStrError, Error as HttpError, StatusCode};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_json::Error as SerdeError;
|
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
|
/// At the time of writing, this can only be triggered when a file is
|
||||||
/// larger than the system's usize allows.
|
/// larger than the system's usize allows.
|
||||||
IntConversion(TryFromIntError),
|
IntConversion(TryFromIntError),
|
||||||
|
#[cfg(feature = "magic")]
|
||||||
|
/// An error received from the magic crate
|
||||||
|
Magic(MagicError),
|
||||||
/// Other errors
|
/// Other errors
|
||||||
Other(String),
|
Other(String),
|
||||||
}
|
}
|
||||||
|
@ -80,30 +85,33 @@ impl fmt::Display for Error {
|
||||||
|
|
||||||
impl error::Error for Error {
|
impl error::Error for Error {
|
||||||
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
|
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
|
||||||
|
use Error::*;
|
||||||
match *self {
|
match *self {
|
||||||
Error::Serde(ref e) => Some(e),
|
Serde(ref e) => Some(e),
|
||||||
Error::UrlEncoded(ref e) => Some(e),
|
UrlEncoded(ref e) => Some(e),
|
||||||
Error::Http(ref e) => Some(e),
|
Http(ref e) => Some(e),
|
||||||
Error::Io(ref e) => Some(e),
|
Io(ref e) => Some(e),
|
||||||
Error::Url(ref e) => Some(e),
|
Url(ref e) => Some(e),
|
||||||
#[cfg(feature = "toml")]
|
#[cfg(feature = "toml")]
|
||||||
Error::TomlSer(ref e) => Some(e),
|
TomlSer(ref e) => Some(e),
|
||||||
#[cfg(feature = "toml")]
|
#[cfg(feature = "toml")]
|
||||||
Error::TomlDe(ref e) => Some(e),
|
TomlDe(ref e) => Some(e),
|
||||||
Error::HeaderStrError(ref e) => Some(e),
|
HeaderStrError(ref e) => Some(e),
|
||||||
Error::HeaderParseError(ref e) => Some(e),
|
HeaderParseError(ref e) => Some(e),
|
||||||
#[cfg(feature = "env")]
|
#[cfg(feature = "env")]
|
||||||
Error::Envy(ref e) => Some(e),
|
Envy(ref e) => Some(e),
|
||||||
Error::SerdeQs(ref e) => Some(e),
|
SerdeQs(ref e) => Some(e),
|
||||||
Error::IntConversion(ref e) => Some(e),
|
IntConversion(ref e) => Some(e),
|
||||||
Error::Api {
|
#[cfg(feature = "magic")]
|
||||||
|
Magic(ref e) => Some(e),
|
||||||
|
Api {
|
||||||
..
|
..
|
||||||
}
|
}
|
||||||
| Error::ClientIdRequired
|
| ClientIdRequired
|
||||||
| Error::ClientSecretRequired
|
| ClientSecretRequired
|
||||||
| Error::AccessTokenRequired
|
| AccessTokenRequired
|
||||||
| Error::MissingField(_)
|
| MissingField(_)
|
||||||
| Error::Other(..) => None,
|
| Other(..) => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -145,14 +153,19 @@ from! {
|
||||||
SerdeError => Serde,
|
SerdeError => Serde,
|
||||||
UrlEncodedError => UrlEncoded,
|
UrlEncodedError => UrlEncoded,
|
||||||
UrlError => Url,
|
UrlError => Url,
|
||||||
#[cfg(feature = "toml")] TomlSerError => TomlSer,
|
#[cfg(feature = "toml")]
|
||||||
#[cfg(feature = "toml")] TomlDeError => TomlDe,
|
TomlSerError => TomlSer,
|
||||||
|
#[cfg(feature = "toml")]
|
||||||
|
TomlDeError => TomlDe,
|
||||||
HeaderStrError => HeaderStrError,
|
HeaderStrError => HeaderStrError,
|
||||||
HeaderParseError => HeaderParseError,
|
HeaderParseError => HeaderParseError,
|
||||||
#[cfg(feature = "env")] EnvyError => Envy,
|
#[cfg(feature = "env")]
|
||||||
|
EnvyError => Envy,
|
||||||
SerdeQsError => SerdeQs,
|
SerdeQsError => SerdeQs,
|
||||||
String => Other,
|
String => Other,
|
||||||
TryFromIntError => IntConversion,
|
TryFromIntError => IntConversion,
|
||||||
|
#[cfg(feature = "magic")]
|
||||||
|
MagicError => Magic,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
|
|
|
@ -163,33 +163,15 @@ macro_rules! route_v2 {
|
||||||
"`, with a description/alt-text.",
|
"`, with a description/alt-text.",
|
||||||
"\n# Errors\nIf `access_token` is not set."),
|
"\n# Errors\nIf `access_token` is not set."),
|
||||||
pub async fn $name(&self $(, $param: $typ)*, description: Option<String>) -> Result<$ret> {
|
pub async fn $name(&self $(, $param: $typ)*, description: Option<String>) -> Result<$ret> {
|
||||||
use reqwest::multipart::{Form, Part};
|
use reqwest::multipart::Form;
|
||||||
use std::io::Read;
|
use log::{debug, as_debug};
|
||||||
use log::{debug, error, as_debug};
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
let call_id = Uuid::new_v4();
|
let call_id = Uuid::new_v4();
|
||||||
|
|
||||||
let form_data = Form::new()
|
let form_data = Form::new()
|
||||||
$(
|
$(
|
||||||
.part(stringify!($param), {
|
.part(stringify!($param), self.get_form_part($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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)*;
|
)*;
|
||||||
|
|
||||||
let form_data = if let Some(description) = description {
|
let form_data = if let Some(description) = description {
|
||||||
|
@ -224,33 +206,16 @@ macro_rules! route_v2 {
|
||||||
$url,
|
$url,
|
||||||
"`\n# Errors\nIf `access_token` is not set."),
|
"`\n# Errors\nIf `access_token` is not set."),
|
||||||
pub async fn $name(&self, $($param: $typ,)*) -> Result<$ret> {
|
pub async fn $name(&self, $($param: $typ,)*) -> Result<$ret> {
|
||||||
use reqwest::multipart::{Form, Part};
|
use reqwest::multipart::Form;
|
||||||
use std::io::Read;
|
use log::{debug, as_debug};
|
||||||
use log::{debug, error, as_debug};
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
|
||||||
let call_id = Uuid::new_v4();
|
let call_id = Uuid::new_v4();
|
||||||
|
|
||||||
let form_data = Form::new()
|
let form_data = Form::new()
|
||||||
$(
|
$(
|
||||||
.part(stringify!($param), {
|
.part(stringify!($param), self.get_form_part($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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)*;
|
)*;
|
||||||
|
|
||||||
let url = &self.route(concat!("/api/v2/", $url));
|
let url = &self.route(concat!("/api/v2/", $url));
|
||||||
|
@ -285,33 +250,16 @@ macro_rules! route {
|
||||||
$url,
|
$url,
|
||||||
"`\n# Errors\nIf `access_token` is not set."),
|
"`\n# Errors\nIf `access_token` is not set."),
|
||||||
pub async fn $name(&self, $($param: $typ,)*) -> Result<$ret> {
|
pub async fn $name(&self, $($param: $typ,)*) -> Result<$ret> {
|
||||||
use reqwest::multipart::{Form, Part};
|
use reqwest::multipart::Form;
|
||||||
use std::io::Read;
|
use log::{debug, as_debug};
|
||||||
use log::{debug, error, as_debug};
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
|
||||||
let call_id = Uuid::new_v4();
|
let call_id = Uuid::new_v4();
|
||||||
|
|
||||||
let form_data = Form::new()
|
let form_data = Form::new()
|
||||||
$(
|
$(
|
||||||
.part(stringify!($param), {
|
.part(stringify!($param), self.get_form_part($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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)*;
|
)*;
|
||||||
|
|
||||||
let url = &self.route(concat!("/api/v1/", $url));
|
let url = &self.route(concat!("/api/v1/", $url));
|
||||||
|
@ -343,33 +291,16 @@ macro_rules! route {
|
||||||
"`, with a description/alt-text.",
|
"`, with a description/alt-text.",
|
||||||
"\n# Errors\nIf `access_token` is not set."),
|
"\n# Errors\nIf `access_token` is not set."),
|
||||||
pub async fn $name(&self $(, $param: $typ)*, description: Option<String>) -> Result<$ret> {
|
pub async fn $name(&self $(, $param: $typ)*, description: Option<String>) -> Result<$ret> {
|
||||||
use reqwest::multipart::{Form, Part};
|
use reqwest::multipart::Form;
|
||||||
use std::io::Read;
|
use log::{debug, as_debug};
|
||||||
use log::{debug, error, as_debug};
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
|
||||||
let call_id = Uuid::new_v4();
|
let call_id = Uuid::new_v4();
|
||||||
|
|
||||||
let form_data = Form::new()
|
let form_data = Form::new()
|
||||||
$(
|
$(
|
||||||
.part(stringify!($param), {
|
.part(stringify!($param), self.get_form_part($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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)*;
|
)*;
|
||||||
|
|
||||||
let form_data = if let Some(description) = description {
|
let form_data = if let Some(description) = description {
|
||||||
|
|
|
@ -23,16 +23,21 @@ use crate::{
|
||||||
};
|
};
|
||||||
use futures::TryStream;
|
use futures::TryStream;
|
||||||
use log::{as_debug, as_serde, debug, error, trace};
|
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 url::Url;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
/// The Mastodon client is a smart pointer to this struct
|
/// The Mastodon client is a smart pointer to this struct
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Debug)]
|
||||||
pub struct MastodonClient {
|
pub struct MastodonClient {
|
||||||
pub(crate) client: Client,
|
pub(crate) client: Client,
|
||||||
/// Raw data about your mastodon instance.
|
/// Raw data about your mastodon instance.
|
||||||
pub data: Data,
|
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.
|
/// Your mastodon application client, handles all requests to and from Mastodon.
|
||||||
|
@ -49,6 +54,18 @@ pub struct MastodonUnauthenticated {
|
||||||
|
|
||||||
impl From<Data> for Mastodon {
|
impl From<Data> for Mastodon {
|
||||||
/// Creates a mastodon instance from the data struct.
|
/// 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 {
|
fn from(data: Data) -> Mastodon {
|
||||||
MastodonClient {
|
MastodonClient {
|
||||||
client: Client::new(),
|
client: Client::new(),
|
||||||
|
@ -156,7 +173,41 @@ impl Mastodon {
|
||||||
stream_direct@"direct",
|
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
|
/// 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 {
|
pub fn new(client: Client, data: Data) -> Self {
|
||||||
Mastodon(Arc::new(MastodonClient {
|
Mastodon(Arc::new(MastodonClient {
|
||||||
client,
|
client,
|
||||||
|
@ -342,6 +393,43 @@ impl Mastodon {
|
||||||
fn authenticated(&self, request: RequestBuilder) -> RequestBuilder {
|
fn authenticated(&self, request: RequestBuilder) -> RequestBuilder {
|
||||||
request.bearer_auth(&self.data.token)
|
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 {
|
impl MastodonUnauthenticated {
|
||||||
|
|
Loading…
Reference in New Issue