file-attr-restore 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. #!/usr/bin/perl
  2. # This script will parse the output of "find ARG [ARG...] -ls" and
  3. # apply (at your discretion) the permissions, owner, and group info
  4. # it reads onto any existing files and dirs (it doesn't try to affect
  5. # symlinks). Run this with --help (-h) for a usage summary.
  6. use strict;
  7. use Getopt::Long;
  8. our($p_opt, $o_opt, $g_opt, $map_file, $dry_run, $verbosity, $help_opt);
  9. &Getopt::Long::Configure('bundling');
  10. &usage if !&GetOptions(
  11. 'all|a' => sub { $p_opt = $o_opt = $g_opt = 1 },
  12. 'perms|p' => \$p_opt,
  13. 'owner|o' => \$o_opt,
  14. 'groups|g' => \$g_opt,
  15. 'map|m=s' => \$map_file,
  16. 'dry-run|n' => \$dry_run,
  17. 'help|h' => \$help_opt,
  18. 'verbose|v+' => \$verbosity,
  19. ) || $help_opt;
  20. our(%uid_hash, %gid_hash);
  21. $" = ', '; # How to join arrays referenced in double-quotes.
  22. &parse_map_file($map_file) if defined $map_file;
  23. my $detail_line = qr{
  24. ^ \s* \d+ \s+ # ignore inode
  25. \d+ \s+ # ignore size
  26. ([-bcdlps]) # 1. File type
  27. ( [-r][-w][-xsS] # 2. user-permissions
  28. [-r][-w][-xsS] # group-permissions
  29. [-r][-w][-xtT] ) \s+ # other-permissions
  30. \d+ \s+ # ignore number of links
  31. (\S+) \s+ # 3. owner
  32. (\S+) \s+ # 4. group
  33. (?: \d+ \s+ )? # ignore size (when present)
  34. \w+ \s+ \d+ \s+ # ignore month and date
  35. \d+ (?: : \d+ )? \s+ # ignore time or year
  36. ([^\r\n]+) $ # 5. name
  37. }x;
  38. while (<>) {
  39. my($type, $perms, $owner, $group, $name) = /$detail_line/;
  40. die "Invalid input line $.:\n$_" unless defined $name;
  41. die "A filename is not properly escaped:\n$_" unless $name =~ /^[^"\\]*(\\(\d\d\d|\D)[^"\\]*)*$/;
  42. my $fn = $name;
  43. $fn =~ s/\\(\d+|.)/ eval "\"\\$1\"" /eg;
  44. if ($type eq '-') {
  45. undef $type unless -f $fn;
  46. } elsif ($type eq 'd') {
  47. undef $type unless -d $fn;
  48. } elsif ($type eq 'b') {
  49. undef $type unless -b $fn;
  50. } elsif ($type eq 'c') {
  51. undef $type unless -c $fn;
  52. } elsif ($type eq 'p') {
  53. undef $type unless -p $fn;
  54. } elsif ($type eq 's') {
  55. undef $type unless -S $fn;
  56. } else {
  57. if ($verbosity) {
  58. if ($type eq 'l') {
  59. $name =~ s/ -> .*//;
  60. $type = 'symlink';
  61. } else {
  62. $type = "type '$type'";
  63. }
  64. print "Skipping $name ($type ignored)\n";
  65. }
  66. next;
  67. }
  68. if (!defined $type) {
  69. my $reason = -e _ ? "types don't match" : 'missing';
  70. print "Skipping $name ($reason)\n";
  71. next;
  72. }
  73. my($cur_mode, $cur_uid, $cur_gid) = (stat(_))[2,4,5];
  74. $cur_mode &= 07777;
  75. my $highs = join('', $perms =~ /..(.)..(.)..(.)/);
  76. $highs =~ tr/-rwxSTst/00001111/;
  77. $perms =~ tr/-STrwxst/00011111/;
  78. my $mode = $p_opt ? oct('0b' . $highs . $perms) : $cur_mode;
  79. my $uid = $o_opt ? $uid_hash{$owner} : $cur_uid;
  80. if (!defined $uid) {
  81. if ($owner =~ /^\d+$/) {
  82. $uid = $owner;
  83. } else {
  84. $uid = getpwnam($owner);
  85. }
  86. $uid_hash{$owner} = $uid;
  87. }
  88. my $gid = $g_opt ? $gid_hash{$group} : $cur_gid;
  89. if (!defined $gid) {
  90. if ($group =~ /^\d+$/) {
  91. $gid = $group;
  92. } else {
  93. $gid = getgrnam($group);
  94. }
  95. $gid_hash{$group} = $gid;
  96. }
  97. my @changes;
  98. if ($mode != $cur_mode) {
  99. push(@changes, 'permissions');
  100. if (!$dry_run && !chmod($mode, $fn)) {
  101. warn "chmod($mode, \"$name\") failed: $!\n";
  102. }
  103. }
  104. if ($uid != $cur_uid || $gid != $cur_gid) {
  105. push(@changes, 'owner') if $uid != $cur_uid;
  106. push(@changes, 'group') if $gid != $cur_gid;
  107. if (!$dry_run) {
  108. if (!chown($uid, $gid, $fn)) {
  109. warn "chown($uid, $gid, \"$name\") failed: $!\n";
  110. }
  111. if (($mode & 06000) && !chmod($mode, $fn)) {
  112. warn "post-chown chmod($mode, \"$name\") failed: $!\n";
  113. }
  114. }
  115. }
  116. if (@changes) {
  117. print "$name: changed @changes\n";
  118. } elsif ($verbosity) {
  119. print "$name: OK\n";
  120. }
  121. }
  122. exit;
  123. sub parse_map_file
  124. {
  125. my($fn) = @_;
  126. open(IN, $fn) or die "Unable to open $fn: $!\n";
  127. while (<IN>) {
  128. if (/^user\s+(\S+)\s+(\S+)/) {
  129. $uid_hash{$1} = $2;
  130. } elsif (/^group\s+(\S+)\s+(\S+)/) {
  131. $gid_hash{$1} = $2;
  132. } else {
  133. die "Invalid line #$. in mapfile `$fn':\n$_";
  134. }
  135. }
  136. close IN;
  137. }
  138. sub usage
  139. {
  140. die <<EOT;
  141. Usage: file-attr-restore [OPTIONS] FILE [FILE...]
  142. -a, --all Restore all the attributes (-pog)
  143. -p, --perms Restore the permissions
  144. -o, --owner Restore the ownership
  145. -g, --groups Restore the group
  146. -m, --map=FILE Read user/group mappings from FILE
  147. -n, --dry-run Don't actually make the changes
  148. -v, --verbose Increase verbosity
  149. -h, --help Show this help text
  150. The FILE arg(s) should have been created by running the "find"
  151. program with "-ls" as the output specifier.
  152. The input file for the --map option must be in this format:
  153. user FROM TO
  154. group FROM TO
  155. The "FROM" should be an user/group mentioned in the input, and the TO
  156. should be either a uid/gid number, or a local user/group name.
  157. EOT
  158. }