feat: add files endpoint with info, download, upload, and delete
Implement FileInfo, FileResponse, SaveFileRequest, and CreatedWithIdResponse models. Add get_file_info, get_file (binary download), upload_file (multipart), and delete_file methods. Tests cover metadata retrieval, binary content download with headers, file upload, deletion, and not-found error with message extraction.
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
use reqwest::Method;
|
||||
|
||||
use crate::client::OnspringClient;
|
||||
use crate::error::Result;
|
||||
use crate::models::{CreatedWithIdResponse, FileInfo, FileResponse, SaveFileRequest};
|
||||
|
||||
impl OnspringClient {
|
||||
/// Gets a file's metadata information.
|
||||
pub async fn get_file_info(
|
||||
&self,
|
||||
record_id: i32,
|
||||
field_id: i32,
|
||||
file_id: i32,
|
||||
) -> Result<FileInfo> {
|
||||
let path = format!(
|
||||
"/Files/recordId/{}/fieldId/{}/fileId/{}",
|
||||
record_id, field_id, file_id
|
||||
);
|
||||
self.request(Method::GET, &path, &[], Option::<&()>::None)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Downloads a file's content.
|
||||
pub async fn get_file(
|
||||
&self,
|
||||
record_id: i32,
|
||||
field_id: i32,
|
||||
file_id: i32,
|
||||
) -> Result<FileResponse> {
|
||||
let path = format!(
|
||||
"/Files/recordId/{}/fieldId/{}/fileId/{}/file",
|
||||
record_id, field_id, file_id
|
||||
);
|
||||
let (_status, headers, data) = self.request_bytes(Method::GET, &path, &[]).await?;
|
||||
|
||||
let content_type = headers
|
||||
.get("content-type")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.map(String::from);
|
||||
|
||||
let file_name = headers
|
||||
.get("content-disposition")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|v| {
|
||||
v.split("filename=")
|
||||
.nth(1)
|
||||
.map(|s| s.trim_matches('"').to_string())
|
||||
});
|
||||
|
||||
Ok(FileResponse {
|
||||
content_type,
|
||||
file_name,
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/// Uploads a file attachment.
|
||||
pub async fn upload_file(&self, request: SaveFileRequest) -> Result<CreatedWithIdResponse> {
|
||||
let file_part = reqwest::multipart::Part::bytes(request.file_data)
|
||||
.file_name(request.file_name)
|
||||
.mime_str(&request.content_type)
|
||||
.map_err(|e| crate::error::OnspringError::InvalidArgument(e.to_string()))?;
|
||||
|
||||
let mut form = reqwest::multipart::Form::new()
|
||||
.text("RecordId", request.record_id.to_string())
|
||||
.text("FieldId", request.field_id.to_string())
|
||||
.part("File", file_part);
|
||||
|
||||
if let Some(notes) = request.notes {
|
||||
form = form.text("Notes", notes);
|
||||
}
|
||||
if let Some(date) = request.modified_date {
|
||||
form = form.text("ModifiedDate", date.to_rfc3339());
|
||||
}
|
||||
|
||||
self.request_multipart("/Files", form).await
|
||||
}
|
||||
|
||||
/// Deletes a file attachment.
|
||||
pub async fn delete_file(&self, record_id: i32, field_id: i32, file_id: i32) -> Result<()> {
|
||||
let path = format!(
|
||||
"/Files/recordId/{}/fieldId/{}/fileId/{}",
|
||||
record_id, field_id, file_id
|
||||
);
|
||||
self.request_no_content(Method::DELETE, &path, &[], Option::<&()>::None)
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
mod apps;
|
||||
mod fields;
|
||||
mod files;
|
||||
mod ping;
|
||||
mod records;
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
use bytes::Bytes;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::Deserialize;
|
||||
|
||||
/// Information about a file attachment.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FileInfo {
|
||||
#[serde(rename = "type")]
|
||||
pub file_type: Option<String>,
|
||||
pub content_type: Option<String>,
|
||||
pub name: Option<String>,
|
||||
pub created_date: Option<DateTime<Utc>>,
|
||||
pub modified_date: Option<DateTime<Utc>>,
|
||||
pub owner: Option<String>,
|
||||
pub notes: Option<String>,
|
||||
pub file_href: Option<String>,
|
||||
}
|
||||
|
||||
/// The content of a downloaded file.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FileResponse {
|
||||
pub content_type: Option<String>,
|
||||
pub file_name: Option<String>,
|
||||
pub data: Bytes,
|
||||
}
|
||||
|
||||
/// Request to upload a file.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SaveFileRequest {
|
||||
pub record_id: i32,
|
||||
pub field_id: i32,
|
||||
pub notes: Option<String>,
|
||||
pub modified_date: Option<DateTime<Utc>>,
|
||||
pub file_name: String,
|
||||
pub file_data: Vec<u8>,
|
||||
pub content_type: String,
|
||||
}
|
||||
|
||||
/// Response from creating a file, containing the new file ID.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreatedWithIdResponse {
|
||||
pub id: i32,
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
mod app;
|
||||
mod enums;
|
||||
mod field;
|
||||
pub mod file;
|
||||
mod paging;
|
||||
pub mod record;
|
||||
|
||||
pub use app::*;
|
||||
pub use enums::*;
|
||||
pub use field::*;
|
||||
pub use file::*;
|
||||
pub use paging::*;
|
||||
pub use record::*;
|
||||
|
||||
Reference in New Issue
Block a user