maintner: make godata.Get use locally cached data when network is unavailable

This is "Airplane mode". I had cached the latest maintner data before
a flight, but using godata.Get in flight failed when it tried to
update it.

Instead, detect the lack of network and just use whatever's available
instead.

Change-Id: Ieea29f844353377b3c36a531642a716f91f34436
Reviewed-on: https://go-review.googlesource.com/44813
Reviewed-by: Kevin Burke <kev@inburke.com>
diff --git a/maintner/netsource.go b/maintner/netsource.go
index df91149..5c4d28f 100644
--- a/maintner/netsource.go
+++ b/maintner/netsource.go
@@ -13,10 +13,12 @@
 	"io"
 	"io/ioutil"
 	"log"
+	"net"
 	"net/http"
 	"net/url"
 	"os"
 	"path/filepath"
+	"strconv"
 	"strings"
 	"time"
 
@@ -69,6 +71,87 @@
 	return ch
 }
 
+// isNoInternetError reports whether the provided error is because there's no
+// network connectivity.
+func isNoInternetError(err error) bool {
+	switch err := err.(type) {
+	case *url.Error:
+		return isNoInternetError(err.Err)
+	case *net.OpError:
+		return isNoInternetError(err.Err)
+	case *net.DNSError:
+		// Trashy:
+		return err.Err == "no such host"
+	default:
+		log.Printf("Unknown error type %T: %#v", err, err)
+		return false
+	}
+}
+
+func (ns *netMutSource) locallyCachedSegments() (segs []fileSeg, err error) {
+	defer func() {
+		if err != nil {
+			log.Printf("No network connection and failed to use local cache: %v", err)
+		} else {
+			log.Printf("No network connection; using %d locally cached segments.", len(segs))
+		}
+	}()
+	fis, err := ioutil.ReadDir(ns.cacheDir)
+	if err != nil {
+		return nil, err
+	}
+	fiMap := map[string]os.FileInfo{}
+	segHex := map[int]string{}
+	segGrowing := map[int]bool{}
+	for _, fi := range fis {
+		name := fi.Name()
+		if !strings.HasSuffix(name, ".mutlog") {
+			continue
+		}
+		fiMap[name] = fi
+
+		if len(name) == len("0000.6897fab4d3afcda332424b2a2a1a4469021074282bc7be5606aaa221.mutlog") {
+			num, err := strconv.Atoi(name[:4])
+			if err != nil {
+				continue
+			}
+			segHex[num] = strings.TrimSuffix(name[5:], ".mutlog")
+		} else if strings.HasSuffix(name, ".growing.mutlog") {
+			num, err := strconv.Atoi(name[:4])
+			if err != nil {
+				continue
+			}
+			segGrowing[num] = true
+		}
+	}
+	for num := 0; ; num++ {
+		if hex, ok := segHex[num]; ok {
+			name := fmt.Sprintf("%04d.%s.mutlog", num, hex)
+			segs = append(segs, fileSeg{
+				seg:    num,
+				file:   filepath.Join(ns.cacheDir, name),
+				size:   fiMap[name].Size(),
+				sha224: hex,
+			})
+			continue
+		}
+		if segGrowing[num] {
+			name := fmt.Sprintf("%04d.growing.mutlog", num)
+			slurp, err := ioutil.ReadFile(filepath.Join(ns.cacheDir, name))
+			if err != nil {
+				return nil, err
+			}
+			segs = append(segs, fileSeg{
+				seg:    num,
+				file:   filepath.Join(ns.cacheDir, name),
+				size:   int64(len(slurp)),
+				sha224: fmt.Sprintf("%x", sha256.Sum224(slurp)),
+			})
+		}
+		return segs, nil
+	}
+}
+
 // waitSizeNot optionally specifies that the request should long-poll waiting for the server
 // to have a sum of log segment sizes different than the value specified.
 func (ns *netMutSource) getServerSegments(ctx context.Context, waitSizeNot int64) ([]LogSegmentJSON, error) {
@@ -117,6 +200,18 @@
 		sumLast := sumSegSize(ns.last)
 
 		segs, err := ns.getServerSegments(ctx, sumLast)
+		if isNoInternetError(err) {
+			if sumLast == 0 {
+				return ns.locallyCachedSegments()
+			}
+			log.Printf("No internet; blocking.")
+			select {
+			case <-ctx.Done():
+				return nil, ctx.Err()
+			case <-time.After(15 * time.Second):
+				continue
+			}
+		}
 		if err != nil {
 			return nil, err
 		}