github.com/tiagovtristao/plz@v13.4.0+incompatible/src/cache/http_cache.go (about)

     1  // Http-based cache.
     2  
     3  package cache
     4  
     5  import (
     6  	"encoding/base64"
     7  	"io"
     8  	"mime"
     9  	"mime/multipart"
    10  	"net/http"
    11  	"os"
    12  	"path"
    13  	"time"
    14  
    15  	"github.com/thought-machine/please/src/core"
    16  	"github.com/thought-machine/please/src/fs"
    17  )
    18  
    19  type httpCache struct {
    20  	URL       string
    21  	Writeable bool
    22  	Timeout   time.Duration
    23  }
    24  
    25  func (cache *httpCache) Store(target *core.BuildTarget, key []byte, files ...string) {
    26  	// TODO(pebers): Change this to upload using multipart, it's quite slow doing every file separately
    27  	//               for targets with many files.
    28  	if cache.Writeable {
    29  		for _, out := range cacheArtifacts(target, files...) {
    30  			if info, err := os.Stat(out); err == nil && info.IsDir() {
    31  				fs.Walk(out, func(name string, isDir bool) error {
    32  					if !isDir {
    33  						cache.StoreExtra(target, key, name)
    34  					}
    35  					return nil
    36  				})
    37  			} else {
    38  				cache.StoreExtra(target, key, out)
    39  			}
    40  		}
    41  	}
    42  }
    43  
    44  func (cache *httpCache) StoreExtra(target *core.BuildTarget, key []byte, file string) {
    45  	if cache.Writeable {
    46  		artifact := path.Join(
    47  			core.OsArch,
    48  			target.Label.PackageName,
    49  			target.Label.Name,
    50  			base64.RawURLEncoding.EncodeToString(key),
    51  			file,
    52  		)
    53  		log.Info("Storing %s: %s in http cache...", target.Label, artifact)
    54  
    55  		// NB. Don't need to close this file, http.Post will do it for us.
    56  		file, err := os.Open(path.Join(target.OutDir(), file))
    57  		if err != nil {
    58  			log.Warning("Failed to read artifact: %s", err)
    59  			return
    60  		}
    61  		response, err := http.Post(cache.URL+"/artifact/"+artifact, "application/octet-stream", file)
    62  		if err != nil {
    63  			log.Warning("Failed to send artifact to %s: %s", cache.URL+"/artifact/"+artifact, err)
    64  			return
    65  		} else if response.StatusCode < 200 || response.StatusCode > 299 {
    66  			log.Warning("Failed to send artifact to %s: got response %s", cache.URL+"/artifact/"+artifact, response.Status)
    67  		}
    68  		response.Body.Close()
    69  	}
    70  }
    71  
    72  func (cache *httpCache) Retrieve(target *core.BuildTarget, key []byte) bool {
    73  	// We can't tell from outside if this works or not (as we can for the dir cache)
    74  	// so we must assume that a target with no artifacts can't be retrieved. It's a weird
    75  	// case but a test already exists in the plz test suite so...
    76  	retrieved := false
    77  	for _, out := range cacheArtifacts(target) {
    78  		if !cache.RetrieveExtra(target, key, out) {
    79  			return false
    80  		}
    81  		retrieved = true
    82  	}
    83  	return retrieved
    84  }
    85  
    86  func (cache *httpCache) RetrieveExtra(target *core.BuildTarget, key []byte, file string) bool {
    87  	log.Debug("Retrieving %s:%s from http cache...", target.Label, file)
    88  
    89  	artifact := path.Join(
    90  		core.OsArch,
    91  		target.Label.PackageName,
    92  		target.Label.Name,
    93  		base64.RawURLEncoding.EncodeToString(key),
    94  		file,
    95  	)
    96  
    97  	response, err := http.Get(cache.URL + "/artifact/" + artifact)
    98  	if err != nil {
    99  		return false
   100  	}
   101  	defer response.Body.Close()
   102  	if response.StatusCode == 404 {
   103  		return false
   104  	} else if response.StatusCode < 200 || response.StatusCode > 299 {
   105  		log.Warning("Error %d from http cache", response.StatusCode)
   106  		return false
   107  	} else if response.Header.Get("Content-Type") == "application/octet-stream" {
   108  		// Single artifact
   109  		return cache.writeFile(target, file, response.Body)
   110  	} else if _, params, err := mime.ParseMediaType(response.Header.Get("Content-Type")); err != nil {
   111  		log.Warning("Couldn't parse response: %s", err)
   112  		return false
   113  	} else {
   114  		// Directory, comes back in multipart
   115  		mr := multipart.NewReader(response.Body, params["boundary"])
   116  		for {
   117  			if part, err := mr.NextPart(); err == io.EOF {
   118  				return true
   119  			} else if err != nil {
   120  				log.Warning("Error reading multipart response: %s", err)
   121  				return false
   122  			} else if !cache.writeFile(target, part.FileName(), part) {
   123  				return false
   124  			}
   125  		}
   126  	}
   127  }
   128  
   129  func (cache *httpCache) writeFile(target *core.BuildTarget, file string, r io.Reader) bool {
   130  	outFile := path.Join(target.OutDir(), file)
   131  	if err := os.MkdirAll(path.Dir(outFile), core.DirPermissions); err != nil {
   132  		log.Errorf("Failed to create directory: %s", err)
   133  		return false
   134  	}
   135  	f, err := os.OpenFile(outFile, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, target.OutMode())
   136  	if err != nil {
   137  		log.Errorf("Failed to open file: %s", err)
   138  		return false
   139  	}
   140  	defer f.Close()
   141  	if _, err := io.Copy(f, r); err != nil {
   142  		log.Errorf("Failed to write file: %s", err)
   143  		return false
   144  	}
   145  	log.Info("Retrieved %s from http cache", target.Label)
   146  	return true
   147  }
   148  
   149  func (cache *httpCache) Clean(target *core.BuildTarget) {
   150  	var reader io.Reader
   151  	artifact := path.Join(
   152  		core.OsArch,
   153  		target.Label.PackageName,
   154  		target.Label.Name,
   155  	)
   156  	req, _ := http.NewRequest("DELETE", cache.URL+"/artifact/"+artifact, reader)
   157  	response, err := http.DefaultClient.Do(req)
   158  	if err != nil {
   159  		log.Warning("Failed to remove artifacts for %s from http cache: %s", target.Label, err)
   160  	}
   161  	response.Body.Close()
   162  }
   163  
   164  func (cache *httpCache) CleanAll() {
   165  	req, _ := http.NewRequest("DELETE", cache.URL, nil)
   166  	if _, err := http.DefaultClient.Do(req); err != nil {
   167  		log.Warning("Failed to remove artifacts from http cache: %s", err)
   168  	}
   169  }
   170  
   171  func (cache *httpCache) Shutdown() {}
   172  
   173  func newHTTPCache(config *core.Configuration) *httpCache {
   174  	return &httpCache{
   175  		URL:       config.Cache.HTTPURL.String(),
   176  		Writeable: config.Cache.HTTPWriteable,
   177  		Timeout:   time.Duration(config.Cache.HTTPTimeout),
   178  	}
   179  }