perkeep/pkg/server/download.go

203 lines
5.5 KiB
Go

/*
Copyright 2011 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package server
import (
"fmt"
"io"
"log"
"net/http"
"os"
"strings"
"time"
"camlistore.org/pkg/blob"
"camlistore.org/pkg/blobserver"
"camlistore.org/pkg/magic"
"camlistore.org/pkg/schema"
"camlistore.org/pkg/search"
"go4.org/readerutil"
"golang.org/x/net/context"
)
const oneYear = 365 * 86400 * time.Second
var debugPack = strings.Contains(os.Getenv("CAMLI_DEBUG_X"), "packserve")
type DownloadHandler struct {
Fetcher blob.Fetcher
Cache blobserver.Storage
// Search is optional. If present, it's used to map a fileref
// to a wholeref, if the Fetcher is of a type that knows how
// to get at a wholeref more efficiently. (e.g. blobpacked)
Search *search.Handler
ForceMIME string // optional
}
func (dh *DownloadHandler) blobSource() blob.Fetcher {
return dh.Fetcher // TODO: use dh.Cache
}
type fileInfo struct {
mime string
name string
size int64
rs io.ReadSeeker
close func() error // release the rs
whyNot string // for testing, why fileInfoPacked failed.
}
func (dh *DownloadHandler) fileInfo(req *http.Request, file blob.Ref) (fi fileInfo, packed bool, err error) {
ctx := context.TODO()
// Fast path for blobpacked.
fi, ok := fileInfoPacked(ctx, dh.Search, dh.Fetcher, req, file)
if debugPack {
log.Printf("download.go: fileInfoPacked: ok=%v, %+v", ok, fi)
}
if ok {
return fi, true, nil
}
fr, err := schema.NewFileReader(dh.blobSource(), file)
if err != nil {
return
}
mime := dh.ForceMIME
if mime == "" {
mime = magic.MIMETypeFromReaderAt(fr)
}
if mime == "" {
mime = "application/octet-stream"
}
return fileInfo{
mime: mime,
name: fr.FileName(),
size: fr.Size(),
rs: fr,
close: fr.Close,
}, false, nil
}
// Fast path for blobpacked.
func fileInfoPacked(ctx context.Context, sh *search.Handler, src blob.Fetcher, req *http.Request, file blob.Ref) (packFileInfo fileInfo, ok bool) {
if sh == nil {
return fileInfo{whyNot: "no search"}, false
}
wf, ok := src.(blobserver.WholeRefFetcher)
if !ok {
return fileInfo{whyNot: "fetcher type"}, false
}
if req != nil && req.Header.Get("Range") != "" {
// TODO: not handled yet. Maybe not even important,
// considering rarity.
return fileInfo{whyNot: "range header"}, false
}
des, err := sh.Describe(ctx, &search.DescribeRequest{BlobRef: file})
if err != nil {
log.Printf("ui: fileInfoPacked: skipping fast path due to error from search: %v", err)
return fileInfo{whyNot: "search error"}, false
}
db, ok := des.Meta[file.String()]
if !ok || db.File == nil {
return fileInfo{whyNot: "search index doesn't know file"}, false
}
fi := db.File
if !fi.WholeRef.Valid() {
return fileInfo{whyNot: "no wholeref from search index"}, false
}
offset := int64(0)
rc, wholeSize, err := wf.OpenWholeRef(fi.WholeRef, offset)
if err == os.ErrNotExist {
return fileInfo{whyNot: "WholeRefFetcher returned ErrNotexist"}, false
}
if wholeSize != fi.Size {
log.Printf("ui: fileInfoPacked: OpenWholeRef size %d != index size %d; ignoring fast path", wholeSize, fi.Size)
return fileInfo{whyNot: "WholeRefFetcher and index don't agree"}, false
}
if err != nil {
log.Printf("ui: fileInfoPacked: skipping fast path due to error from WholeRefFetcher (%T): %v", src, err)
return fileInfo{whyNot: "WholeRefFetcher error"}, false
}
return fileInfo{
mime: fi.MIMEType,
name: fi.FileName,
size: fi.Size,
rs: readerutil.NewFakeSeeker(rc, fi.Size-offset),
close: rc.Close,
}, true
}
func (dh *DownloadHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request, file blob.Ref) {
if req.Method != "GET" && req.Method != "HEAD" {
http.Error(rw, "Invalid download method", http.StatusBadRequest)
return
}
if req.Header.Get("If-Modified-Since") != "" {
// Immutable, so any copy's a good copy.
rw.WriteHeader(http.StatusNotModified)
return
}
fi, packed, err := dh.fileInfo(req, file)
if err != nil {
http.Error(rw, "Can't serve file: "+err.Error(), http.StatusInternalServerError)
return
}
defer fi.close()
h := rw.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"
}
rw.Header().Set("Content-Disposition", "attachment; filename="+fileName)
}
if req.Method == "HEAD" && req.FormValue("verifycontents") != "" {
vbr, ok := blob.Parse(req.FormValue("verifycontents"))
if !ok {
return
}
hash := vbr.Hash()
if hash == nil {
return
}
io.Copy(hash, fi.rs) // ignore errors, caught later
if vbr.HashMatches(hash) {
rw.Header().Set("X-Camli-Contents", vbr.String())
}
return
}
http.ServeContent(rw, req, "", time.Now(), fi.rs)
}