1use 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#[derive(clap::Parser, Command, Debug)]
27pub(crate) struct DumpCmd {
28 #[clap(value_name = "SNAPSHOT[:PATH]")]
32 snap: String,
33
34 #[clap(long, value_name = "FORMAT", default_value = "auto")]
36 archive: ArchiveKind,
37
38 #[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 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 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 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 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 _ = 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 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}