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