Skip to main content

terminus_store/storage/
pack.rs

1use std::collections::{HashMap, HashSet};
2use std::fmt::Display;
3use std::io::{self, Read};
4use std::path::PathBuf;
5use std::time::{SystemTime, UNIX_EPOCH};
6
7use async_trait::async_trait;
8
9use super::cache::*;
10use super::consts::*;
11use super::file::*;
12use super::layer::*;
13
14use flate2::read::GzDecoder;
15use flate2::write::GzEncoder;
16use flate2::Compression;
17use tar::*;
18use tokio::io::AsyncWriteExt;
19
20#[async_trait]
21pub trait Packable {
22    /// Export the given layers by creating a pack, a Vec<u8> that can later be used with `import_layers` on a different store.
23    async fn export_layers(
24        &self,
25        layer_ids: Box<dyn Iterator<Item = [u32; 5]> + Send>,
26    ) -> io::Result<Vec<u8>>;
27
28    /// Import the specified layers from the given pack, a byte slice that was previously generated with `export_layers`, on another store, and possibly even another machine).
29    ///
30    /// After this operation, the specified layers will be retrievable
31    /// from this store, provided they existed in the pack. specified
32    /// layers that are not in the pack are silently ignored.
33    async fn import_layers(
34        &self,
35        pack: &[u8],
36        layer_ids: Box<dyn Iterator<Item = [u32; 5]> + Send>,
37    ) -> io::Result<()>;
38}
39
40#[async_trait]
41impl<T: PersistentLayerStore> Packable for T {
42    async fn export_layers(
43        &self,
44        layer_ids: Box<dyn Iterator<Item = [u32; 5]> + Send>,
45    ) -> io::Result<Vec<u8>> {
46        let mtime = SystemTime::now()
47            .duration_since(UNIX_EPOCH)
48            .unwrap()
49            .as_secs();
50
51        let mut enc = GzEncoder::new(Vec::new(), Compression::default());
52        {
53            let mut tar = tar::Builder::new(&mut enc);
54            for id in layer_ids {
55                tar_append_layer(&mut tar, self, id, mtime).await?;
56            }
57            tar.finish().unwrap();
58        }
59        // TODO: Proper error handling
60        Ok(enc.finish().unwrap())
61    }
62
63    async fn import_layers(
64        &self,
65        pack: &[u8],
66        layer_ids: Box<dyn Iterator<Item = [u32; 5]> + Send>,
67    ) -> io::Result<()> {
68        let mut layer_id_set = HashSet::new();
69        for id in layer_ids {
70            layer_id_set.insert(name_to_string(id));
71            self.create_named_directory(id).await?;
72        }
73
74        let handle = tokio::runtime::Handle::current();
75        tokio::task::block_in_place(|| {
76            let cursor = io::Cursor::new(pack);
77            let tar = GzDecoder::new(cursor);
78            let mut archive = Archive::new(tar);
79
80            // TODO we actually need to validate that these layers, when extracted, will make for a valid store.
81            // In terminus-server we are currently already doing this validation. Due to time constraints, we're not implementing it here.
82            //
83            // This should definitely be done in the future though, to make this part of the library independently usable in a safe manner.
84            for e in archive.entries()? {
85                let mut entry = e?;
86                let path = entry.path()?;
87                let os_file_name = path.file_name().unwrap();
88                let file_name = os_file_name
89                    .to_str()
90                    .ok_or_else(|| {
91                        io::Error::new(
92                            io::ErrorKind::InvalidData,
93                            "unexpected non-utf8 directory name",
94                        )
95                    })?
96                    .to_owned();
97
98                // check if entry is prefixed with a layer id we are interested in
99                let layer_id = path.iter().next().and_then(|p| p.to_str()).unwrap_or("");
100
101                if layer_id_set.contains(layer_id) {
102                    // this conversion should always work cause we are
103                    // only able to match things that went through the
104                    // conversion in the opposite direction.
105                    let layer_id_arr = string_to_name(layer_id).unwrap();
106
107                    let header = entry.header();
108                    if !header.entry_type().is_file() {
109                        continue;
110                    }
111
112                    let mut content = Vec::with_capacity(header.size()? as usize);
113                    entry.read_to_end(&mut content)?;
114
115                    handle.block_on(async move {
116                        let file = self.get_file(layer_id_arr, &file_name).await?;
117                        let mut writer = file.open_write().await?;
118                        writer.write_all(&content).await?;
119                        writer.flush().await?;
120                        writer.sync_all().await?;
121
122                        Ok::<_, io::Error>(())
123                    })?;
124                }
125            }
126
127            for layer_id in layer_id_set {
128                let layer_id_arr = string_to_name(&layer_id).unwrap();
129                handle.block_on(self.finalize_layer(layer_id_arr))?;
130            }
131
132            Ok(())
133        })
134    }
135}
136
137async fn tar_append_file<S: PersistentLayerStore, W: io::Write>(
138    store: &S,
139    tar: &mut tar::Builder<W>,
140    layer: [u32; 5],
141    layer_path: &PathBuf,
142    file_name: &str,
143    mtime: u64,
144) -> io::Result<()> {
145    if store.file_exists(layer, file_name).await? {
146        let file = store.get_file(layer, file_name).await?;
147        let contents = file.map().await?;
148        let cursor = io::Cursor::new(&contents);
149
150        let path = layer_path.join(file_name);
151
152        let mut header = Header::new_gnu();
153        header.set_mode(0o644);
154        header.set_size(file.size().await? as u64);
155        header.set_mtime(mtime);
156        tokio::task::block_in_place(|| tar.append_data(&mut header, path, cursor).unwrap());
157
158        Ok(())
159    } else {
160        Err(io::Error::new(
161            io::ErrorKind::NotFound,
162            "file does not exist",
163        ))
164    }
165}
166
167async fn tar_append_file_if_exists<S: PersistentLayerStore, W: io::Write>(
168    store: &S,
169    tar: &mut tar::Builder<W>,
170    layer: [u32; 5],
171    layer_path: &PathBuf,
172    file_name: &str,
173    mtime: u64,
174) -> io::Result<()> {
175    if store.file_exists(layer, file_name).await? {
176        let file = store.get_file(layer, file_name).await?;
177        let contents = file.map().await?;
178        let cursor = io::Cursor::new(&contents);
179
180        let path = layer_path.join(file_name);
181
182        let mut header = Header::new_gnu();
183        header.set_mode(0o644);
184        header.set_size(file.size().await? as u64);
185        header.set_mtime(mtime);
186        tokio::task::block_in_place(|| tar.append_data(&mut header, path, cursor).unwrap());
187    }
188
189    Ok(())
190}
191
192async fn tar_append_layer<W: io::Write, S: PersistentLayerStore>(
193    tar: &mut tar::Builder<W>,
194    store: &S,
195    layer: [u32; 5],
196    mtime: u64,
197) -> io::Result<()> {
198    let mut header = Header::new_gnu();
199    header.set_mode(0o755);
200    header.set_entry_type(EntryType::Directory);
201    header.set_mtime(mtime);
202    header.set_size(0);
203    let layer_name = name_to_string(layer);
204    let mut path = PathBuf::new();
205    path.push(layer_name);
206    tokio::task::block_in_place(|| {
207        tar.append_data(&mut header, &path, std::io::empty())
208            .unwrap()
209    });
210
211    for f in &SHARED_REQUIRED_FILES {
212        tar_append_file(store, tar, layer, &path, f, mtime).await?;
213    }
214    for f in &SHARED_OPTIONAL_FILES {
215        if f == &FILENAMES.rollup {
216            // skip the rollup file. It will not be resolvable remotely.
217            continue;
218        }
219        tar_append_file_if_exists(store, tar, layer, &path, f, mtime).await?;
220    }
221    if store.file_exists(layer, FILENAMES.parent).await? {
222        // this is a child layer
223        for f in &CHILD_LAYER_REQUIRED_FILES {
224            tar_append_file(store, tar, layer, &path, f, mtime).await?;
225        }
226        for f in &CHILD_LAYER_OPTIONAL_FILES {
227            tar_append_file_if_exists(store, tar, layer, &path, f, mtime).await?;
228        }
229    } else {
230        // this is a base layer
231        for f in &BASE_LAYER_REQUIRED_FILES {
232            tar_append_file(store, tar, layer, &path, f, mtime).await?;
233        }
234        for f in &BASE_LAYER_OPTIONAL_FILES {
235            tar_append_file_if_exists(store, tar, layer, &path, f, mtime).await?;
236        }
237    }
238
239    Ok(())
240}
241
242#[derive(Debug)]
243pub enum PackError {
244    LayerNotFound,
245    Io(io::Error),
246    Utf8Error(std::str::Utf8Error),
247}
248
249impl Display for PackError {
250    fn fmt(&self, formatter: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
251        write!(formatter, "{:?}", self)
252    }
253}
254
255impl From<io::Error> for PackError {
256    fn from(err: io::Error) -> Self {
257        Self::Io(err)
258    }
259}
260impl From<std::str::Utf8Error> for PackError {
261    fn from(err: std::str::Utf8Error) -> Self {
262        Self::Utf8Error(err)
263    }
264}
265
266pub fn pack_layer_parents<R: io::Read>(
267    readable: R,
268) -> Result<HashMap<[u32; 5], Option<[u32; 5]>>, PackError> {
269    let tar = GzDecoder::new(readable);
270    let mut archive = Archive::new(tar);
271
272    // build a set out of the layer ids for easy retrieval
273    let mut result_map = HashMap::new();
274
275    for e in archive.entries()? {
276        let mut entry = e?;
277        let path = entry.path()?;
278
279        let id = string_to_name(
280            path.iter()
281                .next()
282                .expect("expected path to have at least one component")
283                .to_str()
284                .expect("expected proper unicode path"),
285        )?;
286
287        if path.file_name().expect("expected path to have a filename") == "parent.hex" {
288            // this is an element we want to know the parent of
289            // lets read it
290            let mut parent_id_bytes = [0u8; 40];
291            entry.read_exact(&mut parent_id_bytes)?;
292            let parent_id_str = std::str::from_utf8(&parent_id_bytes)?;
293            let parent_id = string_to_name(parent_id_str)?;
294
295            result_map.insert(id, Some(parent_id));
296        } else {
297            // Ensure that an entry for this layer exists
298            // If we encounter the parent file later on, this'll be overwritten with the parent id.
299            // If not, it can be assumed to not have a parent.
300            result_map.entry(id).or_insert(None);
301        }
302    }
303
304    Ok(result_map)
305}
306
307#[async_trait]
308impl Packable for CachedLayerStore {
309    async fn export_layers(
310        &self,
311        layer_ids: Box<dyn Iterator<Item = [u32; 5]> + Send>,
312    ) -> io::Result<Vec<u8>> {
313        self.inner.export_layers(layer_ids).await
314    }
315
316    async fn import_layers(
317        &self,
318        pack: &[u8],
319        layer_ids: Box<dyn Iterator<Item = [u32; 5]> + Send>,
320    ) -> io::Result<()> {
321        self.inner.import_layers(pack, layer_ids).await
322    }
323}
324
325#[cfg(test)]
326mod tests {
327    use super::*;
328    use crate::layer::*;
329    use crate::storage::directory::*;
330    use std::sync::Arc;
331    use tempfile::tempdir;
332
333    #[tokio::test(flavor = "multi_thread")]
334    async fn export_import_layer_with_rollup() {
335        let dir1 = tempdir().unwrap();
336        let store1 = Arc::new(DirectoryLayerStore::new(dir1.path()));
337        let dir2 = tempdir().unwrap();
338        let store2 = Arc::new(DirectoryLayerStore::new(dir2.path()));
339
340        let mut builder = store1.create_base_layer().await.unwrap();
341        let base_name = builder.name();
342
343        builder.add_value_triple(ValueTriple::new_node("cow", "likes", "duck"));
344        builder.add_value_triple(ValueTriple::new_node("duck", "hates", "cow"));
345
346        builder.commit_boxed().await.unwrap();
347
348        let mut builder = store1.create_child_layer(base_name).await.unwrap();
349        let child_name = builder.name();
350
351        builder.remove_value_triple(ValueTriple::new_node("duck", "hates", "cow"));
352        builder.add_value_triple(ValueTriple::new_node("duck", "likes", "cow"));
353
354        builder.commit_boxed().await.unwrap();
355
356        let unrolled_layer = store1.get_layer(child_name).await.unwrap().unwrap();
357
358        store1.clone().rollup(unrolled_layer).await.unwrap();
359
360        let export = store1
361            .export_layers(Box::new(vec![base_name, child_name].into_iter()))
362            .await
363            .unwrap();
364
365        store2
366            .import_layers(&export, Box::new(vec![base_name, child_name].into_iter()))
367            .await
368            .unwrap();
369
370        let imported_layer = store2.get_layer(child_name).await.unwrap().unwrap();
371        let triples: Vec<_> = imported_layer
372            .triples()
373            .map(|t| imported_layer.id_triple_to_string(&t).unwrap())
374            .collect();
375        assert_eq!(
376            vec![
377                ValueTriple::new_node("cow", "likes", "duck"),
378                ValueTriple::new_node("duck", "likes", "cow")
379            ],
380            triples
381        );
382    }
383}