portable_network_archive/command/
append.rs

1use crate::{
2    cli::{
3        CipherAlgorithmArgs, CompressionAlgorithmArgs, DateTime, FileArgsCompat, HashAlgorithmArgs,
4        PasswordArgs,
5    },
6    command::{
7        ask_password, check_password,
8        core::{
9            collect_items, create_entry, entry_option, read_paths, read_paths_stdin, CreateOptions,
10            KeepOptions, OwnerOptions, PathFilter, PathTransformers, StoreAs, TimeFilter,
11            TimeFilters, TimeOptions,
12        },
13        Command,
14    },
15    utils::{
16        re::{bsd::SubstitutionRule, gnu::TransformRule},
17        PathPartExt, VCS_FILES,
18    },
19};
20use anyhow::Context;
21use clap::{ArgGroup, Parser, ValueHint};
22use pna::Archive;
23use std::{
24    env, fs, io,
25    path::{Path, PathBuf},
26};
27
28#[derive(Parser, Clone, Debug)]
29#[command(
30    group(ArgGroup::new("unstable-acl").args(["keep_acl"]).requires("unstable")),
31    group(ArgGroup::new("unstable-include").args(["include"]).requires("unstable")),
32    group(ArgGroup::new("unstable-append-exclude").args(["exclude"]).requires("unstable")),
33    group(ArgGroup::new("unstable-files-from").args(["files_from"]).requires("unstable")),
34    group(ArgGroup::new("unstable-files-from-stdin").args(["files_from_stdin"]).requires("unstable")),
35    group(ArgGroup::new("unstable-exclude-from").args(["exclude_from"]).requires("unstable")),
36    group(ArgGroup::new("unstable-gitignore").args(["gitignore"]).requires("unstable")),
37    group(ArgGroup::new("unstable-substitution").args(["substitutions"]).requires("unstable")),
38    group(ArgGroup::new("unstable-transform").args(["transforms"]).requires("unstable")),
39    group(ArgGroup::new("path-transform").args(["substitutions", "transforms"])),
40    group(ArgGroup::new("read-files-from").args(["files_from", "files_from_stdin"])),
41    group(
42        ArgGroup::new("from-input")
43            .args(["files_from", "files_from_stdin", "exclude_from"])
44            .multiple(true)
45    ),
46    group(ArgGroup::new("null-requires").arg("null").requires("from-input")),
47    group(ArgGroup::new("store-uname").args(["uname"]).requires("keep_permission")),
48    group(ArgGroup::new("store-gname").args(["gname"]).requires("keep_permission")),
49    group(ArgGroup::new("store-numeric-owner").args(["numeric_owner"]).requires("keep_permission")),
50    group(ArgGroup::new("user-flag").args(["numeric_owner", "uname"])),
51    group(ArgGroup::new("group-flag").args(["numeric_owner", "gname"])),
52    group(ArgGroup::new("recursive-flag").args(["recursive", "no_recursive"])),
53    group(ArgGroup::new("keep-dir-flag").args(["keep_dir", "no_keep_dir"])),
54    group(ArgGroup::new("mtime-flag").args(["clamp_mtime"]).requires("mtime")),
55    group(ArgGroup::new("atime-flag").args(["clamp_atime"]).requires("atime")),
56    group(ArgGroup::new("unstable-exclude-vcs").args(["exclude_vcs"]).requires("unstable")),
57    group(ArgGroup::new("unstable-follow_command_links").args(["follow_command_links"]).requires("unstable")),
58    group(ArgGroup::new("unstable-one-file-system").args(["one_file_system"]).requires("unstable")),
59)]
60#[cfg_attr(windows, command(
61    group(ArgGroup::new("windows-unstable-keep-permission").args(["keep_permission"]).requires("unstable")),
62))]
63pub(crate) struct AppendCommand {
64    #[arg(
65        long,
66        help = "Stay in the same file system when collecting files (unstable)"
67    )]
68    one_file_system: bool,
69    #[arg(
70        short,
71        long,
72        visible_alias = "recursion",
73        help = "Add the directory to the archive recursively",
74        default_value_t = true
75    )]
76    recursive: bool,
77    #[arg(
78        long,
79        visible_alias = "no-recursion",
80        help = "Do not recursively add directories to the archives. This is the inverse option of --recursive"
81    )]
82    no_recursive: bool,
83    #[arg(long, help = "Archiving the directories")]
84    keep_dir: bool,
85    #[arg(
86        long,
87        help = "Do not archive directories. This is the inverse option of --keep-dir"
88    )]
89    no_keep_dir: bool,
90    #[arg(
91        long,
92        visible_alias = "preserve-timestamps",
93        help = "Archiving the timestamp of the files"
94    )]
95    pub(crate) keep_timestamp: bool,
96    #[arg(
97        long,
98        visible_alias = "preserve-permissions",
99        help = "Archiving the permissions of the files (unstable on Windows)"
100    )]
101    pub(crate) keep_permission: bool,
102    #[arg(
103        long,
104        visible_alias = "preserve-xattrs",
105        help = "Archiving the extended attributes of the files"
106    )]
107    pub(crate) keep_xattr: bool,
108    #[arg(
109        long,
110        visible_alias = "preserve-acls",
111        help = "Archiving the acl of the files (unstable)"
112    )]
113    pub(crate) keep_acl: bool,
114    #[arg(long, help = "Archiving user to the entries from given name")]
115    pub(crate) uname: Option<String>,
116    #[arg(long, help = "Archiving group to the entries from given name")]
117    pub(crate) gname: Option<String>,
118    #[arg(
119        long,
120        help = "Overrides the user id read from disk; if --uname is not also specified, the user name will be set to match the user id"
121    )]
122    pub(crate) uid: Option<u32>,
123    #[arg(
124        long,
125        help = "Overrides the group id read from disk; if --gname is not also specified, the group name will be set to match the group id"
126    )]
127    pub(crate) gid: Option<u32>,
128    #[arg(
129        long,
130        help = "This is equivalent to --uname \"\" --gname \"\". It causes user and group names to not be stored in the archive"
131    )]
132    pub(crate) numeric_owner: bool,
133    #[arg(long, help = "Overrides the creation time read from disk")]
134    ctime: Option<DateTime>,
135    #[arg(
136        long,
137        help = "Clamp the creation time of the entries to the specified time by --ctime"
138    )]
139    clamp_ctime: bool,
140    #[arg(long, help = "Overrides the access time read from disk")]
141    atime: Option<DateTime>,
142    #[arg(
143        long,
144        help = "Clamp the access time of the entries to the specified time by --atime"
145    )]
146    clamp_atime: bool,
147    #[arg(long, help = "Overrides the modification time read from disk")]
148    mtime: Option<DateTime>,
149    #[arg(
150        long,
151        help = "Clamp the modification time of the entries to the specified time by --mtime"
152    )]
153    clamp_mtime: bool,
154    #[arg(
155        long,
156        requires = "unstable",
157        help = "Only include files and directories older than the specified date (unstable). This compares ctime entries."
158    )]
159    older_ctime: Option<DateTime>,
160    #[arg(
161        long,
162        requires = "unstable",
163        help = "Only include files and directories older than the specified date (unstable). This compares mtime entries."
164    )]
165    older_mtime: Option<DateTime>,
166    #[arg(
167        long,
168        requires = "unstable",
169        help = "Only include files and directories newer than the specified date (unstable). This compares ctime entries."
170    )]
171    newer_ctime: Option<DateTime>,
172    #[arg(
173        long,
174        requires = "unstable",
175        help = "Only include files and directories newer than the specified date (unstable). This compares mtime entries."
176    )]
177    newer_mtime: Option<DateTime>,
178    #[arg(long, help = "Read archiving files from given path (unstable)", value_hint = ValueHint::FilePath)]
179    pub(crate) files_from: Option<String>,
180    #[arg(long, help = "Read archiving files from stdin (unstable)")]
181    pub(crate) files_from_stdin: bool,
182    #[arg(
183        long,
184        help = "Process only files or directories that match the specified pattern. Note that exclusions specified with --exclude take precedence over inclusions (unstable)"
185    )]
186    include: Option<Vec<String>>,
187    #[arg(long, help = "Exclude path glob (unstable)", value_hint = ValueHint::AnyPath)]
188    exclude: Option<Vec<String>>,
189    #[arg(long, help = "Read exclude files from given path (unstable)", value_hint = ValueHint::FilePath)]
190    exclude_from: Option<String>,
191    #[arg(long, help = "Exclude vcs files (unstable)")]
192    exclude_vcs: bool,
193    #[arg(long, help = "Ignore files from .gitignore (unstable)")]
194    pub(crate) gitignore: bool,
195    #[arg(long, visible_aliases = ["dereference"], help = "Follow symbolic links")]
196    follow_links: bool,
197    #[arg(
198        short = 'H',
199        long,
200        help = "Follow symbolic links named on the command line"
201    )]
202    follow_command_links: bool,
203    #[arg(
204        long,
205        help = "Filenames or patterns are separated by null characters, not by newlines"
206    )]
207    null: bool,
208    #[arg(
209        short = 's',
210        value_name = "PATTERN",
211        help = "Modify file or archive member names according to pattern that like BSD tar -s option (unstable)"
212    )]
213    substitutions: Option<Vec<SubstitutionRule>>,
214    #[arg(
215        long = "transform",
216        visible_alias = "xform",
217        value_name = "PATTERN",
218        help = "Modify file or archive member names according to pattern that like GNU tar -transform option (unstable)"
219    )]
220    transforms: Option<Vec<TransformRule>>,
221    #[arg(
222        short = 'C',
223        long = "cd",
224        visible_aliases = ["directory"],
225        value_name = "DIRECTORY",
226        help = "changes the directory before adding the following files",
227        value_hint = ValueHint::DirPath
228    )]
229    working_dir: Option<PathBuf>,
230    #[command(flatten)]
231    pub(crate) compression: CompressionAlgorithmArgs,
232    #[command(flatten)]
233    pub(crate) password: PasswordArgs,
234    #[command(flatten)]
235    pub(crate) cipher: CipherAlgorithmArgs,
236    #[command(flatten)]
237    pub(crate) hash: HashAlgorithmArgs,
238    #[command(flatten)]
239    pub(crate) file: FileArgsCompat,
240}
241
242impl Command for AppendCommand {
243    #[inline]
244    fn execute(self) -> anyhow::Result<()> {
245        append_to_archive(self)
246    }
247}
248
249fn append_to_archive(args: AppendCommand) -> anyhow::Result<()> {
250    let password = ask_password(args.password)?;
251    check_password(&password, &args.cipher);
252    let archive_path = args.file.archive();
253    if !archive_path.exists() {
254        return Err(io::Error::new(
255            io::ErrorKind::NotFound,
256            format!("{} is not exists", archive_path.display()),
257        )
258        .into());
259    }
260    let password = password.as_deref();
261    let option = entry_option(args.compression, args.cipher, args.hash, password);
262    let keep_options = KeepOptions {
263        keep_timestamp: args.keep_timestamp,
264        keep_permission: args.keep_permission,
265        keep_xattr: args.keep_xattr,
266        keep_acl: args.keep_acl,
267    };
268    let owner_options = OwnerOptions::new(
269        args.uname,
270        args.gname,
271        args.uid,
272        args.gid,
273        args.numeric_owner,
274    );
275    let time_options = TimeOptions {
276        mtime: args.mtime.map(|it| it.to_system_time()),
277        clamp_mtime: args.clamp_mtime,
278        ctime: args.ctime.map(|it| it.to_system_time()),
279        clamp_ctime: args.clamp_ctime,
280        atime: args.atime.map(|it| it.to_system_time()),
281        clamp_atime: args.clamp_atime,
282    };
283    let time_filters = TimeFilters {
284        ctime: TimeFilter {
285            newer_than: args.newer_ctime.map(|it| it.to_system_time()),
286            older_than: args.older_ctime.map(|it| it.to_system_time()),
287        },
288        mtime: TimeFilter {
289            newer_than: args.newer_mtime.map(|it| it.to_system_time()),
290            older_than: args.older_mtime.map(|it| it.to_system_time()),
291        },
292    };
293    let create_options = CreateOptions {
294        option,
295        keep_options,
296        owner_options,
297        time_options,
298    };
299    let path_transformers = PathTransformers::new(args.substitutions, args.transforms);
300
301    let archive = open_archive_then_seek_to_end(&archive_path)?;
302
303    let mut files = args.file.files();
304    if args.files_from_stdin {
305        files.extend(read_paths_stdin(args.null)?);
306    } else if let Some(path) = args.files_from {
307        files.extend(read_paths(path, args.null)?);
308    }
309    let filter = {
310        let mut exclude = args.exclude.unwrap_or_default();
311        if let Some(p) = args.exclude_from {
312            exclude.extend(read_paths(p, args.null)?);
313        }
314        if args.exclude_vcs {
315            exclude.extend(VCS_FILES.iter().map(|it| String::from(*it)))
316        }
317        PathFilter {
318            include: args.include.unwrap_or_default().into(),
319            exclude: exclude.into(),
320        }
321    };
322    if let Some(working_dir) = args.working_dir {
323        env::set_current_dir(working_dir)?;
324    }
325    let mut target_items = collect_items(
326        &files,
327        !args.no_recursive,
328        args.keep_dir,
329        args.gitignore,
330        args.follow_links,
331        args.follow_command_links,
332        args.one_file_system,
333        &filter,
334    )?;
335    if time_filters.is_active() {
336        let mut filtered = Vec::new();
337        for item in target_items.into_iter() {
338            let metadata = fs::symlink_metadata(&item.0)
339                .with_context(|| format!("failed to read metadata for {}", item.0.display()))?;
340            if time_filters.is_retain(&metadata) {
341                filtered.push(item);
342            }
343        }
344        target_items = filtered;
345    }
346
347    run_append_archive(&create_options, &path_transformers, archive, target_items)
348}
349
350pub(crate) fn run_append_archive(
351    create_options: &CreateOptions,
352    path_transformers: &Option<PathTransformers>,
353    mut archive: Archive<impl io::Write>,
354    target_items: Vec<(PathBuf, StoreAs)>,
355) -> anyhow::Result<()> {
356    let (tx, rx) = std::sync::mpsc::channel();
357    rayon::scope_fifo(|s| {
358        for file in target_items {
359            let tx = tx.clone();
360            s.spawn_fifo(move |_| {
361                log::debug!("Adding: {}", file.0.display());
362                tx.send(create_entry(&file, create_options, path_transformers))
363                    .unwrap_or_else(|e| log::error!("{e}: {}", file.0.display()));
364            })
365        }
366
367        drop(tx);
368    });
369
370    for entry in rx.into_iter() {
371        archive.add_entry(entry?)?;
372    }
373    archive.finalize()?;
374    Ok(())
375}
376
377pub(crate) fn open_archive_then_seek_to_end(
378    path: impl AsRef<Path>,
379) -> anyhow::Result<Archive<fs::File>> {
380    let archive_path = path.as_ref();
381    let mut num = 1;
382    let file = fs::File::options()
383        .write(true)
384        .read(true)
385        .open(archive_path)?;
386    let mut archive = Archive::read_header(file)?;
387    loop {
388        archive.seek_to_end()?;
389        if !archive.has_next_archive() {
390            break Ok(archive);
391        }
392        num += 1;
393        let file = fs::File::options()
394            .write(true)
395            .read(true)
396            .open(archive_path.with_part(num).unwrap())?;
397        archive = archive.read_next_archive(file)?;
398    }
399}