Commit | Line | Data |
---|---|---|
91a93df0 WD |
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 | } |