gitlab_runner/
artifact.rs

1//! Helpers for artifacts downloaded from gitlab
2use std::{
3    io::{Read, Seek},
4    path::PathBuf,
5};
6
7use zip::{read::ZipFile, result::ZipResult};
8
9/// A file in a gitlab artifact
10///
11/// Most importantly this implements [`Read`] to read out the content of the file
12pub struct ArtifactFile<'a>(ZipFile<'a>);
13
14impl ArtifactFile<'_> {
15    /// Get the name of the file
16    pub fn name(&self) -> &str {
17        self.0.name()
18    }
19
20    /// Get the safe path of the file
21    ///
22    /// The path will be safe to use, that is to say it contains no NUL bytes and will be a
23    /// relative path that doesn't go outside of the root.
24    pub fn path(&self) -> Option<PathBuf> {
25        self.0.enclosed_name()
26    }
27}
28
29impl Read for ArtifactFile<'_> {
30    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
31        self.0.read(buf)
32    }
33}
34
35pub(crate) trait ReadSeek: Read + Seek {}
36impl<T> ReadSeek for T where T: Read + Seek {}
37
38/// An artifact downloaded from gitlab
39///
40/// The artifact holds a set of files which can be read out one by one
41pub struct Artifact {
42    zip: zip::ZipArchive<Box<dyn ReadSeek + Send>>,
43}
44
45impl Artifact {
46    pub(crate) fn new(data: Box<dyn ReadSeek + Send>) -> ZipResult<Self> {
47        let zip = zip::ZipArchive::new(data)?;
48        Ok(Self { zip })
49    }
50
51    /// Iterator of the files names inside the artifacts
52    ///
53    /// The returned file name isn't sanatized in any way and should *not* be used as a path on the
54    /// filesystem. To get a safe path of a given file use [`ArtifactFile::path`]
55    pub fn file_names(&self) -> impl Iterator<Item = &str> {
56        self.zip.file_names()
57    }
58
59    /// Get a file in the artifact by name
60    pub fn file(&mut self, name: &str) -> Option<ArtifactFile> {
61        self.zip.by_name(name).ok().map(ArtifactFile)
62    }
63
64    /// Get a file in the artifact by index
65    pub fn by_index(&mut self, i: usize) -> Option<ArtifactFile> {
66        self.zip.by_index(i).ok().map(ArtifactFile)
67    }
68
69    /// Checks whether the artifact has no files
70    pub fn is_empty(&self) -> bool {
71        self.zip.is_empty()
72    }
73
74    /// The number of files in the artifacts
75    pub fn len(&self) -> usize {
76        self.zip.len()
77    }
78}