Skip to main content

dsc/api/
uploads.rs

1use super::client::DiscourseClient;
2use super::error::http_error;
3use anyhow::{Context, Result, anyhow};
4use serde::{Deserialize, Serialize};
5use std::path::Path;
6
7/// Distilled fields from `/uploads.json` — the response carries more (id,
8/// width/height, dominant_color, etc.) but these are the ones every caller
9/// will reach for.
10#[derive(Debug, Deserialize, Serialize, Clone)]
11pub struct UploadInfo {
12    pub id: u64,
13    pub url: String,
14    #[serde(default)]
15    pub short_url: Option<String>,
16    #[serde(default)]
17    pub short_path: Option<String>,
18    pub original_filename: String,
19    pub filesize: u64,
20    #[serde(default)]
21    pub width: Option<u64>,
22    #[serde(default)]
23    pub height: Option<u64>,
24}
25
26impl DiscourseClient {
27    /// Upload a file. `upload_type` is Discourse's `type` field — typical
28    /// values: `composer` (default; for embedding in posts), `avatar`,
29    /// `profile_background`, `card_background`, `custom_emoji`.
30    pub fn upload_file(&self, file_path: &Path, upload_type: &str) -> Result<UploadInfo> {
31        let make_form = || -> Result<reqwest::blocking::multipart::Form> {
32            let bytes = std::fs::read(file_path)
33                .with_context(|| format!("reading {}", file_path.display()))?;
34            let filename = file_path
35                .file_name()
36                .and_then(|s| s.to_str())
37                .ok_or_else(|| anyhow!("upload path missing filename: {}", file_path.display()))?
38                .to_string();
39            let part = reqwest::blocking::multipart::Part::bytes(bytes).file_name(filename);
40            Ok(reqwest::blocking::multipart::Form::new()
41                .part("file", part)
42                .text("type", upload_type.to_string())
43                .text("synchronous", "true".to_string()))
44        };
45
46        let response = self.send_retrying(|| Ok(self.post("/uploads.json")?.multipart(make_form()?)))?;
47        let status = response.status();
48        let text = response.text().context("reading upload response body")?;
49        if !status.is_success() {
50            return Err(http_error("upload request", status, &text));
51        }
52        let info: UploadInfo =
53            serde_json::from_str(&text).context("parsing upload response")?;
54        Ok(info)
55    }
56}