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}