2013-10-19 00:17:35 +00:00
|
|
|
/*
|
|
|
|
Copyright 2013 The Camlistore Authors
|
|
|
|
|
|
|
|
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 (
|
2013-11-08 16:32:51 +00:00
|
|
|
"encoding/json"
|
2013-10-19 00:17:35 +00:00
|
|
|
"errors"
|
|
|
|
"fmt"
|
2013-11-08 19:49:23 +00:00
|
|
|
"io"
|
2013-11-08 16:25:51 +00:00
|
|
|
"log"
|
2013-11-08 16:32:51 +00:00
|
|
|
"net/http"
|
2013-11-08 18:11:16 +00:00
|
|
|
"os"
|
2013-10-19 00:17:35 +00:00
|
|
|
"strings"
|
|
|
|
"sync"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"camlistore.org/pkg/blob"
|
|
|
|
"camlistore.org/pkg/syncutil"
|
|
|
|
)
|
|
|
|
|
|
|
|
type SortType int
|
|
|
|
|
2013-11-08 19:49:23 +00:00
|
|
|
// TODO: add MarshalJSON and UnmarshalJSON to SortType
|
2013-10-19 00:17:35 +00:00
|
|
|
const (
|
|
|
|
UnspecifiedSort SortType = iota
|
|
|
|
LastModifiedDesc
|
|
|
|
LastModifiedAsc
|
|
|
|
CreatedDesc
|
|
|
|
CreatedAsc
|
|
|
|
)
|
|
|
|
|
2013-11-08 19:49:23 +00:00
|
|
|
// TODO: extend/merge/delete this type? probably dups in this package.
|
|
|
|
type BlobMeta struct {
|
|
|
|
Ref blob.Ref
|
|
|
|
Size int
|
|
|
|
MIMEType string
|
|
|
|
}
|
|
|
|
|
2013-10-19 00:17:35 +00:00
|
|
|
type SearchQuery struct {
|
2013-11-08 16:32:51 +00:00
|
|
|
Constraint *Constraint `json:"constraint"`
|
|
|
|
Limit int `json:"limit"` // optional. default is automatic.
|
|
|
|
Sort SortType `json:"sort"` // optional. default is automatic or unsorted.
|
|
|
|
}
|
|
|
|
|
|
|
|
func (q *SearchQuery) fromHTTP(req *http.Request) error {
|
2013-11-08 19:49:23 +00:00
|
|
|
dec := json.NewDecoder(io.LimitReader(req.Body, 1<<20))
|
2013-11-08 16:32:51 +00:00
|
|
|
if err := dec.Decode(q); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if q.Constraint == nil {
|
|
|
|
return errors.New("query must have at least a root Constraint")
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
2013-10-19 00:17:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type SearchResult struct {
|
2013-11-08 16:32:51 +00:00
|
|
|
Blobs []*SearchResultBlob `json:"blobs"`
|
2013-10-19 00:17:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type SearchResultBlob struct {
|
2013-11-08 16:32:51 +00:00
|
|
|
Blob blob.Ref `json:"blob"`
|
2013-10-19 00:17:35 +00:00
|
|
|
// ... file info, permanode info, blob info ... ?
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *SearchResultBlob) String() string {
|
|
|
|
return fmt.Sprintf("[blob: %s]", r.Blob)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Constraint specifies a blob matching constraint.
|
|
|
|
// A blob matches if it matches all non-zero fields' predicates.
|
|
|
|
// A zero constraint matches nothing.
|
|
|
|
type Constraint struct {
|
|
|
|
// If Logical is non-nil, all other fields are ignored.
|
2013-11-08 16:32:51 +00:00
|
|
|
Logical *LogicalConstraint `json:"logical"`
|
2013-10-19 00:17:35 +00:00
|
|
|
|
|
|
|
// Anything, if true, matches all blobs.
|
2013-11-08 16:32:51 +00:00
|
|
|
Anything bool `json:"anything"`
|
2013-10-19 00:17:35 +00:00
|
|
|
|
2013-11-08 16:32:51 +00:00
|
|
|
CamliType string `json:"camliType"` // camliType of the JSON blob
|
|
|
|
AnyCamliType bool `json:"anyCamliType"` // if true, any camli JSON blob matches
|
|
|
|
BlobRefPrefix string `json:"blobRefPrefix"`
|
2013-10-19 00:17:35 +00:00
|
|
|
|
2013-11-08 19:45:32 +00:00
|
|
|
File *FileConstraint
|
|
|
|
|
2013-10-19 00:17:35 +00:00
|
|
|
// For claims:
|
2013-11-08 16:32:51 +00:00
|
|
|
Claim *ClaimConstraint `json:"claim"`
|
2013-10-19 00:17:35 +00:00
|
|
|
|
2013-11-08 16:32:51 +00:00
|
|
|
BlobSize *BlobSizeConstraint `json:"blobSize"`
|
2013-10-19 00:17:35 +00:00
|
|
|
|
|
|
|
// For permanodes:
|
2013-11-08 16:32:51 +00:00
|
|
|
Attribute *AttributeConstraint `json:"attribute"`
|
2013-10-19 00:17:35 +00:00
|
|
|
}
|
|
|
|
|
2013-11-08 19:45:32 +00:00
|
|
|
type FileConstraint struct {
|
|
|
|
// (All non-zero fields must match)
|
|
|
|
|
|
|
|
MinSize int64 // inclusive
|
|
|
|
MaxSize int64 // inclusive. if zero, ignored.
|
|
|
|
IsImage bool
|
|
|
|
FileName *StringConstraint
|
|
|
|
MIMEType *StringConstraint
|
|
|
|
Time *TimeConstraint
|
|
|
|
ModTime *TimeConstraint
|
|
|
|
EXIF *EXIFConstraint
|
|
|
|
}
|
|
|
|
|
|
|
|
type EXIFConstraint struct {
|
|
|
|
// TODO. need to put this in the index probably.
|
|
|
|
// Maybe: GPS *LocationConstraint
|
|
|
|
// ISO, Aperature, Camera Make/Model, etc.
|
|
|
|
}
|
|
|
|
|
|
|
|
type StringConstraint struct {
|
|
|
|
// All non-zero must match.
|
|
|
|
|
|
|
|
// TODO: CaseInsensitive bool?
|
|
|
|
Empty bool // matches empty string
|
|
|
|
Equals string
|
|
|
|
Contains string
|
|
|
|
HasPrefix string
|
|
|
|
HasSuffix string
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *StringConstraint) stringMatches(s string) bool {
|
|
|
|
if c.Empty && len(s) > 0 {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
if c.Equals != "" && s != c.Equals {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
for _, pair := range []struct {
|
|
|
|
v string
|
|
|
|
fn func(string, string) bool
|
|
|
|
}{
|
|
|
|
{c.Contains, strings.Contains},
|
|
|
|
{c.HasPrefix, strings.HasPrefix},
|
|
|
|
{c.HasSuffix, strings.HasSuffix},
|
|
|
|
} {
|
|
|
|
if pair.v != "" && !pair.fn(s, pair.v) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
type TimeConstraint struct {
|
|
|
|
Before time.Time // <
|
|
|
|
After time.Time // >=
|
|
|
|
InLast time.Duration // >=
|
|
|
|
}
|
|
|
|
|
2013-10-19 00:17:35 +00:00
|
|
|
type ClaimConstraint struct {
|
2013-11-08 16:32:51 +00:00
|
|
|
SignedBy string `json:"signedBy"` // identity
|
|
|
|
SignedAfter time.Time `json:"signedAfter"`
|
|
|
|
SignedBefore time.Time `json:"signedBefore"`
|
2013-10-19 00:17:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type LogicalConstraint struct {
|
2013-11-08 16:32:51 +00:00
|
|
|
Op string `json:"op"` // "and", "or", "xor", "not"
|
|
|
|
A *Constraint `json:"a"`
|
|
|
|
B *Constraint `json:"b"` // only valid if Op == "not"
|
2013-10-19 00:17:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type BlobSizeConstraint struct {
|
2013-11-08 16:32:51 +00:00
|
|
|
Min int `json:"min"` // inclusive
|
|
|
|
Max int `json:"max"` // inclusive. if zero, ignored.
|
2013-10-19 00:17:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type AttributeConstraint struct {
|
|
|
|
// At specifies the time at which to pretend we're resolving attributes.
|
|
|
|
// Attribute claims after this point in time are ignored.
|
|
|
|
// If zero, the current time is used.
|
2013-11-08 16:32:51 +00:00
|
|
|
At time.Time `json:"at"`
|
2013-10-19 00:17:35 +00:00
|
|
|
|
|
|
|
// Attr is the attribute to match.
|
|
|
|
// e.g. "camliContent", "camliMember", "tag"
|
2013-11-08 18:11:16 +00:00
|
|
|
// TODO: field to control whether first vs. all permanode values are considered?
|
2013-11-08 16:32:51 +00:00
|
|
|
Attr string `json:"attr"`
|
|
|
|
Value string `json:"value"` // if non-zero, absolute match
|
|
|
|
ValueAny []string `json:"valueAny"` // Value is any of these strings
|
|
|
|
ValueMatches *Constraint `json:"valueMatches"` // if non-zero, Attr value is blobref in this set of matches
|
|
|
|
ValueSet bool `json:"valueSet"` // value is set to something non-blank
|
2013-10-19 00:17:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// search is the state of an in-progress search
|
|
|
|
type search struct {
|
|
|
|
h *Handler
|
|
|
|
q *SearchQuery
|
|
|
|
res *SearchResult
|
|
|
|
|
|
|
|
mu sync.Mutex
|
|
|
|
matches map[blob.Ref]bool
|
|
|
|
}
|
|
|
|
|
2013-11-08 18:11:16 +00:00
|
|
|
func (s *search) blobMeta(br blob.Ref) (BlobMeta, error) {
|
|
|
|
mime, size, err := s.h.index.GetBlobMIMEType(br)
|
|
|
|
return BlobMeta{Ref: br, Size: int(size), MIMEType: mime}, err
|
|
|
|
}
|
|
|
|
|
2013-10-19 00:17:35 +00:00
|
|
|
// optimizePlan returns an optimized version of c which will hopefully
|
|
|
|
// execute faster than executing c literally.
|
|
|
|
func optimizePlan(c *Constraint) *Constraint {
|
|
|
|
// TODO: what the comment above says.
|
|
|
|
return c
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *Handler) Query(q *SearchQuery) (*SearchResult, error) {
|
|
|
|
res := new(SearchResult)
|
|
|
|
s := &search{
|
|
|
|
h: h,
|
|
|
|
q: q,
|
|
|
|
res: res,
|
|
|
|
matches: make(map[blob.Ref]bool),
|
|
|
|
}
|
|
|
|
ch := make(chan BlobMeta, buffered)
|
|
|
|
errc := make(chan error, 1)
|
|
|
|
go func() {
|
|
|
|
errc <- h.index.EnumerateBlobMeta(ch)
|
|
|
|
}()
|
|
|
|
optConstraint := optimizePlan(q.Constraint)
|
|
|
|
|
|
|
|
for meta := range ch {
|
|
|
|
match, err := optConstraint.blobMatches(s, meta.Ref, meta)
|
|
|
|
if err != nil {
|
|
|
|
// drain ch
|
|
|
|
go func() {
|
|
|
|
for _ = range ch {
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if match {
|
|
|
|
res.Blobs = append(res.Blobs, &SearchResultBlob{
|
|
|
|
Blob: meta.Ref,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if err := <-errc; err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return s.res, nil
|
|
|
|
}
|
|
|
|
|
2013-10-19 02:10:59 +00:00
|
|
|
const camliTypeMIME = "application/json; camliType="
|
|
|
|
|
2013-10-19 00:17:35 +00:00
|
|
|
type blobMatcher interface {
|
|
|
|
blobMatches(s *search, br blob.Ref, blobMeta BlobMeta) (bool, error)
|
|
|
|
}
|
|
|
|
|
|
|
|
type matchFn func(*search, blob.Ref, BlobMeta) (bool, error)
|
|
|
|
|
|
|
|
func alwaysMatch(*search, blob.Ref, BlobMeta) (bool, error) {
|
|
|
|
return true, nil
|
|
|
|
}
|
|
|
|
|
2013-10-19 02:10:59 +00:00
|
|
|
func anyCamliType(s *search, br blob.Ref, bm BlobMeta) (bool, error) {
|
|
|
|
return strings.HasPrefix(bm.MIMEType, camliTypeMIME), nil
|
|
|
|
}
|
|
|
|
|
2013-10-19 00:17:35 +00:00
|
|
|
func (c *Constraint) blobMatches(s *search, br blob.Ref, blobMeta BlobMeta) (bool, error) {
|
|
|
|
var conds []matchFn
|
|
|
|
addCond := func(fn matchFn) {
|
|
|
|
conds = append(conds, fn)
|
|
|
|
}
|
|
|
|
if c.Logical != nil {
|
|
|
|
addCond(c.Logical.blobMatches)
|
|
|
|
}
|
|
|
|
if c.Anything {
|
|
|
|
addCond(alwaysMatch)
|
|
|
|
}
|
|
|
|
if c.CamliType != "" {
|
2013-10-19 00:56:56 +00:00
|
|
|
addCond(func(s *search, br blob.Ref, bm BlobMeta) (bool, error) {
|
2013-10-19 02:10:59 +00:00
|
|
|
return strings.TrimPrefix(bm.MIMEType, camliTypeMIME) == c.CamliType, nil
|
2013-10-19 00:56:56 +00:00
|
|
|
})
|
2013-10-19 00:17:35 +00:00
|
|
|
}
|
2013-10-19 02:10:59 +00:00
|
|
|
if c.AnyCamliType {
|
|
|
|
addCond(anyCamliType)
|
|
|
|
}
|
2013-11-08 16:25:51 +00:00
|
|
|
if c.Attribute != nil {
|
|
|
|
addCond(c.Attribute.blobMatches)
|
|
|
|
}
|
2013-11-08 19:45:32 +00:00
|
|
|
// TODO: ClaimConstraint
|
|
|
|
if c.File != nil {
|
|
|
|
addCond(c.File.blobMatches)
|
|
|
|
}
|
2013-10-19 01:12:23 +00:00
|
|
|
if bs := c.BlobSize; bs != nil {
|
|
|
|
addCond(func(s *search, br blob.Ref, bm BlobMeta) (bool, error) {
|
|
|
|
if bm.Size < bs.Min {
|
|
|
|
return false, nil
|
|
|
|
}
|
|
|
|
if bs.Max > 0 && bm.Size > bs.Max {
|
|
|
|
return false, nil
|
|
|
|
}
|
|
|
|
return true, nil
|
|
|
|
})
|
|
|
|
|
|
|
|
}
|
2013-10-19 00:17:35 +00:00
|
|
|
if pfx := c.BlobRefPrefix; pfx != "" {
|
|
|
|
addCond(func(*search, blob.Ref, BlobMeta) (bool, error) {
|
|
|
|
return strings.HasPrefix(br.String(), pfx), nil
|
|
|
|
})
|
|
|
|
}
|
|
|
|
switch len(conds) {
|
|
|
|
case 0:
|
|
|
|
return false, nil
|
|
|
|
case 1:
|
|
|
|
return conds[0](s, br, blobMeta)
|
|
|
|
default:
|
|
|
|
panic("TODO")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *LogicalConstraint) blobMatches(s *search, br blob.Ref, bm BlobMeta) (bool, error) {
|
|
|
|
switch c.Op {
|
|
|
|
case "and", "xor":
|
|
|
|
if c.A == nil || c.B == nil {
|
|
|
|
return false, errors.New("In LogicalConstraint, need both A and B set")
|
|
|
|
}
|
|
|
|
var g syncutil.Group
|
|
|
|
var av, bv bool
|
|
|
|
g.Go(func() (err error) {
|
|
|
|
av, err = c.A.blobMatches(s, br, bm)
|
|
|
|
return
|
|
|
|
})
|
|
|
|
g.Go(func() (err error) {
|
|
|
|
bv, err = c.B.blobMatches(s, br, bm)
|
|
|
|
return
|
|
|
|
})
|
|
|
|
if err := g.Err(); err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
switch c.Op {
|
|
|
|
case "and":
|
|
|
|
return av && bv, nil
|
|
|
|
case "xor":
|
|
|
|
return av != bv, nil
|
|
|
|
default:
|
|
|
|
panic("unreachable")
|
|
|
|
}
|
|
|
|
case "or":
|
|
|
|
if c.A == nil || c.B == nil {
|
|
|
|
return false, errors.New("In LogicalConstraint, need both A and B set")
|
|
|
|
}
|
|
|
|
av, err := c.A.blobMatches(s, br, bm)
|
|
|
|
if err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
if av {
|
|
|
|
// Short-circuit.
|
|
|
|
return true, nil
|
|
|
|
}
|
|
|
|
return c.B.blobMatches(s, br, bm)
|
|
|
|
case "not":
|
|
|
|
if c.A == nil {
|
|
|
|
return false, errors.New("In LogicalConstraint, need to set A")
|
|
|
|
}
|
|
|
|
if c.B != nil {
|
|
|
|
return false, errors.New("In LogicalConstraint, can't specify B with Op \"not\"")
|
|
|
|
}
|
|
|
|
v, err := c.A.blobMatches(s, br, bm)
|
|
|
|
return !v, err
|
|
|
|
default:
|
|
|
|
return false, fmt.Errorf("In LogicalConstraint, unknown operation %q", c.Op)
|
|
|
|
}
|
|
|
|
}
|
2013-11-08 16:25:51 +00:00
|
|
|
|
|
|
|
func (c *AttributeConstraint) blobMatches(s *search, br blob.Ref, bm BlobMeta) (bool, error) {
|
|
|
|
if bm.MIMEType != "application/json; camliType=permanode" {
|
|
|
|
return false, nil
|
|
|
|
}
|
|
|
|
dr, err := s.h.Describe(&DescribeRequest{
|
|
|
|
BlobRef: br,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
db := dr.Meta[br.String()]
|
|
|
|
if db == nil || db.Permanode == nil {
|
|
|
|
return false, nil
|
|
|
|
}
|
|
|
|
attrs := db.Permanode.Attr // url.Values: a map[string][]string
|
|
|
|
if c.Value != "" {
|
|
|
|
got := attrs.Get(c.Attr)
|
|
|
|
return got == c.Value, nil
|
|
|
|
}
|
|
|
|
if len(c.ValueAny) > 0 {
|
2013-11-08 16:35:21 +00:00
|
|
|
for _, attr := range attrs[c.Attr] {
|
|
|
|
for _, want := range c.ValueAny {
|
|
|
|
if want == attr {
|
|
|
|
return true, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false, nil
|
|
|
|
}
|
|
|
|
if c.ValueSet {
|
|
|
|
for _, attr := range attrs[c.Attr] {
|
|
|
|
if attr != "" {
|
2013-11-08 16:25:51 +00:00
|
|
|
return true, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false, nil
|
|
|
|
}
|
2013-11-08 18:11:16 +00:00
|
|
|
if subc := c.ValueMatches; subc != nil {
|
|
|
|
for _, attr := range attrs[c.Attr] {
|
|
|
|
if attrBr, ok := blob.Parse(attr); ok {
|
|
|
|
meta, err := s.blobMeta(attrBr)
|
|
|
|
if err == os.ErrNotExist {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
matches, err := subc.blobMatches(s, attrBr, meta)
|
|
|
|
if err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
if matches {
|
|
|
|
return true, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false, nil
|
|
|
|
}
|
2013-11-08 16:25:51 +00:00
|
|
|
|
|
|
|
log.Printf("br=%v meta=%+v: %#v", br, bm, dr)
|
|
|
|
panic("TODO: not implemented")
|
|
|
|
return false, nil
|
|
|
|
}
|
2013-11-08 19:45:32 +00:00
|
|
|
|
|
|
|
func (c *FileConstraint) blobMatches(s *search, br blob.Ref, bm BlobMeta) (bool, error) {
|
|
|
|
if bm.MIMEType != "application/json; camliType=file" {
|
|
|
|
return false, nil
|
|
|
|
}
|
|
|
|
fi, err := s.h.index.GetFileInfo(br)
|
|
|
|
if err == os.ErrNotExist {
|
|
|
|
return false, nil
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
if fi.Size < c.MinSize {
|
|
|
|
return false, nil
|
|
|
|
}
|
|
|
|
if c.MaxSize != 0 && fi.Size > c.MaxSize {
|
|
|
|
return false, nil
|
|
|
|
}
|
|
|
|
if c.IsImage && !strings.HasPrefix(fi.MIMEType, "image/") {
|
|
|
|
return false, nil
|
|
|
|
}
|
|
|
|
if sc := c.FileName; sc != nil && !sc.stringMatches(fi.FileName) {
|
|
|
|
return false, nil
|
|
|
|
}
|
|
|
|
if sc := c.MIMEType; sc != nil && !sc.stringMatches(fi.MIMEType) {
|
|
|
|
return false, nil
|
|
|
|
}
|
|
|
|
// TOOD: Time timeconstraint
|
|
|
|
// TOOD: ModTime timeconstraint
|
|
|
|
// TOOD: EXIF timeconstraint
|
|
|
|
return true, nil
|
|
|
|
}
|