perkeep/pkg/search/handler.go

1408 lines
36 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 search
import (
"bytes"
"errors"
"fmt"
"log"
"net/http"
"net/url"
"os"
"sort"
"strconv"
"strings"
"sync"
"time"
"camlistore.org/pkg/blob"
"camlistore.org/pkg/blobserver"
"camlistore.org/pkg/httputil"
"camlistore.org/pkg/images"
"camlistore.org/pkg/jsonconfig"
"camlistore.org/pkg/types"
)
const buffered = 32 // arbitrary channel buffer size
const maxResults = 1000 // arbitrary limit on the number of search results returned
const defaultNumResults = 50
// MaxImageSize is the maximum width or height in pixels that we will serve image
// thumbnails at. It is used in the search result UI.
const MaxImageSize = 2000
func init() {
blobserver.RegisterHandlerConstructor("search", newHandlerFromConfig)
}
type Handler struct {
index Index
owner blob.Ref
}
// IGetRecentPermanodes is the interface encapsulating the GetRecentPermanodes query.
type IGetRecentPermanodes interface {
// GetRecentPermanodes returns recently-modified permanodes.
// This is a higher-level query returning more metadata than the index.GetRecentPermanodes,
// which only scans the blobrefs but doesn't return anything about the permanodes.
// TODO: rename this one?
GetRecentPermanodes(*RecentRequest) (*RecentResponse, error)
}
var (
_ IGetRecentPermanodes = (*Handler)(nil)
)
func NewHandler(index Index, owner blob.Ref) *Handler {
return &Handler{index: index, owner: owner}
}
func newHandlerFromConfig(ld blobserver.Loader, conf jsonconfig.Obj) (http.Handler, error) {
indexPrefix := conf.RequiredString("index") // TODO: add optional help tips here?
ownerBlobStr := conf.RequiredString("owner")
devBlockStartupPrefix := conf.OptionalString("devBlockStartupOn", "")
if err := conf.Validate(); err != nil {
return nil, err
}
if devBlockStartupPrefix != "" {
_, err := ld.GetHandler(devBlockStartupPrefix)
if err != nil {
return nil, fmt.Errorf("search handler references bogus devBlockStartupOn handler %s: %v", devBlockStartupPrefix, err)
}
}
indexHandler, err := ld.GetHandler(indexPrefix)
if err != nil {
return nil, fmt.Errorf("search config references unknown handler %q", indexPrefix)
}
indexer, ok := indexHandler.(Index)
if !ok {
return nil, fmt.Errorf("search config references invalid indexer %q (actually a %T)", indexPrefix, indexHandler)
}
ownerBlobRef, ok := blob.Parse(ownerBlobStr)
if !ok {
return nil, fmt.Errorf("search 'owner' has malformed blobref %q; expecting e.g. sha1-xxxxxxxxxxxx",
ownerBlobStr)
}
return &Handler{
index: indexer,
owner: ownerBlobRef,
}, nil
}
// TODO: figure out a plan for an owner having multiple active public keys, or public
// key rotation
func (h *Handler) Owner() blob.Ref {
return h.owner
}
func (h *Handler) Index() Index {
return h.index
}
func jsonMap() map[string]interface{} {
return make(map[string]interface{})
}
func jsonMapList() []map[string]interface{} {
return make([]map[string]interface{}, 0)
}
func (sh *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
ret := jsonMap()
suffix := httputil.PathSuffix(req)
if req.Method == "GET" {
switch suffix {
case "camli/search/recent":
sh.serveRecentPermanodes(rw, req)
return
case "camli/search/permanodeattr":
sh.servePermanodesWithAttr(rw, req)
return
case "camli/search/describe":
sh.serveDescribe(rw, req)
return
case "camli/search/claims":
sh.serveClaims(rw, req)
return
case "camli/search/files":
sh.serveFiles(rw, req)
return
case "camli/search/signerattrvalue":
sh.serveSignerAttrValue(rw, req)
return
case "camli/search/signerpaths":
sh.serveSignerPaths(rw, req)
return
case "camli/search/edgesto":
sh.serveEdgesTo(rw, req)
return
}
}
// TODO: discovery for the endpoints & better error message with link to discovery info
ret["error"] = "Unsupported search path or method"
ret["errorType"] = "input"
httputil.ReturnJSON(rw, ret)
}
// sanitizeNumResults takes n as a requested number of search results and sanitizes it.
func sanitizeNumResults(n int) int {
if n <= 0 || n > maxResults {
return defaultNumResults
}
return n
}
// RecentRequest is a request to get a RecentResponse.
type RecentRequest struct {
N int // if zero, default number of results
Before time.Time // if zero, now
ThumbnailSize int // if zero, no thumbnails
}
func (r *RecentRequest) URLSuffix() string {
// TODO: Before
return fmt.Sprintf("camli/search/recent?n=%d&thumbnails=%d", r.n(), r.thumbnailSize())
}
// fromHTTP panics with an httputil value on failure
func (r *RecentRequest) fromHTTP(req *http.Request) {
r.N, _ = strconv.Atoi(req.FormValue("n"))
r.ThumbnailSize = thumbnailSize(req)
// TODO: populate Before
}
// n returns the sanitized maximum number of search results.
func (r *RecentRequest) n() int {
return sanitizeNumResults(r.N)
}
func (r *RecentRequest) thumbnailSize() int {
v := r.ThumbnailSize
if v == 0 {
return 0
}
if v < minThumbSize || v > maxThumbSize {
return defThumbSize
}
return v
}
// WithAttrRequest is a request to get a WithAttrResponse.
type WithAttrRequest struct {
N int // max number of results
Signer blob.Ref // if nil, will use the server's default owner (if configured)
// Requested attribute. If blank, all attributes are searched (for Value)
// as fulltext.
Attr string
// Value of the requested attribute. If blank, permanodes which have
// request.Attr as an attribute are searched.
Value string
Fuzzy bool // fulltext search (if supported).
ThumbnailSize int // if zero, no thumbnails
}
func (r *WithAttrRequest) URLSuffix() string {
return fmt.Sprintf("camli/search/permanodeattr?signer=%v&value=%v&fuzzy=%v&attr=%v&max=%v&thumbnails=%v",
r.Signer, url.QueryEscape(r.Value), r.Fuzzy, r.Attr, r.N, r.ThumbnailSize)
}
// fromHTTP panics with an httputil value on failure
func (r *WithAttrRequest) fromHTTP(req *http.Request) {
r.Signer = blob.ParseOrZero(req.FormValue("signer"))
r.Value = req.FormValue("value")
fuzzy := req.FormValue("fuzzy") // exact match if empty
fuzzyMatch := false
if fuzzy != "" {
lowered := strings.ToLower(fuzzy)
if lowered == "true" || lowered == "t" {
fuzzyMatch = true
}
}
r.Attr = req.FormValue("attr") // all attributes if empty
if r.Attr == "" { // and force fuzzy in that case.
fuzzyMatch = true
}
r.Fuzzy = fuzzyMatch
r.ThumbnailSize = thumbnailSize(req)
max := req.FormValue("max")
if max != "" {
maxR, err := strconv.Atoi(max)
if err != nil {
panic(httputil.InvalidParameterError("max"))
}
r.N = maxR
}
r.N = r.n()
}
// n returns the sanitized maximum number of search results.
func (r *WithAttrRequest) n() int {
return sanitizeNumResults(r.N)
}
func (r *WithAttrRequest) thumbnailSize() int {
v := r.ThumbnailSize
if v == 0 {
return 0
}
if v < minThumbSize {
return minThumbSize
}
if v > maxThumbSize {
return maxThumbSize
}
return v
}
// ClaimsRequest is a request to get a ClaimsResponse.
type ClaimsRequest struct {
Permanode blob.Ref
}
// fromHTTP panics with an httputil value on failure
func (r *ClaimsRequest) fromHTTP(req *http.Request) {
r.Permanode = httputil.MustGetBlobRef(req, "permanode")
}
// SignerPathsRequest is a request to get a SignerPathsResponse.
type SignerPathsRequest struct {
Signer blob.Ref
Target blob.Ref
}
// fromHTTP panics with an httputil value on failure
func (r *SignerPathsRequest) fromHTTP(req *http.Request) {
r.Signer = httputil.MustGetBlobRef(req, "signer")
r.Target = httputil.MustGetBlobRef(req, "target")
}
// EdgesRequest is a request to get an EdgesResponse.
type EdgesRequest struct {
// The blob we want to find as a reference.
ToRef blob.Ref
}
// fromHTTP panics with an httputil value on failure
func (r *EdgesRequest) fromHTTP(req *http.Request) {
r.ToRef = httputil.MustGetBlobRef(req, "blobref")
}
// A MetaMap is a map from blobref to a DescribedBlob.
type MetaMap map[string]*DescribedBlob
func (m MetaMap) Get(br blob.Ref) *DescribedBlob {
if !br.Valid() {
return nil
}
return m[br.String()]
}
// RecentResponse is the JSON response from $searchRoot/camli/search/recent.
type RecentResponse struct {
Recent []*RecentItem `json:"recent"`
Meta MetaMap `json:"meta"`
Error string `json:"error,omitempty"`
ErrorType string `json:"errorType,omitempty"`
}
func (r *RecentResponse) Err() error {
if r.Error != "" || r.ErrorType != "" {
if r.ErrorType != "" {
return fmt.Errorf("%s: %s", r.ErrorType, r.Error)
}
return errors.New(r.Error)
}
return nil
}
// DescribeResponse is the JSON response from $searchRoot/camli/search/describe.
type DescribeResponse struct {
Meta MetaMap `json:"meta"`
}
// WithAttrResponse is the JSON response from $searchRoot/camli/search/permanodeattr.
type WithAttrResponse struct {
WithAttr []*WithAttrItem `json:"withAttr"`
Meta MetaMap `json:"meta"`
Error string `json:"error,omitempty"`
ErrorType string `json:"errorType,omitempty"`
}
func (r *WithAttrResponse) Err() error {
if r.Error != "" || r.ErrorType != "" {
if r.ErrorType != "" {
return fmt.Errorf("%s: %s", r.ErrorType, r.Error)
}
return errors.New(r.Error)
}
return nil
}
// ClaimsResponse is the JSON response from $searchRoot/camli/search/claims.
type ClaimsResponse struct {
Claims []*ClaimsItem `json:"claims"`
}
// SignerPathsResponse is the JSON response from $searchRoot/camli/search/signerpaths.
type SignerPathsResponse struct {
Paths []*SignerPathsItem `json:"paths"`
Meta MetaMap `json:"meta"`
}
// A RecentItem is an item returned from $searchRoot/camli/search/recent in the "recent" list.
type RecentItem struct {
BlobRef blob.Ref `json:"blobref"`
ModTime types.Time3339 `json:"modtime"`
Owner blob.Ref `json:"owner"`
}
// A WithAttrItem is an item returned from $searchRoot/camli/search/permanodeattr.
type WithAttrItem struct {
Permanode blob.Ref `json:"permanode"`
}
// A ClaimsItem is an item returned from $searchRoot/camli/search/claims.
type ClaimsItem struct {
BlobRef blob.Ref `json:"blobref"`
Signer blob.Ref `json:"signer"`
Permanode blob.Ref `json:"permanode"`
Date types.Time3339 `json:"date"`
Type string `json:"type"`
Attr string `json:"attr,omitempty"`
Value string `json:"value,omitempty"`
}
// A SignerPathsItem is an item returned from $searchRoot/camli/search/signerpaths.
type SignerPathsItem struct {
ClaimRef blob.Ref `json:"claimRef"`
BaseRef blob.Ref `json:"baseRef"`
Suffix string `json:"suffix"`
}
// EdgesResponse is the JSON response from $searchRoot/camli/search/edgesto.
type EdgesResponse struct {
ToRef blob.Ref `json:"toRef"`
EdgesTo []*EdgeItem `json:"edgesTo"`
}
// An EdgeItem is an item returned from $searchRoot/camli/search/edgesto.
type EdgeItem struct {
From blob.Ref `json:"from"`
FromType string `json:"fromType"`
}
func thumbnailSize(r *http.Request) int {
return thumbnailSizeStr(r.FormValue("thumbnails"))
}
const (
minThumbSize = 25
defThumbSize = 50
maxThumbSize = 800
)
func thumbnailSizeStr(s string) int {
if s == "" {
return 0
}
if i, _ := strconv.Atoi(s); i >= minThumbSize && i <= maxThumbSize {
return i
}
return defThumbSize
}
var testHookBug121 = func() {}
// GetRecentPermanodes returns recently-modified permanodes.
func (sh *Handler) GetRecentPermanodes(req *RecentRequest) (*RecentResponse, error) {
ch := make(chan *Result)
errch := make(chan error)
go func() {
errch <- sh.index.GetRecentPermanodes(ch, sh.owner, req.n())
}()
dr := sh.NewDescribeRequest()
var recent []*RecentItem
for res := range ch {
dr.Describe(res.BlobRef, 2)
recent = append(recent, &RecentItem{
BlobRef: res.BlobRef,
Owner: res.Signer,
ModTime: types.Time3339(time.Unix(res.LastModTime, 0)),
})
testHookBug121() // http://camlistore.org/issue/121
}
if err := <-errch; err != nil {
return nil, err
}
metaMap, err := dr.metaMapThumbs(req.thumbnailSize())
if err != nil {
return nil, err
}
res := &RecentResponse{
Recent: recent,
Meta: metaMap,
}
return res, nil
}
func (sh *Handler) serveRecentPermanodes(rw http.ResponseWriter, req *http.Request) {
defer httputil.RecoverJSON(rw, req)
var rr RecentRequest
rr.fromHTTP(req)
res, err := sh.GetRecentPermanodes(&rr)
if err != nil {
httputil.ServeJSONError(rw, err)
return
}
httputil.ReturnJSON(rw, res)
}
// GetPermanodesWithAttr returns permanodes with attribute req.Attr
// having the req.Value as a value.
// See WithAttrRequest for more details about the query.
func (sh *Handler) GetPermanodesWithAttr(req *WithAttrRequest) (*WithAttrResponse, error) {
ch := make(chan blob.Ref, buffered)
errch := make(chan error)
go func() {
signer := req.Signer
if !signer.Valid() {
signer = sh.owner
}
errch <- sh.index.SearchPermanodesWithAttr(ch,
&PermanodeByAttrRequest{Attribute: req.Attr,
Query: req.Value,
Signer: signer,
FuzzyMatch: req.Fuzzy,
MaxResults: req.N})
}()
dr := sh.NewDescribeRequest()
var withAttr []*WithAttrItem
for res := range ch {
dr.Describe(res, 2)
withAttr = append(withAttr, &WithAttrItem{
Permanode: res,
})
}
metaMap, err := dr.metaMapThumbs(req.thumbnailSize())
if err != nil {
return nil, err
}
res := &WithAttrResponse{
WithAttr: withAttr,
Meta: metaMap,
}
return res, nil
}
// servePermanodesWithAttr uses the indexer to search for the permanodes matching
// the request.
// The valid values for the "attr" key in the request (i.e the only attributes
// for a permanode which are actually indexed as such) are "tag" and "title".
func (sh *Handler) servePermanodesWithAttr(rw http.ResponseWriter, req *http.Request) {
defer httputil.RecoverJSON(rw, req)
var wr WithAttrRequest
wr.fromHTTP(req)
res, err := sh.GetPermanodesWithAttr(&wr)
if err != nil {
httputil.ServeJSONError(rw, err)
return
}
httputil.ReturnJSON(rw, res)
}
// GetClaims returns the claims on req.Permanode signed by sh.owner.
func (sh *Handler) GetClaims(req *ClaimsRequest) (*ClaimsResponse, error) {
// TODO: rename GetOwnerClaims to GetClaims?
if !req.Permanode.Valid() {
return nil, errors.New("Error getting claims: nil permanode.")
}
claims, err := sh.index.GetOwnerClaims(req.Permanode, sh.owner)
if err != nil {
return nil, fmt.Errorf("Error getting claims of %s: %v", req.Permanode.String(), err)
}
sort.Sort(claims)
var jclaims []*ClaimsItem
for _, claim := range claims {
jclaim := &ClaimsItem{
BlobRef: claim.BlobRef,
Signer: claim.Signer,
Permanode: claim.Permanode,
Date: types.Time3339(claim.Date),
Type: claim.Type,
Attr: claim.Attr,
Value: claim.Value,
}
jclaims = append(jclaims, jclaim)
}
res := &ClaimsResponse{
Claims: jclaims,
}
return res, nil
}
func (sh *Handler) serveClaims(rw http.ResponseWriter, req *http.Request) {
defer httputil.RecoverJSON(rw, req)
var cr ClaimsRequest
cr.fromHTTP(req)
res, err := sh.GetClaims(&cr)
if err != nil {
httputil.ServeJSONError(rw, err)
return
}
httputil.ReturnJSON(rw, res)
}
type DescribeRequest struct {
// BlobRefs are the blobs to describe. If length zero, BlobRef
// is used.
BlobRefs []blob.Ref
// BlobRef is the blob to describe.
BlobRef blob.Ref
// Depth is the optional traversal depth to describe from the
// root BlobRef. If zero, a default is used.
Depth int
// Internal details, used while loading.
// Initialized by sh.initDescribeRequest.
sh *Handler
mu sync.Mutex // protects following:
m MetaMap
done map[string]bool // blobref -> described
errs map[string]error // blobref -> error
wg *sync.WaitGroup // for load requests
}
func (r *DescribeRequest) URLSuffix() string {
var buf bytes.Buffer
fmt.Fprintf(&buf, "camli/search/describe?depth=%d", r.Depth)
for _, br := range r.BlobRefs {
buf.WriteString("&blobref=")
buf.WriteString(br.String())
}
if len(r.BlobRefs) == 0 && r.BlobRef.Valid() {
buf.WriteString("&blobref=")
buf.WriteString(r.BlobRef.String())
}
return buf.String()
}
// fromHTTP panics with an httputil value on failure
func (r *DescribeRequest) fromHTTP(req *http.Request) {
req.ParseForm()
if vv := req.Form["blobref"]; len(vv) > 1 {
for _, brs := range vv {
if br, ok := blob.Parse(brs); ok {
r.BlobRefs = append(r.BlobRefs, br)
} else {
panic(httputil.InvalidParameterError("blobref"))
}
}
} else {
r.BlobRef = httputil.MustGetBlobRef(req, "blobref")
}
r.Depth = httputil.OptionalInt(req, "depth")
}
type DescribedBlob struct {
Request *DescribeRequest `json:"-"`
BlobRef blob.Ref `json:"blobRef"`
MIMEType string `json:"mimeType"`
CamliType string `json:"camliType"`
Size int64 `json:"size,"`
// if camliType "permanode"
Permanode *DescribedPermanode `json:"permanode,omitempty"`
// if camliType "file"
File *FileInfo `json:"file,omitempty"`
// if camliType "directory"
Dir *FileInfo `json:"dir,omitempty"`
// if camliType "file", and File.IsImage()
Image *ImageInfo `json:"image,omitempty"`
Thumbnail string `json:"thumbnailSrc,omitempty"`
ThumbnailWidth int `json:"thumbnailWidth,omitempty"`
ThumbnailHeight int `json:"thumbnailHeight,omitempty"`
// Stub is set if this is not loaded, but referenced.
Stub bool `json:"-"`
}
// PermanodeFile returns in path the blobref of the described permanode
// and the blobref of its File camliContent.
// If b isn't a permanode, or doesn't have a camliContent that
// is a file blob, ok is false.
func (b *DescribedBlob) PermanodeFile() (path []blob.Ref, fi *FileInfo, ok bool) {
if b == nil || b.Permanode == nil {
return
}
if contentRef := b.Permanode.Attr.Get("camliContent"); contentRef != "" {
if cdes := b.Request.DescribedBlobStr(contentRef); cdes != nil && cdes.File != nil {
return []blob.Ref{b.BlobRef, cdes.BlobRef}, cdes.File, true
}
}
return
}
// PermanodeDir returns in path the blobref of the described permanode
// and the blobref of its Directory camliContent.
// If b isn't a permanode, or doesn't have a camliContent that
// is a directory blob, ok is false.
func (b *DescribedBlob) PermanodeDir() (path []blob.Ref, fi *FileInfo, ok bool) {
if b == nil || b.Permanode == nil {
return
}
if contentRef := b.Permanode.Attr.Get("camliContent"); contentRef != "" {
if cdes := b.Request.DescribedBlobStr(contentRef); cdes != nil && cdes.Dir != nil {
return []blob.Ref{b.BlobRef, cdes.BlobRef}, cdes.Dir, true
}
}
return
}
func (b *DescribedBlob) DomID() string {
if b == nil {
return ""
}
return b.BlobRef.DomID()
}
func (b *DescribedBlob) Title() string {
if b == nil {
return ""
}
if b.Permanode != nil {
if t := b.Permanode.Attr.Get("title"); t != "" {
return t
}
if contentRef := b.Permanode.Attr.Get("camliContent"); contentRef != "" {
return b.Request.DescribedBlobStr(contentRef).Title()
}
}
if b.File != nil {
return b.File.FileName
}
if b.Dir != nil {
return b.Dir.FileName
}
return ""
}
func (b *DescribedBlob) Description() string {
if b == nil {
return ""
}
if b.Permanode != nil {
return b.Permanode.Attr.Get("description")
}
return ""
}
func (b *DescribedBlob) Members() []*DescribedBlob {
if b == nil {
return nil
}
m := make([]*DescribedBlob, 0)
if b.Permanode != nil {
for _, bstr := range b.Permanode.Attr["camliMember"] {
if br, ok := blob.Parse(bstr); ok {
m = append(m, b.PeerBlob(br))
}
}
}
return m
}
func (b *DescribedBlob) ContentRef() (br blob.Ref, ok bool) {
if b != nil && b.Permanode != nil {
if cref := b.Permanode.Attr.Get("camliContent"); cref != "" {
return blob.Parse(cref)
}
}
return
}
// Given a blobref string returns a Description or nil.
// dr may be nil itself.
func (dr *DescribeRequest) DescribedBlobStr(blobstr string) *DescribedBlob {
if dr == nil {
return nil
}
dr.mu.Lock()
defer dr.mu.Unlock()
return dr.m[blobstr]
}
// PeerBlob returns a DescribedBlob for the provided blobref.
//
// Unlike DescribedBlobStr, the returned DescribedBlob is never nil.
//
// If the blob was never loaded along with the the receiver (or if the
// receiver is nil), a stub DescribedBlob is returned with its Stub
// field set true.
func (b *DescribedBlob) PeerBlob(br blob.Ref) *DescribedBlob {
if b.Request == nil {
return &DescribedBlob{BlobRef: br, Stub: true}
}
b.Request.mu.Lock()
defer b.Request.mu.Unlock()
return b.peerBlob(br)
}
// version of PeerBlob when b.Request.mu is already held.
func (b *DescribedBlob) peerBlob(br blob.Ref) *DescribedBlob {
if peer, ok := b.Request.m[br.String()]; ok {
return peer
}
return &DescribedBlob{Request: b.Request, BlobRef: br, Stub: true}
}
// HasSecureLinkTo returns true if there's a valid link from this blob
// to the other blob. This is used in access control (hence the
// somewhat redundant "Secure" in the name) and should be paranoid
// against e.g. random user/attacker-control attributes making links
// to other blobs.
//
// TODO: don't linear scan here. rewrite this in terms of ResolvePrefixHop,
// passing down some policy perhaps? or maybe that's enough.
func (b *DescribedBlob) HasSecureLinkTo(other blob.Ref) bool {
if b == nil || !other.Valid() {
return false
}
ostr := other.String()
if b.Permanode != nil {
if b.Permanode.Attr.Get("camliContent") == ostr {
return true
}
for _, mstr := range b.Permanode.Attr["camliMember"] {
if mstr == ostr {
return true
}
}
}
return false
}
func (b *DescribedBlob) isPermanode() bool {
return b.Permanode != nil
}
// returns a path relative to the UI handler.
//
// Locking: requires that DescribedRequest is done loading or that
// Request.mu is held (as it is from metaMap)
func (b *DescribedBlob) thumbnail(thumbSize int) (path string, width, height int, ok bool) {
if thumbSize <= 0 || !b.isPermanode() {
return
}
if b.Stub {
return "node.png", thumbSize, thumbSize, true
}
pnAttr := b.Permanode.Attr
if members := pnAttr["camliMember"]; len(members) > 0 {
return "folder.png", thumbSize, thumbSize, true
}
if content, ok := b.ContentRef(); ok {
peer := b.peerBlob(content)
if peer.File != nil {
if peer.File.IsImage() {
image := fmt.Sprintf("thumbnail/%s/%s?mh=%d", peer.BlobRef,
url.QueryEscape(peer.File.FileName), thumbSize)
if peer.Image != nil {
mw, mh := images.ScaledDimensions(
peer.Image.Width, peer.Image.Height,
MaxImageSize, thumbSize)
return image, mw, mh, true
}
return image, thumbSize, thumbSize, true
}
// TODO: different thumbnails based on peer.File.MIMEType.
return "file.png", thumbSize, thumbSize, true
}
if peer.Dir != nil {
return "folder.png", thumbSize, thumbSize, true
}
}
return "node.png", thumbSize, thumbSize, true
}
type DescribedPermanode struct {
Attr url.Values `json:"attr"` // a map[string][]string
}
func (dp *DescribedPermanode) jsonMap() map[string]interface{} {
m := jsonMap()
am := jsonMap()
m["attr"] = am
for k, vv := range dp.Attr {
if len(vv) > 0 {
vl := make([]string, len(vv))
copy(vl[:], vv[:])
am[k] = vl
}
}
return m
}
// NewDescribeRequest returns a new DescribeRequest holding the state
// of blobs and their summarized descriptions. Use DescribeBlob
// one or more times before calling Result.
func (sh *Handler) NewDescribeRequest() *DescribeRequest {
dr := new(DescribeRequest)
sh.initDescribeRequest(dr)
return dr
}
func (sh *Handler) initDescribeRequest(req *DescribeRequest) {
if req.sh != nil {
panic("already initialized")
}
req.sh = sh
req.m = make(MetaMap)
req.errs = make(map[string]error)
req.wg = new(sync.WaitGroup)
}
// Given a blobref and a few hex characters of the digest of the next hop, return the complete
// blobref of the prefix, if that's a valid next hop.
func (sh *Handler) ResolvePrefixHop(parent blob.Ref, prefix string) (child blob.Ref, err error) {
// TODO: this is a linear scan right now. this should be
// optimized to use a new database table of members so this is
// a quick lookup. in the meantime it should be in memcached
// at least.
if len(prefix) < 8 {
return blob.Ref{}, fmt.Errorf("Member prefix %q too small", prefix)
}
dr := sh.NewDescribeRequest()
dr.Describe(parent, 1)
res, err := dr.Result()
if err != nil {
return
}
des, ok := res[parent.String()]
if !ok {
return blob.Ref{}, fmt.Errorf("Failed to describe member %q in parent %q", prefix, parent)
}
if des.Permanode != nil {
if cr, ok := des.ContentRef(); ok && strings.HasPrefix(cr.Digest(), prefix) {
return cr, nil
}
for _, member := range des.Members() {
if strings.HasPrefix(member.BlobRef.Digest(), prefix) {
return member.BlobRef, nil
}
}
}
return blob.Ref{}, fmt.Errorf("Member prefix %q not found in %q", prefix, parent)
}
type DescribeError map[string]error
func (de DescribeError) Error() string {
var buf bytes.Buffer
for b, err := range de {
fmt.Fprintf(&buf, "%s: %v; ", b, err)
}
return fmt.Sprintf("Errors (%d) describing blobs: %s", len(de), buf.String())
}
// Result waits for all outstanding lookups to complete and
// returns the map of blobref (strings) to their described
// results. The returned error is non-nil if any errors
// occured, and will be of type DescribeError.
func (dr *DescribeRequest) Result() (desmap map[string]*DescribedBlob, err error) {
dr.wg.Wait()
// TODO: set "done" / locked flag, so no more DescribeBlob can
// be called.
if len(dr.errs) > 0 {
return dr.m, DescribeError(dr.errs)
}
return dr.m, nil
}
func (dr *DescribeRequest) depth() int {
if dr.Depth > 0 {
return dr.Depth
}
return 4
}
func (dr *DescribeRequest) metaMap() (map[string]*DescribedBlob, error) {
return dr.metaMapThumbs(0)
}
func (dr *DescribeRequest) metaMapThumbs(thumbSize int) (map[string]*DescribedBlob, error) {
// thumbSize of zero means to not include the thumbnails.
dr.wg.Wait()
dr.mu.Lock()
defer dr.mu.Unlock()
for k, err := range dr.errs {
// TODO: include all?
return nil, fmt.Errorf("error populating %s: %v", k, err)
}
m := make(map[string]*DescribedBlob)
for k, desb := range dr.m {
m[k] = desb
if src, w, h, ok := desb.thumbnail(thumbSize); ok {
desb.Thumbnail = src
desb.ThumbnailWidth = w
desb.ThumbnailHeight = h
}
}
return m, nil
}
func (dr *DescribeRequest) describedBlob(b blob.Ref) *DescribedBlob {
dr.mu.Lock()
defer dr.mu.Unlock()
bs := b.String()
if des, ok := dr.m[bs]; ok {
return des
}
des := &DescribedBlob{Request: dr, BlobRef: b}
dr.m[bs] = des
return des
}
func (dr *DescribeRequest) DescribeSync(br blob.Ref) (*DescribedBlob, error) {
dr.Describe(br, 1)
res, err := dr.Result()
if err != nil {
return nil, err
}
return res[br.String()], nil
}
// Describe starts a lookup of br, down to the provided depth.
// It returns immediately.
func (dr *DescribeRequest) Describe(br blob.Ref, depth int) {
if depth <= 0 {
return
}
dr.mu.Lock()
defer dr.mu.Unlock()
if dr.done == nil {
dr.done = make(map[string]bool)
}
brefAndDepth := fmt.Sprintf("%s-%d", br, depth)
if dr.done[brefAndDepth] {
return
}
dr.done[brefAndDepth] = true
dr.wg.Add(1)
go func() {
defer dr.wg.Done()
dr.describeReally(br, depth)
}()
}
func (dr *DescribeRequest) addError(br blob.Ref, err error) {
if err == nil {
return
}
dr.mu.Lock()
defer dr.mu.Unlock()
// TODO: append? meh.
dr.errs[br.String()] = err
}
func (dr *DescribeRequest) describeReally(br blob.Ref, depth int) {
mime, size, err := dr.sh.index.GetBlobMIMEType(br)
if err == os.ErrNotExist {
return
}
if err != nil {
dr.addError(br, err)
return
}
// TODO: convert all this in terms of
// DescribedBlob/DescribedPermanode/DescribedFile, not json
// maps. Then add JSON marhsallers to those types. Add tests.
des := dr.describedBlob(br)
des.setMIMEType(mime)
des.Size = size
switch des.CamliType {
case "permanode":
des.Permanode = new(DescribedPermanode)
dr.populatePermanodeFields(des.Permanode, br, dr.sh.owner, depth)
case "file":
var err error
des.File, err = dr.sh.index.GetFileInfo(br)
if err != nil {
if os.IsNotExist(err) {
log.Printf("index.GetFileInfo(file %s) failed; index stale?", br)
} else {
dr.addError(br, err)
}
return
}
if des.File.IsImage() {
des.Image, err = dr.sh.index.GetImageInfo(br)
if err != nil {
if os.IsNotExist(err) {
log.Printf("index.GetImageInfo(file %s) failed; index stale?", br)
} else {
dr.addError(br, err)
}
}
}
case "directory":
var err error
des.Dir, err = dr.sh.index.GetFileInfo(br)
if err != nil {
if os.IsNotExist(err) {
log.Printf("index.GetFileInfo(directory %s) failed; index stale?", br)
} else {
dr.addError(br, err)
}
}
}
}
func (sh *Handler) serveDescribe(rw http.ResponseWriter, req *http.Request) {
defer httputil.RecoverJSON(rw, req)
var dr DescribeRequest
dr.fromHTTP(req)
sh.initDescribeRequest(&dr)
if dr.BlobRef.Valid() {
dr.Describe(dr.BlobRef, dr.depth())
}
for _, br := range dr.BlobRefs {
dr.Describe(br, dr.depth())
}
metaMap, err := dr.metaMapThumbs(thumbnailSize(req))
if err != nil {
httputil.ServeJSONError(rw, err)
return
}
httputil.ReturnJSON(rw, &DescribeResponse{metaMap})
}
func (sh *Handler) serveFiles(rw http.ResponseWriter, req *http.Request) {
ret := jsonMap()
defer httputil.ReturnJSON(rw, ret)
br, ok := blob.Parse(req.FormValue("wholedigest"))
if !ok {
ret["error"] = "Missing or invalid 'wholedigest' param"
ret["errorType"] = "input"
return
}
files, err := sh.index.ExistingFileSchemas(br)
if err != nil {
ret["error"] = err.Error()
ret["errorType"] = "server"
return
}
strList := []string{}
for _, br := range files {
strList = append(strList, br.String())
}
ret["files"] = strList
return
}
func (dr *DescribeRequest) populatePermanodeFields(pi *DescribedPermanode, pn, signer blob.Ref, depth int) {
pi.Attr = make(url.Values)
attr := pi.Attr
claims, err := dr.sh.index.GetOwnerClaims(pn, signer)
if err != nil {
log.Printf("Error getting claims of %s: %v", pn.String(), err)
dr.addError(pn, fmt.Errorf("Error getting claims of %s: %v", pn.String(), err))
return
}
sort.Sort(claims)
claimLoop:
for _, cl := range claims {
switch cl.Type {
case "del-attribute":
if cl.Value == "" {
delete(attr, cl.Attr)
} else {
sl := attr[cl.Attr]
filtered := make([]string, 0, len(sl))
for _, val := range sl {
if val != cl.Value {
filtered = append(filtered, val)
}
}
attr[cl.Attr] = filtered
}
case "set-attribute":
delete(attr, cl.Attr)
fallthrough
case "add-attribute":
if cl.Value == "" {
continue
}
sl, ok := attr[cl.Attr]
if ok {
for _, exist := range sl {
if exist == cl.Value {
continue claimLoop
}
}
} else {
sl = make([]string, 0, 1)
attr[cl.Attr] = sl
}
attr[cl.Attr] = append(sl, cl.Value)
}
}
// If the content permanode is now known, look up its type
if content, ok := attr["camliContent"]; ok && len(content) > 0 {
if cbr, ok := blob.Parse(content[len(content)-1]); ok {
dr.Describe(cbr, depth-1)
}
}
// Resolve children
if members, ok := attr["camliMember"]; ok {
for _, member := range members {
if membr, ok := blob.Parse(member); ok {
dr.Describe(membr, depth-1)
}
}
}
// Resolve path elements
for k, vv := range attr {
if !strings.HasPrefix(k, "camliPath:") {
continue
}
for _, brs := range vv {
if br, ok := blob.Parse(brs); ok {
dr.Describe(br, depth-1)
}
}
}
}
// SignerAttrValueResponse is the JSON response to $search/camli/search/signerattrvalue
type SignerAttrValueResponse struct {
Permanode blob.Ref `json:"permanode"`
Meta MetaMap `json:"meta"`
}
func (sh *Handler) serveSignerAttrValue(rw http.ResponseWriter, req *http.Request) {
defer httputil.RecoverJSON(rw, req)
signer := httputil.MustGetBlobRef(req, "signer")
attr := httputil.MustGet(req, "attr")
value := httputil.MustGet(req, "value")
pn, err := sh.index.PermanodeOfSignerAttrValue(signer, attr, value)
if err != nil {
httputil.ServeJSONError(rw, err)
return
}
dr := sh.NewDescribeRequest()
dr.Describe(pn, 2)
metaMap, err := dr.metaMap()
if err != nil {
httputil.ServeJSONError(rw, err)
return
}
httputil.ReturnJSON(rw, &SignerAttrValueResponse{
Permanode: pn,
Meta: metaMap,
})
}
// EdgesTo returns edges that reference req.RefTo.
// It filters out since-deleted permanode edges.
func (sh *Handler) EdgesTo(req *EdgesRequest) (*EdgesResponse, error) {
toRef := req.ToRef
toRefStr := toRef.String()
var edgeItems []*EdgeItem
edges, err := sh.index.EdgesTo(toRef, nil)
if err != nil {
panic(err)
}
type edgeOrError struct {
edge *EdgeItem // or nil
err error
}
resc := make(chan edgeOrError)
verify := func(edge *Edge) {
db, err := sh.NewDescribeRequest().DescribeSync(edge.From)
if err != nil {
resc <- edgeOrError{err: err}
return
}
found := false
if db.Permanode != nil {
for attr, vv := range db.Permanode.Attr {
if IsBlobReferenceAttribute(attr) {
for _, v := range vv {
if v == toRefStr {
found = true
}
}
}
}
}
var ei *EdgeItem
if found {
ei = &EdgeItem{
From: edge.From,
FromType: "permanode",
}
}
resc <- edgeOrError{edge: ei}
}
verifying := 0
for _, edge := range edges {
if edge.FromType == "permanode" {
verifying++
go verify(edge)
continue
}
ei := &EdgeItem{
From: edge.From,
FromType: edge.FromType,
}
edgeItems = append(edgeItems, ei)
}
for i := 0; i < verifying; i++ {
res := <-resc
if res.err != nil {
return nil, res.err
}
if res.edge != nil {
edgeItems = append(edgeItems, res.edge)
}
}
return &EdgesResponse{
ToRef: toRef,
EdgesTo: edgeItems,
}, nil
}
// Unlike the index interface's EdgesTo method, the "edgesto" Handler
// here additionally filters out since-deleted permanode edges.
func (sh *Handler) serveEdgesTo(rw http.ResponseWriter, req *http.Request) {
defer httputil.RecoverJSON(rw, req)
var er EdgesRequest
er.fromHTTP(req)
res, err := sh.EdgesTo(&er)
if err != nil {
httputil.ServeJSONError(rw, err)
return
}
httputil.ReturnJSON(rw, res)
}
// GetSignerPaths returns paths with a target of req.Target.
func (sh *Handler) GetSignerPaths(req *SignerPathsRequest) (*SignerPathsResponse, error) {
if !req.Signer.Valid() {
return nil, errors.New("Error getting signer paths: nil signer.")
}
if !req.Target.Valid() {
return nil, errors.New("Error getting signer paths: nil target.")
}
paths, err := sh.index.PathsOfSignerTarget(req.Signer, req.Target)
if err != nil {
return nil, fmt.Errorf("Error getting paths of %s: %v", req.Target.String(), err)
}
var jpaths []*SignerPathsItem
for _, path := range paths {
jpaths = append(jpaths, &SignerPathsItem{
ClaimRef: path.Claim,
BaseRef: path.Base,
Suffix: path.Suffix,
})
}
dr := sh.NewDescribeRequest()
for _, path := range paths {
dr.Describe(path.Base, 2)
}
metaMap, err := dr.metaMap()
if err != nil {
return nil, err
}
res := &SignerPathsResponse{
Paths: jpaths,
Meta: metaMap,
}
return res, nil
}
func (sh *Handler) serveSignerPaths(rw http.ResponseWriter, req *http.Request) {
defer httputil.RecoverJSON(rw, req)
var sr SignerPathsRequest
sr.fromHTTP(req)
res, err := sh.GetSignerPaths(&sr)
if err != nil {
httputil.ServeJSONError(rw, err)
return
}
httputil.ReturnJSON(rw, res)
}
const camliTypePrefix = "application/json; camliType="
func (d *DescribedBlob) setMIMEType(mime string) {
d.MIMEType = mime
if strings.HasPrefix(mime, camliTypePrefix) {
d.CamliType = mime[len(camliTypePrefix):]
}
}