Merge "server/camlistored: add inline query parameter, and set Content-Disposition"

This commit is contained in:
Mathieu Lonjaret 2018-02-20 22:30:36 +00:00 committed by Gerrit Code Review
commit 14758c0d1e
3 changed files with 56 additions and 12 deletions

View File

@ -18,6 +18,7 @@ package server
import (
"archive/zip"
"bytes"
"context"
"errors"
"fmt"
@ -30,6 +31,7 @@ import (
"regexp"
"strings"
"time"
"unicode/utf8"
"go4.org/readerutil"
"perkeep.org/internal/httputil"
@ -263,8 +265,10 @@ func fileInfoPacked(ctx context.Context, sh *search.Handler, src blob.Fetcher, r
// Creates a zip archive of the provided files and serves it in the response.
//
// GET:
// /<file-schema-blobref>
// /<file-schema-blobref>[?inline=1]
// Serves the file described by the requested file schema blobref.
// if inline=1 the Content-Disposition of the response is set to inline, and
// otherwise it set to attachment.
func (dh *DownloadHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method == "POST" {
dh.serveZip(w, r)
@ -314,23 +318,39 @@ func (dh *DownloadHandler) ServeFile(w http.ResponseWriter, r *http.Request, fil
h := w.Header()
h.Set("Content-Length", fmt.Sprint(fi.size))
h.Set("Expires", time.Now().Add(oneYear).Format(http.TimeFormat))
h.Set("Content-Type", fi.mime)
if packed {
h.Set("X-Camlistore-Packed", "1")
}
if fi.mime == "application/octet-stream" {
// Chrome seems to silently do nothing on
// application/octet-stream unless this is set.
// Maybe it's confused by lack of URL it recognizes
// along with lack of mime type?
fileName := fi.name
if fileName == "" {
fileName = "file-" + file.String() + ".dat"
fileName := func(ext string) string {
if fi.name != "" {
return fi.name
}
w.Header().Set("Content-Disposition", "attachment; filename="+fileName)
return "file-" + file.String() + ext
}
if r.FormValue("inline") == "1" {
// TODO(mpl): investigate why at least text files have an incorrect MIME.
if fi.mime == "application/octet-stream" {
// Since e.g. plain text files are seen as "application/octet-stream", we force
// check for that, so we can have the browser display them as text if they are
// indeed actually text.
text, err := isText(fi.rs)
if err != nil {
// TODO: https://perkeep.org/issues/1060
httputil.ServeError(w, r, fmt.Errorf("cannot verify MIME type of file: %v", err))
return
}
if text {
fi.mime = "text/plain"
}
}
h.Set("Content-Disposition", "inline")
} else {
w.Header().Set("Content-Disposition", "attachment; filename="+fileName(".dat"))
}
h.Set("Content-Type", fi.mime)
if r.Method == "HEAD" && r.FormValue("verifycontents") != "" {
vbr, ok := blob.Parse(r.FormValue("verifycontents"))
if !ok {
@ -350,6 +370,24 @@ func (dh *DownloadHandler) ServeFile(w http.ResponseWriter, r *http.Request, fil
http.ServeContent(w, r, "", time.Now(), fi.rs)
}
// isText reports whether the first MB read from rs is valid UTF-8 text.
func isText(rs io.ReadSeeker) (ok bool, err error) {
defer func() {
if _, seekErr := rs.Seek(0, io.SeekStart); seekErr != nil {
if err == nil {
err = seekErr
}
}
}()
var buf bytes.Buffer
if _, err := io.CopyN(&buf, rs, 1e6); err != nil {
if err != io.EOF {
return false, err
}
}
return utf8.Valid(buf.Bytes()), nil
}
// statFiles stats the given refs and returns an error if any one of them is not
// found.
// It is the responsibility of the caller to check that dh.Fetcher is a

View File

@ -135,6 +135,12 @@ func (d DownloadItemsBtnDef) downloadSelection() error {
fileRefs = append(fileRefs, ref.String())
}
if len(fileRefs) < 2 {
// Do not ask for a zip if we only want one file
dom.GetWindow().Open(fmt.Sprintf("%s/%s", downloadPrefix, fileRefs[0]), "", "")
return nil
}
el := dom.GetWindow().Document().CreateElement("input")
input := el.(*dom.HTMLInputElement)
input.Type = "text"

View File

@ -1097,7 +1097,7 @@ cam.IndexPage = React.createClass({
fileName = goog.string.subs('/%s', rm.file.fileName);
}
var downloadUrl = goog.string.subs('%s%s%s', this.props.config.downloadHelper, rm.blobRef, fileName);
var downloadUrl = goog.string.subs('%s%s%s?inline=1', this.props.config.downloadHelper, rm.blobRef, fileName);
return React.DOM.button(
{
key:'viewSelection',