fup/fuphttp: split download logic into a separate file.
It's neatly partitionable away from the rest of the logic, so it's easier to put it in a separate file.
This commit is contained in:
parent
4c4bd46aa8
commit
ad67e1025a
2 changed files with 124 additions and 108 deletions
|
@ -7,7 +7,6 @@ package fuphttp
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -19,7 +18,6 @@ import (
|
||||||
shuncheckedconversions "github.com/google/safehtml/uncheckedconversions"
|
shuncheckedconversions "github.com/google/safehtml/uncheckedconversions"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"gocloud.dev/blob"
|
"gocloud.dev/blob"
|
||||||
"gocloud.dev/gcerrors"
|
|
||||||
"hg.lukegb.com/lukegb/depot/web/fup/hashfs"
|
"hg.lukegb.com/lukegb/depot/web/fup/hashfs"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -84,112 +82,6 @@ func (a *Application) internalError(rw http.ResponseWriter, r *http.Request) {
|
||||||
fmt.Fprintf(rw, "hammed server :(\n")
|
fmt.Fprintf(rw, "hammed server :(\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Application) rawDownload(rw http.ResponseWriter, r *http.Request) {
|
|
||||||
ctx := r.Context()
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
filename := vars["filename"]
|
|
||||||
|
|
||||||
meta, err := metadata(ctx, a.storageBackend, filename)
|
|
||||||
switch errorCode(err) {
|
|
||||||
case gcerrors.NotFound:
|
|
||||||
a.notFound(rw, r)
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
log.Printf("rawDownload(%q) metadata: %v", filename, err)
|
|
||||||
a.internalError(rw, r)
|
|
||||||
return
|
|
||||||
case gcerrors.OK:
|
|
||||||
// OK
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do things with attributes?
|
|
||||||
if a.redirectToBlobstore {
|
|
||||||
redirectExpiry := a.redirectExpiry
|
|
||||||
if !meta.ExpiresAt.IsZero() {
|
|
||||||
// Cap the redirect lifetime to the remaining lifetime of the object.
|
|
||||||
remainingLifetime := meta.ExpiresAt.Sub(time.Now())
|
|
||||||
if remainingLifetime < redirectExpiry {
|
|
||||||
redirectExpiry = remainingLifetime
|
|
||||||
}
|
|
||||||
}
|
|
||||||
u, err := a.storageBackend.SignedURL(ctx, filename, &blob.SignedURLOptions{
|
|
||||||
Expiry: redirectExpiry,
|
|
||||||
})
|
|
||||||
switch errorCode(err) {
|
|
||||||
case gcerrors.NotFound:
|
|
||||||
// This is unlikely to get returned.
|
|
||||||
a.notFound(rw, r)
|
|
||||||
return
|
|
||||||
case gcerrors.OK:
|
|
||||||
http.Redirect(rw, r, u, http.StatusFound)
|
|
||||||
return
|
|
||||||
case gcerrors.Unimplemented:
|
|
||||||
// Fall back to serving directly.
|
|
||||||
default:
|
|
||||||
log.Printf("rawDownload(%q) SignedURL (continuing with fallback): %v", filename, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Perform direct serving.
|
|
||||||
a.rawDownloadDirect(ctx, rw, r, filename, meta)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 errorCode(err) {
|
|
||||||
case gcerrors.NotFound:
|
|
||||||
a.notFound(rw, r)
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
a.internalError(rw, r)
|
|
||||||
return
|
|
||||||
case gcerrors.OK:
|
|
||||||
// OK
|
|
||||||
}
|
|
||||||
defer rdr.Close()
|
|
||||||
|
|
||||||
attrs := meta.Attributes
|
|
||||||
if v := attrs.CacheControl; v != "" {
|
|
||||||
rw.Header().Set("Cache-Control", v)
|
|
||||||
}
|
|
||||||
if v := attrs.ContentDisposition; v != "" {
|
|
||||||
rw.Header().Set("Content-Disposition", v)
|
|
||||||
}
|
|
||||||
if v := attrs.ContentEncoding; v != "" {
|
|
||||||
rw.Header().Set("Content-Encoding", v)
|
|
||||||
}
|
|
||||||
if v := attrs.ContentLanguage; v != "" {
|
|
||||||
rw.Header().Set("Content-Language", v)
|
|
||||||
}
|
|
||||||
if v := attrs.ContentType; v != "" {
|
|
||||||
rw.Header().Set("Content-Type", v)
|
|
||||||
}
|
|
||||||
if v := attrs.ETag; v != "" {
|
|
||||||
rw.Header().Set("ETag", v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we're serving from local disk, use http.ServeContent, using this somewhat opaque method.
|
|
||||||
var ior io.Reader
|
|
||||||
if rdr.As(&ior) {
|
|
||||||
if iors, ok := ior.(io.ReadSeeker); ok {
|
|
||||||
http.ServeContent(rw, r, filename, attrs.ModTime, iors)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if v := attrs.ModTime; !v.IsZero() {
|
|
||||||
rw.Header().Set("Last-Modified", v.UTC().Format(http.TimeFormat))
|
|
||||||
}
|
|
||||||
if v := attrs.Size; v != 0 {
|
|
||||||
rw.Header().Set("Content-Length", fmt.Sprintf("%d", v))
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := io.Copy(rw, rdr); err != nil {
|
|
||||||
log.Printf("rawDownloadDirect(%q) copy: %v", filename, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseTemplate(t *template.Template, fsys fs.FS, name string) (*template.Template, error) {
|
func parseTemplate(t *template.Template, fsys fs.FS, name string) (*template.Template, error) {
|
||||||
bs, err := fs.ReadFile(fsys, name)
|
bs, err := fs.ReadFile(fsys, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
124
web/fup/fuphttp/httpdownload.go
Normal file
124
web/fup/fuphttp/httpdownload.go
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Luke Granger-Brown <depot@lukegb.com>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package fuphttp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"gocloud.dev/blob"
|
||||||
|
"gocloud.dev/gcerrors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (a *Application) rawDownload(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
filename := vars["filename"]
|
||||||
|
|
||||||
|
meta, err := metadata(ctx, a.storageBackend, filename)
|
||||||
|
switch errorCode(err) {
|
||||||
|
case gcerrors.NotFound:
|
||||||
|
a.notFound(rw, r)
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
log.Printf("rawDownload(%q) metadata: %v", filename, err)
|
||||||
|
a.internalError(rw, r)
|
||||||
|
return
|
||||||
|
case gcerrors.OK:
|
||||||
|
// OK
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do things with attributes?
|
||||||
|
if a.redirectToBlobstore {
|
||||||
|
redirectExpiry := a.redirectExpiry
|
||||||
|
if !meta.ExpiresAt.IsZero() {
|
||||||
|
// Cap the redirect lifetime to the remaining lifetime of the object.
|
||||||
|
remainingLifetime := meta.ExpiresAt.Sub(time.Now())
|
||||||
|
if remainingLifetime < redirectExpiry {
|
||||||
|
redirectExpiry = remainingLifetime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
u, err := a.storageBackend.SignedURL(ctx, filename, &blob.SignedURLOptions{
|
||||||
|
Expiry: redirectExpiry,
|
||||||
|
})
|
||||||
|
switch errorCode(err) {
|
||||||
|
case gcerrors.NotFound:
|
||||||
|
// This is unlikely to get returned.
|
||||||
|
a.notFound(rw, r)
|
||||||
|
return
|
||||||
|
case gcerrors.OK:
|
||||||
|
http.Redirect(rw, r, u, http.StatusFound)
|
||||||
|
return
|
||||||
|
case gcerrors.Unimplemented:
|
||||||
|
// Fall back to serving directly.
|
||||||
|
default:
|
||||||
|
log.Printf("rawDownload(%q) SignedURL (continuing with fallback): %v", filename, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform direct serving.
|
||||||
|
a.rawDownloadDirect(ctx, rw, r, filename, meta)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 errorCode(err) {
|
||||||
|
case gcerrors.NotFound:
|
||||||
|
a.notFound(rw, r)
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
a.internalError(rw, r)
|
||||||
|
return
|
||||||
|
case gcerrors.OK:
|
||||||
|
// OK
|
||||||
|
}
|
||||||
|
defer rdr.Close()
|
||||||
|
|
||||||
|
attrs := meta.Attributes
|
||||||
|
if v := attrs.CacheControl; v != "" {
|
||||||
|
rw.Header().Set("Cache-Control", v)
|
||||||
|
}
|
||||||
|
if v := attrs.ContentDisposition; v != "" {
|
||||||
|
rw.Header().Set("Content-Disposition", v)
|
||||||
|
}
|
||||||
|
if v := attrs.ContentEncoding; v != "" {
|
||||||
|
rw.Header().Set("Content-Encoding", v)
|
||||||
|
}
|
||||||
|
if v := attrs.ContentLanguage; v != "" {
|
||||||
|
rw.Header().Set("Content-Language", v)
|
||||||
|
}
|
||||||
|
if v := attrs.ContentType; v != "" {
|
||||||
|
rw.Header().Set("Content-Type", v)
|
||||||
|
}
|
||||||
|
if v := attrs.ETag; v != "" {
|
||||||
|
rw.Header().Set("ETag", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're serving from local disk, use http.ServeContent, using this somewhat opaque method.
|
||||||
|
var ior io.Reader
|
||||||
|
if rdr.As(&ior) {
|
||||||
|
if iors, ok := ior.(io.ReadSeeker); ok {
|
||||||
|
http.ServeContent(rw, r, filename, attrs.ModTime, iors)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if v := attrs.ModTime; !v.IsZero() {
|
||||||
|
rw.Header().Set("Last-Modified", v.UTC().Format(http.TimeFormat))
|
||||||
|
}
|
||||||
|
if v := attrs.Size; v != 0 {
|
||||||
|
rw.Header().Set("Content-Length", fmt.Sprintf("%d", v))
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := io.Copy(rw, rdr); err != nil {
|
||||||
|
log.Printf("rawDownloadDirect(%q) copy: %v", filename, err)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue