rustic_rs/commands/
dump.rs

1//! `dump` subcommand
2
3use std::{
4    fs::File,
5    io::{copy, Cursor, Read, Seek, SeekFrom, Write},
6    path::PathBuf,
7};
8
9use crate::{repository::CliIndexedRepo, status_err, Application, RUSTIC_APP};
10
11use abscissa_core::{Command, Runnable, Shutdown};
12use anyhow::Result;
13use derive_more::FromStr;
14use flate2::{write::GzEncoder, Compression};
15use log::warn;
16use rustic_core::{
17    repofile::{Node, NodeType},
18    vfs::OpenFile,
19    LsOptions,
20};
21use tar::{Builder, EntryType, Header};
22use zip::{write::SimpleFileOptions, ZipWriter};
23
24/// `dump` subcommand
25#[derive(clap::Parser, Command, Debug)]
26pub(crate) struct DumpCmd {
27    /// file from snapshot to dump
28    #[clap(value_name = "SNAPSHOT[:PATH]")]
29    snap: String,
30
31    /// set archive format to use. Possible values: auto, content, tar, targz, zip. For "auto" format is dertermined by file extension (if given) or "tar" for dirs.
32    #[clap(long, value_name = "FORMAT", default_value = "auto")]
33    archive: ArchiveKind,
34
35    /// dump output to the given file. Use this instead of redirecting stdout to a file.
36    #[clap(long)]
37    file: Option<PathBuf>,
38
39    /// Glob pattern to exclude/include (can be specified multiple times)
40    #[clap(long, help_heading = "Exclude options")]
41    glob: Vec<String>,
42
43    /// Same as --glob pattern but ignores the casing of filenames
44    #[clap(long, value_name = "GLOB", help_heading = "Exclude options")]
45    iglob: Vec<String>,
46
47    /// Read glob patterns to exclude/include from this file (can be specified multiple times)
48    #[clap(long, value_name = "FILE", help_heading = "Exclude options")]
49    glob_file: Vec<String>,
50
51    /// Same as --glob-file ignores the casing of filenames in patterns
52    #[clap(long, value_name = "FILE", help_heading = "Exclude options")]
53    iglob_file: Vec<String>,
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, FromStr)]
57enum ArchiveKind {
58    Auto,
59    Content,
60    Tar,
61    TarGz,
62    Zip,
63}
64
65impl Runnable for DumpCmd {
66    fn run(&self) {
67        if let Err(err) = RUSTIC_APP
68            .config()
69            .repository
70            .run_indexed(|repo| self.inner_run(repo))
71        {
72            status_err!("{}", err);
73            RUSTIC_APP.shutdown(Shutdown::Crash);
74        };
75    }
76}
77
78impl DumpCmd {
79    fn inner_run(&self, repo: CliIndexedRepo) -> Result<()> {
80        let config = RUSTIC_APP.config();
81
82        let node =
83            repo.node_from_snapshot_path(&self.snap, |sn| config.snapshot_filter.matches(sn))?;
84
85        let stdout = std::io::stdout();
86
87        let ls_opts = LsOptions::default()
88            .glob(self.glob.clone())
89            .glob_file(self.glob_file.clone())
90            .iglob(self.iglob.clone())
91            .iglob_file(self.iglob_file.clone())
92            .recursive(true);
93
94        let ext = self
95            .file
96            .as_ref()
97            .and_then(|f| f.extension().map(|s| s.to_string_lossy().to_string()));
98
99        let archive = match self.archive {
100            ArchiveKind::Auto => match ext.as_deref() {
101                Some("tar") => ArchiveKind::Tar,
102                Some("tgz") | Some("gz") => ArchiveKind::TarGz,
103                Some("zip") => ArchiveKind::Zip,
104                _ if node.is_dir() => ArchiveKind::Tar,
105                _ => ArchiveKind::Content,
106            },
107            a => a,
108        };
109
110        let mut w: Box<dyn Write> = if let Some(file) = &self.file {
111            let mut file = File::create(file)?;
112            if archive == ArchiveKind::Zip {
113                // when writing zip to a file, we use the optimized writer
114                return write_zip_to_file(&repo, &node, &mut file, &ls_opts);
115            }
116            Box::new(file)
117        } else {
118            Box::new(stdout)
119        };
120
121        match archive {
122            ArchiveKind::Content => dump_content(&repo, &node, &mut w, &ls_opts)?,
123            ArchiveKind::Tar => dump_tar(&repo, &node, &mut w, &ls_opts)?,
124            ArchiveKind::TarGz => dump_tar_gz(&repo, &node, &mut w, &ls_opts)?,
125            ArchiveKind::Zip => dump_zip(&repo, &node, &mut w, &ls_opts)?,
126            _ => {}
127        };
128
129        Ok(())
130    }
131}
132
133fn dump_content(
134    repo: &CliIndexedRepo,
135    node: &Node,
136    w: &mut impl Write,
137    ls_opts: &LsOptions,
138) -> Result<()> {
139    for item in repo.ls(node, ls_opts)? {
140        let (_, node) = item?;
141        repo.dump(&node, w)?;
142    }
143    Ok(())
144}
145
146fn dump_tar_gz(
147    repo: &CliIndexedRepo,
148    node: &Node,
149    w: &mut impl Write,
150    ls_opts: &LsOptions,
151) -> Result<()> {
152    let mut w = GzEncoder::new(w, Compression::default());
153    dump_tar(repo, node, &mut w, ls_opts)
154}
155
156fn dump_tar(
157    repo: &CliIndexedRepo,
158    node: &Node,
159    w: &mut impl Write,
160    ls_opts: &LsOptions,
161) -> Result<()> {
162    let mut ar = Builder::new(w);
163    for item in repo.ls(node, ls_opts)? {
164        let (path, node) = item?;
165        let mut header = Header::new_gnu();
166
167        let entry_type = match &node.node_type {
168            NodeType::File => EntryType::Regular,
169            NodeType::Dir => EntryType::Directory,
170            NodeType::Symlink { .. } => EntryType::Symlink,
171            NodeType::Dev { .. } => EntryType::Block,
172            NodeType::Chardev { .. } => EntryType::Char,
173            NodeType::Fifo => EntryType::Fifo,
174            NodeType::Socket => {
175                warn!(
176                    "socket is not supported. Adding {} as empty file",
177                    path.display()
178                );
179                EntryType::Regular
180            }
181        };
182        header.set_entry_type(entry_type);
183        header.set_size(node.meta.size);
184        if let Some(mode) = node.meta.mode {
185            // TODO: this is some go-mapped mode, but lower bits are the standard unix mode bits -> is this ok?
186            header.set_mode(mode);
187        }
188        if let Some(uid) = node.meta.uid {
189            header.set_uid(uid.into());
190        }
191        if let Some(gid) = node.meta.gid {
192            header.set_uid(gid.into());
193        }
194        if let Some(user) = &node.meta.user {
195            header.set_username(user)?;
196        }
197        if let Some(group) = &node.meta.group {
198            header.set_groupname(group)?;
199        }
200        if let Some(mtime) = node.meta.mtime {
201            header.set_mtime(mtime.timestamp().try_into().unwrap_or_default());
202        }
203
204        // handle special files
205        if node.is_symlink() {
206            header.set_link_name(node.node_type.to_link())?;
207        }
208        match node.node_type {
209            NodeType::Dev { device } | NodeType::Chardev { device } => {
210                header.set_device_minor(device as u32)?;
211                header.set_device_major((device << 32) as u32)?;
212            }
213            _ => {}
214        }
215
216        if node.is_file() {
217            // write file content if this is a regular file
218            let open_file = OpenFileReader {
219                repo,
220                open_file: repo.open_file(&node)?,
221                offset: 0,
222            };
223            ar.append_data(&mut header, path, open_file)?;
224        } else {
225            let data: &[u8] = &[];
226            ar.append_data(&mut header, path, data)?;
227        }
228    }
229    // finish writing
230    _ = ar.into_inner()?;
231    Ok(())
232}
233
234fn dump_zip(
235    repo: &CliIndexedRepo,
236    node: &Node,
237    w: &mut impl Write,
238    ls_opts: &LsOptions,
239) -> Result<()> {
240    let w = SeekWriter {
241        write: w,
242        cursor: Cursor::new(Vec::new()),
243        written: 0,
244    };
245    let mut zip = ZipWriter::new(w);
246    zip.set_flush_on_finish_file(true);
247    write_zip_contents(repo, node, &mut zip, ls_opts)?;
248    let mut inner = zip.finish()?;
249    inner.flush()?;
250    Ok(())
251}
252
253fn write_zip_to_file(
254    repo: &CliIndexedRepo,
255    node: &Node,
256    file: &mut (impl Write + Seek),
257    ls_opts: &LsOptions,
258) -> Result<()> {
259    let mut zip = ZipWriter::new(file);
260    write_zip_contents(repo, node, &mut zip, ls_opts)?;
261    let _ = zip.finish()?;
262    Ok(())
263}
264
265fn write_zip_contents(
266    repo: &CliIndexedRepo,
267    node: &Node,
268    zip: &mut ZipWriter<impl Write + Seek>,
269    ls_opts: &LsOptions,
270) -> Result<()> {
271    for item in repo.ls(node, ls_opts)? {
272        let (path, node) = item?;
273
274        let mut options = SimpleFileOptions::default();
275        if let Some(mode) = node.meta.mode {
276            // TODO: this is some go-mapped mode, but lower bits are the standard unix mode bits -> is this ok?
277            options = options.unix_permissions(mode);
278        }
279        if let Some(mtime) = node.meta.mtime {
280            options =
281                options.last_modified_time(mtime.naive_local().try_into().unwrap_or_default());
282        }
283        if node.is_file() {
284            zip.start_file_from_path(path, options)?;
285            repo.dump(&node, zip)?;
286        } else {
287            zip.add_directory_from_path(path, options)?;
288        }
289    }
290    Ok(())
291}
292
293struct SeekWriter<W> {
294    write: W,
295    cursor: Cursor<Vec<u8>>,
296    written: u64,
297}
298
299impl<W> Read for SeekWriter<W> {
300    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
301        self.cursor.read(buf)
302    }
303}
304
305impl<W: Write> Write for SeekWriter<W> {
306    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
307        self.cursor.write(buf)
308    }
309
310    fn flush(&mut self) -> std::io::Result<()> {
311        _ = self.cursor.seek(SeekFrom::Start(0))?;
312        let n = copy(&mut self.cursor, &mut self.write)?;
313        _ = self.cursor.seek(SeekFrom::Start(0))?;
314        self.cursor.get_mut().clear();
315        self.cursor.get_mut().shrink_to(1_000_000);
316        self.written += n;
317        Ok(())
318    }
319}
320
321impl<W> Seek for SeekWriter<W> {
322    fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
323        match pos {
324            SeekFrom::Start(n) => self.cursor.seek(SeekFrom::Start(n - self.written)),
325            pos => self.cursor.seek(pos),
326        }
327    }
328    fn stream_position(&mut self) -> std::io::Result<u64> {
329        Ok(self.written + self.cursor.stream_position()?)
330    }
331}
332
333struct OpenFileReader<'a> {
334    repo: &'a CliIndexedRepo,
335    open_file: OpenFile,
336    offset: usize,
337}
338
339impl Read for OpenFileReader<'_> {
340    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
341        let data = self
342            .repo
343            .read_file_at(&self.open_file, self.offset, buf.len())
344            .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?;
345        let n = data.len();
346        buf[..n].copy_from_slice(&data);
347        self.offset += n;
348        Ok(n)
349    }
350}