Skip to main content

kellnr_storage/
docs_storage.rs

1use std::collections::HashSet;
2
3use bytes::Bytes;
4
5use crate::cached_crate_storage::DynStorage;
6use crate::storage::ObjectWithMeta;
7use crate::storage_error::StorageError;
8
9pub struct DocsStorage {
10    storage: DynStorage,
11}
12
13impl DocsStorage {
14    pub fn new(storage: DynStorage) -> Self {
15        Self { storage }
16    }
17
18    /// Key for one file under a crate+version's doc tree, e.g.
19    /// `file_key("my-crate", "1.0.0", "doc/my_crate/index.html")`.
20    pub fn file_key(crate_name: &str, version: &str, relative_path: &str) -> String {
21        format!("{crate_name}/{version}/{relative_path}")
22    }
23
24    pub fn version_prefix(crate_name: &str, version: &str) -> String {
25        format!("{crate_name}/{version}/")
26    }
27
28    pub fn crate_prefix(crate_name: &str) -> String {
29        format!("{crate_name}/")
30    }
31
32    pub async fn put(&self, key: &str, data: Bytes) -> Result<(), StorageError> {
33        self.storage
34            .put_overwrite(key, data)
35            .await
36            .map_err(|e| StorageError::DocsStoreFailed {
37                key: key.to_string(),
38                reason: e.to_string(),
39            })
40    }
41
42    pub async fn get(&self, key: &str) -> Result<Bytes, StorageError> {
43        self.storage.get(key).await.map_err(|e| {
44            if matches!(
45                &e,
46                StorageError::S3Error(object_store::Error::NotFound { .. })
47                    | StorageError::FileDoesNotExist(_)
48            ) {
49                return StorageError::DocsNotFound {
50                    key: key.to_string(),
51                };
52            }
53            StorageError::DocsGetFailed {
54                key: key.to_string(),
55                reason: e.to_string(),
56            }
57        })
58    }
59
60    pub async fn get_with_meta(&self, key: &str) -> Result<ObjectWithMeta, StorageError> {
61        self.storage.get_with_meta(key).await.map_err(|e| {
62            if matches!(
63                &e,
64                StorageError::S3Error(object_store::Error::NotFound { .. })
65                    | StorageError::FileDoesNotExist(_)
66            ) {
67                return StorageError::DocsNotFound {
68                    key: key.to_string(),
69                };
70            }
71            StorageError::DocsGetFailed {
72                key: key.to_string(),
73                reason: e.to_string(),
74            }
75        })
76    }
77
78    pub async fn exists(&self, key: &str) -> Result<bool, StorageError> {
79        self.storage.exists(key).await
80    }
81
82    pub async fn delete(&self, key: &str) -> Result<(), StorageError> {
83        self.storage
84            .delete(key)
85            .await
86            .map_err(|e| StorageError::DocsDeleteFailed {
87                key: key.to_string(),
88                reason: e.to_string(),
89            })
90    }
91
92    pub async fn list(&self, prefix: &str) -> Result<Vec<String>, StorageError> {
93        self.storage
94            .list(prefix)
95            .await
96            .map_err(|e| StorageError::DocsListFailed {
97                prefix: prefix.to_string(),
98                reason: e.to_string(),
99            })
100    }
101
102    /// Delete every key under `prefix` (used for crate/version teardown).
103    pub async fn delete_prefix(&self, prefix: &str) -> Result<(), StorageError> {
104        for key in self.list(prefix).await? {
105            self.delete(&key).await?;
106        }
107        Ok(())
108    }
109
110    /// Distinct version-folder names that have *any* key under `crate_name/`.
111    /// Callers still need to verify a given version actually has an
112    /// `index.html` — this only enumerates candidates.
113    pub async fn version_candidates(&self, crate_name: &str) -> Result<Vec<String>, StorageError> {
114        let prefix = Self::crate_prefix(crate_name);
115        let mut versions = HashSet::new();
116        for key in self.list(&prefix).await? {
117            if let Some(rest) = key.strip_prefix(&prefix)
118                && let Some(v) = rest.split('/').next()
119            {
120                versions.insert(v.to_string());
121            }
122        }
123        Ok(versions.into_iter().collect())
124    }
125}