f8061c5365f07267a30a9369382edc70ce4afc44
[jigit.git] / mkjigsnap
1 #!/usr/bin/perl -w
2 #
3 # mkjigsnap
4 #
5 # (c) 2004-2011 Steve McIntyre <steve@einval.com>
6 #
7 # Server-side wrapper; run this on a machine with a mirror to set up
8 # the snapshots for jigit / jigdo downloading
9 #
10 # GPL v2 - see COPYING 
11 #
12 # This script can be run in two modes:
13 #
14 # 1. To build a jigit .conf file for a single jigdo file:
15 #    add the "-n" option with a CD name on the command line
16 #    and only specify a single jigdo to work with using "-j".
17 #
18 # 2. To build a snapshot tree for (potentially multiple) jigdo files:
19 #    do *not* specify the "-n" option, and list as many jigdo files as
20 #    desired, either on the command line using multiple "-j <jigdo>" options
21 #    or (better) via a file listing them with the "-J" option.
22 #
23 # Some things needed:
24 #   (single-jigdo mode only) the CD name of the jigit
25 #   (single-jigdo mode only) the output location; where the jigdo, template
26 #      file and snapshot will be written
27 #   (single-jigdo mode only) the locations of the input jigdo and template
28 #      files
29 #   the location of the mirror
30 #   the keyword(s) to look for (e.g. Debian)
31 #   the snapshot dirname (e.g. today's date)
32 #
33 # Example #1: (single-jigdo mode, used for Ubuntu jigit generation)
34 #
35 #   mkjigsnap -o /tmp/mjs-test -n mjs-test -m /tmp/mirror \
36 #        -j ~/jigdo/update/debian-update-3.0r2.01-i386.jigdo \
37 #        -t ~/jigdo/update/debian-update-3.0r2.01-i386.template \
38 #        -k Debian -k Non-US
39 #        -d 20041017
40 #
41 #   (This creates a single jigit conf file using the supplied jigdo/template
42 #    file pair, looking for jigdo references to files in the "Debian" and
43 #    "Non-US" areas. Output the files into /tmp/mjs-test and call them
44 #    "mjs-test.<ext>", creating a snapshot of the needed files in
45 #    /tmp/mjs-test/20041017 by linking files from /tmp/mirror as needed.)
46 #
47 # Example #2: (multi-jigdo mode, as run to keep
48 #              http://us.cdimage.debian.org/cdimage/snapshot/ up to date)
49 #
50 # mkjigsnap -m /org/ftp/debian -J ~/jigdo.list -T ~/tmp \
51 #      -k Debian \
52 #      -d /org/jigdo-area/snapshot/Debian \
53 #      -f ~/mkjigsnap-failed.log \
54 #      -i ~/mkjigsnap-ignore.list
55 #
56 #   (This reads in all the jigdo files listed in ~/jigdo.list, building a
57 #    list of all the files referenced in the "Debian" area. It will then
58 #    attempt to build a snapshot tree of all those files under
59 #    /org/jigdo-area/snapshot/Debian by linking from /org/ftp/debian. Any
60 #    files that are missing will be listed into the output "missing" file
61 #    ~/mkjigsnap-failed.log for later checking, UNLESS they are already listed
62 #    in the "ignore" file ~/mkjigsnap-ignore.list.)
63 #      
64
65 use strict;
66 use Getopt::Long;
67 use File::Basename;
68 use File::Find;
69 use File::Copy;
70 use Compress::Zlib;
71 Getopt::Long::Configure ('no_ignore_case');
72 Getopt::Long::Configure ('no_auto_abbrev');
73
74 my $mode = "multi";
75 my $dryrun = 0;
76 my $verbose = 0;
77 my $startdate = `date -u`;
78 my ($jlistdonedate, $parsedonedate, $snapdonedate);
79 my @jigdos;
80 my @keywords;
81 my @mirrors;
82 my ($dirname, $failedfile, $ignorefile, $jigdolist, $mirror, $cdname,
83     $outdir, $tempdir, $template);
84 my $result;
85 my $num_jigdos = 0;
86 my $num_unsorted = 0;
87 my $num_unique = 0;
88 my @failed_files;
89 my $old_deleted = 0;
90 my %ignored_fails;
91 my %file_list;
92 my %ref;
93
94 $result = GetOptions("d=s" => \$dirname,
95                      "f=s" => \$failedfile,
96                      "i=s" => \$ignorefile,
97                      "J=s" => \$jigdolist,
98                      "j=s" => \@jigdos,
99                      "k=s" => \@keywords,
100                      "m=s" => \@mirrors,
101                      "N"   => \$dryrun,
102                      "n=s" => \$cdname,
103                      "o=s" => \$outdir,
104                      "T=s" => \$tempdir,
105                      "t=s" => \$template,
106                      "v"   => \$verbose);
107
108 # Sanity-check arguments
109 if (!defined ($dirname)) {
110     die "You must specify the snapshot directory name!\n";
111 }
112 if (!@keywords) {
113     die "You must specify the keywords to match!\n";
114 }
115 if (!@mirrors) {
116     die "You must specify the location(s) of the mirror(s)!\n";
117 }
118 if (@jigdos) {
119     $num_jigdos += scalar(@jigdos);
120 }
121 if (defined($jigdolist)) {
122     $num_jigdos += `wc -w < $jigdolist`;
123 }
124 if ($num_jigdos == 0) {
125     die "No jigdo file(s) specified!\n";
126 }
127 if (defined($cdname)) {
128     $mode = "single";
129 }
130
131 if ($mode eq "single") {
132     if (!defined($cdname)) {
133         die "You must specify the output name for the jigit conf!\n";
134     }
135     if (!defined($outdir)) {
136         die "You must specify where to set up the snapshot!\n";
137     }
138     if (!defined($template)) {
139         die "You must specify the template file!\n";
140     }
141     if ($num_jigdos != 1) {
142         die "More than one jigdo file specified ($num_jigdos) in single-jigdo mode!\n";
143     }
144     # In single-jigdo mode, the snapshot directory is relative to the
145     # output dir
146     $dirname="$outdir/$dirname";
147 } else {
148     if (defined($cdname)) {
149         die "Output name is meaningless for multi-jigdo mode!\n";
150     }
151     if (defined($outdir)) {
152         die "Output dir is meaningless for multi-jigdo mode!\n";
153     }
154     if (defined($template)) {
155         die "Template file name is meaningless for multi-jigdo mode!\n";
156     }
157 }
158
159 # Make a dir tree
160 sub mkdirs {
161     my $input = shift;
162     my $dir;
163     my @components;
164     my $need_slash = 0;
165
166     if (! -d $input) {
167         if ($verbose) {
168             print "mkdirs($input)\n";
169         }
170         if (!$dryrun) {
171             @components = split /\//,$input;
172             foreach my $component (@components) {
173                 if ($need_slash) {
174                     $dir = join ("/", $dir, $component);
175                 } else {
176                     $dir = $component;
177                     $need_slash = 1;
178                 }
179                 mkdir $dir;
180             }
181         } else {
182             print "DRYRUN: not making directory tree $input\n";
183         }
184     }
185 }
186
187 sub delete_redundant {
188     my $ref;
189
190     if (-f) {
191         $ref = $file_list{$File::Find::name};
192         if (!defined($ref)) {
193             if ($verbose) {
194                 print "delete_redundant($File::Find::name)\n";
195             }
196             if (!$dryrun) {
197                 unlink($File::Find::name);
198             } else  {
199                 print "DRYRUN: not deleting $File::Find::name\n";
200             }
201             $old_deleted++;
202             if ( !($old_deleted % 1000) ) {
203                 print "$old_deleted\n";
204             }
205         }
206     }
207 }
208
209 sub parse_ignore_file {
210     my $inputfile = shift;
211     my $num_ignored_loaded = 0;
212     open(INLIST, "$inputfile") or return;
213     while (defined (my $pkg = <INLIST>)) {
214         chomp $pkg;
215         $ignored_fails{$pkg}++;
216         $num_ignored_loaded++;
217     }
218     print "parse_ignore_file: loaded $num_ignored_loaded entries from file $inputfile\n";
219 }
220
221 sub generate_snapshot_tree () {
222     my $done = 0;
223     my $failed = 0;
224     my $ignored = 0;
225
226     $| = 1;
227
228     # Sorting is important here for performance, to help with
229     # directory lookups
230     foreach $_ (sort (keys %ref)) {
231         my $outfile = $dirname . "/" . $_;
232
233         $file_list{$outfile}++;
234         if ($verbose) {
235             print "file_list hash updated for $outfile\n";
236         }
237         if (! -e $outfile) {
238             my $dir = dirname($_);
239             my $filename = basename($_);
240             my $link;
241             my $link_ok = 0;
242             my $infile;
243
244             mkdirs($dirname . "/" . $dir);
245
246             foreach my $mirror (@mirrors) {
247                 $infile = $mirror . "/" . $_;
248                 if (-l $infile) {
249                     $link = readlink($infile);
250                     if ($link =~ m#^/#) {
251                         $infile = $link;
252                     } else {
253                         $infile = dirname($infile) . "/" . $link;
254                     }
255                 }
256                 if ($verbose) {
257                     print "look for $_:\n";
258                 }             
259                 $outfile = $dirname . "/" . $_;
260                 if (!$dryrun) {
261                     if ($verbose) {
262                         print "  try $infile\n";
263                     }
264                     if (link ($infile, $outfile)) {
265                         $link_ok = 1;
266                         last;
267                     }
268                 } else {
269                     print "DRYRUN: not linking $infile to $outfile\n";
270                     $link_ok = 1;
271                     last;
272                 }
273                 $infile = $mirror . "/" . $filename;
274                 if ($verbose) {
275                     print "  fallback: try $infile\n";
276                 }
277                 if (!$dryrun) {
278                     if (link ($infile, $outfile)) {
279                         $link_ok = 1;
280                         last;
281                     }
282                 } else {
283                     print "DRYRUN: not linking $infile to $outfile\n";
284                     $link_ok = 1;
285                     last;
286                 }
287             }
288             if ($link_ok == 0) {
289                 if ($ignored_fails{$_}) {
290                     $ignored++;
291                 } else {
292                     if (!defined($failedfile)) {
293                         # No logfile, print to stdout then
294                         print "\nFailed to create link $outfile\n";
295                     }
296                     $failed++;
297                     push (@failed_files, $_);
298                 }
299             } else {
300                 if ($ignored_fails{$_}) {
301                     print "\n$_ marked as failed, but we found it anyway!\n";
302                 }
303             }
304         }
305         $done++;
306         if ( !($done % 10000) ) {
307             print "$done done, ignored $ignored, failed $failed out of $num_unique\n";
308         }
309     }
310     print "  Finished: $done/$num_unique, $failed failed, ignored $ignored\n\n";
311
312     if (defined($failedfile) && ($failed > 0)) {
313         print "Writing list of failed files to $failedfile\n";
314         open(FAIL_LOG, "> $failedfile") or die "Failed to open $failedfile: $!\n";
315         foreach my $missing (@failed_files) {
316             print FAIL_LOG "$missing\n";
317         }
318         close FAIL_LOG;
319     }
320
321     # Now walk the tree and delete files that we no longer need
322     print "Scanning for now-redundant files\n";
323     find(\&delete_redundant, $dirname);
324     print "  Finished: $old_deleted old files removed\n";
325 }
326
327 # Parse jigdo_list file if we have one
328 if (defined($jigdolist)) {
329     if ($verbose) {
330         print "Checking for jigdos in $jigdolist\n";
331     }
332     open (INLIST, "$jigdolist") or die "Can't open file $jigdolist: $!\n";
333     while ($_ = <INLIST>) {
334         chomp;
335         if (length($_) > 1) {
336             push (@jigdos, $_);
337         }
338     }
339     close INLIST;
340 }
341 $jlistdonedate = `date -u`;
342
343 if ($verbose) {
344     print "Working on $num_jigdos jigdo file(s)\n";
345 }
346 # Walk through the list of jigdos, parsing as we go
347 my $num_parsed = 0;
348 if ($verbose) {
349     print "Reading / parsing jigdo file(s)\n";
350 }
351
352 open (INJIG, "zcat -f @jigdos |");
353 while (<INJIG>) {
354     my $file;
355     chomp;
356     foreach my $keyword (@keywords) {
357         m/^......................=$keyword:(.*)$/ and $file = $1;
358     }
359     if (defined($file)) {
360         $num_unsorted++;
361         if (!exists $ref{$file}) {
362             $num_unique++;
363             $ref{$file} = 1;
364         }
365     }
366 }
367 close(INJIG);
368 $parsedonedate = `date -u`;
369
370 if ($num_unique < 5) {
371     die "Only $num_unique for the snapshot? Something is wrong; abort!\n"
372 }
373
374 # Now look at the snapshot dir
375 if (! -d $dirname) {
376     print "$dirname does not exist\n";
377     if (!$dryrun) {
378         mkdirs($dirname);
379     } else {
380         die "DRYRUN: not making it, so aborting\n";
381     }
382 }
383 if (defined($ignorefile)) {
384     parse_ignore_file($ignorefile);
385 }
386
387 print "Trying to snapshot-link $num_unique files into $dirname\n";
388 generate_snapshot_tree();
389 $snapdonedate = `date -u`;
390
391 chomp ($startdate, $jlistdonedate, $parsedonedate, $snapdonedate);
392
393 print "$startdate: startup\n";
394 print "$jlistdonedate: found $num_jigdos jigdo files\n";
395 print "$parsedonedate: found $num_unsorted files referenced in those jigdo files, $num_unique unique\n";
396 print "$snapdonedate: snapshot done\n";
397
398 if ($mode eq "single") {
399     if ($dryrun) {
400         print "DRYRUN: Not creating files in $outdir\n";
401     } else {
402         foreach my $jigdo (@jigdos) {
403             my ($gzin, $gzout, $line);
404             $gzin = gzopen($jigdo, "rb") or
405                 die "Unable to open jigdo file $jigdo for reading: $!\n";
406             $gzout = gzopen("$outdir/$cdname.jigdo", "wb9") or
407                 die "Unable to open new jigdo file $outdir/$cdname.jigdo for writing: $!\n";
408             while ($gzin->gzreadline($line) > 0) {
409                 $line =~ s:^Template=.*$:Template=$cdname.template:;
410                 $gzout->gzwrite($line);
411             }
412         }
413         copy("$template", "$outdir/$cdname.template") or
414             die "Failed to copy template file $template: $!\n";
415         open (CONF, "> $outdir/$cdname.conf") or
416             die "Failed to open conf file $outdir/$cdname.conf for writing: $!\n";
417         print CONF "JIGDO=$cdname.jigdo\n";
418         print CONF "TEMPLATE=$cdname.template\n";
419         print CONF "SNAPSHOT=snapshot/$dirname\n";
420         close(CONF);
421         print "Jigdo files, config and snapshot made in $outdir\n";
422     }
423 }