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