]>
Commit | Line | Data |
---|---|---|
c443f58b | 1 | use std::collections::HashSet; |
f71e8cc9 | 2 | use std::ffi::OsStr; |
c443f58b | 3 | use std::fs::OpenOptions; |
af309d4d | 4 | use std::os::unix::fs::OpenOptionsExt; |
c443f58b | 5 | use std::path::{Path, PathBuf}; |
c60d34bd | 6 | |
c443f58b WB |
7 | use anyhow::{format_err, Error}; |
8 | use futures::future::FutureExt; | |
9 | use futures::select; | |
10 | use tokio::signal::unix::{signal, SignalKind}; | |
c60d34bd | 11 | |
c443f58b | 12 | use pathpatterns::{MatchEntry, MatchType, PatternFlag}; |
c60d34bd | 13 | |
c443f58b WB |
14 | use proxmox::api::cli::*; |
15 | use proxmox::api::api; | |
c60d34bd | 16 | |
c443f58b | 17 | use proxmox_backup::tools; |
5444fa94 | 18 | use proxmox_backup::pxar::{fuse, format_single_line_entry, ENCODER_MAX_ENTRIES, Flags}; |
c60d34bd | 19 | |
129dda47 CE |
20 | fn extract_archive_from_reader<R: std::io::Read>( |
21 | reader: &mut R, | |
22 | target: &str, | |
5444fa94 | 23 | feature_flags: Flags, |
6a879109 | 24 | allow_existing_dirs: bool, |
129dda47 | 25 | verbose: bool, |
c443f58b | 26 | match_list: &[MatchEntry], |
d44185c4 | 27 | extract_match_default: bool, |
129dda47 | 28 | ) -> Result<(), Error> { |
c443f58b WB |
29 | proxmox_backup::pxar::extract_archive( |
30 | pxar::decoder::Decoder::from_std(reader)?, | |
31 | Path::new(target), | |
32 | &match_list, | |
d44185c4 | 33 | extract_match_default, |
c443f58b WB |
34 | feature_flags, |
35 | allow_existing_dirs, | |
36 | |path| { | |
37 | if verbose { | |
38 | println!("{:?}", path); | |
39 | } | |
40 | }, | |
41 | ) | |
9eae781a DM |
42 | } |
43 | ||
c443f58b WB |
44 | #[api( |
45 | input: { | |
46 | properties: { | |
47 | archive: { | |
48 | description: "Archive name.", | |
49 | }, | |
50 | pattern: { | |
51 | description: "List of paths or pattern matching files to restore", | |
52 | type: Array, | |
53 | items: { | |
54 | type: String, | |
55 | description: "Path or pattern matching files to restore.", | |
56 | }, | |
57 | optional: true, | |
58 | }, | |
59 | target: { | |
60 | description: "Target directory", | |
61 | optional: true, | |
62 | }, | |
63 | verbose: { | |
64 | description: "Verbose output.", | |
65 | optional: true, | |
66 | default: false, | |
67 | }, | |
68 | "no-xattrs": { | |
69 | description: "Ignore extended file attributes.", | |
70 | optional: true, | |
71 | default: false, | |
72 | }, | |
73 | "no-fcaps": { | |
74 | description: "Ignore file capabilities.", | |
75 | optional: true, | |
76 | default: false, | |
77 | }, | |
78 | "no-acls": { | |
79 | description: "Ignore access control list entries.", | |
80 | optional: true, | |
81 | default: false, | |
82 | }, | |
83 | "allow-existing-dirs": { | |
84 | description: "Allows directories to already exist on restore.", | |
85 | optional: true, | |
86 | default: false, | |
87 | }, | |
88 | "files-from": { | |
89 | description: "File containing match pattern for files to restore.", | |
90 | optional: true, | |
91 | }, | |
92 | "no-device-nodes": { | |
93 | description: "Ignore device nodes.", | |
94 | optional: true, | |
95 | default: false, | |
96 | }, | |
97 | "no-fifos": { | |
98 | description: "Ignore fifos.", | |
99 | optional: true, | |
100 | default: false, | |
101 | }, | |
102 | "no-sockets": { | |
103 | description: "Ignore sockets.", | |
104 | optional: true, | |
105 | default: false, | |
106 | }, | |
107 | }, | |
108 | }, | |
109 | )] | |
110 | /// Extract an archive. | |
1ef46b81 | 111 | fn extract_archive( |
c443f58b WB |
112 | archive: String, |
113 | pattern: Option<Vec<String>>, | |
114 | target: Option<String>, | |
115 | verbose: bool, | |
116 | no_xattrs: bool, | |
117 | no_fcaps: bool, | |
118 | no_acls: bool, | |
119 | allow_existing_dirs: bool, | |
120 | files_from: Option<String>, | |
121 | no_device_nodes: bool, | |
122 | no_fifos: bool, | |
123 | no_sockets: bool, | |
124 | ) -> Result<(), Error> { | |
5444fa94 | 125 | let mut feature_flags = Flags::DEFAULT; |
b344461b | 126 | if no_xattrs { |
5444fa94 | 127 | feature_flags ^= Flags::WITH_XATTRS; |
b344461b CE |
128 | } |
129 | if no_fcaps { | |
5444fa94 | 130 | feature_flags ^= Flags::WITH_FCAPS; |
b344461b | 131 | } |
9b384433 | 132 | if no_acls { |
5444fa94 | 133 | feature_flags ^= Flags::WITH_ACL; |
9b384433 | 134 | } |
81a9905e | 135 | if no_device_nodes { |
5444fa94 | 136 | feature_flags ^= Flags::WITH_DEVICE_NODES; |
81a9905e CE |
137 | } |
138 | if no_fifos { | |
5444fa94 | 139 | feature_flags ^= Flags::WITH_FIFOS; |
81a9905e CE |
140 | } |
141 | if no_sockets { | |
5444fa94 | 142 | feature_flags ^= Flags::WITH_SOCKETS; |
81a9905e | 143 | } |
1ef46b81 | 144 | |
c443f58b WB |
145 | let pattern = pattern.unwrap_or_else(Vec::new); |
146 | let target = target.as_ref().map_or_else(|| ".", String::as_str); | |
147 | ||
148 | let mut match_list = Vec::new(); | |
149 | if let Some(filename) = &files_from { | |
150 | for line in proxmox_backup::tools::file_get_non_comment_lines(filename)? { | |
151 | let line = line | |
152 | .map_err(|err| format_err!("error reading {}: {}", filename, err))?; | |
153 | match_list.push( | |
154 | MatchEntry::parse_pattern(line, PatternFlag::PATH_NAME, MatchType::Include) | |
155 | .map_err(|err| format_err!("bad pattern in file '{}': {}", filename, err))?, | |
156 | ); | |
a0ec687c CE |
157 | } |
158 | } | |
129dda47 | 159 | |
c443f58b WB |
160 | for entry in pattern { |
161 | match_list.push( | |
162 | MatchEntry::parse_pattern(entry, PatternFlag::PATH_NAME, MatchType::Include) | |
163 | .map_err(|err| format_err!("error in pattern: {}", err))?, | |
164 | ); | |
a0ec687c CE |
165 | } |
166 | ||
d44185c4 WB |
167 | let extract_match_default = match_list.is_empty(); |
168 | ||
9eae781a DM |
169 | if archive == "-" { |
170 | let stdin = std::io::stdin(); | |
171 | let mut reader = stdin.lock(); | |
c443f58b WB |
172 | extract_archive_from_reader( |
173 | &mut reader, | |
174 | &target, | |
175 | feature_flags, | |
176 | allow_existing_dirs, | |
177 | verbose, | |
178 | &match_list, | |
d44185c4 | 179 | extract_match_default, |
c443f58b | 180 | )?; |
9eae781a | 181 | } else { |
c443f58b WB |
182 | if verbose { |
183 | println!("PXAR extract: {}", archive); | |
184 | } | |
9eae781a DM |
185 | let file = std::fs::File::open(archive)?; |
186 | let mut reader = std::io::BufReader::new(file); | |
c443f58b WB |
187 | extract_archive_from_reader( |
188 | &mut reader, | |
189 | &target, | |
190 | feature_flags, | |
191 | allow_existing_dirs, | |
192 | verbose, | |
193 | &match_list, | |
d44185c4 | 194 | extract_match_default, |
c443f58b | 195 | )?; |
9eae781a | 196 | } |
1ef46b81 | 197 | |
c443f58b | 198 | Ok(()) |
1ef46b81 DM |
199 | } |
200 | ||
c443f58b WB |
201 | #[api( |
202 | input: { | |
203 | properties: { | |
204 | archive: { | |
205 | description: "Archive name.", | |
206 | }, | |
207 | source: { | |
208 | description: "Source directory.", | |
209 | }, | |
210 | verbose: { | |
211 | description: "Verbose output.", | |
212 | optional: true, | |
213 | default: false, | |
214 | }, | |
215 | "no-xattrs": { | |
216 | description: "Ignore extended file attributes.", | |
217 | optional: true, | |
218 | default: false, | |
219 | }, | |
220 | "no-fcaps": { | |
221 | description: "Ignore file capabilities.", | |
222 | optional: true, | |
223 | default: false, | |
224 | }, | |
225 | "no-acls": { | |
226 | description: "Ignore access control list entries.", | |
227 | optional: true, | |
228 | default: false, | |
229 | }, | |
230 | "all-file-systems": { | |
231 | description: "Include mounted sudirs.", | |
232 | optional: true, | |
233 | default: false, | |
234 | }, | |
235 | "no-device-nodes": { | |
236 | description: "Ignore device nodes.", | |
237 | optional: true, | |
238 | default: false, | |
239 | }, | |
240 | "no-fifos": { | |
241 | description: "Ignore fifos.", | |
242 | optional: true, | |
243 | default: false, | |
244 | }, | |
245 | "no-sockets": { | |
246 | description: "Ignore sockets.", | |
247 | optional: true, | |
248 | default: false, | |
249 | }, | |
250 | exclude: { | |
251 | description: "List of paths or pattern matching files to exclude.", | |
252 | optional: true, | |
253 | type: Array, | |
254 | items: { | |
255 | description: "Path or pattern matching files to restore", | |
256 | type: String, | |
257 | }, | |
258 | }, | |
259 | "entries-max": { | |
260 | description: "Max number of entries loaded at once into memory", | |
261 | optional: true, | |
262 | default: ENCODER_MAX_ENTRIES as isize, | |
263 | minimum: 0, | |
264 | maximum: std::isize::MAX, | |
265 | }, | |
266 | }, | |
267 | }, | |
268 | )] | |
269 | /// Create a new .pxar archive. | |
6049b71f | 270 | fn create_archive( |
c443f58b WB |
271 | archive: String, |
272 | source: String, | |
273 | verbose: bool, | |
274 | no_xattrs: bool, | |
275 | no_fcaps: bool, | |
276 | no_acls: bool, | |
277 | all_file_systems: bool, | |
278 | no_device_nodes: bool, | |
279 | no_fifos: bool, | |
280 | no_sockets: bool, | |
281 | exclude: Option<Vec<String>>, | |
282 | entries_max: isize, | |
283 | ) -> Result<(), Error> { | |
239e49f9 | 284 | let pattern_list = { |
c443f58b | 285 | let input = exclude.unwrap_or_else(Vec::new); |
239e49f9 | 286 | let mut pattern_list = Vec::with_capacity(input.len()); |
c443f58b | 287 | for entry in input { |
239e49f9 | 288 | pattern_list.push( |
c443f58b WB |
289 | MatchEntry::parse_pattern(entry, PatternFlag::PATH_NAME, MatchType::Exclude) |
290 | .map_err(|err| format_err!("error in exclude pattern: {}", err))?, | |
291 | ); | |
292 | } | |
239e49f9 | 293 | pattern_list |
c443f58b | 294 | }; |
02c7d8e5 | 295 | |
c443f58b WB |
296 | let device_set = if all_file_systems { |
297 | None | |
298 | } else { | |
299 | Some(HashSet::new()) | |
300 | }; | |
2eeaacb9 | 301 | |
1ef46b81 | 302 | let source = PathBuf::from(source); |
02c7d8e5 | 303 | |
c443f58b WB |
304 | let dir = nix::dir::Dir::open( |
305 | &source, | |
306 | nix::fcntl::OFlag::O_NOFOLLOW, | |
307 | nix::sys::stat::Mode::empty(), | |
308 | )?; | |
02c7d8e5 | 309 | |
af309d4d | 310 | let file = OpenOptions::new() |
02c7d8e5 DM |
311 | .create_new(true) |
312 | .write(true) | |
af309d4d | 313 | .mode(0o640) |
02c7d8e5 DM |
314 | .open(archive)?; |
315 | ||
c443f58b | 316 | let writer = std::io::BufWriter::with_capacity(1024 * 1024, file); |
5444fa94 | 317 | let mut feature_flags = Flags::DEFAULT; |
b344461b | 318 | if no_xattrs { |
5444fa94 | 319 | feature_flags ^= Flags::WITH_XATTRS; |
b344461b CE |
320 | } |
321 | if no_fcaps { | |
5444fa94 | 322 | feature_flags ^= Flags::WITH_FCAPS; |
b344461b | 323 | } |
9b384433 | 324 | if no_acls { |
5444fa94 | 325 | feature_flags ^= Flags::WITH_ACL; |
9b384433 | 326 | } |
81a9905e | 327 | if no_device_nodes { |
5444fa94 | 328 | feature_flags ^= Flags::WITH_DEVICE_NODES; |
81a9905e CE |
329 | } |
330 | if no_fifos { | |
5444fa94 | 331 | feature_flags ^= Flags::WITH_FIFOS; |
81a9905e CE |
332 | } |
333 | if no_sockets { | |
5444fa94 | 334 | feature_flags ^= Flags::WITH_SOCKETS; |
62d123e5 CE |
335 | } |
336 | ||
c443f58b WB |
337 | let writer = pxar::encoder::sync::StandardWriter::new(writer); |
338 | proxmox_backup::pxar::create_archive( | |
339 | dir, | |
340 | writer, | |
239e49f9 | 341 | pattern_list, |
62d123e5 | 342 | feature_flags, |
c443f58b | 343 | device_set, |
fc6047fc | 344 | false, |
c443f58b WB |
345 | |path| { |
346 | if verbose { | |
347 | println!("{:?}", path); | |
348 | } | |
349 | Ok(()) | |
350 | }, | |
6fc053ed | 351 | entries_max as usize, |
c443f58b | 352 | None, |
62d123e5 | 353 | )?; |
c60d34bd | 354 | |
c443f58b | 355 | Ok(()) |
c60d34bd DM |
356 | } |
357 | ||
c443f58b WB |
358 | #[api( |
359 | input: { | |
360 | properties: { | |
361 | archive: { description: "Archive name." }, | |
362 | mountpoint: { description: "Mountpoint for the file system." }, | |
363 | verbose: { | |
364 | description: "Verbose output, running in the foreground (for debugging).", | |
365 | optional: true, | |
366 | default: false, | |
367 | }, | |
368 | }, | |
369 | }, | |
370 | )] | |
f71e8cc9 | 371 | /// Mount the archive to the provided mountpoint via FUSE. |
c443f58b WB |
372 | async fn mount_archive( |
373 | archive: String, | |
374 | mountpoint: String, | |
375 | verbose: bool, | |
376 | ) -> Result<(), Error> { | |
377 | let archive = Path::new(&archive); | |
378 | let mountpoint = Path::new(&mountpoint); | |
f71e8cc9 | 379 | let options = OsStr::new("ro,default_permissions"); |
c443f58b WB |
380 | |
381 | let session = fuse::Session::mount_path(&archive, &options, verbose, mountpoint) | |
382 | .await | |
f71e8cc9 | 383 | .map_err(|err| format_err!("pxar mount failed: {}", err))?; |
f71e8cc9 | 384 | |
c443f58b | 385 | let mut interrupt = signal(SignalKind::interrupt())?; |
f71e8cc9 | 386 | |
c443f58b WB |
387 | select! { |
388 | res = session.fuse() => res?, | |
389 | _ = interrupt.recv().fuse() => { | |
390 | if verbose { | |
391 | eprintln!("interrupted"); | |
392 | } | |
393 | } | |
394 | } | |
255f378a | 395 | |
c443f58b WB |
396 | Ok(()) |
397 | } | |
255f378a | 398 | |
c443f58b WB |
399 | #[api( |
400 | input: { | |
401 | properties: { | |
402 | archive: { | |
403 | description: "Archive name.", | |
404 | }, | |
405 | verbose: { | |
406 | description: "Verbose output.", | |
407 | optional: true, | |
408 | default: false, | |
409 | }, | |
410 | }, | |
411 | }, | |
412 | )] | |
413 | /// List the contents of an archive. | |
414 | fn dump_archive(archive: String, verbose: bool) -> Result<(), Error> { | |
415 | for entry in pxar::decoder::Decoder::open(archive)? { | |
416 | let entry = entry?; | |
255f378a | 417 | |
c443f58b WB |
418 | if verbose { |
419 | println!("{}", format_single_line_entry(&entry)); | |
420 | } else { | |
421 | println!("{:?}", entry.path()); | |
422 | } | |
423 | } | |
424 | Ok(()) | |
425 | } | |
255f378a | 426 | |
c60d34bd | 427 | fn main() { |
c60d34bd | 428 | let cmd_def = CliCommandMap::new() |
c443f58b WB |
429 | .insert( |
430 | "create", | |
431 | CliCommand::new(&API_METHOD_CREATE_ARCHIVE) | |
432 | .arg_param(&["archive", "source"]) | |
433 | .completion_cb("archive", tools::complete_file_name) | |
434 | .completion_cb("source", tools::complete_file_name), | |
435 | ) | |
436 | .insert( | |
437 | "extract", | |
438 | CliCommand::new(&API_METHOD_EXTRACT_ARCHIVE) | |
439 | .arg_param(&["archive", "target"]) | |
440 | .completion_cb("archive", tools::complete_file_name) | |
441 | .completion_cb("target", tools::complete_file_name) | |
442 | .completion_cb("files-from", tools::complete_file_name), | |
c60d34bd | 443 | ) |
c443f58b WB |
444 | .insert( |
445 | "mount", | |
446 | CliCommand::new(&API_METHOD_MOUNT_ARCHIVE) | |
447 | .arg_param(&["archive", "mountpoint"]) | |
448 | .completion_cb("archive", tools::complete_file_name) | |
449 | .completion_cb("mountpoint", tools::complete_file_name), | |
f71e8cc9 | 450 | ) |
c443f58b WB |
451 | .insert( |
452 | "list", | |
453 | CliCommand::new(&API_METHOD_DUMP_ARCHIVE) | |
454 | .arg_param(&["archive"]) | |
455 | .completion_cb("archive", tools::complete_file_name), | |
c60d34bd DM |
456 | ); |
457 | ||
7b22acd0 | 458 | let rpcenv = CliEnvironment::new(); |
c443f58b WB |
459 | run_cli_command(cmd_def, rpcenv, Some(|future| { |
460 | proxmox_backup::tools::runtime::main(future) | |
461 | })); | |
c60d34bd | 462 | } |