diff --git a/web/fup/fuphttp/fuphttp.go b/web/fup/fuphttp/fuphttp.go index 04a2d99671..b99f28b626 100644 --- a/web/fup/fuphttp/fuphttp.go +++ b/web/fup/fuphttp/fuphttp.go @@ -7,7 +7,6 @@ package fuphttp import ( "context" "fmt" - "io" "io/fs" "log" "net/http" @@ -19,7 +18,6 @@ import ( shuncheckedconversions "github.com/google/safehtml/uncheckedconversions" "github.com/gorilla/mux" "gocloud.dev/blob" - "gocloud.dev/gcerrors" "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") } -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) { bs, err := fs.ReadFile(fsys, name) if err != nil { diff --git a/web/fup/fuphttp/httpdownload.go b/web/fup/fuphttp/httpdownload.go new file mode 100644 index 0000000000..b0ec1e3c28 --- /dev/null +++ b/web/fup/fuphttp/httpdownload.go @@ -0,0 +1,124 @@ +// SPDX-FileCopyrightText: 2021 Luke Granger-Brown +// +// 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) + } +}