mirror of https://github.com/perkeep/perkeep.git
203 lines
5.5 KiB
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)
|
|
}
|