Skip to main content

mesa_dev/
helpers.rs

1//! High-level helper functions for common operations.
2//!
3//! These helpers provide convenient methods that combine multiple SDK operations
4//! into single calls.
5//!
6//! # Example
7//!
8//! ```rust,no_run
9//! use mesa_dev::{Mesa, MesaError};
10//! use mesa_dev::helpers::{LargeFile, UploadLargeFilesOptions};
11//! use mesa_dev::models::Author;
12//!
13//! # async fn run() -> Result<(), MesaError> {
14//! let client = Mesa::new("api-key");
15//! let content = std::fs::read("large-model.bin").unwrap();
16//!
17//! let result = mesa_dev::helpers::upload_large_files(
18//!     &client,
19//!     UploadLargeFilesOptions {
20//!         org: "my-org".into(),
21//!         repo: "my-repo".into(),
22//!         branch: "main".into(),
23//!         message: "Add large model".into(),
24//!         author: Author {
25//!             name: "Bot".into(),
26//!             email: "bot@example.com".into(),
27//!             date: None,
28//!         },
29//!         files: vec![LargeFile {
30//!             path: "models/model.bin".into(),
31//!             content,
32//!         }],
33//!         base_sha: None,
34//!     },
35//! ).await?;
36//!
37//! println!("Created commit: {}", result.sha);
38//! # Ok(())
39//! # }
40//! ```
41
42use sha2::{Digest, Sha256};
43
44use crate::MesaClient;
45use crate::error::MesaError;
46use crate::http_client::HttpClient;
47use crate::models::{
48    Author, CommitFileChange, CommitSummary, CreateCommitWithLfsRequest, LfsObjectSpec,
49    UploadLfsObjectsRequest,
50};
51
52/// A large file to upload via LFS.
53#[derive(Debug, Clone)]
54pub struct LargeFile {
55    /// Path in the repository (e.g., "models/large-model.bin").
56    pub path: String,
57    /// File content as bytes.
58    pub content: Vec<u8>,
59}
60
61/// Options for uploading large files.
62#[derive(Debug, Clone)]
63pub struct UploadLargeFilesOptions {
64    /// Organization slug.
65    pub org: String,
66    /// Repository name.
67    pub repo: String,
68    /// Target branch.
69    pub branch: String,
70    /// Commit message.
71    pub message: String,
72    /// Commit author.
73    pub author: Author,
74    /// Files to upload.
75    pub files: Vec<LargeFile>,
76    /// Optional base SHA for optimistic locking.
77    pub base_sha: Option<String>,
78}
79
80/// Upload large files to a repository using LFS.
81///
82/// This convenience function:
83/// 1. Computes SHA-256 of each file
84/// 2. Requests pre-signed upload URLs from the LFS endpoint
85/// 3. Uploads file content directly to S3
86/// 4. Creates a commit with LFS pointer references
87///
88/// # Errors
89///
90/// Returns [`MesaError`] if any step fails (API request, S3 upload, or commit creation).
91///
92/// # Example
93///
94/// ```rust,no_run
95/// use mesa_dev::{Mesa, MesaError};
96/// use mesa_dev::helpers::{upload_large_files, LargeFile, UploadLargeFilesOptions};
97/// use mesa_dev::models::Author;
98///
99/// # async fn run() -> Result<(), MesaError> {
100/// let client = Mesa::new("api-key");
101/// let content = std::fs::read("large-model.bin").unwrap();
102///
103/// let result = upload_large_files(
104///     &client,
105///     UploadLargeFilesOptions {
106///         org: "my-org".into(),
107///         repo: "my-repo".into(),
108///         branch: "main".into(),
109///         message: "Add large model".into(),
110///         author: Author {
111///             name: "Bot".into(),
112///             email: "bot@example.com".into(),
113///             date: None,
114///         },
115///         files: vec![LargeFile {
116///             path: "models/model.bin".into(),
117///             content,
118///         }],
119///         base_sha: None,
120///     },
121/// ).await?;
122///
123/// println!("Created commit: {}", result.sha);
124/// # Ok(())
125/// # }
126/// ```
127pub async fn upload_large_files<C: HttpClient>(
128    client: &MesaClient<C>,
129    options: UploadLargeFilesOptions,
130) -> Result<CommitSummary, MesaError> {
131    let UploadLargeFilesOptions {
132        org,
133        repo,
134        branch,
135        message,
136        author,
137        files,
138        base_sha,
139    } = options;
140
141    // 1. Compute SHA-256 for each file
142    let prepared: Vec<_> = files
143        .iter()
144        .map(|f| {
145            let mut hasher = Sha256::new();
146            hasher.update(&f.content);
147            let hash = hasher.finalize();
148            let oid = hex::encode(hash);
149            let size = f.content.len() as u64;
150            (f, oid, size)
151        })
152        .collect();
153
154    // 2. Request upload URLs
155    let upload_req = UploadLfsObjectsRequest {
156        objects: prepared
157            .iter()
158            .map(|(_, oid, size)| LfsObjectSpec {
159                oid: oid.clone(),
160                size: *size,
161            })
162            .collect(),
163    };
164    let upload_resp = client.lfs(&org, &repo).upload(&upload_req).await?;
165
166    // 3. Upload content for objects that need it
167    for obj_status in &upload_resp.objects {
168        if let Some(ref error) = obj_status.error {
169            return Err(MesaError::LfsUpload {
170                oid: obj_status.oid.clone(),
171                message: error.message.clone(),
172            });
173        }
174
175        if let Some(ref url) = obj_status.upload_url {
176            let (file, _, _) = prepared
177                .iter()
178                .find(|(_, oid, _)| oid == &obj_status.oid)
179                .ok_or_else(|| MesaError::LfsUpload {
180                    oid: obj_status.oid.clone(),
181                    message: "File not found for OID".to_owned(),
182                })?;
183
184            upload_to_s3(url, &file.content).await?;
185        }
186    }
187
188    // 4. Create commit with LFS pointers
189    let commit_files: Vec<CommitFileChange> = prepared
190        .iter()
191        .map(|(f, oid, size)| CommitFileChange::lfs(&f.path, oid, *size))
192        .collect();
193
194    let commit_req = CreateCommitWithLfsRequest {
195        branch,
196        message,
197        author,
198        files: commit_files,
199        base_sha,
200    };
201
202    // Use the commits resource with the LFS-aware request
203    client
204        .commits(&org, &repo)
205        .create_with_lfs(&commit_req)
206        .await
207}
208
209/// Upload content to S3 using a pre-signed URL.
210async fn upload_to_s3(url: &str, content: &[u8]) -> Result<(), MesaError> {
211    // Use reqwest directly for S3 uploads (it's already a dependency when reqwest-client is enabled)
212    #[cfg(feature = "reqwest-client")]
213    {
214        let client = reqwest::Client::new();
215        let response = client
216            .put(url)
217            .body(content.to_vec())
218            .header("Content-Length", content.len().to_string())
219            .send()
220            .await
221            .map_err(|e| MesaError::LfsUpload {
222                oid: String::new(),
223                message: format!("S3 upload request failed: {e}"),
224            })?;
225
226        if !response.status().is_success() {
227            return Err(MesaError::LfsUpload {
228                oid: String::new(),
229                message: format!("S3 upload failed: {}", response.status()),
230            });
231        }
232        Ok(())
233    }
234
235    #[cfg(not(feature = "reqwest-client"))]
236    {
237        // For non-reqwest clients, return an error indicating manual upload is needed
238        let _ = (url, content);
239        Err(MesaError::LfsUpload {
240            oid: String::new(),
241            message: "S3 upload requires reqwest-client feature".to_owned(),
242        })
243    }
244}