snapshot/store/http_store.go (155 lines of code) (raw):

package store import ( "errors" "fmt" "io" "io/ioutil" "net/http" "os" "strings" "time" "github.com/sethgrid/pester" log "github.com/sirupsen/logrus" ) const DefaultHttpTries = 7 // ~2min total of trying with exponential backoff (0 and 1 both mean 1 try total) func MakePesterClient() *pester.Client { client := pester.New() client.Backoff = pester.ExponentialBackoff client.MaxRetries = DefaultHttpTries client.LogHook = func(e pester.ErrEntry) { log.Errorf("Retrying after failed attempt: %+v", e) } return client } func MakeHTTPStore(rootURI string) Store { return MakeCustomHTTPStore(rootURI, MakePesterClient(), &TTLConfig{TTL: DefaultTTL, TTLKey: DefaultTTLKey, TTLFormat: DefaultTTLFormat}) } func MakeCustomHTTPStore(rootURI string, client Client, ttlc *TTLConfig) Store { if !strings.HasSuffix(rootURI, "/") { rootURI = rootURI + "/" } if ttlc == nil { ttlc = &TTLConfig{TTLKey: DefaultTTLKey, TTLFormat: DefaultTTLFormat} } log.Infof("Making new HTTP Store with root URI: %s", rootURI) return &httpStore{rootURI, client, *ttlc} } type Client interface { Do(req *http.Request) (resp *http.Response, err error) } type httpStore struct { rootURI string client Client ttlc TTLConfig } func (s *httpStore) getTTLValue(resp *http.Response) *TTLValue { expire := resp.Header.Get(s.ttlc.TTLKey) ttl, err := time.Parse(s.ttlc.TTLFormat, expire) if err != nil { return nil } return &TTLValue{TTL: ttl, TTLKey: s.ttlc.TTLKey} } func (s *httpStore) OpenForRead(name string) (*Resource, error) { return s.openForRead(name, false) } func (s *httpStore) openForRead(name string, existCheck bool) (*Resource, error) { label := "Read" if existCheck { label = "Exist" } uri := s.rootURI + name log.Infof("%sing %s", label, uri) var req *http.Request if existCheck { req, _ = http.NewRequest("HEAD", uri, nil) } else { req, _ = http.NewRequest("GET", uri, nil) } resp, err := s.client.Do(req) if err != nil { if !existCheck { log.Infof("%s error: %s %v", label, uri, err) } return nil, err } if resp.StatusCode == http.StatusOK { log.Infof("%s result %s %v", label, uri, resp.StatusCode) ttlv := s.getTTLValue(resp) var rc io.ReadCloser if existCheck { rc = ioutil.NopCloser(resp.Body) } else { rc = resp.Body } return NewResource(rc, resp.ContentLength, ttlv), nil } log.Errorf("%s response status error: %s %v", label, uri, resp.Status) if !existCheck { resp.Body.Close() } if resp.StatusCode == http.StatusNotFound { return nil, os.ErrNotExist } else if resp.StatusCode == http.StatusBadRequest { return nil, os.ErrInvalid } return nil, fmt.Errorf("could not open: %+v", resp) } func (s *httpStore) Exists(name string) (bool, error) { r, err := s.openForRead(name, true) if err != nil { if os.IsNotExist(err) { return false, nil } log.Errorf("Exists error: %s %v", name, err) return false, err } log.Infof("Exists ok: %s", name) r.Close() if r.TTLValue != nil && r.TTLValue.TTL.Before(time.Now()) { return false, nil } return true, nil } func (s *httpStore) Write(name string, resource *Resource) error { if resource == nil { log.Info("Writing nil resource is a no op.") return nil } if strings.Contains(name, "/") { log.Errorf("Write error: %s '/' not allowed", name) return errors.New("'/' not allowed in name when writing bundles.") } uri := s.rootURI + name post := func() (*http.Response, error) { req, err := http.NewRequest("POST", uri, resource) if err != nil { return nil, err } req.ContentLength = resource.Length req.Header.Set("Content-Type", "text/plain") ttl := resource.TTLValue if ttl == nil { ttl = &TTLValue{TTL: time.Now().Add(s.ttlc.TTL), TTLKey: s.ttlc.TTLKey} } if ttl.TTLKey != "" { req.Header[ttl.TTLKey] = []string{ttl.TTL.Format(s.ttlc.TTLFormat)} } log.Infof("Writing %s: length: %d header: %v", uri, req.ContentLength, req.Header) return s.client.Do(req) } resp, err := post() if err != nil { log.Errorf("Write error: %s %v", uri, err) } else { resp.Body.Close() if resp.StatusCode != http.StatusOK { log.Errorf("Write response status error: %s -- %s", uri, resp.Status) return errors.New(resp.Status) } } return err } func (s *httpStore) Root() string { return s.rootURI }