1 use std
::collections
::HashSet
;
3 use std
::fs
::OpenOptions
;
4 use std
::os
::unix
::fs
::OpenOptionsExt
;
5 use std
::path
::{Path, PathBuf}
;
7 use std
::sync
::atomic
::{AtomicBool, Ordering}
;
9 use anyhow
::{bail, format_err, Error}
;
10 use futures
::future
::FutureExt
;
12 use tokio
::signal
::unix
::{signal, SignalKind}
;
14 use pathpatterns
::{MatchEntry, MatchType, PatternFlag}
;
15 use pbs_client
::pxar
::{fuse, format_single_line_entry, ENCODER_MAX_ENTRIES, Flags, PxarExtractOptions}
;
17 use proxmox_schema
::api
;
18 use proxmox_router
::cli
::*;
20 fn extract_archive_from_reader
<R
: std
::io
::Read
>(
25 options
: PxarExtractOptions
,
26 ) -> Result
<(), Error
> {
27 pbs_client
::pxar
::extract_archive(
28 pxar
::decoder
::Decoder
::from_std(reader
)?
,
33 println
!("{:?}", path
);
44 description
: "Archive name.",
47 description
: "List of paths or pattern matching files to restore",
51 description
: "Path or pattern matching files to restore.",
56 description
: "Target directory",
60 description
: "Verbose output.",
65 description
: "Ignore extended file attributes.",
70 description
: "Ignore file capabilities.",
75 description
: "Ignore access control list entries.",
79 "allow-existing-dirs": {
80 description
: "Allows directories to already exist on restore.",
85 description
: "File containing match pattern for files to restore.",
89 description
: "Ignore device nodes.",
94 description
: "Ignore fifos.",
99 description
: "Ignore sockets.",
104 description
: "Stop on errors. Otherwise most errors will simply warn.",
111 /// Extract an archive.
112 #[allow(clippy::too_many_arguments)]
115 pattern
: Option
<Vec
<String
>>,
116 target
: Option
<String
>,
121 allow_existing_dirs
: bool
,
122 files_from
: Option
<String
>,
123 no_device_nodes
: bool
,
127 ) -> Result
<(), Error
> {
128 let mut feature_flags
= Flags
::DEFAULT
;
130 feature_flags
.remove(Flags
::WITH_XATTRS
);
133 feature_flags
.remove(Flags
::WITH_FCAPS
);
136 feature_flags
.remove(Flags
::WITH_ACL
);
139 feature_flags
.remove(Flags
::WITH_DEVICE_NODES
);
142 feature_flags
.remove(Flags
::WITH_FIFOS
);
145 feature_flags
.remove(Flags
::WITH_SOCKETS
);
148 let pattern
= pattern
.unwrap_or_else(Vec
::new
);
149 let target
= target
.as_ref().map_or_else(|| ".", String
::as_str
);
151 let mut match_list
= Vec
::new();
152 if let Some(filename
) = &files_from
{
153 for line
in proxmox_sys
::fs
::file_get_non_comment_lines(filename
)?
{
155 .map_err(|err
| format_err
!("error reading {}: {}", filename
, err
))?
;
157 MatchEntry
::parse_pattern(line
, PatternFlag
::PATH_NAME
, MatchType
::Include
)
158 .map_err(|err
| format_err
!("bad pattern in file '{}': {}", filename
, err
))?
,
163 for entry
in pattern
{
165 MatchEntry
::parse_pattern(entry
, PatternFlag
::PATH_NAME
, MatchType
::Include
)
166 .map_err(|err
| format_err
!("error in pattern: {}", err
))?
,
170 let extract_match_default
= match_list
.is_empty();
172 let was_ok
= Arc
::new(AtomicBool
::new(true));
173 let on_error
= if strict
{
174 // by default errors are propagated up
177 let was_ok
= Arc
::clone(&was_ok
);
178 // otherwise we want to log them but not act on them
179 Some(Box
::new(move |err
| {
180 was_ok
.store(false, Ordering
::Release
);
181 eprintln
!("error: {}", err
);
183 }) as Box
<dyn FnMut(Error
) -> Result
<(), Error
> + Send
>)
186 let options
= PxarExtractOptions
{
187 match_list
: &match_list
,
189 extract_match_default
,
194 let stdin
= std
::io
::stdin();
195 let mut reader
= stdin
.lock();
196 extract_archive_from_reader(
205 println
!("PXAR extract: {}", archive
);
207 let file
= std
::fs
::File
::open(archive
)?
;
208 let mut reader
= std
::io
::BufReader
::new(file
);
209 extract_archive_from_reader(
218 if !was_ok
.load(Ordering
::Acquire
) {
219 bail
!("there were errors");
229 description
: "Archive name.",
232 description
: "Source directory.",
235 description
: "Verbose output.",
240 description
: "Ignore extended file attributes.",
245 description
: "Ignore file capabilities.",
250 description
: "Ignore access control list entries.",
254 "all-file-systems": {
255 description
: "Include mounted sudirs.",
260 description
: "Ignore device nodes.",
265 description
: "Ignore fifos.",
270 description
: "Ignore sockets.",
275 description
: "List of paths or pattern matching files to exclude.",
279 description
: "Path or pattern matching files to restore",
284 description
: "Max number of entries loaded at once into memory",
286 default: ENCODER_MAX_ENTRIES
as isize,
293 /// Create a new .pxar archive.
294 #[allow(clippy::too_many_arguments)]
295 async
fn create_archive(
302 all_file_systems
: bool
,
303 no_device_nodes
: bool
,
306 exclude
: Option
<Vec
<String
>>,
308 ) -> Result
<(), Error
> {
310 let input
= exclude
.unwrap_or_else(Vec
::new
);
311 let mut patterns
= Vec
::with_capacity(input
.len());
314 MatchEntry
::parse_pattern(entry
, PatternFlag
::PATH_NAME
, MatchType
::Exclude
)
315 .map_err(|err
| format_err
!("error in exclude pattern: {}", err
))?
,
321 let device_set
= if all_file_systems
{
327 let options
= pbs_client
::pxar
::PxarCreateOptions
{
328 entries_max
: entries_max
as usize,
332 skip_lost_and_found
: false,
336 let source
= PathBuf
::from(source
);
338 let dir
= nix
::dir
::Dir
::open(
340 nix
::fcntl
::OFlag
::O_NOFOLLOW
,
341 nix
::sys
::stat
::Mode
::empty(),
344 let file
= OpenOptions
::new()
350 let writer
= std
::io
::BufWriter
::with_capacity(1024 * 1024, file
);
351 let mut feature_flags
= Flags
::DEFAULT
;
353 feature_flags
.remove(Flags
::WITH_XATTRS
);
356 feature_flags
.remove(Flags
::WITH_FCAPS
);
359 feature_flags
.remove(Flags
::WITH_ACL
);
362 feature_flags
.remove(Flags
::WITH_DEVICE_NODES
);
365 feature_flags
.remove(Flags
::WITH_FIFOS
);
368 feature_flags
.remove(Flags
::WITH_SOCKETS
);
371 let writer
= pxar
::encoder
::sync
::StandardWriter
::new(writer
);
372 pbs_client
::pxar
::create_archive(
378 println
!("{:?}", path
);
392 archive
: { description: "Archive name." }
,
393 mountpoint
: { description: "Mountpoint for the file system." }
,
395 description
: "Verbose output, running in the foreground (for debugging).",
402 /// Mount the archive to the provided mountpoint via FUSE.
403 async
fn mount_archive(
407 ) -> Result
<(), Error
> {
408 let archive
= Path
::new(&archive
);
409 let mountpoint
= Path
::new(&mountpoint
);
410 let options
= OsStr
::new("ro,default_permissions");
412 let session
= fuse
::Session
::mount_path(&archive
, &options
, verbose
, mountpoint
)
414 .map_err(|err
| format_err
!("pxar mount failed: {}", err
))?
;
416 let mut interrupt
= signal(SignalKind
::interrupt())?
;
419 res
= session
.fuse() => res?
,
420 _
= interrupt
.recv().fuse() => {
422 eprintln
!("interrupted");
434 description
: "Archive name.",
437 description
: "Verbose output.",
444 /// List the contents of an archive.
445 fn dump_archive(archive
: String
, verbose
: bool
) -> Result
<(), Error
> {
446 for entry
in pxar
::decoder
::Decoder
::open(archive
)?
{
450 println
!("{}", format_single_line_entry(&entry
));
452 println
!("{:?}", entry
.path());
459 let cmd_def
= CliCommandMap
::new()
462 CliCommand
::new(&API_METHOD_CREATE_ARCHIVE
)
463 .arg_param(&["archive", "source"])
464 .completion_cb("archive", complete_file_name
)
465 .completion_cb("source", complete_file_name
),
469 CliCommand
::new(&API_METHOD_EXTRACT_ARCHIVE
)
470 .arg_param(&["archive", "target"])
471 .completion_cb("archive", complete_file_name
)
472 .completion_cb("target", complete_file_name
)
473 .completion_cb("files-from", complete_file_name
),
477 CliCommand
::new(&API_METHOD_MOUNT_ARCHIVE
)
478 .arg_param(&["archive", "mountpoint"])
479 .completion_cb("archive", complete_file_name
)
480 .completion_cb("mountpoint", complete_file_name
),
484 CliCommand
::new(&API_METHOD_DUMP_ARCHIVE
)
485 .arg_param(&["archive"])
486 .completion_cb("archive", complete_file_name
),
489 let rpcenv
= CliEnvironment
::new();
490 run_cli_command(cmd_def
, rpcenv
, Some(|future
| {
491 proxmox_async
::runtime
::main(future
)