Index: /trunk/DNSDB/ExportBIND.pm
===================================================================
--- /trunk/DNSDB/ExportBIND.pm	(revision 880)
+++ /trunk/DNSDB/ExportBIND.pm	(revision 881)
@@ -40,20 +40,20 @@
 ## export reverse zones
 
-  my $soasth = $dnsdb->{dbh}->prepare("SELECT host,type,val,distance,weight,port,ttl,record_id,location ".
-	"FROM records WHERE rdns_id=? AND type=6");
+  my $soasth = $dnsdb->{dbh}->prepare("SELECT host,val,ttl,record_id,location FROM records WHERE rdns_id=? AND type=6");
   # record order matters for reverse zones because we need to override larger templates with smaller ones.
-  my $recsth = $dnsdb->{dbh}->prepare("SELECT host,type,val,distance,weight,port,ttl,record_id,location,extract(epoch from stamp),expires,stampactive ".
-	"FROM records WHERE rdns_id=? AND NOT type=6 ".
-	"ORDER BY masklen(inetlazy(val)) DESC, inetlazy(val)");
+  my $recsth = $dnsdb->{dbh}->prepare(
+        "SELECT host,type,val,distance,weight,port,ttl,record_id,location,extract(epoch from stamp),expires,stampactive ".
+        "FROM records WHERE rdns_id=? AND NOT type=6 ".
+        "ORDER BY masklen(inetlazy(val)) DESC, inetlazy(val), record_id");
 
   # Fetch active zone list
   my $revsth = $dnsdb->{dbh}->prepare("SELECT rdns_id,revnet,status,changed,default_location FROM revzones WHERE status=1 ".
-	"ORDER BY masklen(revnet),revnet DESC, rdns_id");
+        "ORDER BY masklen(revnet),revnet DESC, rdns_id");
   # Unflag changed zones, so we can maybe cache the export and not redo everything every time
   my $zonesth = $dnsdb->{dbh}->prepare("UPDATE revzones SET changed='n' WHERE rdns_id=?");
+
+  my %recflags;  # need this to be independent for forward vs reverse zones, as they're not merged
+
   $revsth->execute();
