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