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}