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:
parent
0e69199569
commit
7bac0aee74
2 changed files with 75 additions and 2 deletions
|
@ -11,6 +11,7 @@ import (
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/safehtml"
|
"github.com/google/safehtml"
|
||||||
|
@ -34,7 +35,10 @@ type Config struct {
|
||||||
|
|
||||||
// If set, redirects to a signed URL if possible instead of serving directly.
|
// If set, redirects to a signed URL if possible instead of serving directly.
|
||||||
RedirectToBlobstore bool
|
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.
|
// Set one of these, but not both.
|
||||||
StorageURL string
|
StorageURL string
|
||||||
|
@ -99,10 +103,45 @@ func (a *Application) rawDownload(rw http.ResponseWriter, r *http.Request) {
|
||||||
// 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?
|
// Do things with attributes?
|
||||||
if a.redirectToBlobstore {
|
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{
|
u, err := a.storageBackend.SignedURL(ctx, filename, &blob.SignedURLOptions{
|
||||||
Expiry: a.redirectExpiry,
|
Expiry: redirectExpiry,
|
||||||
})
|
})
|
||||||
switch gcerrors.Code(err) {
|
switch gcerrors.Code(err) {
|
||||||
case gcerrors.NotFound:
|
case gcerrors.NotFound:
|
||||||
|
|
|
@ -97,6 +97,14 @@ func TestRetrieve(t *testing.T) {
|
||||||
if err := tc.backend.WriteAll(ctx, "hello.txt", []byte("hello world\n"), nil); err != nil {
|
if err := tc.backend.WriteAll(ctx, "hello.txt", []byte("hello world\n"), nil); err != nil {
|
||||||
t.Fatalf("WriteAll: %v", err)
|
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)
|
a, err := fuphttp.New(ctx, &ccfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -177,6 +185,32 @@ func TestRetrieve(t *testing.T) {
|
||||||
t.Errorf("read data was %q; want %q", got, want)
|
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")
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue