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