Source file src/cmd/vendor/golang.org/x/mod/sumdb/client.go

     1  // Copyright 2019 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package sumdb
     6  
     7  import (
     8  	"bytes"
     9  	"errors"
    10  	"fmt"
    11  	"strings"
    12  	"sync"
    13  	"sync/atomic"
    14  
    15  	"golang.org/x/mod/module"
    16  	"golang.org/x/mod/sumdb/note"
    17  	"golang.org/x/mod/sumdb/tlog"
    18  )
    19  
    20  // A ClientOps provides the external operations
    21  // (file caching, HTTP fetches, and so on) needed by the [Client].
    22  // The methods must be safe for concurrent use by multiple goroutines.
    23  type ClientOps interface {
    24  	// ReadRemote reads and returns the content served at the given path
    25  	// on the remote database server. The path begins with "/lookup" or "/tile/",
    26  	// and there is no need to parse the path in any way.
    27  	// It is the implementation's responsibility to turn that path into a full URL
    28  	// and make the HTTP request. ReadRemote should return an error for
    29  	// any non-200 HTTP response status.
    30  	ReadRemote(path string) ([]byte, error)
    31  
    32  	// ReadConfig reads and returns the content of the named configuration file.
    33  	// There are only a fixed set of configuration files.
    34  	//
    35  	// "key" returns a file containing the verifier key for the server.
    36  	//
    37  	// serverName + "/latest" returns a file containing the latest known
    38  	// signed tree from the server.
    39  	// To signal that the client wishes to start with an "empty" signed tree,
    40  	// ReadConfig can return a successful empty result (0 bytes of data).
    41  	ReadConfig(file string) ([]byte, error)
    42  
    43  	// WriteConfig updates the content of the named configuration file,
    44  	// changing it from the old []byte to the new []byte.
    45  	// If the old []byte does not match the stored configuration,
    46  	// WriteConfig must return ErrWriteConflict.
    47  	// Otherwise, WriteConfig should atomically replace old with new.
    48  	// The "key" configuration file is never written using WriteConfig.
    49  	WriteConfig(file string, old, new []byte) error
    50  
    51  	// ReadCache reads and returns the content of the named cache file.
    52  	// Any returned error will be treated as equivalent to the file not existing.
    53  	// There can be arbitrarily many cache files, such as:
    54  	//	serverName/lookup/pkg@version
    55  	//	serverName/tile/8/1/x123/456
    56  	ReadCache(file string) ([]byte, error)
    57  
    58  	// WriteCache writes the named cache file.
    59  	WriteCache(file string, data []byte)
    60  
    61  	// Log prints the given log message (such as with log.Print)
    62  	Log(msg string)
    63  
    64  	// SecurityError prints the given security error log message.
    65  	// The Client returns ErrSecurity from any operation that invokes SecurityError,
    66  	// but the return value is mainly for testing. In a real program,
    67  	// SecurityError should typically print the message and call log.Fatal or os.Exit.
    68  	SecurityError(msg string)
    69  }
    70  
    71  // ErrWriteConflict signals a write conflict during Client.WriteConfig.
    72  var ErrWriteConflict = errors.New("write conflict")
    73  
    74  // ErrSecurity is returned by [Client] operations that invoke Client.SecurityError.
    75  var ErrSecurity = errors.New("security error: misbehaving server")
    76  
    77  // A Client is a client connection to a checksum database.
    78  // All the methods are safe for simultaneous use by multiple goroutines.
    79  type Client struct {
    80  	ops ClientOps // access to operations in the external world
    81  
    82  	didLookup uint32
    83  
    84  	// one-time initialized data
    85  	initOnce   sync.Once
    86  	initErr    error          // init error, if any
    87  	name       string         // name of accepted verifier
    88  	verifiers  note.Verifiers // accepted verifiers (just one, but Verifiers for note.Open)
    89  	tileReader tileReader
    90  	tileHeight int
    91  	nosumdb    string
    92  
    93  	record    parCache // cache of record lookup, keyed by path@vers
    94  	tileCache parCache // cache of c.readTile, keyed by tile
    95  
    96  	latestMu  sync.Mutex
    97  	latest    tlog.Tree // latest known tree head
    98  	latestMsg []byte    // encoded signed note for latest
    99  
   100  	tileSavedMu sync.Mutex
   101  	tileSaved   map[tlog.Tile]bool // which tiles have been saved using c.ops.WriteCache already
   102  }
   103  
   104  // NewClient returns a new [Client] using the given [ClientOps].
   105  func NewClient(ops ClientOps) *Client {
   106  	return &Client{
   107  		ops: ops,
   108  	}
   109  }
   110  
   111  // init initializes the client (if not already initialized)
   112  // and returns any initialization error.
   113  func (c *Client) init() error {
   114  	c.initOnce.Do(c.initWork)
   115  	return c.initErr
   116  }
   117  
   118  // initWork does the actual initialization work.
   119  func (c *Client) initWork() {
   120  	defer func() {
   121  		if c.initErr != nil {
   122  			c.initErr = fmt.Errorf("initializing sumdb.Client: %v", c.initErr)
   123  		}
   124  	}()
   125  
   126  	c.tileReader.c = c
   127  	if c.tileHeight == 0 {
   128  		c.tileHeight = 8
   129  	}
   130  	c.tileSaved = make(map[tlog.Tile]bool)
   131  
   132  	vkey, err := c.ops.ReadConfig("key")
   133  	if err != nil {
   134  		c.initErr = err
   135  		return
   136  	}
   137  	verifier, err := note.NewVerifier(strings.TrimSpace(string(vkey)))
   138  	if err != nil {
   139  		c.initErr = err
   140  		return
   141  	}
   142  	c.verifiers = note.VerifierList(verifier)
   143  	c.name = verifier.Name()
   144  
   145  	if c.latest.N == 0 {
   146  		c.latest.Hash, err = tlog.TreeHash(0, nil)
   147  		if err != nil {
   148  			c.initErr = err
   149  			return
   150  		}
   151  	}
   152  
   153  	data, err := c.ops.ReadConfig(c.name + "/latest")
   154  	if err != nil {
   155  		c.initErr = err
   156  		return
   157  	}
   158  	if err := c.mergeLatest(data); err != nil {
   159  		c.initErr = err
   160  		return
   161  	}
   162  }
   163  
   164  // SetTileHeight sets the tile height for the Client.
   165  // Any call to SetTileHeight must happen before the first call to [Client.Lookup].
   166  // If SetTileHeight is not called, the Client defaults to tile height 8.
   167  // SetTileHeight can be called at most once,
   168  // and if so it must be called before the first call to Lookup.
   169  func (c *Client) SetTileHeight(height int) {
   170  	if atomic.LoadUint32(&c.didLookup) != 0 {
   171  		panic("SetTileHeight used after Lookup")
   172  	}
   173  	if height <= 0 {
   174  		panic("invalid call to SetTileHeight")
   175  	}
   176  	if c.tileHeight != 0 {
   177  		panic("multiple calls to SetTileHeight")
   178  	}
   179  	c.tileHeight = height
   180  }
   181  
   182  // SetGONOSUMDB sets the list of comma-separated GONOSUMDB patterns for the Client.
   183  // For any module path matching one of the patterns,
   184  // [Client.Lookup] will return ErrGONOSUMDB.
   185  // SetGONOSUMDB can be called at most once,
   186  // and if so it must be called before the first call to Lookup.
   187  func (c *Client) SetGONOSUMDB(list string) {
   188  	if atomic.LoadUint32(&c.didLookup) != 0 {
   189  		panic("SetGONOSUMDB used after Lookup")
   190  	}
   191  	if c.nosumdb != "" {
   192  		panic("multiple calls to SetGONOSUMDB")
   193  	}
   194  	c.nosumdb = list
   195  }
   196  
   197  // ErrGONOSUMDB is returned by [Client.Lookup] for paths that match
   198  // a pattern listed in the GONOSUMDB list (set by [Client.SetGONOSUMDB],
   199  // usually from the environment variable).
   200  var ErrGONOSUMDB = errors.New("skipped (listed in GONOSUMDB)")
   201  
   202  func (c *Client) skip(target string) bool {
   203  	return module.MatchPrefixPatterns(c.nosumdb, target)
   204  }
   205  
   206  // Lookup returns the go.sum lines for the given module path and version.
   207  // The version may end in a /go.mod suffix, in which case Lookup returns
   208  // the go.sum lines for the module's go.mod-only hash.
   209  func (c *Client) Lookup(path, vers string) (lines []string, err error) {
   210  	atomic.StoreUint32(&c.didLookup, 1)
   211  
   212  	if c.skip(path) {
   213  		return nil, ErrGONOSUMDB
   214  	}
   215  
   216  	defer func() {
   217  		if err != nil {
   218  			err = fmt.Errorf("%s@%s: %v", path, vers, err)
   219  		}
   220  	}()
   221  
   222  	if err := c.init(); err != nil {
   223  		return nil, err
   224  	}
   225  
   226  	// Prepare encoded cache filename / URL.
   227  	epath, err := module.EscapePath(path)
   228  	if err != nil {
   229  		return nil, err
   230  	}
   231  	evers, err := module.EscapeVersion(strings.TrimSuffix(vers, "/go.mod"))
   232  	if err != nil {
   233  		return nil, err
   234  	}
   235  	remotePath := "/lookup/" + epath + "@" + evers
   236  	file := c.name + remotePath
   237  
   238  	// Fetch the data.
   239  	// The lookupCache avoids redundant ReadCache/GetURL operations
   240  	// (especially since go.sum lines tend to come in pairs for a given
   241  	// path and version) and also avoids having multiple of the same
   242  	// request in flight at once.
   243  	type cached struct {
   244  		data []byte
   245  		err  error
   246  	}
   247  	result := c.record.Do(file, func() interface{} {
   248  		// Try the on-disk cache, or else get from web.
   249  		writeCache := false
   250  		data, err := c.ops.ReadCache(file)
   251  		if err != nil {
   252  			data, err = c.ops.ReadRemote(remotePath)
   253  			if err != nil {
   254  				return cached{nil, err}
   255  			}
   256  			writeCache = true
   257  		}
   258  
   259  		// Validate the record before using it for anything.
   260  		id, text, treeMsg, err := tlog.ParseRecord(data)
   261  		if err != nil {
   262  			return cached{nil, err}
   263  		}
   264  		if err := c.mergeLatest(treeMsg); err != nil {
   265  			return cached{nil, err}
   266  		}
   267  		if err := c.checkRecord(id, text); err != nil {
   268  			return cached{nil, err}
   269  		}
   270  
   271  		// Now that we've validated the record,
   272  		// save it to the on-disk cache (unless that's where it came from).
   273  		if writeCache {
   274  			c.ops.WriteCache(file, data)
   275  		}
   276  
   277  		return cached{data, nil}
   278  	}).(cached)
   279  	if result.err != nil {
   280  		return nil, result.err
   281  	}
   282  
   283  	// Extract the lines for the specific version we want
   284  	// (with or without /go.mod).
   285  	prefix := path + " " + vers + " "
   286  	var hashes []string
   287  	for _, line := range strings.Split(string(result.data), "\n") {
   288  		if strings.HasPrefix(line, prefix) {
   289  			hashes = append(hashes, line)
   290  		}
   291  	}
   292  	return hashes, nil
   293  }
   294  
   295  // mergeLatest merges the tree head in msg
   296  // with the Client's current latest tree head,
   297  // ensuring the result is a consistent timeline.
   298  // If the result is inconsistent, mergeLatest calls c.ops.SecurityError
   299  // with a detailed security error message and then
   300  // (only if c.ops.SecurityError does not exit the program) returns ErrSecurity.
   301  // If the Client's current latest tree head moves forward,
   302  // mergeLatest updates the underlying configuration file as well,
   303  // taking care to merge any independent updates to that configuration.
   304  func (c *Client) mergeLatest(msg []byte) error {
   305  	// Merge msg into our in-memory copy of the latest tree head.
   306  	when, err := c.mergeLatestMem(msg)
   307  	if err != nil {
   308  		return err
   309  	}
   310  	if when != msgFuture {
   311  		// msg matched our present or was in the past.
   312  		// No change to our present, so no update of config file.
   313  		return nil
   314  	}
   315  
   316  	// Flush our extended timeline back out to the configuration file.
   317  	// If the configuration file has been updated in the interim,
   318  	// we need to merge any updates made there as well.
   319  	// Note that writeConfig is an atomic compare-and-swap.
   320  	for {
   321  		msg, err := c.ops.ReadConfig(c.name + "/latest")
   322  		if err != nil {
   323  			return err
   324  		}
   325  		when, err := c.mergeLatestMem(msg)
   326  		if err != nil {
   327  			return err
   328  		}
   329  		if when != msgPast {
   330  			// msg matched our present or was from the future,
   331  			// and now our in-memory copy matches.
   332  			return nil
   333  		}
   334  
   335  		// msg (== config) is in the past, so we need to update it.
   336  		c.latestMu.Lock()
   337  		latestMsg := c.latestMsg
   338  		c.latestMu.Unlock()
   339  		if err := c.ops.WriteConfig(c.name+"/latest", msg, latestMsg); err != ErrWriteConflict {
   340  			// Success or a non-write-conflict error.
   341  			return err
   342  		}
   343  	}
   344  }
   345  
   346  const (
   347  	msgPast = 1 + iota
   348  	msgNow
   349  	msgFuture
   350  )
   351  
   352  // mergeLatestMem is like mergeLatest but is only concerned with
   353  // updating the in-memory copy of the latest tree head (c.latest)
   354  // not the configuration file.
   355  // The when result explains when msg happened relative to our
   356  // previous idea of c.latest:
   357  // msgPast means msg was from before c.latest,
   358  // msgNow means msg was exactly c.latest, and
   359  // msgFuture means msg was from after c.latest, which has now been updated.
   360  func (c *Client) mergeLatestMem(msg []byte) (when int, err error) {
   361  	if len(msg) == 0 {
   362  		// Accept empty msg as the unsigned, empty timeline.
   363  		c.latestMu.Lock()
   364  		latest := c.latest
   365  		c.latestMu.Unlock()
   366  		if latest.N == 0 {
   367  			return msgNow, nil
   368  		}
   369  		return msgPast, nil
   370  	}
   371  
   372  	note, err := note.Open(msg, c.verifiers)
   373  	if err != nil {
   374  		return 0, fmt.Errorf("reading tree note: %v\nnote:\n%s", err, msg)
   375  	}
   376  	tree, err := tlog.ParseTree([]byte(note.Text))
   377  	if err != nil {
   378  		return 0, fmt.Errorf("reading tree: %v\ntree:\n%s", err, note.Text)
   379  	}
   380  
   381  	// Other lookups may be calling mergeLatest with other heads,
   382  	// so c.latest is changing underfoot. We don't want to hold the
   383  	// c.mu lock during tile fetches, so loop trying to update c.latest.
   384  	c.latestMu.Lock()
   385  	latest := c.latest
   386  	latestMsg := c.latestMsg
   387  	c.latestMu.Unlock()
   388  
   389  	for {
   390  		// If the tree head looks old, check that it is on our timeline.
   391  		if tree.N <= latest.N {
   392  			if err := c.checkTrees(tree, msg, latest, latestMsg); err != nil {
   393  				return 0, err
   394  			}
   395  			if tree.N < latest.N {
   396  				return msgPast, nil
   397  			}
   398  			return msgNow, nil
   399  		}
   400  
   401  		// The tree head looks new. Check that we are on its timeline and try to move our timeline forward.
   402  		if err := c.checkTrees(latest, latestMsg, tree, msg); err != nil {
   403  			return 0, err
   404  		}
   405  
   406  		// Install our msg if possible.
   407  		// Otherwise we will go around again.
   408  		c.latestMu.Lock()
   409  		installed := false
   410  		if c.latest == latest {
   411  			installed = true
   412  			c.latest = tree
   413  			c.latestMsg = msg
   414  		} else {
   415  			latest = c.latest
   416  			latestMsg = c.latestMsg
   417  		}
   418  		c.latestMu.Unlock()
   419  
   420  		if installed {
   421  			return msgFuture, nil
   422  		}
   423  	}
   424  }
   425  
   426  // checkTrees checks that older (from olderNote) is contained in newer (from newerNote).
   427  // If an error occurs, such as malformed data or a network problem, checkTrees returns that error.
   428  // If on the other hand checkTrees finds evidence of misbehavior, it prepares a detailed
   429  // message and calls log.Fatal.
   430  func (c *Client) checkTrees(older tlog.Tree, olderNote []byte, newer tlog.Tree, newerNote []byte) error {
   431  	thr := tlog.TileHashReader(newer, &c.tileReader)
   432  	h, err := tlog.TreeHash(older.N, thr)
   433  	if err != nil {
   434  		if older.N == newer.N {
   435  			return fmt.Errorf("checking tree#%d: %v", older.N, err)
   436  		}
   437  		return fmt.Errorf("checking tree#%d against tree#%d: %v", older.N, newer.N, err)
   438  	}
   439  	if h == older.Hash {
   440  		return nil
   441  	}
   442  
   443  	// Detected a fork in the tree timeline.
   444  	// Start by reporting the inconsistent signed tree notes.
   445  	var buf bytes.Buffer
   446  	fmt.Fprintf(&buf, "SECURITY ERROR\n")
   447  	fmt.Fprintf(&buf, "go.sum database server misbehavior detected!\n\n")
   448  	indent := func(b []byte) []byte {
   449  		return bytes.Replace(b, []byte("\n"), []byte("\n\t"), -1)
   450  	}
   451  	fmt.Fprintf(&buf, "old database:\n\t%s\n", indent(olderNote))
   452  	fmt.Fprintf(&buf, "new database:\n\t%s\n", indent(newerNote))
   453  
   454  	// The notes alone are not enough to prove the inconsistency.
   455  	// We also need to show that the newer note's tree hash for older.N
   456  	// does not match older.Hash. The consumer of this report could
   457  	// of course consult the server to try to verify the inconsistency,
   458  	// but we are holding all the bits we need to prove it right now,
   459  	// so we might as well print them and make the report not depend
   460  	// on the continued availability of the misbehaving server.
   461  	// Preparing this data only reuses the tiled hashes needed for
   462  	// tlog.TreeHash(older.N, thr) above, so assuming thr is caching tiles,
   463  	// there are no new access to the server here, and these operations cannot fail.
   464  	fmt.Fprintf(&buf, "proof of misbehavior:\n\t%v", h)
   465  	if p, err := tlog.ProveTree(newer.N, older.N, thr); err != nil {
   466  		fmt.Fprintf(&buf, "\tinternal error: %v\n", err)
   467  	} else if err := tlog.CheckTree(p, newer.N, newer.Hash, older.N, h); err != nil {
   468  		fmt.Fprintf(&buf, "\tinternal error: generated inconsistent proof\n")
   469  	} else {
   470  		for _, h := range p {
   471  			fmt.Fprintf(&buf, "\n\t%v", h)
   472  		}
   473  	}
   474  	c.ops.SecurityError(buf.String())
   475  	return ErrSecurity
   476  }
   477  
   478  // checkRecord checks that record #id's hash matches data.
   479  func (c *Client) checkRecord(id int64, data []byte) error {
   480  	c.latestMu.Lock()
   481  	latest := c.latest
   482  	c.latestMu.Unlock()
   483  
   484  	if id >= latest.N {
   485  		return fmt.Errorf("cannot validate record %d in tree of size %d", id, latest.N)
   486  	}
   487  	hashes, err := tlog.TileHashReader(latest, &c.tileReader).ReadHashes([]int64{tlog.StoredHashIndex(0, id)})
   488  	if err != nil {
   489  		return err
   490  	}
   491  	if hashes[0] == tlog.RecordHash(data) {
   492  		return nil
   493  	}
   494  	return fmt.Errorf("cannot authenticate record data in server response")
   495  }
   496  
   497  // tileReader is a *Client wrapper that implements tlog.TileReader.
   498  // The separate type avoids exposing the ReadTiles and SaveTiles
   499  // methods on Client itself.
   500  type tileReader struct {
   501  	c *Client
   502  }
   503  
   504  func (r *tileReader) Height() int {
   505  	return r.c.tileHeight
   506  }
   507  
   508  // ReadTiles reads and returns the requested tiles,
   509  // either from the on-disk cache or the server.
   510  func (r *tileReader) ReadTiles(tiles []tlog.Tile) ([][]byte, error) {
   511  	// Read all the tiles in parallel.
   512  	data := make([][]byte, len(tiles))
   513  	errs := make([]error, len(tiles))
   514  	var wg sync.WaitGroup
   515  	for i, tile := range tiles {
   516  		wg.Add(1)
   517  		go func(i int, tile tlog.Tile) {
   518  			defer wg.Done()
   519  			defer func() {
   520  				if e := recover(); e != nil {
   521  					errs[i] = fmt.Errorf("panic: %v", e)
   522  				}
   523  			}()
   524  			data[i], errs[i] = r.c.readTile(tile)
   525  		}(i, tile)
   526  	}
   527  	wg.Wait()
   528  
   529  	for _, err := range errs {
   530  		if err != nil {
   531  			return nil, err
   532  		}
   533  	}
   534  
   535  	return data, nil
   536  }
   537  
   538  // tileCacheKey returns the cache key for the tile.
   539  func (c *Client) tileCacheKey(tile tlog.Tile) string {
   540  	return c.name + "/" + tile.Path()
   541  }
   542  
   543  // tileRemotePath returns the remote path for the tile.
   544  func (c *Client) tileRemotePath(tile tlog.Tile) string {
   545  	return "/" + tile.Path()
   546  }
   547  
   548  // readTile reads a single tile, either from the on-disk cache or the server.
   549  func (c *Client) readTile(tile tlog.Tile) ([]byte, error) {
   550  	type cached struct {
   551  		data []byte
   552  		err  error
   553  	}
   554  
   555  	result := c.tileCache.Do(tile, func() interface{} {
   556  		// Try the requested tile in on-disk cache.
   557  		data, err := c.ops.ReadCache(c.tileCacheKey(tile))
   558  		if err == nil {
   559  			c.markTileSaved(tile)
   560  			return cached{data, nil}
   561  		}
   562  
   563  		// Try the full tile in on-disk cache (if requested tile not already full).
   564  		// We only save authenticated tiles to the on-disk cache,
   565  		// so the recreated prefix is equally authenticated.
   566  		full := tile
   567  		full.W = 1 << uint(tile.H)
   568  		if tile != full {
   569  			data, err := c.ops.ReadCache(c.tileCacheKey(full))
   570  			if err == nil {
   571  				c.markTileSaved(tile) // don't save tile later; we already have full
   572  				return cached{data[:len(data)/full.W*tile.W], nil}
   573  			}
   574  		}
   575  
   576  		// Try requested tile from server.
   577  		data, err = c.ops.ReadRemote(c.tileRemotePath(tile))
   578  		if err == nil {
   579  			return cached{data, nil}
   580  		}
   581  
   582  		// Try full tile on server.
   583  		// If the partial tile does not exist, it should be because
   584  		// the tile has been completed and only the complete one
   585  		// is available.
   586  		if tile != full {
   587  			data, err := c.ops.ReadRemote(c.tileRemotePath(full))
   588  			if err == nil {
   589  				// Note: We could save the full tile in the on-disk cache here,
   590  				// but we don't know if it is valid yet, and we will only find out
   591  				// about the partial data, not the full data. So let SaveTiles
   592  				// save the partial tile, and we'll just refetch the full tile later
   593  				// once we can validate more (or all) of it.
   594  				return cached{data[:len(data)/full.W*tile.W], nil}
   595  			}
   596  		}
   597  
   598  		// Nothing worked.
   599  		// Return the error from the server fetch for the requested (not full) tile.
   600  		return cached{nil, err}
   601  	}).(cached)
   602  
   603  	return result.data, result.err
   604  }
   605  
   606  // markTileSaved records that tile is already present in the on-disk cache,
   607  // so that a future SaveTiles for that tile can be ignored.
   608  func (c *Client) markTileSaved(tile tlog.Tile) {
   609  	c.tileSavedMu.Lock()
   610  	c.tileSaved[tile] = true
   611  	c.tileSavedMu.Unlock()
   612  }
   613  
   614  // SaveTiles saves the now validated tiles.
   615  func (r *tileReader) SaveTiles(tiles []tlog.Tile, data [][]byte) {
   616  	c := r.c
   617  
   618  	// Determine which tiles need saving.
   619  	// (Tiles that came from the cache need not be saved back.)
   620  	save := make([]bool, len(tiles))
   621  	c.tileSavedMu.Lock()
   622  	for i, tile := range tiles {
   623  		if !c.tileSaved[tile] {
   624  			save[i] = true
   625  			c.tileSaved[tile] = true
   626  		}
   627  	}
   628  	c.tileSavedMu.Unlock()
   629  
   630  	for i, tile := range tiles {
   631  		if save[i] {
   632  			// If WriteCache fails here (out of disk space? i/o error?),
   633  			// c.tileSaved[tile] is still true and we will not try to write it again.
   634  			// Next time we run maybe we'll redownload it again and be
   635  			// more successful.
   636  			c.ops.WriteCache(c.name+"/"+tile.Path(), data[i])
   637  		}
   638  	}
   639  }
   640  

View as plain text