Skip to main content

ambient_ci/
image_store.rs

1//! Manage a collection of virtual machine images.
2
3use std::{
4    collections::HashMap,
5    path::{Path, PathBuf},
6};
7
8use serde::{Deserialize, Serialize};
9
10use crate::{
11    checksummer::Checksummer,
12    util::{cat_text_file, mkdir, now, write_file, UtilError},
13};
14
15/// Build metadata for an image.
16pub struct MetadataBuilder {
17    source: PathBuf,
18    name: String,
19    filename: PathBuf,
20    sha256: String,
21    description: Option<String>,
22    url: Option<String>,
23    import_date: String,
24    uefi: bool,
25}
26
27impl MetadataBuilder {
28    /// Create a new metadata builder.
29    pub fn new(name: &str, source: &Path) -> Result<Self, ImageStoreError> {
30        let imported_image = PathBuf::from(format!("{name}.qcow2"));
31
32        let sha256 = Checksummer::new(source)
33            .sha256()
34            .map_err(|err| ImageStoreError::Digest(source.into(), err))?
35            .to_string();
36
37        Ok(Self {
38            source: source.into(),
39            name: name.into(),
40            filename: imported_image,
41            sha256,
42            description: None,
43            url: None,
44            import_date: now()?,
45            uefi: false,
46        })
47    }
48
49    /// Set description of image.
50    pub fn description(&mut self, value: &str) {
51        self.description = Some(value.into());
52    }
53
54    /// Set URL from whence image came.
55    pub fn url(&mut self, value: &str) {
56        self.url = Some(value.into());
57    }
58
59    /// Does image require UEFI?
60    pub fn uefi(&mut self, value: bool) {
61        self.uefi = value;
62    }
63
64    /// Build metadata.
65    pub fn build(self) -> Metadata {
66        Metadata {
67            source: self.source,
68            name: self.name,
69            filename: self.filename,
70            sha256: self.sha256,
71            description: self.description,
72            url: self.url,
73            import_date: self.import_date,
74            uefi: self.uefi,
75        }
76    }
77}
78
79/// Metadata for a virtual machine image.
80#[derive(Debug, Serialize, Deserialize)]
81pub struct Metadata {
82    // File from which image was copied into image store.
83    #[serde(skip)]
84    source: PathBuf,
85
86    /// User-supplied Name of image.
87    pub name: String,
88
89    /// Filename relative to image store.
90    pub filename: PathBuf,
91
92    /// SHA256 checksum of image file.
93    pub sha256: String,
94
95    /// A user-supplied description of image.
96    #[serde(skip_serializing_if = "Option::is_none")]
97    pub description: Option<String>,
98
99    /// URL to origin of image.
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub url: Option<String>,
102
103    /// Date when image was imported.
104    pub import_date: String,
105
106    /// Boot image with UEFI?
107    pub uefi: bool,
108}
109
110impl Metadata {
111    /// Serialize metadata to JSON.
112    pub fn to_json(&self) -> Result<String, ImageStoreError> {
113        let json: String = serde_json::to_string_pretty(&self)
114            .map_err(|err| ImageStoreError::JsonImageSer(self.name.clone(), err))?;
115        Ok(json)
116    }
117
118    /// Does image require UEFI?
119    pub fn uefi(&self) -> bool {
120        self.uefi
121    }
122}
123
124/// A collection of virtual machine images.
125#[derive(Debug)]
126pub struct ImageStore {
127    dirname: PathBuf,
128    metadata: HashMap<String, Metadata>,
129}
130
131impl ImageStore {
132    /// Create a new [`ImageStore`] that refers to a directory.
133    pub fn new(dirname: &Path) -> Result<Self, ImageStoreError> {
134        let filename = Self::metadata_filename(dirname);
135        let metadata = if filename.exists() {
136            let json = cat_text_file(&filename).map_err(ImageStoreError::Util)?;
137            serde_json::from_str(&json).map_err(|err| ImageStoreError::JsonParse(filename, err))?
138        } else {
139            HashMap::new()
140        };
141
142        Ok(Self {
143            dirname: dirname.into(),
144            metadata,
145        })
146    }
147
148    /// Save changes to disk.
149    pub fn save(&self) -> Result<(), ImageStoreError> {
150        let filename = Self::metadata_filename(&self.dirname);
151        let json: String = serde_json::to_string(&self.metadata)
152            .map_err(|err| ImageStoreError::JsonSer(filename.clone(), err))?;
153        write_file(&filename, json.as_bytes()).map_err(ImageStoreError::Util)?;
154        Ok(())
155    }
156
157    /// Does the store contain an image with a given name?
158    pub fn contains(&self, name: &str) -> bool {
159        self.metadata.contains_key(name)
160    }
161
162    fn metadata_filename(dirname: &Path) -> PathBuf {
163        dirname.join("images.json")
164    }
165
166    /// Name of image file.
167    pub fn image_filename(&self, metadata: &Metadata) -> PathBuf {
168        self.dirname.join(&metadata.filename)
169    }
170
171    /// List names of all images.
172    pub fn image_names(&self) -> Result<Vec<String>, ImageStoreError> {
173        Ok(self.metadata.keys().map(|name| name.to_string()).collect())
174    }
175
176    /// Metadata for an image.
177    pub fn get_metadata(&self, name: &str) -> Option<&Metadata> {
178        self.metadata.get(name)
179    }
180
181    /// Import an image to the store.
182    pub fn import(&mut self, metadata: Metadata) -> Result<(), ImageStoreError> {
183        if !self.dirname.exists() {
184            mkdir(&self.dirname).map_err(ImageStoreError::Util)?;
185        }
186
187        let filename = self.dirname.join(&metadata.filename);
188        std::fs::copy(&metadata.source, &filename)
189            .map_err(|err| ImageStoreError::Copy(filename.clone(), err))?;
190
191        self.metadata.insert(metadata.name.clone(), metadata);
192
193        Ok(())
194    }
195
196    /// Remove an image from a store.
197    pub fn remove(&mut self, name: &str) -> Result<(), ImageStoreError> {
198        let filename = self.image_name(name);
199        std::fs::remove_file(&filename)
200            .map_err(|err| ImageStoreError::RemoveImageFile(filename, err))?;
201        self.metadata.remove(name);
202        Ok(())
203    }
204
205    fn image_name(&self, name: &str) -> PathBuf {
206        self.dirname.join(format!("{name}.qcow2"))
207    }
208}
209
210/// Possible errors from managing a set of virtual machine images.
211#[derive(Debug, thiserror::Error)]
212pub enum ImageStoreError {
213    /// Can't compute checksum.
214    #[error("failed to compute checksum for file {0}")]
215    Digest(PathBuf, #[source] crate::checksummer::ChecksummerError),
216
217    /// Can't image store metadata as JSON.
218    #[error("failed to parse image store metadata file {0} as JSON")]
219    JsonParse(PathBuf, #[source] serde_json::Error),
220
221    /// Can't encode image store metadata as JSON.
222    #[error("failed to encode image store {0} metadata as JSON")]
223    JsonSer(PathBuf, #[source] serde_json::Error),
224
225    /// Can't encodce image metadata as JSON.
226    #[error("failed to encode image {0} metadata as JSON")]
227    JsonImageSer(String, #[source] serde_json::Error),
228
229    /// Error from `util` module.
230    #[error(transparent)]
231    Util(#[from] UtilError),
232
233    /// Can't copy image file.
234    #[error("failed to copy image {0} into image store")]
235    Copy(PathBuf, #[source] std::io::Error),
236
237    /// Can't remove image file.
238    #[error("failed to remove image file {0}")]
239    RemoveImageFile(PathBuf, #[source] std::io::Error),
240}