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.
This commit is contained in:
Luke Granger-Brown 2021-03-21 15:17:46 +00:00
parent 7bac0aee74
commit 4c4bd46aa8
3 changed files with 117 additions and 37 deletions

42
web/fup/fuphttp/errors.go Normal file
View file

@ -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)
}

View file

@ -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)
}

View file

@ -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
}