Skip to main content

rustic_rs/commands/
dump.rs

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