1use 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#[derive(clap::Parser, Command, Debug)]
26pub(crate) struct DumpCmd {
27 #[clap(value_name = "SNAPSHOT[:PATH]")]
29 snap: String,
30
31 #[clap(long, value_name = "FORMAT", default_value = "auto")]
33 archive: ArchiveKind,
34
35 #[clap(long)]
37 file: Option<PathBuf>,
38
39 #[clap(long, help_heading = "Exclude options")]
41 glob: Vec<String>,
42
43 #[clap(long, value_name = "GLOB", help_heading = "Exclude options")]
45 iglob: Vec<String>,
46
47 #[clap(long, value_name = "FILE", help_heading = "Exclude options")]
49 glob_file: Vec<String>,
50
51 #[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 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 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 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 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 _ = 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 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}