1mod api;
21
22use crate::error::{Result, TrustformersError};
23use api::{CommitFile, LFS_INLINE_THRESHOLD_BYTES};
24use std::path::{Path, PathBuf};
25use tracing::{debug, info, warn};
26
27const HF_HUB_URL: &str = "https://huggingface.co";
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
31pub enum RepoType {
32 #[default]
34 Model,
35 Dataset,
37 Space,
39}
40
41impl RepoType {
42 pub fn as_str(&self) -> &'static str {
44 match self {
45 RepoType::Model => "model",
46 RepoType::Dataset => "dataset",
47 RepoType::Space => "space",
48 }
49 }
50}
51
52#[derive(Debug, Clone)]
54pub struct UploadConfig {
55 pub token: String,
57 pub repo_id: String,
59 pub repo_type: RepoType,
61 pub revision: String,
63 pub commit_message: String,
65 pub create_if_missing: bool,
67 pub private: bool,
69 pub base_url: String,
73 pub dry_run: bool,
77}
78
79impl Default for UploadConfig {
80 fn default() -> Self {
81 Self {
82 token: String::new(),
83 repo_id: String::new(),
84 repo_type: RepoType::Model,
85 revision: "main".to_string(),
86 commit_message: "Upload via TrustformeRS".to_string(),
87 create_if_missing: true,
88 private: false,
89 base_url: HF_HUB_URL.to_string(),
90 dry_run: false,
91 }
92 }
93}
94
95#[derive(Debug, Clone)]
97pub struct UploadFile {
98 pub local_path: PathBuf,
100 pub repo_path: String,
102}
103
104impl UploadFile {
105 pub fn new(local_path: impl Into<PathBuf>, repo_path: impl Into<String>) -> Self {
107 Self {
108 local_path: local_path.into(),
109 repo_path: repo_path.into(),
110 }
111 }
112}
113
114#[derive(Debug, Clone, PartialEq, Eq)]
116pub struct UploadResult {
117 pub repo_id: String,
119 pub revision: String,
121 pub commit_url: Option<String>,
124 pub commit_oid: Option<String>,
126 pub files_uploaded: Vec<String>,
128 pub dry_run: bool,
131}
132
133pub struct HubUploader {
135 config: UploadConfig,
136}
137
138impl HubUploader {
139 pub fn new(config: UploadConfig) -> Self {
141 Self { config }
142 }
143
144 pub fn validate(&self) -> Result<()> {
151 if self.config.token.is_empty() {
152 return Err(missing_credentials_error(&self.config.repo_id));
153 }
154 if self.config.repo_id.is_empty() {
155 return Err(TrustformersError::InvalidInput {
156 message: "Repository ID cannot be empty".to_string(),
157 parameter: Some("repo_id".to_string()),
158 expected: None,
159 received: None,
160 suggestion: None,
161 });
162 }
163 if !self.config.repo_id.contains('/') {
164 return Err(TrustformersError::InvalidInput {
165 message: "Repository ID must be in format 'username/repo-name'".to_string(),
166 parameter: Some("repo_id".to_string()),
167 expected: Some("username/repo-name".to_string()),
168 received: Some(self.config.repo_id.clone()),
169 suggestion: None,
170 });
171 }
172 if self.config.revision.is_empty() {
173 return Err(TrustformersError::InvalidInput {
174 message: "Revision/branch name cannot be empty".to_string(),
175 parameter: Some("revision".to_string()),
176 expected: None,
177 received: None,
178 suggestion: None,
179 });
180 }
181 Ok(())
182 }
183
184 pub fn repo_exists(&self) -> Result<bool> {
187 self.validate()?;
188 if self.config.dry_run {
189 debug!(repo_id = %self.config.repo_id, "repo_exists: dry run, skipping network call");
190 return Ok(false);
191 }
192 api::run_blocking(api::repo_exists(
193 &self.config.base_url,
194 self.config.repo_type,
195 &self.config.repo_id,
196 &self.config.token,
197 ))
198 .map_err(|e| hub_error_to_trustformers(e, &self.config.repo_id))
199 }
200
201 pub fn create_repo(&self) -> Result<String> {
205 self.validate()?;
206 if self.config.dry_run {
207 let url = format!("{}/{}", self.config.base_url, self.config.repo_id);
208 info!(repo_id = %self.config.repo_id, "create_repo: dry run, not contacting the Hub");
209 return Ok(url);
210 }
211 api::run_blocking(api::create_repo(
212 &self.config.base_url,
213 self.config.repo_type,
214 &self.config.repo_id,
215 self.config.private,
216 &self.config.token,
217 ))
218 .map_err(|e| hub_error_to_trustformers(e, &self.config.repo_id))
219 }
220
221 pub fn upload_file(&self, file: &UploadFile) -> Result<UploadResult> {
223 self.upload_files(std::slice::from_ref(file))
224 }
225
226 pub fn upload_files(&self, files: &[UploadFile]) -> Result<UploadResult> {
237 self.validate()?;
238
239 if files.is_empty() {
240 return Err(TrustformersError::InvalidInput {
241 message: "File list cannot be empty".to_string(),
242 parameter: Some("files".to_string()),
243 expected: None,
244 received: None,
245 suggestion: None,
246 });
247 }
248
249 let mut repo_paths = Vec::with_capacity(files.len());
250 let mut commit_files = Vec::with_capacity(files.len());
251
252 for file in files {
253 if !file.local_path.exists() {
254 return Err(TrustformersError::Io {
255 message: format!("File not found: {}", file.local_path.display()),
256 path: Some(file.local_path.display().to_string()),
257 suggestion: Some("Ensure all files exist before uploading".to_string()),
258 });
259 }
260 if file.repo_path.is_empty() {
261 return Err(TrustformersError::InvalidInput {
262 message: "Repository path cannot be empty for one of the files".to_string(),
263 parameter: Some("repo_path".to_string()),
264 expected: None,
265 received: None,
266 suggestion: None,
267 });
268 }
269
270 let content = std::fs::read(&file.local_path).map_err(|e| TrustformersError::Io {
271 message: format!("Cannot read file: {e}"),
272 path: Some(file.local_path.display().to_string()),
273 suggestion: None,
274 })?;
275 if content.len() as u64 >= LFS_INLINE_THRESHOLD_BYTES {
276 return Err(hub_error_to_trustformers(
277 HubError::LfsRequired {
278 path: file.repo_path.clone(),
279 size: content.len() as u64,
280 },
281 &self.config.repo_id,
282 ));
283 }
284
285 repo_paths.push(file.repo_path.clone());
286 commit_files.push(CommitFile {
287 repo_path: file.repo_path.clone(),
288 content,
289 });
290 }
291
292 if self.config.dry_run {
293 info!(
294 file_count = files.len(),
295 repo_id = %self.config.repo_id,
296 "upload_files: dry run, not contacting the Hub"
297 );
298 return Ok(UploadResult {
299 repo_id: self.config.repo_id.clone(),
300 revision: self.config.revision.clone(),
301 commit_url: None,
302 commit_oid: None,
303 files_uploaded: repo_paths,
304 dry_run: true,
305 });
306 }
307
308 if self.config.create_if_missing && !self.repo_exists()? {
309 info!(repo_id = %self.config.repo_id, "Repository does not exist yet; creating it");
310 self.create_repo()?;
311 }
312
313 let outcome = api::run_blocking(api::commit(
314 &self.config.base_url,
315 self.config.repo_type,
316 &self.config.repo_id,
317 &self.config.revision,
318 &self.config.commit_message,
319 &commit_files,
320 &[],
321 &self.config.token,
322 ))
323 .map_err(|e| hub_error_to_trustformers(e, &self.config.repo_id))?;
324
325 info!(
326 file_count = files.len(),
327 repo_id = %self.config.repo_id,
328 commit_url = ?outcome.commit_url,
329 "Uploaded files to Hub"
330 );
331
332 Ok(UploadResult {
333 repo_id: self.config.repo_id.clone(),
334 revision: self.config.revision.clone(),
335 commit_url: outcome.commit_url,
336 commit_oid: outcome.commit_oid,
337 files_uploaded: repo_paths,
338 dry_run: false,
339 })
340 }
341
342 pub fn upload_directory(&self, local_dir: &Path, repo_prefix: &str) -> Result<UploadResult> {
347 self.validate()?;
348
349 if !local_dir.is_dir() {
350 return Err(TrustformersError::Io {
351 message: format!("Not a directory: {}", local_dir.display()),
352 path: Some(local_dir.display().to_string()),
353 suggestion: Some("Provide a path to an existing directory".to_string()),
354 });
355 }
356
357 let files = collect_files_recursive(local_dir, local_dir, repo_prefix)?;
358
359 if files.is_empty() {
360 warn!(
361 dir = %local_dir.display(),
362 "Directory is empty; nothing to upload"
363 );
364 return Ok(UploadResult {
365 repo_id: self.config.repo_id.clone(),
366 revision: self.config.revision.clone(),
367 commit_url: None,
368 commit_oid: None,
369 files_uploaded: vec![],
370 dry_run: self.config.dry_run,
371 });
372 }
373
374 self.upload_files(&files)
375 }
376
377 pub fn delete_file(&self, repo_path: &str) -> Result<()> {
379 self.validate()?;
380
381 if repo_path.is_empty() {
382 return Err(TrustformersError::InvalidInput {
383 message: "Repository path cannot be empty".to_string(),
384 parameter: Some("repo_path".to_string()),
385 expected: None,
386 received: None,
387 suggestion: None,
388 });
389 }
390
391 if self.config.dry_run {
392 info!(repo_path = %repo_path, repo_id = %self.config.repo_id, "delete_file: dry run, not contacting the Hub");
393 return Ok(());
394 }
395
396 api::run_blocking(api::commit(
397 &self.config.base_url,
398 self.config.repo_type,
399 &self.config.repo_id,
400 &self.config.revision,
401 &format!("Delete {repo_path}"),
402 &[],
403 std::slice::from_ref(&repo_path.to_string()),
404 &self.config.token,
405 ))
406 .map_err(|e| hub_error_to_trustformers(e, &self.config.repo_id))?;
407
408 info!(repo_path = %repo_path, repo_id = %self.config.repo_id, "Deleted file from Hub");
409 Ok(())
410 }
411}
412
413fn collect_files_recursive(
415 base_dir: &Path,
416 current_dir: &Path,
417 repo_prefix: &str,
418) -> Result<Vec<UploadFile>> {
419 let mut files = Vec::new();
420
421 let entries = std::fs::read_dir(current_dir).map_err(|e| TrustformersError::Io {
422 message: format!("Cannot read directory: {e}"),
423 path: Some(current_dir.display().to_string()),
424 suggestion: None,
425 })?;
426
427 for entry_result in entries {
428 let entry = entry_result.map_err(|e| TrustformersError::Io {
429 message: format!("Cannot read directory entry: {e}"),
430 path: Some(current_dir.display().to_string()),
431 suggestion: None,
432 })?;
433
434 let path = entry.path();
435
436 if path.is_dir() {
437 let mut sub_files = collect_files_recursive(base_dir, &path, repo_prefix)?;
438 files.append(&mut sub_files);
439 } else {
440 let relative = path.strip_prefix(base_dir).map_err(|e| TrustformersError::Io {
441 message: format!("Path prefix stripping failed: {e}"),
442 path: Some(path.display().to_string()),
443 suggestion: None,
444 })?;
445
446 let repo_path = if repo_prefix.is_empty() {
447 relative.display().to_string()
448 } else {
449 format!("{}/{}", repo_prefix, relative.display())
450 };
451
452 let repo_path = repo_path.replace('\\', "/");
454
455 files.push(UploadFile {
456 local_path: path.clone(),
457 repo_path,
458 });
459 }
460 }
461
462 Ok(files)
463}
464
465pub struct HubUploaderBuilder {
467 config: UploadConfig,
468}
469
470impl HubUploaderBuilder {
471 pub fn new(token: impl Into<String>, repo_id: impl Into<String>) -> Self {
473 let config = UploadConfig {
474 token: token.into(),
475 repo_id: repo_id.into(),
476 ..Default::default()
477 };
478 Self { config }
479 }
480
481 pub fn repo_type(mut self, repo_type: RepoType) -> Self {
483 self.config.repo_type = repo_type;
484 self
485 }
486
487 pub fn revision(mut self, revision: impl Into<String>) -> Self {
489 self.config.revision = revision.into();
490 self
491 }
492
493 pub fn commit_message(mut self, msg: impl Into<String>) -> Self {
495 self.config.commit_message = msg.into();
496 self
497 }
498
499 pub fn private(mut self, private: bool) -> Self {
501 self.config.private = private;
502 self
503 }
504
505 pub fn create_if_missing(mut self, create: bool) -> Self {
507 self.config.create_if_missing = create;
508 self
509 }
510
511 pub fn base_url(mut self, base_url: impl Into<String>) -> Self {
513 self.config.base_url = base_url.into();
514 self
515 }
516
517 pub fn dry_run(mut self, dry_run: bool) -> Self {
519 self.config.dry_run = dry_run;
520 self
521 }
522
523 pub fn build(self) -> Result<HubUploader> {
525 let uploader = HubUploader::new(self.config);
526 uploader.validate()?;
527 Ok(uploader)
528 }
529}
530
531#[derive(Debug, Clone, PartialEq, Eq)]
535pub enum HubError {
536 Unauthorized { message: String },
538 MissingCredentials { message: String },
541 NotFound {
543 repo_id: String,
544 path: Option<String>,
545 },
546 RequestFailed { status_code: u16, message: String },
548 Io {
550 message: String,
551 path: Option<String>,
552 },
553 InvalidInput { message: String },
555 Network { message: String },
557 FeatureUnavailable { message: String },
559 LfsRequired { path: String, size: u64 },
562}
563
564impl std::fmt::Display for HubError {
565 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
566 match self {
567 HubError::Unauthorized { message } => write!(f, "Unauthorized: {message}"),
568 HubError::MissingCredentials { message } => write!(f, "Missing credentials: {message}"),
569 HubError::NotFound { repo_id, path } => {
570 if let Some(p) = path {
571 write!(f, "Not found: {repo_id}/{p}")
572 } else {
573 write!(f, "Not found: {repo_id}")
574 }
575 },
576 HubError::RequestFailed {
577 status_code,
578 message,
579 } => {
580 write!(f, "Request failed (HTTP {status_code}): {message}")
581 },
582 HubError::Io { message, path } => {
583 if let Some(p) = path {
584 write!(f, "IO error at {p}: {message}")
585 } else {
586 write!(f, "IO error: {message}")
587 }
588 },
589 HubError::InvalidInput { message } => write!(f, "Invalid input: {message}"),
590 HubError::Network { message } => write!(f, "Network error: {message}"),
591 HubError::FeatureUnavailable { message } => write!(f, "Feature unavailable: {message}"),
592 HubError::LfsRequired { path, size } => {
593 write!(
594 f,
595 "'{path}' is {size} bytes, at or above the {}-byte inline-upload threshold, \
596 and would require real Git-LFS object storage, which is not implemented",
597 LFS_INLINE_THRESHOLD_BYTES
598 )
599 },
600 }
601 }
602}
603
604impl std::error::Error for HubError {}
605
606impl From<TrustformersError> for HubError {
607 fn from(e: TrustformersError) -> Self {
608 HubError::RequestFailed {
609 status_code: 0,
610 message: e.to_string(),
611 }
612 }
613}
614
615fn hub_error_to_trustformers(error: HubError, repo_id: &str) -> TrustformersError {
618 match error {
619 HubError::MissingCredentials { .. } => missing_credentials_error(repo_id),
620 HubError::Unauthorized { message } => TrustformersError::Hub {
621 message,
622 model_id: repo_id.to_string(),
623 endpoint: None,
624 suggestion: Some("Check that the API token is valid and has write access".to_string()),
625 recovery_actions: vec![],
626 },
627 HubError::FeatureUnavailable { message } => TrustformersError::Hub {
628 message,
629 model_id: repo_id.to_string(),
630 endpoint: None,
631 suggestion: Some("Rebuild with `--features hub`".to_string()),
632 recovery_actions: vec![],
633 },
634 HubError::LfsRequired { path, size } => TrustformersError::Hub {
635 message: format!(
636 "'{path}' is {size} bytes and would require real Git-LFS object storage, which \
637 is not implemented"
638 ),
639 model_id: repo_id.to_string(),
640 endpoint: None,
641 suggestion: Some("Upload large files through the Hub web UI or the CLI's `huggingface-cli upload-large-folder` until LFS object upload is implemented here".to_string()),
642 recovery_actions: vec![],
643 },
644 HubError::NotFound { repo_id, path } => TrustformersError::Hub {
645 message: format!("Not found: {repo_id}{}", path.map(|p| format!("/{p}")).unwrap_or_default()),
646 model_id: repo_id,
647 endpoint: None,
648 suggestion: None,
649 recovery_actions: vec![],
650 },
651 HubError::RequestFailed { status_code, message } => TrustformersError::Hub {
652 message: format!("HTTP {status_code}: {message}"),
653 model_id: repo_id.to_string(),
654 endpoint: None,
655 suggestion: None,
656 recovery_actions: vec![],
657 },
658 HubError::Network { message } => TrustformersError::Hub {
659 message,
660 model_id: repo_id.to_string(),
661 endpoint: None,
662 suggestion: Some("Check network connectivity".to_string()),
663 recovery_actions: vec![],
664 },
665 HubError::Io { message, path } => TrustformersError::Io {
666 message,
667 path,
668 suggestion: None,
669 },
670 HubError::InvalidInput { message } => TrustformersError::InvalidInput {
671 message,
672 parameter: None,
673 expected: None,
674 received: None,
675 suggestion: None,
676 },
677 }
678}
679
680fn missing_credentials_error(repo_id: &str) -> TrustformersError {
681 TrustformersError::Hub {
682 message: "Missing credentials: a Hub API token is required to upload".to_string(),
683 model_id: repo_id.to_string(),
684 endpoint: None,
685 suggestion: Some(
686 "Set `UploadConfig::token` (e.g. from the `HF_TOKEN` environment variable)".to_string(),
687 ),
688 recovery_actions: vec![],
689 }
690}
691
692#[derive(Debug, Clone)]
696pub struct HubUploadConfig {
697 pub repo_id: String,
699 pub token: String,
701 pub commit_message: String,
703 pub private: bool,
705 pub revision: Option<String>,
707 pub base_url: Option<String>,
709 pub dry_run: bool,
711}
712
713impl HubUploadConfig {
714 pub fn new(
716 repo_id: impl Into<String>,
717 token: impl Into<String>,
718 commit_message: impl Into<String>,
719 ) -> Self {
720 Self {
721 repo_id: repo_id.into(),
722 token: token.into(),
723 commit_message: commit_message.into(),
724 private: false,
725 revision: None,
726 base_url: None,
727 dry_run: false,
728 }
729 }
730
731 pub fn with_private(mut self, private: bool) -> Self {
733 self.private = private;
734 self
735 }
736
737 pub fn with_revision(mut self, revision: impl Into<String>) -> Self {
739 self.revision = Some(revision.into());
740 self
741 }
742
743 pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
745 self.base_url = Some(base_url.into());
746 self
747 }
748
749 pub fn with_dry_run(mut self, dry_run: bool) -> Self {
751 self.dry_run = dry_run;
752 self
753 }
754
755 pub fn effective_revision(&self) -> &str {
757 self.revision.as_deref().unwrap_or("main")
758 }
759
760 fn validate(&self) -> std::result::Result<(), HubError> {
761 if self.token.is_empty() {
762 return Err(HubError::MissingCredentials {
763 message: "API token cannot be empty".to_string(),
764 });
765 }
766 if self.repo_id.is_empty() {
767 return Err(HubError::InvalidInput {
768 message: "repo_id cannot be empty".to_string(),
769 });
770 }
771 if !self.repo_id.contains('/') {
772 return Err(HubError::InvalidInput {
773 message: format!(
774 "repo_id must be in 'username/repo-name' format, got '{}'",
775 self.repo_id
776 ),
777 });
778 }
779 Ok(())
780 }
781}
782
783#[derive(Debug, Clone, Default)]
787pub struct HubUploadProgress {
788 pub total_files: usize,
790 pub uploaded_files: usize,
792 pub total_bytes: u64,
794 pub uploaded_bytes: u64,
796}
797
798impl HubUploadProgress {
799 pub fn new(total_files: usize, total_bytes: u64) -> Self {
801 Self {
802 total_files,
803 uploaded_files: 0,
804 total_bytes,
805 uploaded_bytes: 0,
806 }
807 }
808
809 pub fn record_file(&mut self, bytes: u64) {
811 self.uploaded_files += 1;
812 self.uploaded_bytes += bytes;
813 }
814
815 pub fn fraction(&self) -> f64 {
817 if self.total_bytes == 0 {
818 if self.total_files == 0 {
819 1.0
820 } else {
821 self.uploaded_files as f64 / self.total_files as f64
822 }
823 } else {
824 self.uploaded_bytes as f64 / self.total_bytes as f64
825 }
826 }
827
828 pub fn is_complete(&self) -> bool {
830 self.uploaded_files >= self.total_files
831 }
832}
833
834pub fn sha256(data: &[u8]) -> String {
838 use sha2::{Digest, Sha256};
839 let mut hasher = Sha256::new();
840 hasher.update(data);
841 hex::encode(hasher.finalize())
842}
843
844pub fn sha256_file(path: &Path) -> std::result::Result<String, HubError> {
846 let data = std::fs::read(path).map_err(|e| HubError::Io {
847 message: format!("Cannot read file for hashing: {e}"),
848 path: Some(path.display().to_string()),
849 })?;
850 Ok(sha256(&data))
851}
852
853#[derive(Debug, Clone)]
857pub struct SingleFileUploadResult {
858 pub remote_url: String,
860 pub commit_url: Option<String>,
862 pub commit_oid: Option<String>,
864 pub file_size: u64,
866 pub sha256: String,
868}
869
870impl HubUploader {
873 pub fn from_hub_config(cfg: HubUploadConfig) -> std::result::Result<Self, HubError> {
875 cfg.validate()?;
876 let revision = cfg.effective_revision().to_string();
877 let upload_config = UploadConfig {
878 token: cfg.token,
879 repo_id: cfg.repo_id,
880 repo_type: RepoType::Model,
881 revision,
882 commit_message: cfg.commit_message,
883 create_if_missing: true,
884 private: cfg.private,
885 base_url: cfg.base_url.unwrap_or_else(|| HF_HUB_URL.to_string()),
886 dry_run: cfg.dry_run,
887 };
888 Ok(Self::new(upload_config))
889 }
890
891 pub fn upload_file_path(
893 &self,
894 local_path: &str,
895 remote_path: &str,
896 ) -> std::result::Result<SingleFileUploadResult, HubError> {
897 let path = Path::new(local_path);
898 if !path.exists() {
899 return Err(HubError::Io {
900 message: format!("File not found: {local_path}"),
901 path: Some(local_path.to_string()),
902 });
903 }
904 if remote_path.is_empty() {
905 return Err(HubError::InvalidInput {
906 message: "remote_path cannot be empty".to_string(),
907 });
908 }
909
910 let metadata = path.metadata().map_err(|e| HubError::Io {
911 message: format!("Cannot read file metadata: {e}"),
912 path: Some(local_path.to_string()),
913 })?;
914 let file_size = metadata.len();
915 let sha256 = sha256_file(path)?;
916
917 let result =
918 self.upload_file(&UploadFile::new(path, remote_path)).map_err(HubError::from)?;
919
920 let remote_url = format!(
921 "{}/{}/blob/{}/{}",
922 self.config.base_url, self.config.repo_id, self.config.revision, remote_path
923 );
924
925 Ok(SingleFileUploadResult {
926 remote_url,
927 commit_url: result.commit_url,
928 commit_oid: result.commit_oid,
929 file_size,
930 sha256,
931 })
932 }
933
934 pub fn upload_model(
938 &self,
939 model_dir: &str,
940 ) -> std::result::Result<Vec<SingleFileUploadResult>, HubError> {
941 let base = Path::new(model_dir);
942 if !base.is_dir() {
943 return Err(HubError::Io {
944 message: format!("Not a directory: {model_dir}"),
945 path: Some(model_dir.to_string()),
946 });
947 }
948 self.upload_dir_filtered(base, |name| {
949 name.ends_with(".json")
951 || name.ends_with(".safetensors")
952 || name.ends_with(".bin")
953 || name.ends_with(".pt")
954 || name.ends_with(".ckpt")
955 || name.ends_with(".msgpack")
956 || name.ends_with(".model")
957 || name == "README.md"
958 })
959 }
960
961 pub fn upload_tokenizer(
965 &self,
966 tokenizer_dir: &str,
967 ) -> std::result::Result<Vec<SingleFileUploadResult>, HubError> {
968 let base = Path::new(tokenizer_dir);
969 if !base.is_dir() {
970 return Err(HubError::Io {
971 message: format!("Not a directory: {tokenizer_dir}"),
972 path: Some(tokenizer_dir.to_string()),
973 });
974 }
975 self.upload_dir_filtered(base, |name| {
976 name.ends_with("tokenizer.json")
977 || name.ends_with("tokenizer_config.json")
978 || name.ends_with("vocab.json")
979 || name.ends_with("vocab.txt")
980 || name.ends_with("merges.txt")
981 || name.ends_with("special_tokens_map.json")
982 || name.ends_with("added_tokens.json")
983 || name.ends_with(".model")
984 || name.ends_with("spiece.model")
985 })
986 }
987
988 pub fn create_repo_typed(&self, repo_type: RepoType) -> std::result::Result<String, HubError> {
992 let mut cfg = self.config.clone();
993 cfg.repo_type = repo_type;
994 let tmp = HubUploader::new(cfg);
995 tmp.create_repo().map_err(HubError::from)
996 }
997
998 pub fn delete_remote_file(&self, remote_path: &str) -> std::result::Result<(), HubError> {
1000 self.delete_file(remote_path).map_err(HubError::from)
1001 }
1002
1003 fn upload_dir_filtered<F>(
1006 &self,
1007 base: &Path,
1008 filter: F,
1009 ) -> std::result::Result<Vec<SingleFileUploadResult>, HubError>
1010 where
1011 F: Fn(&str) -> bool,
1012 {
1013 let entries = collect_files_recursive_hub(base, base)?;
1014 let mut results = Vec::new();
1015 for (local_path, repo_path) in entries {
1016 let file_name = local_path.file_name().and_then(|n| n.to_str()).unwrap_or("");
1017 if !filter(file_name) {
1018 continue;
1019 }
1020 let local_str = local_path.display().to_string();
1021 let result = self.upload_file_path(&local_str, &repo_path)?;
1022 results.push(result);
1023 }
1024 Ok(results)
1025 }
1026}
1027
1028fn collect_files_recursive_hub(
1030 base: &Path,
1031 current: &Path,
1032) -> std::result::Result<Vec<(PathBuf, String)>, HubError> {
1033 let mut files = Vec::new();
1034 let entries = std::fs::read_dir(current).map_err(|e| HubError::Io {
1035 message: format!("Cannot read directory: {e}"),
1036 path: Some(current.display().to_string()),
1037 })?;
1038 for entry_result in entries {
1039 let entry = entry_result.map_err(|e| HubError::Io {
1040 message: format!("Cannot read directory entry: {e}"),
1041 path: Some(current.display().to_string()),
1042 })?;
1043 let path = entry.path();
1044 if path.is_dir() {
1045 let mut sub = collect_files_recursive_hub(base, &path)?;
1046 files.append(&mut sub);
1047 } else {
1048 let relative = path.strip_prefix(base).map_err(|e| HubError::Io {
1049 message: format!("Path strip prefix failed: {e}"),
1050 path: Some(path.display().to_string()),
1051 })?;
1052 let repo_path = relative.display().to_string().replace('\\', "/");
1053 files.push((path, repo_path));
1054 }
1055 }
1056 Ok(files)
1057}
1058
1059#[cfg(test)]
1062mod tests;