Skip to main content

quilt_rs/
paths.rs

1//!
2//! Incapsulated knowlegde about directory structure of the files in `.quilt`, packages and working directories.
3
4use std::path::Path;
5use std::path::PathBuf;
6
7#[cfg(test)]
8use tempfile::TempDir;
9use tracing::error;
10
11use crate::Res;
12use crate::io::storage::Storage;
13use crate::lineage::Home;
14use quilt_uri::Host;
15use quilt_uri::ManifestUri;
16use quilt_uri::Namespace;
17
18pub const AUTH_CLIENT: &str = "client.json";
19pub const AUTH_CREDENTIALS: &str = "credentials.json";
20pub const AUTH_DIR: &str = ".auth";
21pub const AUTH_TOKENS: &str = "tokens.json";
22
23/// List authenticated host directories under the given data directory.
24///
25/// Returns a sorted list of directory names found in `<data_dir>/.auth/`.
26// TODO: Also include registries from data.json/Lineage file.
27pub fn list_auth_hosts(data_dir: &Path) -> Vec<String> {
28    let auth_dir = data_dir.join(AUTH_DIR);
29    let mut hosts: Vec<String> = Vec::new();
30    if auth_dir.exists()
31        && let Ok(entries) = std::fs::read_dir(&auth_dir)
32    {
33        for entry in entries.flatten() {
34            if entry.file_type().is_ok_and(|t| t.is_dir())
35                && let Some(name) = entry.file_name().to_str()
36            {
37                hosts.push(name.to_string());
38            }
39        }
40    }
41    hosts.sort();
42    hosts
43}
44
45const LINEAGE_FILE: &str = ".quilt/data.json";
46
47const INSTALLED_DIR: &str = ".quilt/installed";
48// Local cache directory under `<data_dir>`. Distinct from the S3 key
49// prefix of the same name in `quilt-uri::paths` — sharing the literal
50// value today is incidental, the two contracts can evolve independently.
51const MANIFEST_DIR: &str = ".quilt/packages";
52const OBJECTS_DIR: &str = ".quilt/objects";
53
54pub use quilt_uri::paths::get_manifest_key;
55pub use quilt_uri::paths::tag_key;
56
57/// Path to the package home directory within the home directory
58pub fn package_home(home: &Home, namespace: &Namespace) -> PathBuf {
59    home.join(namespace.to_string())
60}
61
62/// Helper for getting paths.
63/// We heavily rely on where we put files,
64/// and this struct contains info of the directory structure .
65#[derive(Debug, Clone, PartialEq, Eq, Default)]
66pub struct DomainPaths {
67    root_dir: PathBuf,
68}
69
70impl DomainPaths {
71    pub fn new(root_dir: PathBuf) -> Self {
72        DomainPaths { root_dir }
73    }
74
75    pub fn auth_host(&self, host: &Host) -> PathBuf {
76        self.root_dir
77            .join(AUTH_DIR)
78            .join(PathBuf::from(host.to_string()))
79    }
80
81    /// Path to the installed manifest.
82    ///
83    /// Takes `(namespace, hash)` rather than `&ManifestUri` because the
84    /// installed manifest may belong to either a remote-backed package
85    /// (where a `ManifestUri` is available) or a local-only package
86    /// (created via `flow::create`, where there is no bucket or origin).
87    /// A local commit also produces a hash that has no remote
88    /// counterpart yet.
89    pub fn installed_manifest(&self, namespace: &Namespace, hash: &str) -> PathBuf {
90        self.installed_manifests_dir(namespace).join(hash)
91    }
92
93    /// Directory for storing installed manifests
94    pub fn installed_manifests_dir(&self, namespace: &Namespace) -> PathBuf {
95        self.root_dir
96            .join(INSTALLED_DIR)
97            .join(namespace.to_string())
98    }
99
100    /// Path to the lineage file
101    pub fn lineage(&self) -> PathBuf {
102        self.root_dir.join(LINEAGE_FILE)
103    }
104
105    /// Path to the manifest cached in semi-temporary directory
106    pub fn cached_manifest(&self, uri: &ManifestUri) -> PathBuf {
107        self.root_dir
108            .join(MANIFEST_DIR)
109            .join(&uri.bucket)
110            .join(&uri.hash)
111    }
112
113    /// Directory for storing cached manifests for a bucket
114    pub fn cached_manifests_dir(&self, bucket: &str) -> PathBuf {
115        self.root_dir.join(MANIFEST_DIR).join(bucket)
116    }
117
118    /// Directory for storing pristine hashed files
119    pub fn objects_dir(&self) -> PathBuf {
120        self.root_dir.join(OBJECTS_DIR)
121    }
122
123    /// Path to the pristine hashed file
124    pub fn object(&self, hash: &[u8]) -> PathBuf {
125        self.objects_dir().join(hex::encode(hash))
126    }
127
128    /// What directories are essential when we initiate `LocalDomain`
129    fn required(&self) -> Vec<PathBuf> {
130        vec![
131            self.root_dir.join(INSTALLED_DIR),
132            self.objects_dir(),
133            self.root_dir.join(MANIFEST_DIR),
134        ]
135    }
136
137    /// What directories are essential when we initiate `InstalledPackage`
138    fn required_for_installing(&self, home: &Home, namespace: &Namespace) -> Res<Vec<PathBuf>> {
139        let mut paths = vec![];
140        paths.extend(self.required());
141        paths.extend(vec![
142            package_home(home, namespace),
143            self.installed_manifests_dir(namespace),
144        ]);
145        Ok(paths)
146    }
147
148    pub async fn scaffold_for_installing(
149        &self,
150        storage: &impl Storage,
151        home: &Home,
152        namespace: &Namespace,
153    ) -> Res {
154        scaffold_paths(storage, self.required_for_installing(home, namespace)?).await
155    }
156
157    /// What directories are essential when we work with cached manifests
158    fn required_for_caching(&self, bucket: &str) -> Vec<PathBuf> {
159        let mut paths = vec![];
160        paths.extend(self.required());
161        paths.extend(vec![self.cached_manifests_dir(bucket)]);
162        paths
163    }
164
165    pub async fn scaffold_for_caching(&self, storage: &impl Storage, bucket: &str) -> Res {
166        scaffold_paths(storage, self.required_for_caching(bucket)).await
167    }
168
169    #[cfg(test)]
170    pub fn from_temp_dir() -> Res<(Self, TempDir)> {
171        let temp_dir = TempDir::new()?;
172        Ok((DomainPaths::new(temp_dir.path().to_path_buf()), temp_dir))
173    }
174}
175
176pub async fn copy_cached_to_installed(
177    paths: &DomainPaths,
178    storage: &impl Storage,
179    manifest_uri: &ManifestUri,
180) -> Res {
181    match storage
182        .copy(
183            paths.cached_manifest(manifest_uri),
184            paths.installed_manifest(&manifest_uri.namespace, &manifest_uri.hash),
185        )
186        .await
187    {
188        Ok(_) => Ok(()),
189        Err(e) => {
190            error!(
191                "Failed to copy cached manifest to installed location for manifest_uri {}: {}",
192                manifest_uri, e
193            );
194            Err(e)
195        }
196    }
197}
198
199/// Takes list of the required paths and create directories
200async fn scaffold_paths(storage: &impl Storage, paths: Vec<PathBuf>) -> Res {
201    for path in paths {
202        storage.create_dir_all(&path).await?;
203    }
204    Ok(())
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210
211    use test_log::test;
212
213    #[test]
214    fn test_required_paths() {
215        let paths = DomainPaths::new(PathBuf::from("foo/bar"));
216        let scaffolded_paths = paths.required();
217        assert_eq!(
218            scaffolded_paths,
219            vec![
220                PathBuf::from("foo/bar/.quilt/installed"),
221                PathBuf::from("foo/bar/.quilt/objects"),
222                PathBuf::from("foo/bar/.quilt/packages"),
223            ]
224        );
225    }
226
227    #[test]
228    fn test_package_home() -> Res {
229        let home = Home::from("/home/user/quilt");
230        let namespace = Namespace::from(("test", "package"));
231
232        let pkg_home = package_home(&home, &namespace);
233        assert_eq!(pkg_home, PathBuf::from("/home/user/quilt/test/package"));
234
235        Ok(())
236    }
237}