depot/web/fup/fuphttp/httpdownload.go
Luke Granger-Brown ad67e1025a 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.
2021-03-21 15:23:43 +00:00

124 lines
3 KiB
Go

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