1use 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
15pub 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 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 pub fn description(&mut self, value: &str) {
51 self.description = Some(value.into());
52 }
53
54 pub fn url(&mut self, value: &str) {
56 self.url = Some(value.into());
57 }
58
59 pub fn uefi(&mut self, value: bool) {
61 self.uefi = value;
62 }
63
64 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#[derive(Debug, Serialize, Deserialize)]
81pub struct Metadata {
82 #[serde(skip)]
84 source: PathBuf,
85
86 pub name: String,
88
89 pub filename: PathBuf,
91
92 pub sha256: String,
94
95 #[serde(skip_serializing_if = "Option::is_none")]
97 pub description: Option<String>,
98
99 #[serde(skip_serializing_if = "Option::is_none")]
101 pub url: Option<String>,
102
103 pub import_date: String,
105
106 pub uefi: bool,
108}
109
110impl Metadata {
111 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 pub fn uefi(&self) -> bool {
120 self.uefi
121 }
122}
123
124#[derive(Debug)]
126pub struct ImageStore {
127 dirname: PathBuf,
128 metadata: HashMap<String, Metadata>,
129}
130
131impl ImageStore {
132 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 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 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 pub fn image_filename(&self, metadata: &Metadata) -> PathBuf {
168 self.dirname.join(&metadata.filename)
169 }
170
171 pub fn image_names(&self) -> Result<Vec<String>, ImageStoreError> {
173 Ok(self.metadata.keys().map(|name| name.to_string()).collect())
174 }
175
176 pub fn get_metadata(&self, name: &str) -> Option<&Metadata> {
178 self.metadata.get(name)
179 }
180
181 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 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#[derive(Debug, thiserror::Error)]
212pub enum ImageStoreError {
213 #[error("failed to compute checksum for file {0}")]
215 Digest(PathBuf, #[source] crate::checksummer::ChecksummerError),
216
217 #[error("failed to parse image store metadata file {0} as JSON")]
219 JsonParse(PathBuf, #[source] serde_json::Error),
220
221 #[error("failed to encode image store {0} metadata as JSON")]
223 JsonSer(PathBuf, #[source] serde_json::Error),
224
225 #[error("failed to encode image {0} metadata as JSON")]
227 JsonImageSer(String, #[source] serde_json::Error),
228
229 #[error(transparent)]
231 Util(#[from] UtilError),
232
233 #[error("failed to copy image {0} into image store")]
235 Copy(PathBuf, #[source] std::io::Error),
236
237 #[error("failed to remove image file {0}")]
239 RemoveImageFile(PathBuf, #[source] std::io::Error),
240}