mirror of https://github.com/perkeep/perkeep.git
server/camlistored: add inline query parameter, and set Content-Disposition
The download handler was setting the Content-Disposition to attachment when the MIME type was "application/octet-stream". That means when e.g. a plain text file (and probably others) is (incorrectly) detected as "application/octet-stream", it is saved to disk instead of being opened as text by the browser, when using the "View Original" button of the web UI. Conversely, when using the "Download" button of the web UI, the file should always be saved to disk, and never be opened by the browser. Therefore, we add the ?inline request parameter to the downloadhandler GET handler to unambiguously request whether the response should have an inline or an attachment Content-Disposition. The View Original button action now always requests an inlined response. The Download button action now detects whether the selection was only about one file, and if yes it requests the file as an attachment (instead of using the POST handler that zips all the requested files). Change-Id: I4bbc42f17fb4ea0aff9e17056e80156611d4b419
This commit is contained in:
parent
ed07089172
commit
3d519b2773
|
@ -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"
|
||||
|
@ -265,8 +267,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)
|
||||
|
@ -315,23 +319,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 {
|
||||
|
@ -351,6 +371,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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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',
|
||||
|
|
Loading…
Reference in New Issue