873a800395ffe642d82f7f2c6cbbfa3e0c0035dd
[jigit.git] / mkjigsnap
1 #!/usr/bin/perl -w
2 #
3 # mkjigsnap
4 #
5 # (c) 2004-2012 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 \
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 $single_jigdo;
81 my @keywords;
82 my @mirrors;
83 my ($dirname, $failedfile, $ignorefile, $jigdolist, $mirror, $cdname,
84     $outdir, $tempdir, $template, $check_checksums, $checksum_out);
85 my $result;
86 my $num_jigdos = 0;
87 my $num_unsorted = 0;
88 my $num_unique = 0;
89 my @failed_files;
90 my @ck_failed_files;
91 my $old_deleted = 0;
92 my %ignored_fails;
93 my %file_list;
94 my %ref;
95
96 $result = GetOptions("c"   => \$check_checksums,
97                      "C=s" => \$checksum_out,
98                      "d=s" => \$dirname,
99                      "f=s" => \$failedfile,
100                      "i=s" => \$ignorefile,
101                      "J=s" => \$jigdolist,
102                      "j=s" => \@jigdos,
103                      "k=s" => \@keywords,
104                      "m=s" => \@mirrors,
105                      "N"   => \$dryrun,
106                      "n=s" => \$cdname,
107                      "o=s" => \$outdir,
108                      "T=s" => \$tempdir,
109                      "t=s" => \$template,
110                      "v"   => \$verbose);
111
112 # Sanity-check arguments
113 if (!defined ($dirname)) {
114     die "You must specify the snapshot directory name!\n";
115 }
116 if (!@keywords) {
117     die "You must specify the keywords to match!\n";
118 }
119 if (!@mirrors) {
120     die "You must specify the location(s) of the mirror(s)!\n";
121 }
122 if (@jigdos) {
123     $num_jigdos += scalar(@jigdos);
124 }
125 if (defined($jigdolist)) {
126     $num_jigdos += `wc -w < $jigdolist`;
127 }
128 if ($num_jigdos == 0) {
129     die "No jigdo file(s) specified!\n";
130 }
131 if (defined($cdname)) {
132     $mode = "single";
133 }
134
135 if ($mode eq "single") {
136     if (!defined($cdname)) {
137         die "You must specify the output name for the jigit conf!\n";
138     }
139     if (!defined($outdir)) {
140         die "You must specify where to set up the snapshot!\n";
141     }
142     if (!defined($template)) {
143         die "You must specify the template file!\n";
144     }
145     if ($num_jigdos != 1) {
146         die "More than one jigdo file specified ($num_jigdos) in single-jigdo mode!\n";
147     }
148     # In single-jigdo mode, the snapshot directory is relative to the
149     # output dir
150     $dirname="$outdir/$dirname";
151     # And store the path to the jigdo file for later use
152     $single_jigdo = $jigdos[0];
153 } else {
154     if (defined($cdname)) {
155         die "Output name is meaningless for multi-jigdo mode!\n";
156     }
157     if (defined($outdir)) {
158         die "Output dir is meaningless for multi-jigdo mode!\n";
159     }
160     if (defined($template)) {
161         die "Template file name is meaningless for multi-jigdo mode!\n";
162     }
163 }
164
165 # Make a dir tree
166 sub mkdirs {
167     my $input = shift;
168     my $dir;
169     my @components;
170     my $need_slash = 0;
171
172     if (! -d $input) {
173         if ($verbose) {
174             print "mkdirs($input)\n";
175         }
176         if (!$dryrun) {
177             @components = split /\//,$input;
178             foreach my $component (@components) {
179                 if ($need_slash) {
180                     $dir = join ("/", $dir, $component);
181                 } else {
182                     $dir = $component;
183                     $need_slash = 1;
184                 }
185                 mkdir $dir;
186             }
187         } else {
188             print "DRYRUN: not making directory tree $input\n";
189         }
190     }
191 }
192
193 sub delete_redundant {
194     my $link;
195
196     if (-f) {
197         $link = $file_list{$File::Find::name};
198         if (!defined($link)) {
199             if ($verbose) {
200                 print "delete_redundant($File::Find::name)\n";
201             }
202             if (!$dryrun) {
203                 unlink($File::Find::name);
204             } else  {
205                 print "DRYRUN: not deleting $File::Find::name\n";
206             }
207             $old_deleted++;
208             if ( !($old_deleted % 1000) ) {
209                 print "$old_deleted\n";
210             }
211         }
212     }
213 }
214
215 sub parse_ignore_file {
216     my $inputfile = shift;
217     my $num_ignored_loaded = 0;
218     open(INLIST, "$inputfile") or return;
219     while (defined (my $pkg = <INLIST>)) {
220         chomp $pkg;
221         $ignored_fails{$pkg}++;
222         $num_ignored_loaded++;
223     }
224     print "parse_ignore_file: loaded $num_ignored_loaded entries from file $inputfile\n";
225 }
226
227 sub generate_snapshot_tree () {
228     my $done = 0;
229     my $failed = 0;
230     my $ignored = 0;
231     my $ck_failed = 0;
232
233     $| = 1;
234
235     # Sorting is important here for performance, to help with
236     # directory lookups
237     foreach $_ (sort (keys %ref)) {
238         my $outfile = $dirname . "/" . $_;
239
240         $file_list{$outfile}++;
241         if ($verbose) {
242             print "file_list hash updated for $outfile\n";
243         }
244         if (! -e $outfile) {
245             my $dir = dirname($_);
246             my $filename = basename($_);
247             my $link;
248             my $link_ok = 0;
249             my $infile;
250
251             mkdirs($dirname . "/" . $dir);
252
253             foreach my $mirror (@mirrors) {
254                 $infile = $mirror . "/" . $_;
255                 if (-l $infile) {
256                     $link = readlink($infile);
257                     if ($link =~ m#^/#) {
258                         $infile = $link;
259                     } else {
260                         $infile = dirname($infile) . "/" . $link;
261                     }
262                 }
263                 if ($verbose) {
264                     print "look for $_:\n";
265                 }             
266                 $outfile = $dirname . "/" . $_;
267                 if (!$dryrun) {
268                     if ($verbose) {
269                         print "  try $infile\n";
270                     }
271                     if (link ($infile, $outfile)) {
272                         $link_ok = 1;
273                         last;
274                     }
275                 } else {
276                     print "DRYRUN: not linking $infile to $outfile\n";
277                     $link_ok = 1;
278                     last;
279                 }
280                 $infile = $mirror . "/" . $filename;
281                 if ($verbose) {
282                     print "  fallback: try $infile\n";
283                 }
284                 if (!$dryrun) {
285                     if (link ($infile, $outfile)) {
286                         $link_ok = 1;
287                         last;
288                     }
289                 } else {
290                     print "DRYRUN: not linking $infile to $outfile\n";
291                     $link_ok = 1;
292                     last;
293                 }
294             }
295             if ($link_ok == 0) {
296                 if ($ignored_fails{$_}) {
297                     $ignored++;
298                 } else {
299                     if (!defined($failedfile)) {
300                         # No logfile, print to stdout then
301                         print "\nFailed to create link $outfile\n";
302                     }
303                     $failed++;
304                     push (@failed_files, $_);
305                 }
306             } else {
307                 if ($ignored_fails{$_}) {
308                     print "\n$_ marked as failed, but we found it anyway!\n";
309                 }
310             }
311         }
312         if (-e $outfile && $check_checksums) {
313             my $jigsum = `jigsum $outfile 2>/dev/null`;
314             my $checksum;
315             if ($jigsum =~ m/^(......................)/) {
316                 $checksum = $1;
317                                 if (!($ref{$_} =~ m/\Q$checksum\E/ )) {
318                     print "\nChecksum failure: $_\n";
319                     $ck_failed++;
320                     push (@ck_failed_files, $_);
321                 }
322             } else {
323                 print "\nFailed to jigsum $_\n";
324             }
325                 }
326         $done++;
327         if ( !($done % 10000) ) {
328             print "$done done, ignored $ignored, failed $failed ck_failed $ck_failed out of $num_unique\n";
329         }
330     }
331     print "  Finished: $done/$num_unique, $failed failed, $ck_failed ck_failed, ignored $ignored\n\n";
332
333     if (defined($failedfile) && ($failed > 0)) {
334         print "Writing list of failed files to $failedfile\n";
335         open(FAIL_LOG, "> $failedfile") or die "Failed to open $failedfile: $!\n";
336         foreach my $missing (@failed_files) {
337             print FAIL_LOG "$missing\n";
338         }
339         close FAIL_LOG;
340     }
341
342     # Now walk the tree and delete files that we no longer need
343     print "Scanning for now-redundant files\n";
344     find(\&delete_redundant, $dirname);
345     print "  Finished: $old_deleted old files removed\n";
346 }
347
348 # Parse jigdo_list file if we have one
349 if (defined($jigdolist)) {
350     if ($verbose) {
351         print "Checking for jigdos in $jigdolist\n";
352     }
353     open (INLIST, "$jigdolist") or die "Can't open file $jigdolist: $!\n";
354     while ($_ = <INLIST>) {
355         chomp;
356         if (length($_) > 1) {
357             push (@jigdos, $_);
358         }
359     }
360     close INLIST;
361 }
362 $jlistdonedate = `date -u`;
363
364 print "Working on $num_jigdos jigdo file(s)\n";
365 # Walk through the list of jigdos, parsing as we go
366 my $num_parsed = 0;
367 print "Reading / parsing jigdo file(s)\n";
368
369 while (@jigdos) {
370     my (@tmpjigdos) = splice (@jigdos, 0, 200);
371     open (INJIG, "zcat -f @tmpjigdos |");
372     while (<INJIG>) {
373         my ($file, $jigsum);
374         chomp;
375         foreach my $keyword (@keywords) {
376             if (m/^(......................)=$keyword:(.*)$/) {
377                 $jigsum = $1;
378                 $file = $2;
379                 $file =~ s?^/??;
380             }
381         }
382         if (defined($file)) {
383             $num_unsorted++;
384             if (!exists $ref{$file}) {
385                 $num_unique++;
386                 $ref{$file} = $jigsum;
387             } else {
388                 if (!($ref{$file} =~ /\Q$jigsum\E/ )) {
389                     print "  ERROR: $file referenced again with different checksum!\n";
390                     print "    (old " . $ref{$file} . " new $jigsum\n";
391                 }
392             }
393             if ( !($num_unsorted % 100000) ) {
394                 print "  found $num_unsorted total, $num_unique unique files\n";
395             }
396         }
397     }
398     close(INJIG);
399 }
400 $parsedonedate = `date -u`;
401 print "  found $num_unsorted total, $num_unique unique files in $num_jigdos jigdo files\n";
402
403 if ($checksum_out) {
404     open(CK_OUT, "> $checksum_out") or die "Can't open $checksum_out for writing: $!\n";
405     foreach $_ (sort (keys %ref)) {
406         print CK_OUT $ref{$_} . "  $_\n";
407     }
408     close(CK_OUT);
409 }       
410
411 if ($num_unique < 5) {
412     die "Only $num_unique for the snapshot? Something is wrong; abort!\n"
413 }
414
415 # Now look at the snapshot dir
416 if (! -d $dirname) {
417     print "$dirname does not exist\n";
418     if (!$dryrun) {
419         mkdirs($dirname);
420     } else {
421         die "DRYRUN: not making it, so aborting\n";
422     }
423 }
424 if (defined($ignorefile)) {
425     parse_ignore_file($ignorefile);
426 }
427
428 print "Trying to snapshot-link $num_unique files into $dirname\n";
429 generate_snapshot_tree();
430 $snapdonedate = `date -u`;
431
432 chomp ($startdate, $jlistdonedate, $parsedonedate, $snapdonedate);
433
434 print "$startdate: startup\n";
435 print "$jlistdonedate: found $num_jigdos jigdo files\n";
436 print "$parsedonedate: found $num_unsorted files referenced in those jigdo files, $num_unique unique\n";
437 print "$snapdonedate: snapshot done\n";
438
439 if ($mode eq "single") {
440     if ($dryrun) {
441         print "DRYRUN: Not creating files in $outdir\n";
442     } else {
443         my ($gzin, $gzout, $line);
444         $gzin = gzopen($single_jigdo, "rb") or
445             die "Unable to open jigdo file $single_jigdo for reading: $!\n";
446         $gzout = gzopen("$outdir/$cdname.jigdo", "wb9") or
447             die "Unable to open new jigdo file $outdir/$cdname.jigdo for writing: $!\n";
448         while ($gzin->gzreadline($line) > 0) {
449             $line =~ s:^Template=.*$:Template=$cdname.template:;
450             $gzout->gzwrite($line);
451         }
452         $gzin->close();
453         $gzout->close();
454         copy("$template", "$outdir/$cdname.template") or
455             die "Failed to copy template file $template: $!\n";
456         open (CONF, "> $outdir/$cdname.conf") or
457             die "Failed to open conf file $outdir/$cdname.conf for writing: $!\n";
458         print CONF "JIGDO=$cdname.jigdo\n";
459         print CONF "TEMPLATE=$cdname.template\n";
460         print CONF "SNAPSHOT=snapshot/$dirname\n";
461         close(CONF);
462         print "Jigdo files, config and snapshot made in $outdir\n";
463     }
464 }