Skip to main content

hf_fetch_model/
cache_layout.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! Centralized `hf-hub` cache path construction.
4//!
5//! All paths follow the `hf-hub` 0.5 cache layout:
6//! `{cache_root}/models--org--name/{snapshots,blobs,refs}/...`
7//!
8//! This module is the single source of truth for cache directory structure.
9//! When `hf-hub` bumps, update this module and rerun the
10//! `cache_layout_matches_hf_hub` integration test.
11
12use std::path::{Path, PathBuf};
13
14use hf_hub::{Repo, RepoType};
15
16/// Repo folder name: `"models--org--name"`.
17///
18/// Delegates to [`hf_hub::Repo::folder_name()`].
19#[must_use]
20pub fn repo_folder_name(repo_id: &str) -> String {
21    // BORROW: explicit .to_owned() for &str → owned String required by Repo::new
22    Repo::new(repo_id.to_owned(), RepoType::Model).folder_name()
23}
24
25/// Repo root directory: `{cache_root}/models--org--name/`.
26#[must_use]
27pub fn repo_dir(cache_root: &Path, repo_id: &str) -> PathBuf {
28    cache_root.join(repo_folder_name(repo_id))
29}
30
31/// Snapshots directory: `{repo_dir}/snapshots/`.
32#[must_use]
33pub fn snapshots_dir(repo_dir: &Path) -> PathBuf {
34    repo_dir.join("snapshots")
35}
36
37/// Snapshot directory for a specific commit: `{repo_dir}/snapshots/{commit_hash}/`.
38#[must_use]
39pub fn snapshot_dir(repo_dir: &Path, commit_hash: &str) -> PathBuf {
40    snapshots_dir(repo_dir).join(commit_hash)
41}
42
43/// Pointer path: `{repo_dir}/snapshots/{commit_hash}/{filename}`.
44#[must_use]
45pub fn pointer_path(repo_dir: &Path, commit_hash: &str, filename: &str) -> PathBuf {
46    snapshot_dir(repo_dir, commit_hash).join(filename)
47}
48
49/// Blobs directory: `{repo_dir}/blobs/`.
50#[must_use]
51pub fn blobs_dir(repo_dir: &Path) -> PathBuf {
52    repo_dir.join("blobs")
53}
54
55/// Blob path: `{repo_dir}/blobs/{etag}`.
56#[must_use]
57pub fn blob_path(repo_dir: &Path, etag: &str) -> PathBuf {
58    blobs_dir(repo_dir).join(etag)
59}
60
61/// Temp blob path for chunked downloads: `{repo_dir}/blobs/{etag}.chunked.part`.
62///
63/// Uses string concatenation rather than [`Path::with_extension`] to handle
64/// etags containing periods (e.g., `"abc.def"` → `"abc.def.chunked.part"`,
65/// not `"abc.chunked.part"`).
66#[must_use]
67pub fn temp_blob_path(repo_dir: &Path, etag: &str) -> PathBuf {
68    // BORROW: explicit .to_owned() for &str → owned String for path concatenation
69    let mut name = etag.to_owned();
70    name.push_str(".chunked.part");
71    blobs_dir(repo_dir).join(name)
72}
73
74/// Refs directory: `{repo_dir}/refs/`.
75#[must_use]
76pub fn refs_dir(repo_dir: &Path) -> PathBuf {
77    repo_dir.join("refs")
78}
79
80/// Ref file path: `{repo_dir}/refs/{revision}`.
81#[must_use]
82pub fn ref_path(repo_dir: &Path, revision: &str) -> PathBuf {
83    refs_dir(repo_dir).join(revision)
84}
85
86#[cfg(test)]
87mod tests {
88    #![allow(clippy::panic, clippy::unwrap_used, clippy::expect_used)]
89
90    use super::*;
91
92    #[test]
93    fn blob_path_joins_repo_dir_and_etag() {
94        let rd = Path::new("/tmp/models--x--y");
95        assert_eq!(blob_path(rd, "abc123"), rd.join("blobs").join("abc123"));
96    }
97
98    #[test]
99    fn temp_blob_path_preserves_periods_in_etag() {
100        // Guards the specific behaviour called out in the `temp_blob_path`
101        // doc comment: etags containing periods must not be truncated by
102        // `Path::with_extension`. `"abc.def"` + `".chunked.part"` →
103        // `"abc.def.chunked.part"`, NOT `"abc.chunked.part"`.
104        let rd = Path::new("/tmp/models--x--y");
105        assert_eq!(
106            temp_blob_path(rd, "abc.def"),
107            rd.join("blobs").join("abc.def.chunked.part")
108        );
109    }
110}