imgbb/lib.rs
1// Constants for API endpoints and configuration
2const IMGBB_API_URL: &str = "https://api.imgbb.com/1/upload";
3static APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
4
5use base64::engine::{general_purpose, Engine};
6use std::path::Path;
7use std::time::Duration;
8
9/// Module for ImgBB API error
10pub mod error;
11pub use error::Error;
12
13/// Module for ImgBB API response model
14pub mod model;
15use model::*;
16
17/// Module for ImgBB uploader
18pub mod uploader;
19use uploader::*;
20
21/// Main client for interacting with the ImgBB API
22///
23/// The `ImgBB` struct provides methods for uploading and deleting images
24/// from the ImgBB service.
25///
26/// # Examples
27///
28/// Basic usage:
29///
30/// ```rust,no_run
31/// use imgbb::ImgBB;
32///
33/// async fn example() -> Result<(), imgbb::Error> {
34///     // Create a new ImgBB client
35///     let imgbb = ImgBB::new("your_api_key");
36///
37///     // Upload an image file
38///     let response = imgbb.upload_file("path/to/image.jpg").await?;
39///
40///     // Print the image URL
41///     println!("Image URL: {}", response.data.unwrap().url.unwrap());
42///
43///     Ok(())
44/// }
45#[derive(Debug)]
46pub struct ImgBB {
47    client: reqwest::Client,
48    api_key: String,
49}
50
51/// Builder for creating a customized ImgBB client
52///
53/// This builder allows you to customize the ImgBB client with options
54/// such as timeout duration and user agent.
55///
56/// # Examples
57///
58/// ```rust,no_run
59/// use imgbb::ImgBB;
60/// use std::time::Duration;
61///
62/// // Create a client with a 30-second timeout and custom user agent
63/// let imgbb = ImgBB::builder("your_api_key")
64///     .timeout(Duration::from_secs(30))
65///     .user_agent("MyApp/1.0")
66///     .build()
67///     .unwrap();
68/// ```
69#[derive(Debug)]
70pub struct ImgBBBuilder {
71    api_key: String,
72    timeout: Option<Duration>,
73    user_agent: Option<String>,
74    client: Option<reqwest::Client>,
75}
76
77impl ImgBB {
78    /// Creates a new ImgBB client with the given API key
79    ///
80    /// # Examples
81    ///
82    /// ```rust,no_run
83    /// use imgbb::ImgBB;
84    ///
85    /// let imgbb = ImgBB::new("your_api_key");
86    /// ```
87    pub fn new<T>(api_key: T) -> Self
88    where
89        T: Into<String>,
90    {
91        Self {
92            client: reqwest::Client::builder()
93                .user_agent(APP_USER_AGENT)
94                .build()
95                .unwrap(),
96            api_key: api_key.into(),
97        }
98    }
99
100    /// Creates a new builder for a customized ImgBB client
101    ///
102    /// Use this method to create a builder for configuring the ImgBB client
103    /// with custom options like timeout and user agent.
104    ///
105    /// # Examples
106    ///
107    /// ```rust,no_run
108    /// use imgbb::ImgBB;
109    /// use std::time::Duration;
110    ///
111    /// let imgbb = ImgBB::builder("your_api_key")
112    ///     .timeout(Duration::from_secs(15))
113    ///     .build()
114    ///     .unwrap();
115    /// ```
116    pub fn builder<T>(api_key: T) -> ImgBBBuilder
117    where
118        T: Into<String>,
119    {
120        ImgBBBuilder {
121            api_key: api_key.into(),
122            timeout: None,
123            user_agent: None,
124            client: None,
125        }
126    }
127
128    /// Creates a new ImgBB client with the given API key and reqwest client
129    ///
130    /// This method allows you to provide your own reqwest client with custom
131    /// configuration options that will be used for all API requests.
132    ///
133    /// # Examples
134    ///
135    /// ```rust,no_run
136    /// use imgbb::ImgBB;
137    /// use reqwest::Client;
138    ///
139    /// // Create a custom reqwest client
140    /// let client = Client::builder()
141    ///     .timeout(std::time::Duration::from_secs(30))
142    ///     .user_agent("MyCustomApp/1.0")
143    ///     .build()
144    ///     .unwrap();
145    ///
146    /// // Use the custom client with ImgBB
147    /// let imgbb = ImgBB::new_with_client("your_api_key", client);
148    /// ```
149    pub fn new_with_client<T>(api_key: T, client: reqwest::Client) -> Self
150    where
151        T: Into<String>,
152    {
153        Self {
154            client,
155            api_key: api_key.into(),
156        }
157    }
158
159    /// Read base64 data and return an [Uploader](Uploader) struct to upload in the next step
160    pub fn read_base64<T>(&self, data: T) -> Uploader
161    where
162        T: AsRef<str>,
163    {
164        Uploader {
165            api_key: self.api_key.clone(),
166            data: Some(data.as_ref().to_string()),
167            expiration: None,
168            client: &self.client,
169        }
170    }
171
172    /// Read bytes data and return an [Uploader](Uploader) struct to upload in the next step
173    pub fn read_bytes<T>(&self, data: T) -> Uploader
174    where
175        T: AsRef<[u8]>,
176    {
177        let d = general_purpose::STANDARD.encode(data.as_ref());
178        Uploader {
179            api_key: self.api_key.clone(),
180            data: Some(d),
181            expiration: None,
182            client: &self.client,
183        }
184    }
185
186    /// Read file from path and return an [Uploader](Uploader) struct to upload in the next step
187    pub fn read_file<P>(&self, path: P) -> Result<Uploader, Error>
188    where
189        P: AsRef<Path>,
190    {
191        let f = std::fs::read(path)?;
192        let d = Some(general_purpose::STANDARD.encode(f));
193
194        Ok(Uploader {
195            api_key: self.api_key.clone(),
196            data: d,
197            expiration: None,
198            client: &self.client,
199        })
200    }
201
202    /// Create a new uploader with custom options
203    pub fn upload_builder(&self) -> UploaderBuilder {
204        UploaderBuilder {
205            api_key: self.api_key.clone(),
206            data: None,
207            expiration: None,
208            name: None,
209            title: None,
210            album: None,
211            client: self.client.clone(),
212        }
213    }
214
215    /// Delete an image from ImgBB using the given delete URL
216    ///
217    /// # Arguments
218    ///
219    /// * `delete_url` - The delete URL for the image
220    ///
221    /// # Errors
222    ///
223    /// Returns an error if:
224    /// - The API request fails
225    /// - The API returns an error response
226    /// - The API key is invalid
227    pub async fn delete<T>(&self, delete_url: T) -> Result<(), Error>
228    where
229        T: Into<String>,
230    {
231        let query = [("key", self.api_key.as_str())];
232        let res = self.client
233            .delete(&delete_url.into())
234            .query(&query)
235            .send()
236            .await?;
237
238        let status = res.status();
239        let body = res.text().await?;
240
241        // Try to parse the response
242        match serde_json::from_str::<Response>(&body) {
243            Ok(response) => {
244                if let Some(error) = response.error {
245                    let error_code = error.code.unwrap_or(0);
246                    let error_message = error.message.unwrap_or_else(|| "Unknown error".to_string());
247                    
248                    return match error_code {
249                        100 => Err(Error::InvalidApiKey),
250                        400 => Err(Error::InvalidParameters(error_message)),
251                        429 => Err(Error::RateLimitExceeded),
252                        _ => Err(Error::ApiError {
253                            message: error_message,
254                            status: Some(status.as_u16()),
255                            code: Some(error_code),
256                        }),
257                    };
258                }
259                Ok(())
260            },
261            Err(_) => {
262                if status.is_success() {
263                    Ok(())
264                } else {
265                    Err(Error::ApiError {
266                        message: format!("Delete failed: {}", body),
267                        status: Some(status.as_u16()),
268                        code: None,
269                    })
270                }
271            }
272        }
273    }
274
275    /// Straightforward upload base64 data to ImgBB
276    pub async fn upload_base64<T>(&self, data: T) -> Result<Response, Error>
277    where
278        T: AsRef<str>,
279    {
280        self.read_base64(data).upload().await
281    }
282
283    /// Straightforward upload bytes data to ImgBB
284    pub async fn upload_bytes<T>(&self, data: T) -> Result<Response, Error>
285    where
286        T: AsRef<[u8]>,
287    {
288        self.read_bytes(data).upload().await
289    }
290
291    /// Straightforward upload file to ImgBB
292    pub async fn upload_file<P>(&self, path: P) -> Result<Response, Error>
293    where
294        P: AsRef<Path>,
295    {
296        self.read_file(path)?.upload().await
297    }
298
299    /// Upload base64 data to ImgBB with expiration time (seconds)
300    pub async fn upload_base64_with_expiration<T>(
301        &self,
302        data: T,
303        expiration: u64,
304    ) -> Result<Response, Error>
305    where
306        T: AsRef<str>,
307    {
308        let mut uploader = self.read_base64(data);
309        uploader.expiration(expiration);
310        uploader.upload().await
311    }
312
313    /// Upload bytes data to ImgBB with expiration time (seconds)
314    pub async fn upload_bytes_with_expiration<T>(
315        &self,
316        data: T,
317        expiration: u64,
318    ) -> Result<Response, Error>
319    where
320        T: AsRef<[u8]>,
321    {
322        let mut uploader = self.read_bytes(data);
323        uploader.expiration(expiration);
324        uploader.upload().await
325    }
326
327    /// Upload file to ImgBB with expiration time (seconds)
328    pub async fn upload_file_with_expiration<P>(
329        &self,
330        path: P,
331        expiration: u64,
332    ) -> Result<Response, Error>
333    where
334        P: AsRef<Path>,
335    {
336        let mut uploader = self.read_file(path)?;
337        uploader.expiration(expiration);
338        uploader.upload().await
339    }
340}
341
342impl ImgBBBuilder {
343    /// Set a custom timeout for all requests
344    ///
345    /// # Arguments
346    ///
347    /// * `timeout` - The timeout duration for requests
348    ///
349    /// # Examples
350    ///
351    /// ```rust,no_run
352    /// use imgbb::ImgBB;
353    /// use std::time::Duration;
354    ///
355    /// let imgbb = ImgBB::builder("your_api_key")
356    ///     .timeout(Duration::from_secs(10))
357    ///     .build()
358    ///     .unwrap();
359    /// ```
360    pub fn timeout(mut self, timeout: Duration) -> Self {
361        self.timeout = Some(timeout);
362        self
363    }
364
365    /// Set a custom user agent
366    ///
367    /// # Arguments
368    ///
369    /// * `user_agent` - The user agent string to use for requests
370    ///
371    /// # Examples
372    ///
373    /// ```rust,no_run
374    /// use imgbb::ImgBB;
375    ///
376    /// let imgbb = ImgBB::builder("your_api_key")
377    ///     .user_agent("MyApp/1.0")
378    ///     .build()
379    ///     .unwrap();
380    /// ```
381    pub fn user_agent<T>(mut self, user_agent: T) -> Self
382    where
383        T: Into<String>,
384    {
385        self.user_agent = Some(user_agent.into());
386        self
387    }
388
389    /// Set a custom reqwest client
390    ///
391    /// This method allows you to provide your own reqwest client with custom
392    /// configuration options that will be used for all API requests.
393    ///
394    /// Note: If you provide a custom client, any timeout or user agent
395    /// settings specified on the builder will be ignored in favor of
396    /// the custom client's configuration.
397    ///
398    /// # Arguments
399    ///
400    /// * `client` - The reqwest client to use for API requests
401    ///
402    /// # Examples
403    ///
404    /// ```rust,no_run
405    /// use imgbb::ImgBB;
406    /// use reqwest::Client;
407    ///
408    /// // Create a custom reqwest client
409    /// let client = Client::builder()
410    ///     .timeout(std::time::Duration::from_secs(30))
411    ///     .user_agent("MyCustomApp/1.0")
412    ///     .build()
413    ///     .unwrap();
414    ///
415    /// // Use the custom client with ImgBB
416    /// let imgbb = ImgBB::builder("your_api_key")
417    ///     .client(client)
418    ///     .build()
419    ///     .unwrap();
420    /// ```
421    pub fn client(mut self, client: reqwest::Client) -> Self {
422        self.client = Some(client);
423        self
424    }
425
426    /// Build the ImgBB client
427    ///
428    /// This method builds the ImgBB client with the configured options.
429    /// If a custom client was provided, it will be used; otherwise,
430    /// a new client will be created with the specified timeout and user agent.
431    ///
432    /// # Examples
433    ///
434    /// ```rust,no_run
435    /// use imgbb::ImgBB;
436    ///
437    /// let imgbb = ImgBB::builder("your_api_key")
438    ///     .build()
439    ///     .unwrap();
440    /// ```
441    ///
442    /// # Errors
443    ///
444    /// Returns an error if the reqwest client builder fails to build.
445    pub fn build(self) -> Result<ImgBB, Error> {
446        // If a custom client was provided, use it
447        if let Some(client) = self.client {
448            return Ok(ImgBB {
449                client,
450                api_key: self.api_key,
451            });
452        }
453
454        // Otherwise, build a new client with the provided options
455        let mut client_builder = reqwest::Client::builder();
456
457        // Set user agent
458        client_builder = client_builder.user_agent(
459            self.user_agent.unwrap_or_else(|| APP_USER_AGENT.to_string()),
460        );
461
462        // Set timeout if provided
463        if let Some(timeout) = self.timeout {
464            client_builder = client_builder.timeout(timeout);
465        }
466
467        // Build the client
468        let client = client_builder
469            .build()
470            .map_err(Error::from)?;
471
472        Ok(ImgBB {
473            client,
474            api_key: self.api_key,
475        })
476    }
477}
478
479/// A builder for creating an uploader with more options
480///
481/// This builder allows for more flexible configuration of image uploads,
482/// including setting the name, title, and album for the image.
483///
484/// # Examples
485///
486/// ```rust,no_run
487/// use imgbb::ImgBB;
488///
489/// async fn example() -> Result<(), imgbb::Error> {
490///     let imgbb = ImgBB::new("your_api_key");
491///
492///     // Create an uploader with custom options
493///     let response = imgbb.upload_builder()
494///         .file("path/to/image.jpg")?
495///         .name("custom_name")
496///         .title("My Image")
497///         .expiration(86400) // 24 hours
498///         .album("album_id")
499///         .upload()
500///         .await?;
501///
502///     println!("Upload successful: {}", response.data.unwrap().url.unwrap());
503///
504///     Ok(())
505/// }
506/// ```
507#[derive(Clone)]
508pub struct UploaderBuilder {
509    api_key: String,
510    data: Option<String>,
511    expiration: Option<u64>,
512    name: Option<String>,
513    title: Option<String>,
514    album: Option<String>,
515    client: reqwest::Client,
516}
517
518impl UploaderBuilder {
519    /// Set the base64 data for upload
520    ///
521    /// # Arguments
522    ///
523    /// * `data` - Base64 encoded string of the image
524    pub fn data<T>(mut self, data: T) -> Self
525    where
526        T: AsRef<str>,
527    {
528        self.data = Some(data.as_ref().to_owned());
529        self
530    }
531
532    /// Set the raw bytes data for upload, which will be encoded as base64
533    ///
534    /// # Arguments
535    ///
536    /// * `data` - Raw bytes of the image
537    pub fn bytes<T>(mut self, data: T) -> Self
538    where
539        T: AsRef<[u8]>,
540    {
541        self.data = Some(general_purpose::STANDARD.encode(data.as_ref()));
542        self
543    }
544
545    /// Set data from a file path
546    ///
547    /// # Arguments
548    ///
549    /// * `path` - Path to the image file
550    ///
551    /// # Errors
552    ///
553    /// Returns an error if the file cannot be read
554    pub fn file<P>(mut self, path: P) -> Result<Self, Error>
555    where
556        P: AsRef<Path>,
557    {
558        let f = std::fs::read(path)?;
559        self.data = Some(general_purpose::STANDARD.encode(f));
560        Ok(self)
561    }
562
563    /// Set the expiration time in seconds
564    ///
565    /// # Arguments
566    ///
567    /// * `expiration` - Time in seconds until the image expires
568    pub fn expiration(mut self, expiration: u64) -> Self {
569        self.expiration = Some(expiration);
570        self
571    }
572
573    /// Set the image name
574    ///
575    /// # Arguments
576    ///
577    /// * `name` - Name for the uploaded image
578    pub fn name<T>(mut self, name: T) -> Self
579    where
580        T: Into<String>,
581    {
582        self.name = Some(name.into());
583        self
584    }
585
586    /// Set the image title
587    ///
588    /// # Arguments
589    ///
590    /// * `title` - Title for the uploaded image
591    pub fn title<T>(mut self, title: T) -> Self
592    where
593        T: Into<String>,
594    {
595        self.title = Some(title.into());
596        self
597    }
598
599    /// Set the album ID
600    ///
601    /// # Arguments
602    ///
603    /// * `album` - ID of the album to add the image to
604    pub fn album<T>(mut self, album: T) -> Self
605    where
606        T: Into<String>,
607    {
608        self.album = Some(album.into());
609        self
610    }
611
612    /// Upload the image with all specified options
613    ///
614    /// # Errors
615    ///
616    /// Returns an error if:
617    /// - No image data has been set
618    /// - The API request fails
619    /// - The API returns an error response
620    pub async fn upload(self) -> Result<Response, Error> {
621        if self.data.is_none() {
622            return Err(Error::MissingField("data".to_string()));
623        }
624
625        let mut query = vec![("key", self.api_key.as_str())];
626        let mut form = vec![("image", self.data.as_ref().unwrap().as_str())];
627
628        // Store expiration string to extend its lifetime
629        let expiration_str;
630        if let Some(exp) = &self.expiration {
631            expiration_str = exp.to_string();
632            query.push(("expiration", expiration_str.as_str()));
633        }
634
635        if let Some(name) = &self.name {
636            form.push(("name", name.as_str()));
637        }
638
639        if let Some(title) = &self.title {
640            form.push(("title", title.as_str()));
641        }
642
643        if let Some(album) = &self.album {
644            form.push(("album", album.as_str()));
645        }
646
647        let res = self.client
648            .post(IMGBB_API_URL)
649            .query(&query)
650            .form(&form)
651            .send()
652            .await?;
653
654        let status = res.status();
655        let body = res.text().await?;
656
657        // Try to parse the response
658        match serde_json::from_str::<Response>(&body) {
659            Ok(response) => {
660                if let Some(error) = response.error {
661                    let error_code = error.code.unwrap_or(0);
662                    let error_message = error.message.unwrap_or_else(|| "Unknown error".to_string());
663                    
664                    return match error_code {
665                        100 => Err(Error::InvalidApiKey),
666                        400 => Err(Error::InvalidParameters(error_message)),
667                        429 => Err(Error::RateLimitExceeded),
668                        _ => Err(Error::ApiError {
669                            message: error_message,
670                            status: Some(status.as_u16()),
671                            code: Some(error_code),
672                        }),
673                    };
674                }
675                Ok(response)
676            },
677            Err(_) => Err(Error::ApiError {
678                message: format!("Failed to parse response: {}", body),
679                status: Some(status.as_u16()),
680                code: None,
681            }),
682        }
683    }
684}