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 | |
5f4e991c WD |
35 | [-r][-w][-xsS] # group-permissions |
36 | [-r][-w][-xtT] ) \s+ # other-permissions | |
91a93df0 | 37 | \d+ \s+ # ignore number of links |
5f4e991c WD |
38 | (\S+) \s+ # 3. owner |
39 | (\S+) \s+ # 4. group | |
91a93df0 WD |
40 | (?: \d+ \s+ )? # ignore size (when present) |
41 | \w+ \s+ \d+ \s+ # ignore month and date | |
42 | \d+ (?: : \d+ )? \s+ # ignore time or year | |
5f4e991c | 43 | ([^\r\n]+) $ # 5. name |
91a93df0 WD |
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)[^"\\]*)*$/; | |
41875726 | 50 | my $fn = $name; |
71f9e467 | 51 | $fn =~ s/\\(\d+|.)/ eval "\"\\$1\"" /eg; |
91a93df0 WD |
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 | } | |
131 | exit; | |
132 | ||
133 | sub 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 | ||
149 | sub usage | |
150 | { | |
151 | die <<EOT; | |
152 | Usage: 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 | ||
162 | The FILE arg(s) should have been created by running the "find" | |
163 | program with "-ls" as the output specifier. | |
164 | ||
165 | The input file for the --map option must be in this format: | |
166 | ||
167 | user FROM TO | |
168 | group FROM TO | |
169 | ||
170 | The "FROM" should be an user/group mentioned in the input, and the TO | |
171 | should be either a uid/gid number, or a local user/group name. | |
172 | EOT | |
173 | } |