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