1#[cfg(not(target_family = "wasm"))]
17pub mod sync;
18
19#[cfg(not(target_family = "wasm"))]
20use std::path::PathBuf;
21
22use bon::bon;
23use futures::{Stream, StreamExt};
24use serde::{Deserialize, Serialize};
25use url::Url;
26
27use crate::client::HFClient;
28use crate::error::{HFError, HFResult, NotFoundContext};
29use crate::progress::{DownloadEvent, EmitEvent, Progress, UploadEvent};
30use crate::repository::download::{HFByteStream, wrap_stream_with_progress};
31use crate::retry;
32
33const BUCKET_BATCH_CHUNK_SIZE: usize = 1000;
34const BUCKET_PATHS_INFO_BATCH_SIZE: usize = 1000;
35
36#[derive(Clone)]
64pub struct HFBucket {
65 pub(crate) hf_client: HFClient,
66 pub(super) owner: String,
67 pub(super) name: String,
68}
69
70impl std::fmt::Debug for HFBucket {
71 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72 f.debug_struct("HFBucket")
73 .field("owner", &self.owner)
74 .field("name", &self.name)
75 .finish()
76 }
77}
78
79impl HFBucket {
80 pub fn new(client: HFClient, owner: impl Into<String>, name: impl Into<String>) -> Self {
82 Self {
83 hf_client: client,
84 owner: owner.into(),
85 name: name.into(),
86 }
87 }
88
89 pub fn client(&self) -> &HFClient {
91 &self.hf_client
92 }
93
94 pub fn owner(&self) -> &str {
96 &self.owner
97 }
98
99 pub fn name(&self) -> &str {
101 &self.name
102 }
103
104 pub fn bucket_id(&self) -> String {
106 format!("{}/{}", self.owner, self.name)
107 }
108}
109
110#[bon]
111impl HFBucket {
112 #[builder(finish_fn = send, derive(Debug, Clone))]
116 pub async fn info(&self) -> HFResult<BucketInfo> {
117 let bucket_id = self.bucket_id();
118 let url = self.hf_client.bucket_api_url(&bucket_id);
119
120 let headers = self.hf_client.auth_headers();
121 let response = retry::retry(self.hf_client.retry_config(), || {
122 self.hf_client.http_client().get(&url).headers(headers.clone()).send()
123 })
124 .await?;
125
126 let response = self
127 .hf_client
128 .check_response(response, Some(&bucket_id), NotFoundContext::Bucket)
129 .await?;
130 Ok(response.json().await?)
131 }
132
133 #[builder(finish_fn = send, derive(Debug, Clone))]
145 pub fn list_tree(
146 &self,
147 #[builder(into)]
149 prefix: Option<String>,
150 #[builder(default)]
152 recursive: bool,
153 ) -> HFResult<impl Stream<Item = HFResult<BucketTreeEntry>> + '_> {
154 let bucket_id = self.bucket_id();
155 let url_str = format!("{}/api/buckets/{}/tree", self.hf_client.endpoint(), bucket_id);
156 let mut url = Url::parse(&url_str)?;
157 if let Some(ref prefix) = prefix {
158 crate::client::append_path_segments(&mut url, prefix)?;
159 }
160
161 let mut query = vec![];
162 if recursive {
163 query.push(("recursive".to_string(), "true".to_string()));
164 }
165
166 Ok(self.hf_client.paginate(url, query, None))
167 }
168
169 #[builder(finish_fn = send, derive(Debug, Clone))]
180 pub async fn get_paths_info(
181 &self,
182 paths: Vec<String>,
184 ) -> HFResult<Vec<BucketTreeEntry>> {
185 let bucket_id = self.bucket_id();
186 let url = format!("{}/api/buckets/{}/paths-info", self.hf_client.endpoint(), bucket_id);
187
188 let headers = self.hf_client.auth_headers();
189 let mut all_entries = Vec::new();
190 for chunk in paths.chunks(BUCKET_PATHS_INFO_BATCH_SIZE) {
191 let body = serde_json::json!({ "paths": chunk });
192
193 let response = retry::retry(self.hf_client.retry_config(), || {
194 self.hf_client
195 .http_client()
196 .post(&url)
197 .headers(headers.clone())
198 .json(&body)
199 .send()
200 })
201 .await?;
202
203 let response = self
204 .hf_client
205 .check_response(response, Some(&bucket_id), NotFoundContext::Bucket)
206 .await?;
207 let entries: Vec<BucketTreeEntry> = response.json().await?;
208 all_entries.extend(entries);
209 }
210
211 Ok(all_entries)
212 }
213
214 #[builder(finish_fn = send, derive(Debug, Clone))]
230 pub async fn download_file_stream(
231 &self,
232 #[builder(into)]
234 remote_path: String,
235 #[builder(into)]
239 progress: Option<Progress>,
240 ) -> HFResult<(Option<u64>, HFByteStream)> {
241 Box::pin(self.download_file_stream_impl(remote_path, progress)).await
242 }
243
244 #[cfg(not(target_family = "wasm"))]
252 #[builder(finish_fn = send, derive(Debug, Clone))]
253 pub async fn get_file_metadata(
254 &self,
255 #[builder(into)]
257 remote_path: String,
258 ) -> HFResult<BucketFileMetadata> {
259 let bucket_id = self.bucket_id();
260 let mut url = Url::parse(&format!("{}/buckets/{}/resolve", self.hf_client.endpoint(), bucket_id))?;
261 crate::client::append_path_segments(&mut url, &remote_path)?;
262
263 let headers = self.hf_client.auth_headers();
264 let response = retry::retry(self.hf_client.retry_config(), || {
265 self.hf_client
266 .no_redirect_client()
267 .head(url.clone())
268 .headers(headers.clone())
269 .send()
270 })
271 .await?;
272
273 let response = self
274 .hf_client
275 .check_response(response, Some(&bucket_id), NotFoundContext::Entry { path: remote_path })
276 .await?;
277
278 let size = response
279 .headers()
280 .get(reqwest::header::CONTENT_LENGTH)
281 .and_then(|v| v.to_str().ok())
282 .and_then(|v| v.parse::<u64>().ok())
283 .unwrap_or(0);
284
285 let xet_hash = response
286 .headers()
287 .get("x-xet-hash")
288 .and_then(|v| v.to_str().ok())
289 .unwrap_or("")
290 .to_string();
291
292 Ok(BucketFileMetadata { size, xet_hash })
293 }
294
295 #[builder(finish_fn = send, derive(Debug, Clone))]
329 pub async fn batch_operations(
330 &self,
331 #[builder(default)]
333 add_files: Vec<BucketAddFile>,
334 #[builder(default)]
336 delete: Vec<String>,
337 #[builder(default)]
339 copy: Vec<BucketCopyFile>,
340 ) -> HFResult<()> {
341 let bucket_id = self.bucket_id();
342 let url = format!("{}/api/buckets/{}/batch", self.hf_client.endpoint(), bucket_id);
343
344 let mut lines: Vec<String> = Vec::new();
345
346 for add in &add_files {
347 let mut obj = serde_json::json!({
348 "type": "addFile",
349 "path": add.path,
350 "xetHash": add.xet_hash,
351 "size": add.size,
352 });
353 if let Some(mtime) = add.mtime {
354 obj["mtime"] = serde_json::Value::Number(mtime.into());
355 }
356 if let Some(ref ct) = add.content_type {
357 obj["contentType"] = serde_json::Value::String(ct.clone());
358 }
359 lines.push(serde_json::to_string(&obj)?);
360 }
361
362 for path in &delete {
363 let obj = serde_json::json!({
364 "type": "deleteFile",
365 "path": path,
366 });
367 lines.push(serde_json::to_string(&obj)?);
368 }
369
370 for copy in © {
371 let mut obj = serde_json::json!({
372 "type": "copyFile",
373 "path": copy.path,
374 "xetHash": copy.xet_hash,
375 "sourceRepoType": copy.source_repo_type.as_str(),
376 "sourceRepoId": copy.source_repo_id,
377 });
378 if let Some(size) = copy.size {
379 obj["size"] = serde_json::Value::Number(size.into());
380 }
381 if let Some(mtime) = copy.mtime {
382 obj["mtime"] = serde_json::Value::Number(mtime.into());
383 }
384 if let Some(ref ct) = copy.content_type {
385 obj["contentType"] = serde_json::Value::String(ct.clone());
386 }
387 lines.push(serde_json::to_string(&obj)?);
388 }
389
390 let headers = self.hf_client.auth_headers();
391 for chunk in lines.chunks(BUCKET_BATCH_CHUNK_SIZE) {
392 let body = chunk.join("\n") + "\n";
393
394 let response = retry::retry(self.hf_client.retry_config(), || {
395 self.hf_client
396 .http_client()
397 .post(&url)
398 .headers(headers.clone())
399 .header(reqwest::header::CONTENT_TYPE, "application/x-ndjson")
400 .body(body.clone())
401 .send()
402 })
403 .await?;
404
405 self.hf_client
406 .check_response(response, Some(&bucket_id), NotFoundContext::Bucket)
407 .await?;
408 }
409
410 Ok(())
411 }
412
413 #[builder(finish_fn = send, derive(Debug, Clone))]
421 pub async fn delete_files(
422 &self,
423 paths: Vec<String>,
425 ) -> HFResult<()> {
426 self.batch_operations().delete(paths).send().await
427 }
428
429 #[builder(finish_fn = send, derive(Debug, Clone))]
452 pub async fn upload_source_files(
453 &self,
454 files: Vec<(String, crate::repository::AddSource)>,
456 #[builder(into)]
458 progress: Option<Progress>,
459 ) -> HFResult<()> {
460 Box::pin(self.upload_sources_impl(files, progress)).await
461 }
462
463 #[cfg(not(target_family = "wasm"))]
474 #[builder(finish_fn = send, derive(Debug, Clone))]
475 pub async fn upload_files(
476 &self,
477 files: Vec<BucketUpload>,
479 #[builder(into)]
481 progress: Option<Progress>,
482 ) -> HFResult<()> {
483 Box::pin(self.upload_files_impl(files, progress)).await
484 }
485
486 #[cfg(not(target_family = "wasm"))]
498 #[builder(finish_fn = send, derive(Debug, Clone))]
499 pub async fn download_files(
500 &self,
501 files: Vec<BucketDownload>,
503 #[builder(into)]
505 progress: Option<Progress>,
506 ) -> HFResult<()> {
507 Box::pin(self.download_files_impl(files, progress)).await
508 }
509}
510
511impl HFBucket {
512 async fn download_file_stream_impl(
515 &self,
516 remote_path: String,
517 progress: Option<Progress>,
518 ) -> HFResult<(Option<u64>, HFByteStream)> {
519 let bucket_id = self.bucket_id();
520
521 let entries = self.get_paths_info().paths(vec![remote_path.clone()]).send().await?;
522 let (xet_hash, file_size) = entries
523 .into_iter()
524 .find_map(|e| match e {
525 BucketTreeEntry::File {
526 path, size, xet_hash, ..
527 } if path == remote_path => Some((xet_hash, size)),
528 _ => None,
529 })
530 .ok_or_else(|| HFError::EntryNotFound {
531 path: remote_path.clone(),
532 repo_id: bucket_id.clone(),
533 context: None,
534 })?;
535
536 if !xet_hash.is_empty() {
537 let stream = self.xet_download_stream(&xet_hash, file_size).await?;
538 progress.emit(DownloadEvent::Start {
539 total_files: 1,
540 total_bytes: file_size,
541 });
542 let boxed: HFByteStream = Box::new(Box::pin(stream));
543 let wrapped = wrap_stream_with_progress(boxed, progress, remote_path, file_size);
544 #[cfg(target_family = "wasm")]
545 let wrapped = crate::repository::download::buffer_wasm_stream(wrapped);
546 return Ok((Some(file_size), wrapped));
547 }
548
549 let mut url = Url::parse(&format!("{}/buckets/{}/resolve", self.hf_client.endpoint(), bucket_id))?;
550 crate::client::append_path_segments(&mut url, &remote_path)?;
551 let headers = self.hf_client.auth_headers();
552 let response = retry::retry(self.hf_client.retry_config(), || {
553 self.hf_client.http_client().get(url.clone()).headers(headers.clone()).send()
554 })
555 .await?;
556 let response = self
557 .hf_client
558 .check_response(
559 response,
560 Some(&bucket_id),
561 NotFoundContext::Entry {
562 path: remote_path.clone(),
563 },
564 )
565 .await?;
566
567 let content_length = response.content_length().or(Some(file_size));
568 let total_bytes = content_length.unwrap_or(file_size);
569 let stream = response.bytes_stream().map(|r| r.map_err(HFError::from));
570 progress.emit(DownloadEvent::Start {
571 total_files: 1,
572 total_bytes,
573 });
574 let boxed: HFByteStream = Box::new(Box::pin(stream));
575 let wrapped = wrap_stream_with_progress(boxed, progress, remote_path, total_bytes);
576 #[cfg(target_family = "wasm")]
577 let wrapped = crate::repository::download::buffer_wasm_stream(wrapped);
578 Ok((content_length, wrapped))
579 }
580
581 #[cfg(not(target_family = "wasm"))]
584 async fn upload_files_impl(&self, files: Vec<BucketUpload>, progress: Option<Progress>) -> HFResult<()> {
585 if files.is_empty() {
586 return Ok(());
587 }
588
589 let total_bytes: u64 = files
590 .iter()
591 .filter_map(|f| std::fs::metadata(&f.local).ok())
592 .map(|m| m.len())
593 .sum();
594 progress.emit(UploadEvent::Start {
595 total_files: files.len(),
596 total_bytes,
597 });
598
599 let xet_files: Vec<(String, crate::repository::AddSource)> = files
600 .iter()
601 .map(|f| (f.remote.clone(), crate::repository::AddSource::file(f.local.clone())))
602 .collect();
603
604 let xet_infos = self.xet_upload(&xet_files, &progress).await?;
605
606 let add_files: Vec<BucketAddFile> = files
607 .iter()
608 .zip(xet_infos.iter())
609 .map(|(f, xet_info)| {
610 let metadata = std::fs::metadata(&f.local).ok();
611 let size = metadata.as_ref().map(|m| m.len()).or(xet_info.file_size).unwrap_or(0);
612 let mtime = metadata
613 .as_ref()
614 .and_then(|m| m.modified().ok())
615 .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
616 .map(|d| d.as_secs());
617
618 BucketAddFile {
619 path: f.remote.clone(),
620 xet_hash: xet_info.hash.clone(),
621 size,
622 mtime,
623 content_type: None,
624 }
625 })
626 .collect();
627
628 self.batch_operations().add_files(add_files).send().await?;
629
630 progress.emit(UploadEvent::Complete);
631 Ok(())
632 }
633
634 #[cfg(not(target_family = "wasm"))]
637 async fn download_files_impl(&self, files: Vec<BucketDownload>, progress: Option<Progress>) -> HFResult<()> {
638 if files.is_empty() {
639 return Ok(());
640 }
641
642 let remote_paths: Vec<String> = files.iter().map(|f| f.remote.clone()).collect();
643 let entries = self.get_paths_info().paths(remote_paths).send().await?;
644
645 let entry_map: std::collections::HashMap<String, BucketTreeEntry> = entries
646 .into_iter()
647 .map(|e| {
648 let path = match &e {
649 BucketTreeEntry::File { path, .. } => path.clone(),
650 BucketTreeEntry::Directory { path, .. } => path.clone(),
651 };
652 (path, e)
653 })
654 .collect();
655
656 let mut xet_batch_files = Vec::new();
657 let mut total_bytes: u64 = 0;
658
659 for f in &files {
660 match entry_map.get(&f.remote) {
661 Some(BucketTreeEntry::File { xet_hash, size, .. }) => {
662 total_bytes += size;
663 xet_batch_files.push(crate::xet::XetBatchFile {
664 hash: xet_hash.clone(),
665 file_size: *size,
666 path: f.local.clone(),
667 filename: f.remote.clone(),
668 });
669 },
670 Some(BucketTreeEntry::Directory { path, .. }) => {
671 return Err(HFError::InvalidParameter(format!("Cannot download directory entry: {path}")));
672 },
673 None => {
674 return Err(HFError::EntryNotFound {
675 path: f.remote.clone(),
676 repo_id: self.bucket_id(),
677 context: None,
678 });
679 },
680 }
681 }
682
683 progress.emit(DownloadEvent::Start {
684 total_files: xet_batch_files.len(),
685 total_bytes,
686 });
687
688 self.xet_download_batch(&xet_batch_files, &progress).await?;
689
690 progress.emit(DownloadEvent::Complete);
691 Ok(())
692 }
693
694 async fn upload_sources_impl(
697 &self,
698 uploads: Vec<(String, crate::repository::AddSource)>,
699 progress: Option<Progress>,
700 ) -> HFResult<()> {
701 if uploads.is_empty() {
702 return Ok(());
703 }
704
705 let total_bytes: u64 = uploads
706 .iter()
707 .map(|(_, source)| match source {
708 crate::repository::AddSource::Bytes(b) => b.len() as u64,
709 crate::repository::AddSource::Stream(s) => s.size(),
710 #[cfg(not(target_family = "wasm"))]
711 crate::repository::AddSource::File(p) => std::fs::metadata(p).map(|m| m.len()).unwrap_or(0),
712 })
713 .sum();
714 progress.emit(UploadEvent::Start {
715 total_files: uploads.len(),
716 total_bytes,
717 });
718
719 let xet_infos = self.xet_upload(&uploads, &progress).await?;
720
721 let add_files: Vec<BucketAddFile> = uploads
722 .iter()
723 .zip(xet_infos.iter())
724 .map(|((remote, _source), xet_info)| BucketAddFile {
725 path: remote.clone(),
726 xet_hash: xet_info.hash.clone(),
727 size: xet_info.file_size.unwrap_or(0),
728 mtime: None,
729 content_type: None,
730 })
731 .collect();
732
733 self.batch_operations().add_files(add_files).send().await?;
734
735 progress.emit(UploadEvent::Complete);
736 Ok(())
737 }
738}
739
740#[derive(Debug, Clone)]
746pub struct BucketAddFile {
747 pub path: String,
749 pub xet_hash: String,
751 pub size: u64,
753 pub mtime: Option<u64>,
755 pub content_type: Option<String>,
757}
758
759#[derive(Debug, Clone, Copy, PartialEq, Eq)]
765pub enum BucketCopySourceType {
766 Bucket,
768 Model,
770 Dataset,
772 Space,
774}
775
776impl BucketCopySourceType {
777 pub fn as_str(&self) -> &'static str {
779 match self {
780 BucketCopySourceType::Bucket => "bucket",
781 BucketCopySourceType::Model => "model",
782 BucketCopySourceType::Dataset => "dataset",
783 BucketCopySourceType::Space => "space",
784 }
785 }
786}
787
788#[derive(Debug, Clone)]
794pub struct BucketCopyFile {
795 pub path: String,
797 pub xet_hash: String,
799 pub source_repo_type: BucketCopySourceType,
801 pub source_repo_id: String,
803 pub size: Option<u64>,
805 pub mtime: Option<u64>,
809 pub content_type: Option<String>,
811}
812
813#[derive(Debug, Clone, Serialize, Deserialize)]
818pub struct BucketInfo {
819 pub id: String,
821 pub private: bool,
823 #[serde(rename = "createdAt")]
825 pub created_at: String,
826 pub size: u64,
828 #[serde(rename = "totalFiles")]
830 pub total_files: u64,
831}
832
833#[derive(Debug, Clone, Serialize, Deserialize)]
837pub struct BucketUrl {
838 pub url: String,
840}
841
842#[derive(Debug, Clone, Serialize, Deserialize)]
847#[serde(tag = "type", rename_all = "lowercase")]
848pub enum BucketTreeEntry {
849 File {
851 path: String,
853 size: u64,
855 #[serde(rename = "xetHash")]
857 xet_hash: String,
858 mtime: Option<String>,
860 uploaded_at: Option<String>,
862 },
863 Directory {
865 path: String,
867 uploaded_at: Option<String>,
869 },
870}
871
872#[derive(Debug, Clone)]
876pub struct BucketFileMetadata {
877 pub size: u64,
879 pub xet_hash: String,
881}
882
883#[cfg(not(target_family = "wasm"))]
890#[derive(Debug, Clone)]
891pub struct BucketUpload {
892 pub local: PathBuf,
895 pub remote: String,
899}
900
901#[cfg(not(target_family = "wasm"))]
902impl BucketUpload {
903 pub fn new(local: impl Into<PathBuf>, remote: impl Into<String>) -> Self {
906 Self {
907 local: local.into(),
908 remote: remote.into(),
909 }
910 }
911}
912
913#[cfg(not(target_family = "wasm"))]
920#[derive(Debug, Clone)]
921pub struct BucketDownload {
922 pub remote: String,
927 pub local: PathBuf,
930}
931
932#[cfg(not(target_family = "wasm"))]
933impl BucketDownload {
934 pub fn new(remote: impl Into<String>, local: impl Into<PathBuf>) -> Self {
937 Self {
938 remote: remote.into(),
939 local: local.into(),
940 }
941 }
942}
943
944#[bon]
945impl HFClient {
946 pub fn bucket(&self, owner: impl Into<String>, name: impl Into<String>) -> HFBucket {
951 HFBucket::new(self.clone(), owner, name)
952 }
953
954 #[builder(finish_fn = send, derive(Debug, Clone))]
967 pub async fn create_bucket(
968 &self,
969 namespace: &str,
971 name: &str,
973 #[builder(default)]
975 private: bool,
976 #[builder(into)]
978 resource_group_id: Option<String>,
979 #[builder(default)]
981 exist_ok: bool,
982 ) -> HFResult<BucketUrl> {
983 let url = format!("{}/api/buckets/{}/{}", self.endpoint(), namespace, name);
984
985 let mut body = serde_json::json!({});
986 if private {
987 body["private"] = serde_json::Value::Bool(true);
988 }
989 if let Some(rg) = resource_group_id {
990 body["resourceGroupId"] = serde_json::Value::String(rg);
991 }
992
993 let headers = self.auth_headers();
994 let response = retry::retry(self.retry_config(), || {
995 self.http_client().post(&url).headers(headers.clone()).json(&body).send()
996 })
997 .await?;
998
999 let bucket_id = format!("{}/{}", namespace, name);
1000
1001 if response.status().as_u16() == 409 && exist_ok {
1002 return Ok(BucketUrl {
1003 url: format!("{}/buckets/{}", self.endpoint(), bucket_id),
1004 });
1005 }
1006
1007 let response = self
1008 .check_response(response, Some(&bucket_id), NotFoundContext::Generic)
1009 .await?;
1010 Ok(response.json().await?)
1011 }
1012
1013 #[builder(finish_fn = send, derive(Debug, Clone))]
1020 pub async fn delete_bucket(
1021 &self,
1022 bucket_id: &str,
1024 #[builder(default)]
1026 missing_ok: bool,
1027 ) -> HFResult<()> {
1028 let url = self.bucket_api_url(bucket_id);
1029
1030 let headers = self.auth_headers();
1031 let response =
1032 retry::retry(self.retry_config(), || self.http_client().delete(&url).headers(headers.clone()).send())
1033 .await?;
1034
1035 if response.status().as_u16() == 404 && missing_ok {
1036 return Ok(());
1037 }
1038
1039 self.check_response(response, Some(bucket_id), NotFoundContext::Bucket).await?;
1040 Ok(())
1041 }
1042
1043 #[builder(finish_fn = send, derive(Debug, Clone))]
1053 pub fn list_buckets(
1054 &self,
1055 #[builder(into)]
1057 namespace: String,
1058 ) -> HFResult<impl Stream<Item = HFResult<BucketInfo>> + '_> {
1059 let url = Url::parse(&format!("{}/api/buckets/{}", self.endpoint(), namespace))?;
1060 Ok(self.paginate(url, vec![], None))
1061 }
1062
1063 #[builder(finish_fn = send, derive(Debug, Clone))]
1072 pub async fn move_bucket(
1073 &self,
1074 from_id: &str,
1076 to_id: &str,
1078 ) -> HFResult<()> {
1079 let url = format!("{}/api/repos/move", self.endpoint());
1080 let body = serde_json::json!({
1081 "fromRepo": from_id,
1082 "toRepo": to_id,
1083 "type": "bucket",
1084 });
1085
1086 let headers = self.auth_headers();
1087 let response = retry::retry(self.retry_config(), || {
1088 self.http_client().post(&url).headers(headers.clone()).json(&body).send()
1089 })
1090 .await?;
1091
1092 self.check_response(response, None, NotFoundContext::Generic).await?;
1093 Ok(())
1094 }
1095}
1096
1097#[cfg(feature = "blocking")]
1098#[bon]
1099impl crate::blocking::HFClientSync {
1100 #[builder(finish_fn = send, derive(Debug, Clone))]
1103 pub fn create_bucket(
1104 &self,
1105 namespace: &str,
1106 name: &str,
1107 #[builder(default)] private: bool,
1108 #[builder(into)] resource_group_id: Option<String>,
1109 #[builder(default)] exist_ok: bool,
1110 ) -> HFResult<BucketUrl> {
1111 self.runtime.block_on(
1112 self.inner
1113 .create_bucket()
1114 .namespace(namespace)
1115 .name(name)
1116 .private(private)
1117 .maybe_resource_group_id(resource_group_id)
1118 .exist_ok(exist_ok)
1119 .send(),
1120 )
1121 }
1122
1123 #[builder(finish_fn = send, derive(Debug, Clone))]
1126 pub fn delete_bucket(&self, bucket_id: &str, #[builder(default)] missing_ok: bool) -> HFResult<()> {
1127 self.runtime
1128 .block_on(self.inner.delete_bucket().bucket_id(bucket_id).missing_ok(missing_ok).send())
1129 }
1130
1131 #[builder(finish_fn = send, derive(Debug, Clone))]
1134 pub fn list_buckets(&self, #[builder(into)] namespace: String) -> HFResult<Vec<BucketInfo>> {
1135 self.runtime.block_on(async move {
1136 let stream = self.inner.list_buckets().namespace(namespace).send()?;
1137 futures::pin_mut!(stream);
1138 let mut items = Vec::new();
1139 while let Some(item) = stream.next().await {
1140 items.push(item?);
1141 }
1142 Ok(items)
1143 })
1144 }
1145
1146 #[builder(finish_fn = send, derive(Debug, Clone))]
1149 pub fn move_bucket(&self, from_id: &str, to_id: &str) -> HFResult<()> {
1150 self.runtime
1151 .block_on(self.inner.move_bucket().from_id(from_id).to_id(to_id).send())
1152 }
1153}
1154
1155#[cfg(feature = "blocking")]
1156#[bon]
1157impl crate::blocking::HFBucketSync {
1158 #[builder(finish_fn = send, derive(Debug, Clone))]
1160 pub fn info(&self) -> HFResult<BucketInfo> {
1161 self.runtime.block_on(self.inner.info().send())
1162 }
1163
1164 #[builder(finish_fn = send, derive(Debug, Clone))]
1167 pub fn list_tree(
1168 &self,
1169 #[builder(into)] prefix: Option<String>,
1170 #[builder(default)] recursive: bool,
1171 ) -> HFResult<Vec<BucketTreeEntry>> {
1172 self.runtime.block_on(async move {
1173 let stream = self.inner.list_tree().maybe_prefix(prefix).recursive(recursive).send()?;
1174 futures::pin_mut!(stream);
1175 let mut items = Vec::new();
1176 while let Some(item) = stream.next().await {
1177 items.push(item?);
1178 }
1179 Ok(items)
1180 })
1181 }
1182
1183 #[builder(finish_fn = send, derive(Debug, Clone))]
1186 pub fn get_paths_info(&self, paths: Vec<String>) -> HFResult<Vec<BucketTreeEntry>> {
1187 self.runtime.block_on(self.inner.get_paths_info().paths(paths).send())
1188 }
1189
1190 #[builder(finish_fn = send, derive(Debug, Clone))]
1193 pub fn get_file_metadata(&self, #[builder(into)] remote_path: String) -> HFResult<BucketFileMetadata> {
1194 self.runtime
1195 .block_on(self.inner.get_file_metadata().remote_path(remote_path).send())
1196 }
1197
1198 #[builder(finish_fn = send, derive(Debug, Clone))]
1201 pub fn batch_operations(
1202 &self,
1203 #[builder(default)] add_files: Vec<BucketAddFile>,
1204 #[builder(default)] delete: Vec<String>,
1205 #[builder(default)] copy: Vec<BucketCopyFile>,
1206 ) -> HFResult<()> {
1207 self.runtime.block_on(
1208 self.inner
1209 .batch_operations()
1210 .add_files(add_files)
1211 .delete(delete)
1212 .copy(copy)
1213 .send(),
1214 )
1215 }
1216
1217 #[builder(finish_fn = send, derive(Debug, Clone))]
1220 pub fn delete_files(&self, paths: Vec<String>) -> HFResult<()> {
1221 self.runtime.block_on(self.inner.delete_files().paths(paths).send())
1222 }
1223
1224 #[builder(finish_fn = send, derive(Debug, Clone))]
1227 pub fn upload_source_files(
1228 &self,
1229 files: Vec<(String, crate::repository::AddSource)>,
1230 #[builder(into)] progress: Option<Progress>,
1231 ) -> HFResult<()> {
1232 self.runtime
1233 .block_on(self.inner.upload_source_files().files(files).maybe_progress(progress).send())
1234 }
1235
1236 #[builder(finish_fn = send, derive(Debug, Clone))]
1239 pub fn upload_files(&self, files: Vec<BucketUpload>, #[builder(into)] progress: Option<Progress>) -> HFResult<()> {
1240 self.runtime
1241 .block_on(self.inner.upload_files().files(files).maybe_progress(progress).send())
1242 }
1243
1244 #[builder(finish_fn = send, derive(Debug, Clone))]
1247 pub fn download_files(
1248 &self,
1249 files: Vec<BucketDownload>,
1250 #[builder(into)] progress: Option<Progress>,
1251 ) -> HFResult<()> {
1252 self.runtime
1253 .block_on(self.inner.download_files().files(files).maybe_progress(progress).send())
1254 }
1255}
1256
1257#[cfg(test)]
1258mod tests {
1259 use super::{BucketCopyFile, BucketCopySourceType, HFBucket};
1260
1261 #[test]
1262 fn test_bucket_accessors() {
1263 let client = crate::HFClient::builder().build().unwrap();
1264 let bucket = HFBucket::new(client, "my-org", "my-bucket");
1265
1266 assert_eq!(bucket.owner(), "my-org");
1267 assert_eq!(bucket.name(), "my-bucket");
1268 assert_eq!(bucket.bucket_id(), "my-org/my-bucket");
1269 }
1270
1271 #[test]
1272 fn test_bucket_copy_source_type_wire_strings() {
1273 assert_eq!(BucketCopySourceType::Bucket.as_str(), "bucket");
1274 assert_eq!(BucketCopySourceType::Model.as_str(), "model");
1275 assert_eq!(BucketCopySourceType::Dataset.as_str(), "dataset");
1276 assert_eq!(BucketCopySourceType::Space.as_str(), "space");
1277 }
1278
1279 #[test]
1280 fn test_bucket_copy_file_optional_fields_construct() {
1281 let with_extras = BucketCopyFile {
1282 path: "p".into(),
1283 xet_hash: "x".into(),
1284 source_repo_type: BucketCopySourceType::Bucket,
1285 source_repo_id: "u/b".into(),
1286 size: Some(42),
1287 mtime: Some(1_700_000_000_000),
1288 content_type: Some("text/plain".into()),
1289 };
1290 assert_eq!(with_extras.size, Some(42));
1291 assert_eq!(with_extras.mtime, Some(1_700_000_000_000));
1292 assert_eq!(with_extras.content_type.as_deref(), Some("text/plain"));
1293
1294 let bare = BucketCopyFile {
1295 path: "p".into(),
1296 xet_hash: "x".into(),
1297 source_repo_type: BucketCopySourceType::Bucket,
1298 source_repo_id: "u/b".into(),
1299 size: None,
1300 mtime: None,
1301 content_type: None,
1302 };
1303 assert!(bare.size.is_none() && bare.mtime.is_none() && bare.content_type.is_none());
1304 }
1305}