From 4c4bd46aa80fff0e840efa8786e89bef2aa5d7db Mon Sep 17 00:00:00 2001 From: Luke Granger-Brown Date: Sun, 21 Mar 2021 15:17:46 +0000 Subject: [PATCH] fup: factor out metadata retrieval into a separate file We'll need this for cleanup operations as well. This should likely be factored out again into an entirely separate package that deals with storage access. --- web/fup/fuphttp/errors.go | 42 ++++++++++++++++++++++++ web/fup/fuphttp/fuphttp.go | 47 ++++++--------------------- web/fup/fuphttp/metadata.go | 65 +++++++++++++++++++++++++++++++++++++ 3 files changed, 117 insertions(+), 37 deletions(-) create mode 100644 web/fup/fuphttp/errors.go create mode 100644 web/fup/fuphttp/metadata.go diff --git a/web/fup/fuphttp/errors.go b/web/fup/fuphttp/errors.go new file mode 100644 index 0000000000..59da066eee --- /dev/null +++ b/web/fup/fuphttp/errors.go @@ -0,0 +1,42 @@ +package fuphttp + +import ( + "errors" + "fmt" + + "gocloud.dev/gcerrors" +) + +type fupError struct { + Code gcerrors.ErrorCode + msg string + err error +} + +func (e *fupError) Error() string { + var msg string + if e.msg == "" { + msg = fmt.Sprintf("code=%v", e.Code) + } else { + msg = fmt.Sprintf("%s (code=%v)", e.msg, e.Code) + } + if e.err != nil { + msg = fmt.Sprintf("%s: %s", msg, e.err) + } + return msg +} + +func (e *fupError) Unwrap() error { + return e.err +} + +func errorCode(err error) gcerrors.ErrorCode { + if err == nil { + return gcerrors.OK + } + var e *fupError + if errors.As(err, &e) { + return e.Code + } + return gcerrors.Code(err) +} diff --git a/web/fup/fuphttp/fuphttp.go b/web/fup/fuphttp/fuphttp.go index 05f2be1439..04a2d99671 100644 --- a/web/fup/fuphttp/fuphttp.go +++ b/web/fup/fuphttp/fuphttp.go @@ -11,7 +11,6 @@ import ( "io/fs" "log" "net/http" - "strconv" "time" "github.com/google/safehtml" @@ -90,52 +89,25 @@ func (a *Application) rawDownload(rw http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) filename := vars["filename"] - attrs, err := a.storageBackend.Attributes(ctx, filename) - switch gcerrors.Code(err) { + meta, err := metadata(ctx, a.storageBackend, filename) + switch errorCode(err) { case gcerrors.NotFound: a.notFound(rw, r) return default: - log.Printf("rawDownload(%q) Attributes: %v", filename, err) + log.Printf("rawDownload(%q) metadata: %v", filename, err) a.internalError(rw, r) return case gcerrors.OK: // OK } - var exp time.Time - if expStr, ok := attrs.Metadata["expires-at"]; ok { - // Check for expiry. - expSec, err := strconv.ParseInt(expStr, 10, 64) - if err != nil { - log.Printf("parsing expiration %q for %q: %v", expStr, filename, err) - a.internalError(rw, r) - return - } - - exp = time.Unix(expSec, 0) - if exp.Before(time.Now()) { - // This file has expired. Delete it from the backing storage. - err := a.storageBackend.Delete(ctx, filename) - switch gcerrors.Code(err) { - case gcerrors.NotFound: - // Something probably deleted it before we could get there. - case gcerrors.OK: - // This was fine. - default: - log.Printf("deleting expired file %q: %v", filename, err) - } - a.notFound(rw, r) - return - } - } - // Do things with attributes? if a.redirectToBlobstore { redirectExpiry := a.redirectExpiry - if !exp.IsZero() { + if !meta.ExpiresAt.IsZero() { // Cap the redirect lifetime to the remaining lifetime of the object. - remainingLifetime := exp.Sub(time.Now()) + remainingLifetime := meta.ExpiresAt.Sub(time.Now()) if remainingLifetime < redirectExpiry { redirectExpiry = remainingLifetime } @@ -143,7 +115,7 @@ func (a *Application) rawDownload(rw http.ResponseWriter, r *http.Request) { u, err := a.storageBackend.SignedURL(ctx, filename, &blob.SignedURLOptions{ Expiry: redirectExpiry, }) - switch gcerrors.Code(err) { + switch errorCode(err) { case gcerrors.NotFound: // This is unlikely to get returned. a.notFound(rw, r) @@ -159,13 +131,13 @@ func (a *Application) rawDownload(rw http.ResponseWriter, r *http.Request) { } // Perform direct serving. - a.rawDownloadDirect(ctx, rw, r, filename, attrs) + a.rawDownloadDirect(ctx, rw, r, filename, meta) } -func (a *Application) rawDownloadDirect(ctx context.Context, rw http.ResponseWriter, r *http.Request, filename string, attrs *blob.Attributes) { +func (a *Application) rawDownloadDirect(ctx context.Context, rw http.ResponseWriter, r *http.Request, filename string, meta *Metadata) { // TODO(lukegb): Range header and conditionals?. rdr, err := a.storageBackend.NewReader(ctx, filename, nil) - switch gcerrors.Code(err) { + switch errorCode(err) { case gcerrors.NotFound: a.notFound(rw, r) return @@ -177,6 +149,7 @@ func (a *Application) rawDownloadDirect(ctx context.Context, rw http.ResponseWri } defer rdr.Close() + attrs := meta.Attributes if v := attrs.CacheControl; v != "" { rw.Header().Set("Cache-Control", v) } diff --git a/web/fup/fuphttp/metadata.go b/web/fup/fuphttp/metadata.go new file mode 100644 index 0000000000..bc404c3f86 --- /dev/null +++ b/web/fup/fuphttp/metadata.go @@ -0,0 +1,65 @@ +package fuphttp + +import ( + "context" + "fmt" + "log" + "strconv" + "time" + + "gocloud.dev/blob" + "gocloud.dev/gcerrors" +) + +type Metadata struct { + // ExpiresAt is the expiry time of the object. + // May be the zero time, if this object does not expire. + // Stored in expires-at. + ExpiresAt time.Time + + // Attributes is the underlying gocloud Attributes. + Attributes *blob.Attributes +} + +// metadata retrieves the Metadata for the object. +// Note: if the object is expired, it will delete it. +func metadata(ctx context.Context, bucket *blob.Bucket, filename string) (*Metadata, error) { + attrs, err := bucket.Attributes(ctx, filename) + if err != nil { + return nil, fmt.Errorf("Attributes(%q): %w", filename, err) + } + + m := Metadata{ + Attributes: attrs, + } + + var exp time.Time + if expStr, ok := attrs.Metadata["expires-at"]; ok { + // Check for expiry. + expSec, err := strconv.ParseInt(expStr, 10, 64) + if err != nil { + return nil, fmt.Errorf("parsing expiration %q for %q: %v", expStr, filename, err) + } + + exp = time.Unix(expSec, 0) + if exp.Before(time.Now()) { + // This file has expired. Delete it from the backing storage. + err := bucket.Delete(ctx, filename) + switch errorCode(err) { + case gcerrors.NotFound: + // Something probably deleted it before we could get there. + case gcerrors.OK: + // This was fine. + default: + log.Printf("deleting expired file %q: %v", filename, err) + } + return nil, &fupError{ + Code: gcerrors.NotFound, + } + } + + m.ExpiresAt = exp + } + + return &m, nil +}