#[cfg(not(target_family = "wasm"))]
pub mod sync;
#[cfg(not(target_family = "wasm"))]
use std::path::PathBuf;
use bon::bon;
use futures::{Stream, StreamExt};
use serde::{Deserialize, Serialize};
use url::Url;
use crate::client::HFClient;
use crate::error::{HFError, HFResult, NotFoundContext};
use crate::progress::{DownloadEvent, EmitEvent, Progress, UploadEvent};
use crate::repository::download::{HFByteStream, wrap_stream_with_progress};
use crate::retry;
const BUCKET_BATCH_CHUNK_SIZE: usize = 1000;
const BUCKET_PATHS_INFO_BATCH_SIZE: usize = 1000;
#[derive(Clone)]
pub struct HFBucket {
pub(crate) hf_client: HFClient,
pub(super) owner: String,
pub(super) name: String,
}
impl std::fmt::Debug for HFBucket {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("HFBucket")
.field("owner", &self.owner)
.field("name", &self.name)
.finish()
}
}
impl HFBucket {
pub fn new(client: HFClient, owner: impl Into<String>, name: impl Into<String>) -> Self {
Self {
hf_client: client,
owner: owner.into(),
name: name.into(),
}
}
pub fn client(&self) -> &HFClient {
&self.hf_client
}
pub fn owner(&self) -> &str {
&self.owner
}
pub fn name(&self) -> &str {
&self.name
}
pub fn bucket_id(&self) -> String {
format!("{}/{}", self.owner, self.name)
}
}
#[bon]
impl HFBucket {
#[builder(finish_fn = send, derive(Debug, Clone))]
pub async fn info(&self) -> HFResult<BucketInfo> {
let bucket_id = self.bucket_id();
let url = self.hf_client.bucket_api_url(&bucket_id);
let headers = self.hf_client.auth_headers();
let response = retry::retry(self.hf_client.retry_config(), || {
self.hf_client.http_client().get(&url).headers(headers.clone()).send()
})
.await?;
let response = self
.hf_client
.check_response(response, Some(&bucket_id), NotFoundContext::Bucket)
.await?;
Ok(response.json().await?)
}
#[builder(finish_fn = send, derive(Debug, Clone))]
pub fn list_tree(
&self,
#[builder(into)]
prefix: Option<String>,
#[builder(default)]
recursive: bool,
) -> HFResult<impl Stream<Item = HFResult<BucketTreeEntry>> + '_> {
let bucket_id = self.bucket_id();
let url_str = format!("{}/api/buckets/{}/tree", self.hf_client.endpoint(), bucket_id);
let mut url = Url::parse(&url_str)?;
if let Some(ref prefix) = prefix {
crate::client::append_path_segments(&mut url, prefix)?;
}
let mut query = vec![];
if recursive {
query.push(("recursive".to_string(), "true".to_string()));
}
Ok(self.hf_client.paginate(url, query, None))
}
#[builder(finish_fn = send, derive(Debug, Clone))]
pub async fn get_paths_info(
&self,
paths: Vec<String>,
) -> HFResult<Vec<BucketTreeEntry>> {
let bucket_id = self.bucket_id();
let url = format!("{}/api/buckets/{}/paths-info", self.hf_client.endpoint(), bucket_id);
let headers = self.hf_client.auth_headers();
let mut all_entries = Vec::new();
for chunk in paths.chunks(BUCKET_PATHS_INFO_BATCH_SIZE) {
let body = serde_json::json!({ "paths": chunk });
let response = retry::retry(self.hf_client.retry_config(), || {
self.hf_client
.http_client()
.post(&url)
.headers(headers.clone())
.json(&body)
.send()
})
.await?;
let response = self
.hf_client
.check_response(response, Some(&bucket_id), NotFoundContext::Bucket)
.await?;
let entries: Vec<BucketTreeEntry> = response.json().await?;
all_entries.extend(entries);
}
Ok(all_entries)
}
#[builder(finish_fn = send, derive(Debug, Clone))]
pub async fn download_file_stream(
&self,
#[builder(into)]
remote_path: String,
#[builder(into)]
progress: Option<Progress>,
) -> HFResult<(Option<u64>, HFByteStream)> {
Box::pin(self.download_file_stream_impl(remote_path, progress)).await
}
#[cfg(not(target_family = "wasm"))]
#[builder(finish_fn = send, derive(Debug, Clone))]
pub async fn get_file_metadata(
&self,
#[builder(into)]
remote_path: String,
) -> HFResult<BucketFileMetadata> {
let bucket_id = self.bucket_id();
let mut url = Url::parse(&format!("{}/buckets/{}/resolve", self.hf_client.endpoint(), bucket_id))?;
crate::client::append_path_segments(&mut url, &remote_path)?;
let headers = self.hf_client.auth_headers();
let response = retry::retry(self.hf_client.retry_config(), || {
self.hf_client
.no_redirect_client()
.head(url.clone())
.headers(headers.clone())
.send()
})
.await?;
let response = self
.hf_client
.check_response(response, Some(&bucket_id), NotFoundContext::Entry { path: remote_path })
.await?;
let size = response
.headers()
.get(reqwest::header::CONTENT_LENGTH)
.and_then(|v| v.to_str().ok())
.and_then(|v| v.parse::<u64>().ok())
.unwrap_or(0);
let xet_hash = response
.headers()
.get("x-xet-hash")
.and_then(|v| v.to_str().ok())
.unwrap_or("")
.to_string();
Ok(BucketFileMetadata { size, xet_hash })
}
#[builder(finish_fn = send, derive(Debug, Clone))]
pub async fn batch_operations(
&self,
#[builder(default)]
add_files: Vec<BucketAddFile>,
#[builder(default)]
delete: Vec<String>,
#[builder(default)]
copy: Vec<BucketCopyFile>,
) -> HFResult<()> {
let bucket_id = self.bucket_id();
let url = format!("{}/api/buckets/{}/batch", self.hf_client.endpoint(), bucket_id);
let mut lines: Vec<String> = Vec::new();
for add in &add_files {
let mut obj = serde_json::json!({
"type": "addFile",
"path": add.path,
"xetHash": add.xet_hash,
"size": add.size,
});
if let Some(mtime) = add.mtime {
obj["mtime"] = serde_json::Value::Number(mtime.into());
}
if let Some(ref ct) = add.content_type {
obj["contentType"] = serde_json::Value::String(ct.clone());
}
lines.push(serde_json::to_string(&obj)?);
}
for path in &delete {
let obj = serde_json::json!({
"type": "deleteFile",
"path": path,
});
lines.push(serde_json::to_string(&obj)?);
}
for copy in © {
let mut obj = serde_json::json!({
"type": "copyFile",
"path": copy.path,
"xetHash": copy.xet_hash,
"sourceRepoType": copy.source_repo_type.as_str(),
"sourceRepoId": copy.source_repo_id,
});
if let Some(size) = copy.size {
obj["size"] = serde_json::Value::Number(size.into());
}
if let Some(mtime) = copy.mtime {
obj["mtime"] = serde_json::Value::Number(mtime.into());
}
if let Some(ref ct) = copy.content_type {
obj["contentType"] = serde_json::Value::String(ct.clone());
}
lines.push(serde_json::to_string(&obj)?);
}
let headers = self.hf_client.auth_headers();
for chunk in lines.chunks(BUCKET_BATCH_CHUNK_SIZE) {
let body = chunk.join("\n") + "\n";
let response = retry::retry(self.hf_client.retry_config(), || {
self.hf_client
.http_client()
.post(&url)
.headers(headers.clone())
.header(reqwest::header::CONTENT_TYPE, "application/x-ndjson")
.body(body.clone())
.send()
})
.await?;
self.hf_client
.check_response(response, Some(&bucket_id), NotFoundContext::Bucket)
.await?;
}
Ok(())
}
#[builder(finish_fn = send, derive(Debug, Clone))]
pub async fn delete_files(
&self,
paths: Vec<String>,
) -> HFResult<()> {
self.batch_operations().delete(paths).send().await
}
#[builder(finish_fn = send, derive(Debug, Clone))]
pub async fn upload_source_files(
&self,
files: Vec<(String, crate::repository::AddSource)>,
#[builder(into)]
progress: Option<Progress>,
) -> HFResult<()> {
Box::pin(self.upload_sources_impl(files, progress)).await
}
#[cfg(not(target_family = "wasm"))]
#[builder(finish_fn = send, derive(Debug, Clone))]
pub async fn upload_files(
&self,
files: Vec<BucketUpload>,
#[builder(into)]
progress: Option<Progress>,
) -> HFResult<()> {
Box::pin(self.upload_files_impl(files, progress)).await
}
#[cfg(not(target_family = "wasm"))]
#[builder(finish_fn = send, derive(Debug, Clone))]
pub async fn download_files(
&self,
files: Vec<BucketDownload>,
#[builder(into)]
progress: Option<Progress>,
) -> HFResult<()> {
Box::pin(self.download_files_impl(files, progress)).await
}
}
impl HFBucket {
async fn download_file_stream_impl(
&self,
remote_path: String,
progress: Option<Progress>,
) -> HFResult<(Option<u64>, HFByteStream)> {
let bucket_id = self.bucket_id();
let entries = self.get_paths_info().paths(vec![remote_path.clone()]).send().await?;
let (xet_hash, file_size) = entries
.into_iter()
.find_map(|e| match e {
BucketTreeEntry::File {
path, size, xet_hash, ..
} if path == remote_path => Some((xet_hash, size)),
_ => None,
})
.ok_or_else(|| HFError::EntryNotFound {
path: remote_path.clone(),
repo_id: bucket_id.clone(),
context: None,
})?;
if !xet_hash.is_empty() {
let stream = self.xet_download_stream(&xet_hash, file_size).await?;
progress.emit(DownloadEvent::Start {
total_files: 1,
total_bytes: file_size,
});
let boxed: HFByteStream = Box::new(Box::pin(stream));
let wrapped = wrap_stream_with_progress(boxed, progress, remote_path, file_size);
#[cfg(target_family = "wasm")]
let wrapped = crate::repository::download::buffer_wasm_stream(wrapped);
return Ok((Some(file_size), wrapped));
}
let mut url = Url::parse(&format!("{}/buckets/{}/resolve", self.hf_client.endpoint(), bucket_id))?;
crate::client::append_path_segments(&mut url, &remote_path)?;
let headers = self.hf_client.auth_headers();
let response = retry::retry(self.hf_client.retry_config(), || {
self.hf_client.http_client().get(url.clone()).headers(headers.clone()).send()
})
.await?;
let response = self
.hf_client
.check_response(
response,
Some(&bucket_id),
NotFoundContext::Entry {
path: remote_path.clone(),
},
)
.await?;
let content_length = response.content_length().or(Some(file_size));
let total_bytes = content_length.unwrap_or(file_size);
let stream = response.bytes_stream().map(|r| r.map_err(HFError::from));
progress.emit(DownloadEvent::Start {
total_files: 1,
total_bytes,
});
let boxed: HFByteStream = Box::new(Box::pin(stream));
let wrapped = wrap_stream_with_progress(boxed, progress, remote_path, total_bytes);
#[cfg(target_family = "wasm")]
let wrapped = crate::repository::download::buffer_wasm_stream(wrapped);
Ok((content_length, wrapped))
}
#[cfg(not(target_family = "wasm"))]
async fn upload_files_impl(&self, files: Vec<BucketUpload>, progress: Option<Progress>) -> HFResult<()> {
if files.is_empty() {
return Ok(());
}
let total_bytes: u64 = files
.iter()
.filter_map(|f| std::fs::metadata(&f.local).ok())
.map(|m| m.len())
.sum();
progress.emit(UploadEvent::Start {
total_files: files.len(),
total_bytes,
});
let xet_files: Vec<(String, crate::repository::AddSource)> = files
.iter()
.map(|f| (f.remote.clone(), crate::repository::AddSource::file(f.local.clone())))
.collect();
let xet_infos = self.xet_upload(&xet_files, &progress).await?;
let add_files: Vec<BucketAddFile> = files
.iter()
.zip(xet_infos.iter())
.map(|(f, xet_info)| {
let metadata = std::fs::metadata(&f.local).ok();
let size = metadata.as_ref().map(|m| m.len()).or(xet_info.file_size).unwrap_or(0);
let mtime = metadata
.as_ref()
.and_then(|m| m.modified().ok())
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_secs());
BucketAddFile {
path: f.remote.clone(),
xet_hash: xet_info.hash.clone(),
size,
mtime,
content_type: None,
}
})
.collect();
self.batch_operations().add_files(add_files).send().await?;
progress.emit(UploadEvent::Complete);
Ok(())
}
#[cfg(not(target_family = "wasm"))]
async fn download_files_impl(&self, files: Vec<BucketDownload>, progress: Option<Progress>) -> HFResult<()> {
if files.is_empty() {
return Ok(());
}
let remote_paths: Vec<String> = files.iter().map(|f| f.remote.clone()).collect();
let entries = self.get_paths_info().paths(remote_paths).send().await?;
let entry_map: std::collections::HashMap<String, BucketTreeEntry> = entries
.into_iter()
.map(|e| {
let path = match &e {
BucketTreeEntry::File { path, .. } => path.clone(),
BucketTreeEntry::Directory { path, .. } => path.clone(),
};
(path, e)
})
.collect();
let mut xet_batch_files = Vec::new();
let mut total_bytes: u64 = 0;
for f in &files {
match entry_map.get(&f.remote) {
Some(BucketTreeEntry::File { xet_hash, size, .. }) => {
total_bytes += size;
xet_batch_files.push(crate::xet::XetBatchFile {
hash: xet_hash.clone(),
file_size: *size,
path: f.local.clone(),
filename: f.remote.clone(),
});
},
Some(BucketTreeEntry::Directory { path, .. }) => {
return Err(HFError::InvalidParameter(format!("Cannot download directory entry: {path}")));
},
None => {
return Err(HFError::EntryNotFound {
path: f.remote.clone(),
repo_id: self.bucket_id(),
context: None,
});
},
}
}
progress.emit(DownloadEvent::Start {
total_files: xet_batch_files.len(),
total_bytes,
});
self.xet_download_batch(&xet_batch_files, &progress).await?;
progress.emit(DownloadEvent::Complete);
Ok(())
}
async fn upload_sources_impl(
&self,
uploads: Vec<(String, crate::repository::AddSource)>,
progress: Option<Progress>,
) -> HFResult<()> {
if uploads.is_empty() {
return Ok(());
}
let total_bytes: u64 = uploads
.iter()
.map(|(_, source)| match source {
crate::repository::AddSource::Bytes(b) => b.len() as u64,
crate::repository::AddSource::Stream(s) => s.size(),
#[cfg(not(target_family = "wasm"))]
crate::repository::AddSource::File(p) => std::fs::metadata(p).map(|m| m.len()).unwrap_or(0),
})
.sum();
progress.emit(UploadEvent::Start {
total_files: uploads.len(),
total_bytes,
});
let xet_infos = self.xet_upload(&uploads, &progress).await?;
let add_files: Vec<BucketAddFile> = uploads
.iter()
.zip(xet_infos.iter())
.map(|((remote, _source), xet_info)| BucketAddFile {
path: remote.clone(),
xet_hash: xet_info.hash.clone(),
size: xet_info.file_size.unwrap_or(0),
mtime: None,
content_type: None,
})
.collect();
self.batch_operations().add_files(add_files).send().await?;
progress.emit(UploadEvent::Complete);
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct BucketAddFile {
pub path: String,
pub xet_hash: String,
pub size: u64,
pub mtime: Option<u64>,
pub content_type: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BucketCopySourceType {
Bucket,
Model,
Dataset,
Space,
}
impl BucketCopySourceType {
pub fn as_str(&self) -> &'static str {
match self {
BucketCopySourceType::Bucket => "bucket",
BucketCopySourceType::Model => "model",
BucketCopySourceType::Dataset => "dataset",
BucketCopySourceType::Space => "space",
}
}
}
#[derive(Debug, Clone)]
pub struct BucketCopyFile {
pub path: String,
pub xet_hash: String,
pub source_repo_type: BucketCopySourceType,
pub source_repo_id: String,
pub size: Option<u64>,
pub mtime: Option<u64>,
pub content_type: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BucketInfo {
pub id: String,
pub private: bool,
#[serde(rename = "createdAt")]
pub created_at: String,
pub size: u64,
#[serde(rename = "totalFiles")]
pub total_files: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BucketUrl {
pub url: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum BucketTreeEntry {
File {
path: String,
size: u64,
#[serde(rename = "xetHash")]
xet_hash: String,
mtime: Option<String>,
uploaded_at: Option<String>,
},
Directory {
path: String,
uploaded_at: Option<String>,
},
}
#[derive(Debug, Clone)]
pub struct BucketFileMetadata {
pub size: u64,
pub xet_hash: String,
}
#[cfg(not(target_family = "wasm"))]
#[derive(Debug, Clone)]
pub struct BucketUpload {
pub local: PathBuf,
pub remote: String,
}
#[cfg(not(target_family = "wasm"))]
impl BucketUpload {
pub fn new(local: impl Into<PathBuf>, remote: impl Into<String>) -> Self {
Self {
local: local.into(),
remote: remote.into(),
}
}
}
#[cfg(not(target_family = "wasm"))]
#[derive(Debug, Clone)]
pub struct BucketDownload {
pub remote: String,
pub local: PathBuf,
}
#[cfg(not(target_family = "wasm"))]
impl BucketDownload {
pub fn new(remote: impl Into<String>, local: impl Into<PathBuf>) -> Self {
Self {
remote: remote.into(),
local: local.into(),
}
}
}
#[bon]
impl HFClient {
pub fn bucket(&self, owner: impl Into<String>, name: impl Into<String>) -> HFBucket {
HFBucket::new(self.clone(), owner, name)
}
#[builder(finish_fn = send, derive(Debug, Clone))]
pub async fn create_bucket(
&self,
namespace: &str,
name: &str,
#[builder(default)]
private: bool,
#[builder(into)]
resource_group_id: Option<String>,
#[builder(default)]
exist_ok: bool,
) -> HFResult<BucketUrl> {
let url = format!("{}/api/buckets/{}/{}", self.endpoint(), namespace, name);
let mut body = serde_json::json!({});
if private {
body["private"] = serde_json::Value::Bool(true);
}
if let Some(rg) = resource_group_id {
body["resourceGroupId"] = serde_json::Value::String(rg);
}
let headers = self.auth_headers();
let response = retry::retry(self.retry_config(), || {
self.http_client().post(&url).headers(headers.clone()).json(&body).send()
})
.await?;
let bucket_id = format!("{}/{}", namespace, name);
if response.status().as_u16() == 409 && exist_ok {
return Ok(BucketUrl {
url: format!("{}/buckets/{}", self.endpoint(), bucket_id),
});
}
let response = self
.check_response(response, Some(&bucket_id), NotFoundContext::Generic)
.await?;
Ok(response.json().await?)
}
#[builder(finish_fn = send, derive(Debug, Clone))]
pub async fn delete_bucket(
&self,
bucket_id: &str,
#[builder(default)]
missing_ok: bool,
) -> HFResult<()> {
let url = self.bucket_api_url(bucket_id);
let headers = self.auth_headers();
let response =
retry::retry(self.retry_config(), || self.http_client().delete(&url).headers(headers.clone()).send())
.await?;
if response.status().as_u16() == 404 && missing_ok {
return Ok(());
}
self.check_response(response, Some(bucket_id), NotFoundContext::Bucket).await?;
Ok(())
}
#[builder(finish_fn = send, derive(Debug, Clone))]
pub fn list_buckets(
&self,
#[builder(into)]
namespace: String,
) -> HFResult<impl Stream<Item = HFResult<BucketInfo>> + '_> {
let url = Url::parse(&format!("{}/api/buckets/{}", self.endpoint(), namespace))?;
Ok(self.paginate(url, vec![], None))
}
#[builder(finish_fn = send, derive(Debug, Clone))]
pub async fn move_bucket(
&self,
from_id: &str,
to_id: &str,
) -> HFResult<()> {
let url = format!("{}/api/repos/move", self.endpoint());
let body = serde_json::json!({
"fromRepo": from_id,
"toRepo": to_id,
"type": "bucket",
});
let headers = self.auth_headers();
let response = retry::retry(self.retry_config(), || {
self.http_client().post(&url).headers(headers.clone()).json(&body).send()
})
.await?;
self.check_response(response, None, NotFoundContext::Generic).await?;
Ok(())
}
}
#[cfg(feature = "blocking")]
#[bon]
impl crate::blocking::HFClientSync {
#[builder(finish_fn = send, derive(Debug, Clone))]
pub fn create_bucket(
&self,
namespace: &str,
name: &str,
#[builder(default)] private: bool,
#[builder(into)] resource_group_id: Option<String>,
#[builder(default)] exist_ok: bool,
) -> HFResult<BucketUrl> {
self.runtime.block_on(
self.inner
.create_bucket()
.namespace(namespace)
.name(name)
.private(private)
.maybe_resource_group_id(resource_group_id)
.exist_ok(exist_ok)
.send(),
)
}
#[builder(finish_fn = send, derive(Debug, Clone))]
pub fn delete_bucket(&self, bucket_id: &str, #[builder(default)] missing_ok: bool) -> HFResult<()> {
self.runtime
.block_on(self.inner.delete_bucket().bucket_id(bucket_id).missing_ok(missing_ok).send())
}
#[builder(finish_fn = send, derive(Debug, Clone))]
pub fn list_buckets(&self, #[builder(into)] namespace: String) -> HFResult<Vec<BucketInfo>> {
self.runtime.block_on(async move {
let stream = self.inner.list_buckets().namespace(namespace).send()?;
futures::pin_mut!(stream);
let mut items = Vec::new();
while let Some(item) = stream.next().await {
items.push(item?);
}
Ok(items)
})
}
#[builder(finish_fn = send, derive(Debug, Clone))]
pub fn move_bucket(&self, from_id: &str, to_id: &str) -> HFResult<()> {
self.runtime
.block_on(self.inner.move_bucket().from_id(from_id).to_id(to_id).send())
}
}
#[cfg(feature = "blocking")]
#[bon]
impl crate::blocking::HFBucketSync {
#[builder(finish_fn = send, derive(Debug, Clone))]
pub fn info(&self) -> HFResult<BucketInfo> {
self.runtime.block_on(self.inner.info().send())
}
#[builder(finish_fn = send, derive(Debug, Clone))]
pub fn list_tree(
&self,
#[builder(into)] prefix: Option<String>,
#[builder(default)] recursive: bool,
) -> HFResult<Vec<BucketTreeEntry>> {
self.runtime.block_on(async move {
let stream = self.inner.list_tree().maybe_prefix(prefix).recursive(recursive).send()?;
futures::pin_mut!(stream);
let mut items = Vec::new();
while let Some(item) = stream.next().await {
items.push(item?);
}
Ok(items)
})
}
#[builder(finish_fn = send, derive(Debug, Clone))]
pub fn get_paths_info(&self, paths: Vec<String>) -> HFResult<Vec<BucketTreeEntry>> {
self.runtime.block_on(self.inner.get_paths_info().paths(paths).send())
}
#[builder(finish_fn = send, derive(Debug, Clone))]
pub fn get_file_metadata(&self, #[builder(into)] remote_path: String) -> HFResult<BucketFileMetadata> {
self.runtime
.block_on(self.inner.get_file_metadata().remote_path(remote_path).send())
}
#[builder(finish_fn = send, derive(Debug, Clone))]
pub fn batch_operations(
&self,
#[builder(default)] add_files: Vec<BucketAddFile>,
#[builder(default)] delete: Vec<String>,
#[builder(default)] copy: Vec<BucketCopyFile>,
) -> HFResult<()> {
self.runtime.block_on(
self.inner
.batch_operations()
.add_files(add_files)
.delete(delete)
.copy(copy)
.send(),
)
}
#[builder(finish_fn = send, derive(Debug, Clone))]
pub fn delete_files(&self, paths: Vec<String>) -> HFResult<()> {
self.runtime.block_on(self.inner.delete_files().paths(paths).send())
}
#[builder(finish_fn = send, derive(Debug, Clone))]
pub fn upload_source_files(
&self,
files: Vec<(String, crate::repository::AddSource)>,
#[builder(into)] progress: Option<Progress>,
) -> HFResult<()> {
self.runtime
.block_on(self.inner.upload_source_files().files(files).maybe_progress(progress).send())
}
#[builder(finish_fn = send, derive(Debug, Clone))]
pub fn upload_files(&self, files: Vec<BucketUpload>, #[builder(into)] progress: Option<Progress>) -> HFResult<()> {
self.runtime
.block_on(self.inner.upload_files().files(files).maybe_progress(progress).send())
}
#[builder(finish_fn = send, derive(Debug, Clone))]
pub fn download_files(
&self,
files: Vec<BucketDownload>,
#[builder(into)] progress: Option<Progress>,
) -> HFResult<()> {
self.runtime
.block_on(self.inner.download_files().files(files).maybe_progress(progress).send())
}
}
#[cfg(test)]
mod tests {
use super::{BucketCopyFile, BucketCopySourceType, HFBucket};
#[test]
fn test_bucket_accessors() {
let client = crate::HFClient::builder().build().unwrap();
let bucket = HFBucket::new(client, "my-org", "my-bucket");
assert_eq!(bucket.owner(), "my-org");
assert_eq!(bucket.name(), "my-bucket");
assert_eq!(bucket.bucket_id(), "my-org/my-bucket");
}
#[test]
fn test_bucket_copy_source_type_wire_strings() {
assert_eq!(BucketCopySourceType::Bucket.as_str(), "bucket");
assert_eq!(BucketCopySourceType::Model.as_str(), "model");
assert_eq!(BucketCopySourceType::Dataset.as_str(), "dataset");
assert_eq!(BucketCopySourceType::Space.as_str(), "space");
}
#[test]
fn test_bucket_copy_file_optional_fields_construct() {
let with_extras = BucketCopyFile {
path: "p".into(),
xet_hash: "x".into(),
source_repo_type: BucketCopySourceType::Bucket,
source_repo_id: "u/b".into(),
size: Some(42),
mtime: Some(1_700_000_000_000),
content_type: Some("text/plain".into()),
};
assert_eq!(with_extras.size, Some(42));
assert_eq!(with_extras.mtime, Some(1_700_000_000_000));
assert_eq!(with_extras.content_type.as_deref(), Some("text/plain"));
let bare = BucketCopyFile {
path: "p".into(),
xet_hash: "x".into(),
source_repo_type: BucketCopySourceType::Bucket,
source_repo_id: "u/b".into(),
size: None,
mtime: None,
content_type: None,
};
assert!(bare.size.is_none() && bare.mtime.is_none() && bare.content_type.is_none());
}
}