-
-  my %recflags;  # need this to be independent for forward vs reverse zones, as they're not merged
-
   while (my ($revid,$revzone,$revstat,$changed,$defloc) = $revsth->fetchrow_array) {
     my $cidr = NetAddr::IP->new($revzone);
@@ -69,5 +69,4 @@
     # fetch a list of views/locations present in the zone.  we need to publish a file for each one.
     # in the event that no locations are present (~~ $viewlist is empty), /%view collapses to nothing in the zone path
-#    my (@loclist) = $dnsdb->{dbh}->selectrow_array("SELECT DISTINCT location FROM records WHERE rdns_id = ?", undef, $revid);
     my $tmplocs = $dnsdb->{dbh}->selectall_arrayref("SELECT DISTINCT location FROM records WHERE rdns_id = ?", undef, $revid);
     my @loclist;
@@ -80,4 +79,5 @@
     eval {
 
+##fixme:  use tmpfile module for more secure temp files?  want the zone name at least in it anyway, not sure that works...
       my $arpazone = DNSDB::_ZONE($cidr, 'ZONE', 'r', '.').($cidr->{isv6} ? '.ip6.arpa' : '.in-addr.arpa');
       my $zfile = $cidr->network->addr."-".$cidr->masklen;
@@ -96,14 +96,28 @@
         $zfilepath =~ s,[^\w./-],_,g;
 
-#        open $zonefiles{$loc}, ">", $zfilepath;
+        # safety check, may need tweaking for race conditions
+        my $zpathbase = $zfilepath;
+        $zpathbase =~ s{/[^/]+$}{};
+        if (!-e $zpathbase) {
+          mkdir $zpathbase;
+        } else {
+          die "$zpathbase is not a directory\n" unless -d $zpathbase;
+        }
 
         # write fresh records if:
-        #  - we are not using the cache
+        #  - the zone contains records which expire in less than 10 minutes or became valid less than 10 minutes ago
+        # note, no need to multi-bump the serial
+        if ( ($dnsdb->{dbh}->selectrow_array("SELECT COUNT(*) FROM records WHERE rdns_id = ? AND ".
+                "stampactive='t' AND @(extract(epoch from stamp-now())) < 600", undef, $revid))[0] ) {
+          $changed = 1;
+          $dnsdb->_updateserial(domain_id => $domid);
+        }
+#  - we are not using the cache
+# if ($dnsdb->{usecache}
         #  - force_refresh is set
         #  - the zone has changed
-        #  - the cache file does not exist
-        #  - the cache file is empty
-        if ($dnsdb->{force_refresh} || $changed || !-e $zfilepath || -z $zfilepath) {
-#        if (!$dnsdb->{usecache} || $dnsdb->{force_refresh} || $changed || !-e $cachefile || -z $cachefile) {
+        #  - the zone file does not exist
+        #  - the zone file is empty
+        elsif ($dnsdb->{force_refresh} || $changed || !-e $zfilepath || -z $zfilepath) {
 #          if ($dnsdb->{usecache}) {
 #            open ZONECACHE, ">$tmpcache" or die "Error creating temporary file $tmpcache: $!\n";
@@ -112,25 +126,40 @@
           open $zonefiles{$loc}, ">", $zfilepath or die "Error creating temporary file $zfilepath: $!\n";
 
+          # Header for human convenience
+##fixme?  vary arpazone/cidr in header and error message per showrev_arpa, or possibly
+# new dedicated setting, or possibly interact with with bind_export_fqdn?
           printf {$zonefiles{$loc}} "; %s in view %s exported %s\n", $arpazone, $loc, scalar(localtime)
-            or die "Error writing header [$cidr, '$loc']: $!\n";
-
-          # need to fetch this separately since the rest of the records all (should) have real IPs in val
+            or die "Error writing header [$arpazone, '$loc']: $!\n";
+
+          # Fetch the SOA separately as we publish it separately for each location with this loop,
+          # mainly because we want it first in the zone file
           $soasth->execute($revid);
-          my (@zsoa) = $soasth->fetchrow_array();
+          my ($soa_host, $soa_val, $soa_ttl, $soa_id, $soa_loc) = $soasth->fetchrow_array;
+
 ##fixme: do we even need @loclist passed in?
-          printrec_bind($dnsdb, \%zonefiles, \@loclist, $zsoa[7], 'y', \%recflags, $cidr,
-            $zsoa[0], $zsoa[1], $zsoa[2], $zsoa[3], $zsoa[4], $zsoa[5], $zsoa[6], $loc, '');
+          printrec_bind($dnsdb, \%zonefiles, \@loclist, $soa_id, 'y', \%recflags, $cidr,
+            $soa_host, 6, $soa_val, 0, 0, 0, $soa_ttl, $loc, '');
+
         } # if force_refresh etc
 
         # tag the zonefile for publication in the view
         push @{$viewzones{$loc}}, $arpazone;
+
       } # foreach @loclist
 
       # now the meat of the records
       $recsth->execute($revid);
-
       while (my ($host, $type, $val, $dist, $weight, $port, $ttl, $recid, $loc, $stamp, $expires, $stampactive)
 		= $recsth->fetchrow_array) {
         next if $recflags{$recid};
+
+        # Spaces are evil.
+        $val =~ s/^\s+//;
+        $val =~ s/\s+$//;
+        if ($typemap{$type} ne 'TXT') {
+          # Leading or trailng spaces could be legit in TXT records.
+          $host =~ s/^\s+//;
+          $host =~ s/\s+$//;
+        }
 
         # Check for out-of-zone data
@@ -149,13 +178,4 @@
         } # is $val a raw .arpa name?
 
-        # Spaces are evil.
-        $val =~ s/^\s+//;
-        $val =~ s/\s+$//;
-        if ($typemap{$type} ne 'TXT') {
-          # Leading or trailng spaces could be legit in TXT records.
-          $host =~ s/^\s+//;
-          $host =~ s/\s+$//;
-        }
-
         printrec_bind($dnsdb, \%zonefiles, \@loclist, $recid, 'y', \%recflags, $revzone,
 		$host, $type, $val, $dist, $weight, $port, $ttl, $loc, $stamp, $expires, $stampactive);
@@ -202,6 +222,5 @@
 ## and now the domains
 
-  $soasth = $dnsdb->{dbh}->prepare("SELECT host,type,val,distance,weight,port,ttl,record_id,location ".
-	"FROM records WHERE domain_id=? AND type=6");
+  $soasth = $dnsdb->{dbh}->prepare("SELECT host,val,ttl,record_id,location FROM records WHERE domain_id=? AND type=6");
   # record order needs to match reverse zone ordering for IP values, or A+PTR
   # template records don't cascade/expand correctly to match the reverse zones.
@@ -211,14 +230,15 @@
   # ordering by nominal parent-child label hierarchy (as actually found live
   # in some AXFRed zone files) would take a lot of chewing on data
-  $recsth = $dnsdb->{dbh}->prepare("SELECT host,type,val,distance,weight,port,ttl,record_id,location,extract(epoch from stamp),expires,stampactive ".
-	"FROM records WHERE domain_id=? AND NOT type=6 ".
-	"ORDER BY masklen(inetlazy(val)) DESC, inetlazy(val), record_id");
+  $recsth = $dnsdb->{dbh}->prepare(
+        "SELECT host,type,val,distance,weight,port,ttl,record_id,location,extract(epoch from stamp),expires,stampactive ".
+        "FROM records WHERE domain_id=? AND NOT type=6 ".
+        "ORDER BY masklen(inetlazy(val)) DESC, inetlazy(val), record_id");
 #      "FROM records WHERE domain_id=? AND type < 65280");     # Just exclude all types relating to rDNS
 
   # Fetch active zone list
-  my $domsth = $dnsdb->{dbh}->prepare("SELECT domain_id,domain,status,changed FROM domains WHERE status=1 ORDER BY domain_id");
+  my $domsth = $dnsdb->{dbh}->prepare("SELECT domain_id,domain,status,changed,default_location FROM domains WHERE status=1 ".
+        "ORDER BY domain_id");
   # Unflag changed zones, so we can maybe cache the export and not redo everything every time
   $zonesth = $dnsdb->{dbh}->prepare("UPDATE domains SET changed='n' WHERE domain_id=?");
-  $domsth->execute();
 
   # Clear %reclfags, since we explicitly want to NOT carry "I've published this
@@ -231,4 +251,5 @@
 #  %recflags = ();
 
+  $domsth->execute();
   while (my ($domid,$domain,$domstat,$changed) = $domsth->fetchrow_array) {
 
@@ -240,4 +261,5 @@
       push @loclist, ($tloc->[0] eq '' ? 'common' : $tloc->[0]);
     }
+
     my %zonefiles;  # zone file handles
 
@@ -247,16 +269,21 @@
       my $zfile = $domain;  # can probably drop this intermediate
       my $tmpcache = "tmp.$zfile.$$";	# safety net.  don't overwrite a previous known-good file
+
       foreach my $loc (@loclist) {
         my $zfilepath = $dnsdb->{bind_export_zone_path};
         $zfilepath =~ s/\%view/$loc/;
         $zfilepath =~ s/\%zone/$zfile/;
-#        $zfilepath =~ s/\%arpazone/$arpazone/;
 
         # Just In Case(TM)
         $zfilepath =~ s,[^\w./-],_,g;
 
-#      open $zonefiles{$loc}, ">", $zfilepath;
-print "open zonefile for '$loc', '$zfilepath'\n";
-
+        # safety check, may need tweaking for race conditions
+        my $zpathbase = $zfilepath;
+        $zpathbase =~ s{/[^/]+$}{};
+        if (!-e $zpathbase) {
+          mkdir $zpathbase;
+        } else {
+          die "$zpathbase is not a directory\n" unless -d $zpathbase;
+        }
 
         # write fresh records if:
@@ -275,27 +302,32 @@
           $dnsdb->_updateserial(domain_id => $domid);
         }
+#  - we are not using the cache
+# if ($dnsdb->{usecache}
+        #  - force_refresh is set
+        #  - the zone has changed
+        #  - the zone file does not exist
+        #  - the zone file is empty
 #        if (!$self->{usecache} || $self->{force_refresh} || $changed || !-e $cachefile || -z $cachefile) {
         if ($dnsdb->{force_refresh} || $changed || !-e $zfilepath || -z $zfilepath) {
-          open $zonefiles{$loc}, ">", $zfilepath or die "Error creating temporary file $zfilepath: $!\n";
-
 #          if ($self->{usecache}) {
 #            open ZONECACHE, ">$tmpcache" or die "Error creating temporary file $tmpcache: $!\n";
 #            $zonefilehandle = *ZONECACHE;
 #          }
-
-          # need to fetch this separately so the SOA comes first in the flatfile....
-          # Just In Case we need/want to reimport from the flatfile later on.
-          $soasth->execute($domid);
-          my (@zsoa) = $soasth->fetchrow_array();
-
-          # drop in a header line so we know when things went KABOOM
+          open $zonefiles{$loc}, ">", $zfilepath or die "Error creating temporary file $zfilepath: $!\n";
+
+          # Header for human convenience
           printf {$zonefiles{$loc}} "; %s in view %s exported %s\n", $domain, $loc, scalar(localtime)
 		or die "Error writing header [$domain, '$loc']: $!\n";
 
-          printrec_bind($dnsdb, \%zonefiles, \@loclist, $zsoa[7], 'n', \%recflags, $domain,
-            $zsoa[0], $zsoa[1], $zsoa[2], $zsoa[3], $zsoa[4], $zsoa[5], $zsoa[6], $loc, '');
-
-#          $self->_printrec_tiny($zonefilehandle, $zsoa[7], 'n',\%recflags, $domain,
-#            $zsoa[0],$zsoa[1],$zsoa[2],$zsoa[3],$zsoa[4],$zsoa[5],$zsoa[6],$zsoa[8],'');
+          # Fetch the SOA separately as we publish it separately for each location with this loop,
+          # mainly because we want it first in the zone file
+          $soasth->execute($domid);
+          my ($soa_host, $soa_val, $soa_ttl, $soa_id, $soa_loc) = $soasth->fetchrow_array;
+$dnsdb->{dbh}->selectrow_array(
+            "SELECT host,val,ttl,record_id,location FROM records WHERE domain_id=? AND type=6");
+
+##fixme: do we even need @loclist passed in?
+          printrec_bind($dnsdb, \%zonefiles, \@loclist, $soa_id, 'n', \%recflags, $domain,
+            $soa_host, 6, $soa_val, 0, 0, 0, $soa_ttl, $loc, '');
 
         } # if force_refresh etc
@@ -303,6 +335,8 @@
         # tag the zonefile for publication in the view
         push @{$viewzones{$loc}}, $domain;
+
       } # foreach @loclist
 
+      # now the meat of the records
       $recsth->execute($domid);
       while (my ($host,$type,$val,$dist,$weight,$port,$ttl,$recid,$loc,$stamp,$expires,$stampactive) = $recsth->fetchrow_array) {
@@ -327,8 +361,8 @@
         }
 
-        $recflags{$recid} = 1;
-
         printrec_bind($dnsdb, \%zonefiles, \@loclist, $recid, 'n', \%recflags, $domain,
           $host, $type, $val, $dist, $weight, $port, $ttl, $loc, $stamp, $expires, $stampactive);
+
+        $recflags{$recid} = 1;
 
       } # while ($recsth)
