fup: implement deleting expired files on request

This is a very basic implementation of gating that we only return files which
haven't expired.  If the file has expired, then we just delete it.
This commit is contained in:
Luke Granger-Brown 2021-03-21 14:56:42 +00:00
parent 0e69199569
commit 7bac0aee74
2 changed files with 75 additions and 2 deletions

View file

@ -11,6 +11,7 @@ import (
"io/fs"
"log"
"net/http"
"strconv"
"time"
"github.com/google/safehtml"
@ -34,7 +35,10 @@ type Config struct {
// If set, redirects to a signed URL if possible instead of serving directly.
RedirectToBlobstore bool
RedirectExpiry time.Duration // Defaults to 5 minutes.
// RedirectExpiry sets the maximum lifetime of a signed URL, if RedirectToBlobstore is in use and the backend supports signed URLs.
// Note that if a file has an expiry then the signed URL's validity will be capped at the expiry of the underlying file.
RedirectExpiry time.Duration // Defaults to 5 minutes.
// Set one of these, but not both.
StorageURL string
@ -99,10 +103,45 @@ func (a *Application) rawDownload(rw http.ResponseWriter, r *http.Request) {
// 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() {
// Cap the redirect lifetime to the remaining lifetime of the object.
remainingLifetime := exp.Sub(time.Now())
if remainingLifetime < redirectExpiry {
redirectExpiry = remainingLifetime
}
}
u, err := a.storageBackend.SignedURL(ctx, filename, &blob.SignedURLOptions{
Expiry: a.redirectExpiry,
Expiry: redirectExpiry,
})
switch gcerrors.Code(err) {
case gcerrors.NotFound:

View file

@ -97,6 +97,14 @@ func TestRetrieve(t *testing.T) {
if err := tc.backend.WriteAll(ctx, "hello.txt", []byte("hello world\n"), nil); err != nil {
t.Fatalf("WriteAll: %v", err)
}
if err := tc.backend.WriteAll(ctx, "expired.txt", []byte("hello world\n"), &blob.WriterOptions{
Metadata: map[string]string{
// This unix timestamp is very much in the past.
"expires-at": "1000",
},
}); err != nil {
t.Fatalf("WriteAll: %v", err)
}
a, err := fuphttp.New(ctx, &ccfg)
if err != nil {
@ -177,6 +185,32 @@ func TestRetrieve(t *testing.T) {
t.Errorf("read data was %q; want %q", got, want)
}
})
t.Run("expired file", func(t *testing.T) {
// Verify the file is there.
if ok, err := tc.backend.Exists(ctx, "expired.txt"); err != nil {
t.Fatalf("Exists(%q) before test: %v", "expired.txt", err)
} else if !ok {
t.Fatalf("Exists(%q) before test returned false", "expired.txt")
}
resp, err := s.Client().Get(fmt.Sprintf("%s/raw/expired.txt", s.URL))
if err != nil {
t.Fatalf("Get: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusNotFound {
t.Errorf("response status was %v; want %v", resp.StatusCode, http.StatusNotFound)
}
// Verify the file is NOT there.
if ok, err := tc.backend.Exists(ctx, "expired.txt"); err != nil {
t.Fatalf("Exists(%q) after retrieving: %v", "expired.txt", err)
} else if ok {
t.Fatalf("Exists(%q) after retrieving returned true", "expired.txt")
}
})
})
}
}