@@ -175,6 +175,79 @@
}
}
/// Upload a file as multipart form data.
pub async fn upload_file(
&self,
path: &str,
file_path: &std::path::Path,
field_name: &str,
) -> Result<serde_json::Value, ApiError> {
let file_name = file_path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
let file_bytes = tokio::fs::read(file_path).await.map_err(|e| ApiError::Api {
status: 0,
message: format!("Failed to read file: {e}"),
})?;
let part = reqwest::multipart::Part::bytes(file_bytes)
.file_name(file_name)
.mime_str("application/octet-stream")
.map_err(|e| ApiError::Api {
status: 0,
message: format!("Invalid MIME type: {e}"),
})?;
let form = reqwest::multipart::Form::new().part(field_name.to_string(), part);
// Build request without default Content-Type header (multipart sets its own)
let url = self.url(path);
let resp = self
.http
.post(&url)
.multipart(form)
.send()
.await?;
let status = resp.status();
if status.is_success() {
Ok(resp.json().await?)
} else {
let text = resp.text().await.unwrap_or_default();
Err(ApiError::Api {
status: status.as_u16(),
message: text,
})
}
}
/// Download a file from the server and save to disk.
pub async fn download_file(
&self,
path: &str,
output: &std::path::Path,
) -> Result<(), ApiError> {
let resp = self.http.get(self.url(path)).send().await?;
let status = resp.status();
if !status.is_success() {
let text = resp.text().await.unwrap_or_default();
return Err(ApiError::Api {
status: status.as_u16(),
message: text,
});
}
let bytes = resp.bytes().await?;
tokio::fs::write(output, &bytes).await.map_err(|e| ApiError::Api {
status: 0,
message: format!("Failed to write file: {e}"),
})?;
Ok(())
}
pub fn base_url(&self) -> &str {
&self.base_url
}