@@ -6,14 +6,19 @@ use serde::ser::{Serialize, SerializeSeq, Serializer};
66use tokio:: fs:: File ;
77use tokio:: io:: AsyncReadExt ;
88
9- #[ cfg( doc) ]
10- use crate :: error:: Error ;
11- use crate :: error:: Result ;
9+ use crate :: error:: { Error , Result , UrlError } ;
1210#[ cfg( feature = "http" ) ]
1311use crate :: http:: Http ;
1412use crate :: model:: channel:: Message ;
1513use crate :: model:: id:: AttachmentId ;
1614
15+ #[ derive( Clone , Debug ) ]
16+ pub enum AttachmentData < ' a > {
17+ Bytes ( Bytes ) ,
18+ File ( & ' a File ) ,
19+ Path ( & ' a Path ) ,
20+ }
21+
1722/// A builder for creating a new attachment from a file path, file data, or URL.
1823///
1924/// [Discord docs](https://discord.com/developers/docs/resources/channel#attachment-object-attachment-structure).
@@ -23,50 +28,44 @@ use crate::model::id::AttachmentId;
2328pub struct CreateAttachment < ' a > {
2429 pub filename : Cow < ' static , str > ,
2530 pub description : Option < Cow < ' a , str > > ,
26- pub data : Bytes ,
31+ pub data : AttachmentData < ' a > ,
2732}
2833
2934impl < ' a > CreateAttachment < ' a > {
3035 /// Builds an [`CreateAttachment`] from the raw attachment data.
3136 pub fn bytes ( data : impl Into < Bytes > , filename : impl Into < Cow < ' static , str > > ) -> Self {
3237 CreateAttachment {
33- data : data. into ( ) ,
3438 filename : filename. into ( ) ,
3539 description : None ,
40+ data : AttachmentData :: Bytes ( data. into ( ) ) ,
3641 }
3742 }
3843
3944 /// Builds an [`CreateAttachment`] by reading a local file.
4045 ///
4146 /// # Errors
4247 ///
43- /// [`Error::Io`] if reading the file fails.
44- pub async fn path ( path : impl AsRef < Path > ) -> Result < Self > {
45- async fn inner ( path : & Path ) -> Result < CreateAttachment < ' static > > {
46- let mut file = File :: open ( path) . await ?;
47- let mut data = Vec :: new ( ) ;
48- file. read_to_end ( & mut data) . await ?;
49-
50- let filename = path
51- . file_name ( )
52- . ok_or_else ( || std:: io:: Error :: other ( "attachment path must not be a directory" ) ) ?;
53-
54- Ok ( CreateAttachment :: bytes ( data, filename. to_string_lossy ( ) . into_owned ( ) ) )
55- }
56-
57- inner ( path. as_ref ( ) ) . await
48+ /// Returns [`Error::Io`] if the path is not a valid file path.
49+ pub fn path ( path : & ' a Path ) -> Result < Self > {
50+ let filename = path
51+ . file_name ( )
52+ . ok_or_else ( || std:: io:: Error :: other ( "attachment path must not be a directory" ) ) ?
53+ . to_string_lossy ( )
54+ . into_owned ( ) ;
55+ Ok ( CreateAttachment {
56+ filename : filename. into ( ) ,
57+ description : None ,
58+ data : AttachmentData :: Path ( path) ,
59+ } )
5860 }
5961
6062 /// Builds an [`CreateAttachment`] by reading from a file handler.
61- ///
62- /// # Errors
63- ///
64- /// [`Error::Io`] error if reading the file fails.
65- pub async fn file ( file : & File , filename : impl Into < Cow < ' static , str > > ) -> Result < Self > {
66- let mut data = Vec :: new ( ) ;
67- file. try_clone ( ) . await ?. read_to_end ( & mut data) . await ?;
68-
69- Ok ( CreateAttachment :: bytes ( data, filename) )
63+ pub fn file ( file : & ' a File , filename : impl Into < Cow < ' static , str > > ) -> Self {
64+ CreateAttachment {
65+ filename : filename. into ( ) ,
66+ description : None ,
67+ data : AttachmentData :: File ( file) ,
68+ }
7069 }
7170
7271 /// Builds an [`CreateAttachment`] by downloading attachment data from a URL.
@@ -86,25 +85,51 @@ impl<'a> CreateAttachment<'a> {
8685 Ok ( CreateAttachment :: bytes ( data, filename) )
8786 }
8887
89- /// Converts the stored data to the base64 representation .
88+ /// Returns the underlying data for the attachment .
9089 ///
91- /// This is used in the library internally because Discord expects image data as base64 in many
92- /// places.
93- #[ must_use]
94- pub fn to_base64 ( & self ) -> String {
90+ /// # Errors
91+ ///
92+ /// Returns an error if fetching the data failed in some way. If the attachment is a
93+ /// [`CreateAttachment::path`], then the file at the specified path was unable to be read. If
94+ /// instead it's [`CreateAttachment::file`], then cloning the handle to the file failed, likely
95+ /// due to hitting the system's limit on number of open file handles.
96+ pub async fn get_data ( & self ) -> Result < Bytes > {
97+ match & self . data {
98+ AttachmentData :: Bytes ( bytes) => Ok ( bytes. clone ( ) ) ,
99+ AttachmentData :: Path ( path) => {
100+ let mut file = File :: open ( path) . await ?;
101+ let mut data = Vec :: new ( ) ;
102+ file. read_to_end ( & mut data) . await ?;
103+ Ok ( data. into ( ) )
104+ } ,
105+ AttachmentData :: File ( file) => {
106+ let mut data = Vec :: new ( ) ;
107+ file. try_clone ( ) . await ?. read_to_end ( & mut data) . await ?;
108+ Ok ( data. into ( ) )
109+ } ,
110+ }
111+ }
112+
113+ /// Converts the attachment data to a base64-encoded data URI.
114+ ///
115+ /// # Errors
116+ ///
117+ /// See [`CreateAttachment::get_data`] for details.
118+ pub async fn encode ( & self ) -> Result < ImageData > {
95119 use base64:: engine:: { Config , Engine } ;
96120
97121 const PREFIX : & str = "data:image/png;base64," ;
122+ let data = self . get_data ( ) . await ?;
98123
99124 let engine = base64:: prelude:: BASE64_STANDARD ;
100- let encoded_size = base64:: encoded_len ( self . data . len ( ) , engine. config ( ) . encode_padding ( ) )
125+ let encoded_size = base64:: encoded_len ( data. len ( ) , engine. config ( ) . encode_padding ( ) )
101126 . and_then ( |len| len. checked_add ( PREFIX . len ( ) ) )
102127 . expect ( "buffer capacity overflow" ) ;
103128
104129 let mut encoded = String :: with_capacity ( encoded_size) ;
105130 encoded. push_str ( PREFIX ) ;
106- engine. encode_string ( & self . data , & mut encoded) ;
107- encoded
131+ engine. encode_string ( & data, & mut encoded) ;
132+ Ok ( ImageData ( encoded. into ( ) ) )
108133 }
109134
110135 /// Sets a description for the file (max 1024 characters).
@@ -114,6 +139,40 @@ impl<'a> CreateAttachment<'a> {
114139 }
115140}
116141
142+ /// A wrapper around some base64-encoded image data. Used when an endpoint expects the image
143+ /// payload directly as part of the JSON body, instead of as a multipart upload.
144+ #[ derive( Clone , Debug , Serialize ) ]
145+ #[ serde( transparent) ]
146+ pub struct ImageData < ' a > ( Cow < ' a , str > ) ;
147+
148+ impl < ' a > ImageData < ' a > {
149+ /// Constructs image data from a base64-encoded blob of data. The string must be a valid data
150+ /// URI, for example:
151+ ///
152+ /// ```
153+ /// use serenity::builder::ImageData;
154+ ///
155+ /// let s = "data:image/png;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=";
156+ /// assert!(ImageData::from_base64(s).is_ok());
157+ /// ```
158+ ///
159+ /// # Errors
160+ ///
161+ /// Returns a [`Error::Url`] if the string is not a valid data URI. See the [Discord
162+ /// docs](https://discord.com/developers/docs/reference#image-data).
163+ pub fn from_base64 ( s : impl Into < Cow < ' a , str > > ) -> Result < Self > {
164+ let s = s. into ( ) ;
165+ if let Some ( ( "data" , tail) ) = s. split_once ( ':' ) {
166+ if let Some ( ( mimetype, encoding) ) = tail. split_once ( ';' ) {
167+ if mimetype. split_once ( '/' ) . is_some ( ) && encoding. starts_with ( "base64," ) {
168+ return Ok ( Self ( s) ) ;
169+ }
170+ }
171+ }
172+ Err ( Error :: Url ( UrlError :: InvalidDataURI ) )
173+ }
174+ }
175+
117176#[ derive( Clone , Debug , Serialize ) ]
118177struct ExistingAttachment {
119178 id : AttachmentId ,
@@ -235,10 +294,9 @@ impl<'a> EditAttachments<'a> {
235294 ///
236295 /// Opposite of [`Self::keep`].
237296 pub fn remove ( mut self , id : AttachmentId ) -> Self {
238- #[ expect( clippy:: match_like_matches_macro) ] // `matches!` is less clear here
239297 self . new_and_existing_attachments . retain ( |a| match a {
240- NewOrExisting :: Existing ( a) if a. id == id => false ,
241- _ => true ,
298+ NewOrExisting :: Existing ( a) => a. id != id,
299+ NewOrExisting :: New ( _ ) => true ,
242300 } ) ;
243301 self
244302 }
@@ -251,22 +309,19 @@ impl<'a> EditAttachments<'a> {
251309 }
252310
253311 /// Clones all new attachments into a new Vec, keeping only data and filename, because those
254- /// are needed for the multipart form data. The data is taken out of `self` in the process, so
255- /// this method can only be called once.
312+ /// are needed for the multipart form data.
256313 #[ cfg( feature = "http" ) ]
257- pub ( crate ) fn take_files ( & mut self ) -> Vec < CreateAttachment < ' a > > {
258- let mut files = Vec :: new ( ) ;
259- for attachment in & mut self . new_and_existing_attachments {
260- if let NewOrExisting :: New ( attachment) = attachment {
261- let cloned_attachment = CreateAttachment :: bytes (
262- std:: mem:: take ( & mut attachment. data ) ,
263- attachment. filename . clone ( ) ,
264- ) ;
265-
266- files. push ( cloned_attachment) ;
267- }
268- }
269- files
314+ pub ( crate ) fn new_attachments ( & self ) -> Vec < CreateAttachment < ' a > > {
315+ self . new_and_existing_attachments
316+ . iter ( )
317+ . filter_map ( |attachment| {
318+ if let NewOrExisting :: New ( attachment) = & attachment {
319+ Some ( attachment. clone ( ) )
320+ } else {
321+ None
322+ }
323+ } )
324+ . collect ( )
270325 }
271326}
272327
0 commit comments