2011-03-13 23:38:32 +00:00
|
|
|
/*
|
2018-01-04 00:52:49 +00:00
|
|
|
Copyright 2011 The Perkeep Authors
|
2011-03-13 23:38:32 +00:00
|
|
|
|
|
|
|
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 (
|
2011-07-03 00:16:30 +00:00
|
|
|
"bytes"
|
2017-11-26 09:05:38 +00:00
|
|
|
"context"
|
2014-08-29 21:11:58 +00:00
|
|
|
"encoding/json"
|
2013-02-08 05:55:17 +00:00
|
|
|
"errors"
|
2011-03-13 23:38:32 +00:00
|
|
|
"fmt"
|
2014-05-08 19:04:08 +00:00
|
|
|
"io/ioutil"
|
2018-01-13 21:40:52 +00:00
|
|
|
"log"
|
Update from r60 to [almost] Go 1.
A lot is still broken, but most stuff at least compiles now.
The directory tree has been rearranged now too. Go libraries are now
under "pkg". Fully qualified, they are e.g. "camlistore.org/pkg/jsonsign".
The go tool cannot yet fetch from arbitrary domains, but discussion is
happening now on which mechanism to use to allow that.
For now, put the camlistore root under $GOPATH/src. Typically $GOPATH
is $HOME, so Camlistore should be at $HOME/src/camlistore.org.
Then you can:
$ go build ./server/camlistored
... etc
The build.pl script is currently disabled. It'll be resurrected at
some point, but with a very different role (helping create a fake
GOPATH and running the go build command, if things are installed at
the wrong place, and/or running fileembed generators).
Many things are certainly broken.
Many things are disabled. (MySQL, all indexing, etc).
Many things need to be moved into
camlistore.org/third_party/{code.google.com,github.com} and updated
from their r60 to Go 1 versions, where applicable.
The GoMySQL stuff should be updated to use database/sql and the ziutek
library implementing database/sql/driver.
Help wanted.
Change-Id: If71217dc5c8f0e70dbe46e9504ca5131c6eeacde
2012-02-19 05:53:06 +00:00
|
|
|
"net/http"
|
|
|
|
"net/url"
|
2014-04-06 21:48:48 +00:00
|
|
|
"regexp"
|
2011-03-14 03:51:58 +00:00
|
|
|
"sort"
|
2011-08-25 13:15:38 +00:00
|
|
|
"strconv"
|
2011-05-30 22:44:25 +00:00
|
|
|
"strings"
|
2011-03-14 00:14:48 +00:00
|
|
|
"time"
|
2011-05-30 05:52:31 +00:00
|
|
|
|
2015-12-01 16:19:49 +00:00
|
|
|
"go4.org/jsonconfig"
|
2016-02-05 14:56:16 +00:00
|
|
|
"go4.org/types"
|
2018-01-03 05:03:30 +00:00
|
|
|
"perkeep.org/internal/httputil"
|
Rename import paths from camlistore.org to perkeep.org.
Part of the project renaming, issue #981.
After this, users will need to mv their $GOPATH/src/camlistore.org to
$GOPATH/src/perkeep.org. Sorry.
This doesn't yet rename the tools like camlistored, camput, camget,
camtool, etc.
Also, this only moves the lru package to internal. More will move to
internal later.
Also, this doesn't yet remove the "/pkg/" directory. That'll likely
happen later.
This updates some docs, but not all.
devcam test now passes again, even with Go 1.10 (which requires vet
checks are clean too). So a bunch of vet tests are fixed in this CL
too, and a bunch of other broken tests are now fixed (introduced from
the past week of merging the CL backlog).
Change-Id: If580db1691b5b99f8ed6195070789b1f44877dd4
2018-01-01 22:41:41 +00:00
|
|
|
"perkeep.org/pkg/blob"
|
|
|
|
"perkeep.org/pkg/blobserver"
|
|
|
|
"perkeep.org/pkg/index"
|
2018-01-31 18:30:47 +00:00
|
|
|
"perkeep.org/pkg/jsonsign"
|
2021-01-17 03:05:35 +00:00
|
|
|
"perkeep.org/pkg/schema"
|
Rename import paths from camlistore.org to perkeep.org.
Part of the project renaming, issue #981.
After this, users will need to mv their $GOPATH/src/camlistore.org to
$GOPATH/src/perkeep.org. Sorry.
This doesn't yet rename the tools like camlistored, camput, camget,
camtool, etc.
Also, this only moves the lru package to internal. More will move to
internal later.
Also, this doesn't yet remove the "/pkg/" directory. That'll likely
happen later.
This updates some docs, but not all.
devcam test now passes again, even with Go 1.10 (which requires vet
checks are clean too). So a bunch of vet tests are fixed in this CL
too, and a bunch of other broken tests are now fixed (introduced from
the past week of merging the CL backlog).
Change-Id: If580db1691b5b99f8ed6195070789b1f44877dd4
2018-01-01 22:41:41 +00:00
|
|
|
"perkeep.org/pkg/types/camtypes"
|
2018-01-31 18:30:47 +00:00
|
|
|
"perkeep.org/pkg/types/serverconfig"
|
2011-03-13 23:38:32 +00:00
|
|
|
)
|
|
|
|
|
2013-04-01 18:18:49 +00:00
|
|
|
const buffered = 32 // arbitrary channel buffer size
|
|
|
|
const maxResults = 1000 // arbitrary limit on the number of search results returned
|
|
|
|
const defaultNumResults = 50
|
2011-07-02 15:15:00 +00:00
|
|
|
|
2013-07-29 08:02:40 +00:00
|
|
|
// 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
|
|
|
|
|
2014-04-06 21:48:48 +00:00
|
|
|
var blobRefPattern = regexp.MustCompile(blob.Pattern)
|
|
|
|
|
2011-05-30 05:52:31 +00:00
|
|
|
func init() {
|
|
|
|
blobserver.RegisterHandlerConstructor("search", newHandlerFromConfig)
|
|
|
|
}
|
|
|
|
|
2018-01-16 23:03:16 +00:00
|
|
|
var (
|
|
|
|
_ QueryDescriber = (*Handler)(nil)
|
|
|
|
)
|
|
|
|
|
2013-10-06 06:46:39 +00:00
|
|
|
// Handler handles search queries.
|
2011-06-17 03:45:47 +00:00
|
|
|
type Handler struct {
|
2013-11-16 23:00:30 +00:00
|
|
|
index index.Interface
|
2018-01-31 18:30:47 +00:00
|
|
|
owner *index.Owner
|
2014-05-08 19:04:08 +00:00
|
|
|
// optional for search aliases
|
|
|
|
fetcher blob.Fetcher
|
2013-11-16 23:18:16 +00:00
|
|
|
|
|
|
|
// Corpus optionally specifies the full in-memory metadata corpus
|
|
|
|
// to use.
|
|
|
|
// TODO: this may be required in the future, or folded into the index
|
|
|
|
// interface.
|
|
|
|
corpus *index.Corpus
|
2013-12-08 04:19:20 +00:00
|
|
|
|
2016-05-12 04:05:43 +00:00
|
|
|
lh *index.LocationHelper
|
|
|
|
|
2013-12-08 04:19:20 +00:00
|
|
|
// WebSocket hub
|
|
|
|
wsHub *wsHub
|
2011-05-30 05:52:31 +00:00
|
|
|
}
|
|
|
|
|
2015-02-02 09:45:41 +00:00
|
|
|
// GetRecentPermanoder is the interface containing the GetRecentPermanodes method.
|
|
|
|
type GetRecentPermanoder interface {
|
2013-02-08 05:34:25 +00:00
|
|
|
// 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.
|
2018-01-16 23:03:16 +00:00
|
|
|
GetRecentPermanodes(context.Context, *RecentRequest) (*RecentResponse, error)
|
2013-02-08 05:34:25 +00:00
|
|
|
}
|
|
|
|
|
2015-02-02 09:45:41 +00:00
|
|
|
var _ GetRecentPermanoder = (*Handler)(nil)
|
2013-02-08 05:34:25 +00:00
|
|
|
|
2018-01-31 18:30:47 +00:00
|
|
|
func NewHandler(ix index.Interface, owner *index.Owner) *Handler {
|
2013-12-08 04:19:20 +00:00
|
|
|
sh := &Handler{
|
2016-05-12 04:05:43 +00:00
|
|
|
index: ix,
|
2013-12-08 04:19:20 +00:00
|
|
|
owner: owner,
|
|
|
|
}
|
2017-04-21 00:05:45 +00:00
|
|
|
sh.lh = index.NewLocationHelper(sh.index.(*index.Index))
|
2013-12-08 04:19:20 +00:00
|
|
|
sh.wsHub = newWebsocketHub(sh)
|
|
|
|
go sh.wsHub.run()
|
2013-12-08 06:22:45 +00:00
|
|
|
sh.subscribeToNewBlobs()
|
2013-12-08 04:19:20 +00:00
|
|
|
return sh
|
2011-07-06 18:20:25 +00:00
|
|
|
}
|
|
|
|
|
2014-05-08 19:04:08 +00:00
|
|
|
func (h *Handler) InitHandler(lh blobserver.FindHandlerByTyper) error {
|
|
|
|
_, handler, err := lh.FindHandlerByType("storage-filesystem")
|
|
|
|
if err != nil || handler == nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
h.fetcher = handler.(blob.Fetcher)
|
|
|
|
registerKeyword(newNamedSearch(h))
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2017-12-11 02:07:07 +00:00
|
|
|
func (h *Handler) subscribeToNewBlobs() {
|
2013-12-08 06:22:45 +00:00
|
|
|
ch := make(chan blob.Ref, buffered)
|
2017-12-11 02:07:07 +00:00
|
|
|
blobserver.GetHub(h.index).RegisterListener(ch)
|
2013-12-08 06:22:45 +00:00
|
|
|
go func() {
|
2016-04-22 04:34:24 +00:00
|
|
|
ctx := context.Background()
|
2013-12-08 06:22:45 +00:00
|
|
|
for br := range ch {
|
2017-12-11 02:07:07 +00:00
|
|
|
h.index.RLock()
|
|
|
|
bm, err := h.index.GetBlobMeta(ctx, br)
|
2013-12-08 06:22:45 +00:00
|
|
|
if err == nil {
|
2017-12-11 02:07:07 +00:00
|
|
|
h.wsHub.newBlobRecv <- bm.CamliType
|
2013-12-08 06:22:45 +00:00
|
|
|
}
|
2017-12-11 02:07:07 +00:00
|
|
|
h.index.RUnlock()
|
2013-12-08 06:22:45 +00:00
|
|
|
}
|
|
|
|
}()
|
|
|
|
}
|
|
|
|
|
2013-11-17 18:52:37 +00:00
|
|
|
func (h *Handler) SetCorpus(c *index.Corpus) {
|
|
|
|
h.corpus = c
|
2016-05-12 04:05:43 +00:00
|
|
|
h.lh.SetCorpus(c)
|
2013-11-17 18:52:37 +00:00
|
|
|
}
|
|
|
|
|
2014-08-29 21:11:58 +00:00
|
|
|
// SendStatusUpdate sends a JSON status map to any connected WebSocket clients.
|
|
|
|
func (h *Handler) SendStatusUpdate(status json.RawMessage) {
|
|
|
|
h.wsHub.statusUpdate <- status
|
|
|
|
}
|
|
|
|
|
Update from r60 to [almost] Go 1.
A lot is still broken, but most stuff at least compiles now.
The directory tree has been rearranged now too. Go libraries are now
under "pkg". Fully qualified, they are e.g. "camlistore.org/pkg/jsonsign".
The go tool cannot yet fetch from arbitrary domains, but discussion is
happening now on which mechanism to use to allow that.
For now, put the camlistore root under $GOPATH/src. Typically $GOPATH
is $HOME, so Camlistore should be at $HOME/src/camlistore.org.
Then you can:
$ go build ./server/camlistored
... etc
The build.pl script is currently disabled. It'll be resurrected at
some point, but with a very different role (helping create a fake
GOPATH and running the go build command, if things are installed at
the wrong place, and/or running fileembed generators).
Many things are certainly broken.
Many things are disabled. (MySQL, all indexing, etc).
Many things need to be moved into
camlistore.org/third_party/{code.google.com,github.com} and updated
from their r60 to Go 1 versions, where applicable.
The GoMySQL stuff should be updated to use database/sql and the ziutek
library implementing database/sql/driver.
Help wanted.
Change-Id: If71217dc5c8f0e70dbe46e9504ca5131c6eeacde
2012-02-19 05:53:06 +00:00
|
|
|
func newHandlerFromConfig(ld blobserver.Loader, conf jsonconfig.Obj) (http.Handler, error) {
|
2011-05-30 05:52:31 +00:00
|
|
|
indexPrefix := conf.RequiredString("index") // TODO: add optional help tips here?
|
2018-01-31 18:30:47 +00:00
|
|
|
ownerCfg := conf.RequiredObject("owner")
|
|
|
|
ownerId := ownerCfg.RequiredString("identity")
|
|
|
|
ownerSecring := ownerCfg.RequiredString("secringFile")
|
2014-05-08 19:04:08 +00:00
|
|
|
|
2012-11-07 21:40:17 +00:00
|
|
|
devBlockStartupPrefix := conf.OptionalString("devBlockStartupOn", "")
|
2013-11-16 20:54:11 +00:00
|
|
|
slurpToMemory := conf.OptionalBool("slurpToMemory", false)
|
2011-05-30 05:52:31 +00:00
|
|
|
if err := conf.Validate(); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2012-11-07 21:40:17 +00:00
|
|
|
if devBlockStartupPrefix != "" {
|
|
|
|
_, err := ld.GetHandler(devBlockStartupPrefix)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("search handler references bogus devBlockStartupOn handler %s: %v", devBlockStartupPrefix, err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2011-05-30 05:52:31 +00:00
|
|
|
indexHandler, err := ld.GetHandler(indexPrefix)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("search config references unknown handler %q", indexPrefix)
|
2011-03-13 23:38:32 +00:00
|
|
|
}
|
2013-11-16 23:00:30 +00:00
|
|
|
indexer, ok := indexHandler.(index.Interface)
|
2011-05-30 05:52:31 +00:00
|
|
|
if !ok {
|
|
|
|
return nil, fmt.Errorf("search config references invalid indexer %q (actually a %T)", indexPrefix, indexHandler)
|
|
|
|
}
|
2018-01-31 18:30:47 +00:00
|
|
|
|
|
|
|
owner, err := newOwner(serverconfig.Owner{
|
|
|
|
Identity: ownerId,
|
|
|
|
SecringFile: ownerSecring,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("could not create Owner %v", err)
|
2011-05-30 05:52:31 +00:00
|
|
|
}
|
2018-01-31 18:30:47 +00:00
|
|
|
h := NewHandler(indexer, owner)
|
|
|
|
|
2013-11-16 23:18:16 +00:00
|
|
|
if slurpToMemory {
|
|
|
|
ii := indexer.(*index.Index)
|
2016-08-23 23:25:54 +00:00
|
|
|
ii.Lock()
|
2013-11-16 23:18:16 +00:00
|
|
|
corpus, err := ii.KeepInMemory()
|
|
|
|
if err != nil {
|
2016-08-23 23:25:54 +00:00
|
|
|
ii.Unlock()
|
2013-11-16 23:18:16 +00:00
|
|
|
return nil, fmt.Errorf("error slurping index to memory: %v", err)
|
|
|
|
}
|
2016-05-12 04:05:43 +00:00
|
|
|
h.SetCorpus(corpus)
|
2016-08-23 23:25:54 +00:00
|
|
|
ii.Unlock()
|
2013-11-16 23:18:16 +00:00
|
|
|
}
|
2014-05-08 19:04:08 +00:00
|
|
|
|
2013-11-16 23:18:16 +00:00
|
|
|
return h, nil
|
2011-03-13 23:38:32 +00:00
|
|
|
}
|
|
|
|
|
2018-01-31 18:30:47 +00:00
|
|
|
func newOwner(ownerCfg serverconfig.Owner) (*index.Owner, error) {
|
|
|
|
entity, err := jsonsign.EntityFromSecring(ownerCfg.Identity, ownerCfg.SecringFile)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
armoredPublicKey, err := jsonsign.ArmoredPublicKey(entity)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return index.NewOwner(ownerCfg.Identity, blob.RefFromString(armoredPublicKey)), nil
|
|
|
|
}
|
|
|
|
|
2013-10-06 06:46:39 +00:00
|
|
|
// Owner returns Handler owner's public key blobref.
|
2018-01-31 18:30:47 +00:00
|
|
|
// TODO(mpl): we're changing the index & search funcs to take a keyID (string)
|
|
|
|
// or an *index.Owner, so any new func should probably not take/use h.Owner()
|
|
|
|
// either.
|
2013-08-04 02:54:30 +00:00
|
|
|
func (h *Handler) Owner() blob.Ref {
|
2013-10-06 06:46:39 +00:00
|
|
|
// TODO: figure out a plan for an owner having multiple active public keys, or public
|
|
|
|
// key rotation
|
2018-01-31 18:30:47 +00:00
|
|
|
return h.owner.BlobRef()
|
2011-06-23 19:11:01 +00:00
|
|
|
}
|
|
|
|
|
2013-11-16 23:00:30 +00:00
|
|
|
func (h *Handler) Index() index.Interface {
|
2011-06-23 19:11:01 +00:00
|
|
|
return h.index
|
|
|
|
}
|
2011-03-14 00:14:48 +00:00
|
|
|
|
2018-01-13 21:40:52 +00:00
|
|
|
// HasLegacySHA1 reports whether the server has legacy SHA-1 blobs indexed.
|
|
|
|
func (h *Handler) HasLegacySHA1() bool {
|
|
|
|
idx, ok := h.index.(*index.Index)
|
|
|
|
if !ok {
|
|
|
|
log.Printf("Cannot guess for legacy SHA1 because we don't have an *index.Index")
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
ok, err := idx.HasLegacySHA1()
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("Cannot guess for legacy SHA1: %v", err)
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
return ok
|
|
|
|
}
|
|
|
|
|
2013-12-08 04:19:20 +00:00
|
|
|
var getHandler = map[string]func(*Handler, http.ResponseWriter, *http.Request){
|
|
|
|
"ws": (*Handler).serveWebSocket,
|
|
|
|
"recent": (*Handler).serveRecentPermanodes,
|
|
|
|
"permanodeattr": (*Handler).servePermanodesWithAttr,
|
|
|
|
"describe": (*Handler).serveDescribe,
|
|
|
|
"claims": (*Handler).serveClaims,
|
|
|
|
"files": (*Handler).serveFiles,
|
|
|
|
"signerattrvalue": (*Handler).serveSignerAttrValue,
|
|
|
|
"signerpaths": (*Handler).serveSignerPaths,
|
|
|
|
"edgesto": (*Handler).serveEdgesTo,
|
|
|
|
}
|
|
|
|
|
2014-04-19 19:58:55 +00:00
|
|
|
var postHandler = map[string]func(*Handler, http.ResponseWriter, *http.Request){
|
|
|
|
"describe": (*Handler).serveDescribe,
|
|
|
|
"query": (*Handler).serveQuery,
|
|
|
|
}
|
|
|
|
|
2017-12-11 02:07:07 +00:00
|
|
|
func (h *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
2013-06-12 09:17:30 +00:00
|
|
|
suffix := httputil.PathSuffix(req)
|
2011-05-30 22:44:25 +00:00
|
|
|
|
2014-04-19 19:58:55 +00:00
|
|
|
handlers := getHandler
|
|
|
|
switch {
|
|
|
|
case httputil.IsGet(req):
|
|
|
|
// use default from above
|
|
|
|
case req.Method == "POST":
|
|
|
|
handlers = postHandler
|
|
|
|
default:
|
|
|
|
handlers = nil
|
|
|
|
}
|
|
|
|
fn := handlers[strings.TrimPrefix(suffix, "camli/search/")]
|
|
|
|
if fn != nil {
|
2017-12-11 02:07:07 +00:00
|
|
|
fn(h, rw, req)
|
2014-04-19 19:58:55 +00:00
|
|
|
return
|
2013-11-08 16:32:51 +00:00
|
|
|
}
|
2011-05-30 22:44:25 +00:00
|
|
|
|
2011-06-09 19:55:38 +00:00
|
|
|
// TODO: discovery for the endpoints & better error message with link to discovery info
|
2015-05-02 12:01:54 +00:00
|
|
|
ret := camtypes.SearchErrorResponse{
|
|
|
|
Error: "Unsupported search path or method",
|
|
|
|
ErrorType: "input",
|
|
|
|
}
|
|
|
|
httputil.ReturnJSON(rw, &ret)
|
2011-05-30 22:44:25 +00:00
|
|
|
}
|
|
|
|
|
2013-04-01 18:18:49 +00:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2013-02-08 05:34:25 +00:00
|
|
|
// RecentRequest is a request to get a RecentResponse.
|
|
|
|
type RecentRequest struct {
|
2015-06-16 16:14:08 +00:00
|
|
|
N int // if zero, default number of results
|
|
|
|
Before time.Time // if zero, now
|
2013-02-08 05:34:25 +00:00
|
|
|
}
|
|
|
|
|
2013-02-08 05:55:17 +00:00
|
|
|
func (r *RecentRequest) URLSuffix() string {
|
2013-11-26 04:35:59 +00:00
|
|
|
var buf bytes.Buffer
|
2015-06-16 16:14:08 +00:00
|
|
|
fmt.Fprintf(&buf, "camli/search/recent?n=%d", r.n())
|
2013-11-26 04:35:59 +00:00
|
|
|
if !r.Before.IsZero() {
|
|
|
|
fmt.Fprintf(&buf, "&before=%s", types.Time3339(r.Before))
|
|
|
|
}
|
|
|
|
return buf.String()
|
2013-02-08 05:55:17 +00:00
|
|
|
}
|
|
|
|
|
2013-02-11 22:35:19 +00:00
|
|
|
// fromHTTP panics with an httputil value on failure
|
|
|
|
func (r *RecentRequest) fromHTTP(req *http.Request) {
|
2013-02-08 05:34:25 +00:00
|
|
|
r.N, _ = strconv.Atoi(req.FormValue("n"))
|
2013-11-26 04:35:59 +00:00
|
|
|
if before := req.FormValue("before"); before != "" {
|
|
|
|
r.Before = time.Time(types.ParseTime3339OrZero(before))
|
|
|
|
}
|
2013-02-08 05:34:25 +00:00
|
|
|
}
|
|
|
|
|
2013-04-01 18:18:49 +00:00
|
|
|
// n returns the sanitized maximum number of search results.
|
2013-02-08 05:34:25 +00:00
|
|
|
func (r *RecentRequest) n() int {
|
2013-04-01 18:18:49 +00:00
|
|
|
return sanitizeNumResults(r.N)
|
2013-02-08 05:34:25 +00:00
|
|
|
}
|
|
|
|
|
2013-02-08 17:27:23 +00:00
|
|
|
// WithAttrRequest is a request to get a WithAttrResponse.
|
|
|
|
type WithAttrRequest struct {
|
2013-08-04 02:54:30 +00:00
|
|
|
N int // max number of results
|
|
|
|
Signer blob.Ref // if nil, will use the server's default owner (if configured)
|
2013-02-08 17:27:23 +00:00
|
|
|
// Requested attribute. If blank, all attributes are searched (for Value)
|
|
|
|
// as fulltext.
|
|
|
|
Attr string
|
|
|
|
// Value of the requested attribute. If blank, permanodes which have
|
2013-07-10 11:10:18 +00:00
|
|
|
// request.Attr as an attribute are searched.
|
2015-06-16 16:14:08 +00:00
|
|
|
Value string
|
|
|
|
Fuzzy bool // fulltext search (if supported).
|
2016-07-28 00:06:03 +00:00
|
|
|
// At, if non-zero, specifies that the attribute must have been set at
|
|
|
|
// the latest at At.
|
|
|
|
At time.Time
|
2013-02-08 17:27:23 +00:00
|
|
|
}
|
|
|
|
|
2013-07-10 11:10:18 +00:00
|
|
|
func (r *WithAttrRequest) URLSuffix() string {
|
2016-07-28 00:06:03 +00:00
|
|
|
s := fmt.Sprintf("camli/search/permanodeattr?signer=%v&value=%v&fuzzy=%v&attr=%v&max=%v",
|
2015-06-16 16:14:08 +00:00
|
|
|
r.Signer, url.QueryEscape(r.Value), r.Fuzzy, r.Attr, r.N)
|
2016-07-28 00:06:03 +00:00
|
|
|
if !r.At.IsZero() {
|
|
|
|
s += fmt.Sprintf("&at=%s", types.Time3339(r.At))
|
|
|
|
}
|
|
|
|
return s
|
2013-07-10 11:10:18 +00:00
|
|
|
}
|
|
|
|
|
2013-02-11 22:35:19 +00:00
|
|
|
// fromHTTP panics with an httputil value on failure
|
|
|
|
func (r *WithAttrRequest) fromHTTP(req *http.Request) {
|
2013-08-04 02:54:30 +00:00
|
|
|
r.Signer = blob.ParseOrZero(req.FormValue("signer"))
|
2013-02-08 17:27:23 +00:00
|
|
|
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
|
|
|
|
max := req.FormValue("max")
|
|
|
|
if max != "" {
|
|
|
|
maxR, err := strconv.Atoi(max)
|
|
|
|
if err != nil {
|
2013-02-11 22:35:19 +00:00
|
|
|
panic(httputil.InvalidParameterError("max"))
|
2013-02-08 17:27:23 +00:00
|
|
|
}
|
2013-04-01 18:18:49 +00:00
|
|
|
r.N = maxR
|
2013-02-08 17:27:23 +00:00
|
|
|
}
|
2013-04-01 18:18:49 +00:00
|
|
|
r.N = r.n()
|
2016-07-28 00:06:03 +00:00
|
|
|
if at := req.FormValue("at"); at != "" {
|
|
|
|
r.At = time.Time(types.ParseTime3339OrZero(at))
|
|
|
|
}
|
2013-04-01 18:18:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// n returns the sanitized maximum number of search results.
|
|
|
|
func (r *WithAttrRequest) n() int {
|
|
|
|
return sanitizeNumResults(r.N)
|
2013-02-11 22:35:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// ClaimsRequest is a request to get a ClaimsResponse.
|
|
|
|
type ClaimsRequest struct {
|
2013-08-04 02:54:30 +00:00
|
|
|
Permanode blob.Ref
|
2014-03-12 17:23:55 +00:00
|
|
|
|
|
|
|
// AttrFilter optionally filters claims about the given attribute.
|
|
|
|
// If empty, all claims for the given Permanode are returned.
|
|
|
|
AttrFilter string
|
2013-02-11 22:35:19 +00:00
|
|
|
}
|
|
|
|
|
2014-01-14 19:48:49 +00:00
|
|
|
func (r *ClaimsRequest) URLSuffix() string {
|
2014-03-19 21:59:13 +00:00
|
|
|
return fmt.Sprintf("camli/search/claims?permanode=%v&attrFilter=%s",
|
|
|
|
r.Permanode, url.QueryEscape(r.AttrFilter))
|
2014-01-14 19:48:49 +00:00
|
|
|
}
|
|
|
|
|
2013-02-11 22:35:19 +00:00
|
|
|
// fromHTTP panics with an httputil value on failure
|
|
|
|
func (r *ClaimsRequest) fromHTTP(req *http.Request) {
|
|
|
|
r.Permanode = httputil.MustGetBlobRef(req, "permanode")
|
2014-03-12 17:23:55 +00:00
|
|
|
r.AttrFilter = req.FormValue("attrFilter")
|
2013-02-08 17:27:23 +00:00
|
|
|
}
|
|
|
|
|
2013-02-12 23:11:59 +00:00
|
|
|
// SignerPathsRequest is a request to get a SignerPathsResponse.
|
|
|
|
type SignerPathsRequest struct {
|
2013-08-04 02:54:30 +00:00
|
|
|
Signer blob.Ref
|
|
|
|
Target blob.Ref
|
2013-02-12 23:11:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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")
|
|
|
|
}
|
|
|
|
|
2013-04-26 15:31:05 +00:00
|
|
|
// EdgesRequest is a request to get an EdgesResponse.
|
|
|
|
type EdgesRequest struct {
|
|
|
|
// The blob we want to find as a reference.
|
2013-08-04 02:54:30 +00:00
|
|
|
ToRef blob.Ref
|
2013-04-26 15:31:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// fromHTTP panics with an httputil value on failure
|
|
|
|
func (r *EdgesRequest) fromHTTP(req *http.Request) {
|
|
|
|
r.ToRef = httputil.MustGetBlobRef(req, "blobref")
|
|
|
|
}
|
|
|
|
|
2014-03-17 23:27:37 +00:00
|
|
|
// TODO(mpl): it looks like we never populate RecentResponse.Error*, shouldn't we remove them?
|
|
|
|
// Same for WithAttrResponse. I suppose it doesn't matter much if we end up removing GetRecentPermanodes anyway...
|
|
|
|
|
2013-02-07 17:33:00 +00:00
|
|
|
// RecentResponse is the JSON response from $searchRoot/camli/search/recent.
|
|
|
|
type RecentResponse struct {
|
2013-02-08 05:34:25 +00:00
|
|
|
Recent []*RecentItem `json:"recent"`
|
|
|
|
Meta MetaMap `json:"meta"`
|
2013-02-08 05:55:17 +00:00
|
|
|
|
|
|
|
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
|
2013-02-07 17:33:00 +00:00
|
|
|
}
|
|
|
|
|
2013-02-08 17:27:23 +00:00
|
|
|
// WithAttrResponse is the JSON response from $searchRoot/camli/search/permanodeattr.
|
|
|
|
type WithAttrResponse struct {
|
|
|
|
WithAttr []*WithAttrItem `json:"withAttr"`
|
|
|
|
Meta MetaMap `json:"meta"`
|
2013-07-10 11:10:18 +00:00
|
|
|
|
|
|
|
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
|
2013-02-08 17:27:23 +00:00
|
|
|
}
|
|
|
|
|
2013-02-11 22:35:19 +00:00
|
|
|
// ClaimsResponse is the JSON response from $searchRoot/camli/search/claims.
|
|
|
|
type ClaimsResponse struct {
|
|
|
|
Claims []*ClaimsItem `json:"claims"`
|
|
|
|
}
|
|
|
|
|
2013-02-08 17:27:23 +00:00
|
|
|
// SignerPathsResponse is the JSON response from $searchRoot/camli/search/signerpaths.
|
|
|
|
type SignerPathsResponse struct {
|
|
|
|
Paths []*SignerPathsItem `json:"paths"`
|
|
|
|
Meta MetaMap `json:"meta"`
|
|
|
|
}
|
|
|
|
|
2013-02-07 16:05:31 +00:00
|
|
|
// A RecentItem is an item returned from $searchRoot/camli/search/recent in the "recent" list.
|
|
|
|
type RecentItem struct {
|
2013-08-04 02:54:30 +00:00
|
|
|
BlobRef blob.Ref `json:"blobref"`
|
|
|
|
ModTime types.Time3339 `json:"modtime"`
|
|
|
|
Owner blob.Ref `json:"owner"`
|
2013-02-07 16:05:31 +00:00
|
|
|
}
|
|
|
|
|
2013-02-08 17:27:23 +00:00
|
|
|
// A WithAttrItem is an item returned from $searchRoot/camli/search/permanodeattr.
|
|
|
|
type WithAttrItem struct {
|
2013-08-04 02:54:30 +00:00
|
|
|
Permanode blob.Ref `json:"permanode"`
|
2013-02-08 17:27:23 +00:00
|
|
|
}
|
|
|
|
|
2013-02-11 22:35:19 +00:00
|
|
|
// A ClaimsItem is an item returned from $searchRoot/camli/search/claims.
|
|
|
|
type ClaimsItem struct {
|
2013-08-04 02:54:30 +00:00
|
|
|
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"`
|
2013-02-11 22:35:19 +00:00
|
|
|
}
|
|
|
|
|
2013-02-08 17:27:23 +00:00
|
|
|
// A SignerPathsItem is an item returned from $searchRoot/camli/search/signerpaths.
|
|
|
|
type SignerPathsItem struct {
|
2013-08-04 02:54:30 +00:00
|
|
|
ClaimRef blob.Ref `json:"claimRef"`
|
|
|
|
BaseRef blob.Ref `json:"baseRef"`
|
|
|
|
Suffix string `json:"suffix"`
|
2013-02-08 17:27:23 +00:00
|
|
|
}
|
|
|
|
|
2013-04-26 15:31:05 +00:00
|
|
|
// EdgesResponse is the JSON response from $searchRoot/camli/search/edgesto.
|
|
|
|
type EdgesResponse struct {
|
2013-08-04 02:54:30 +00:00
|
|
|
ToRef blob.Ref `json:"toRef"`
|
|
|
|
EdgesTo []*EdgeItem `json:"edgesTo"`
|
2013-04-26 15:31:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// An EdgeItem is an item returned from $searchRoot/camli/search/edgesto.
|
|
|
|
type EdgeItem struct {
|
2021-01-17 03:05:35 +00:00
|
|
|
From blob.Ref `json:"from"`
|
|
|
|
FromType schema.CamliType `json:"fromType"`
|
2013-04-26 15:31:05 +00:00
|
|
|
}
|
|
|
|
|
2013-03-05 18:15:22 +00:00
|
|
|
var testHookBug121 = func() {}
|
|
|
|
|
2013-02-12 23:11:59 +00:00
|
|
|
// GetRecentPermanodes returns recently-modified permanodes.
|
2018-01-16 23:03:16 +00:00
|
|
|
func (h *Handler) GetRecentPermanodes(ctx context.Context, req *RecentRequest) (*RecentResponse, error) {
|
2017-12-11 02:07:07 +00:00
|
|
|
h.index.RLock()
|
|
|
|
defer h.index.RUnlock()
|
2016-04-22 04:34:24 +00:00
|
|
|
|
2013-11-16 23:00:30 +00:00
|
|
|
ch := make(chan camtypes.RecentPermanode)
|
2013-09-04 01:22:05 +00:00
|
|
|
errch := make(chan error, 1)
|
2013-11-26 04:35:59 +00:00
|
|
|
before := time.Now()
|
|
|
|
if !req.Before.IsZero() {
|
|
|
|
before = req.Before
|
|
|
|
}
|
2011-03-14 00:14:48 +00:00
|
|
|
go func() {
|
2018-01-31 18:30:47 +00:00
|
|
|
// TODO(mpl): change index funcs to take signer keyID. dont care for now, just
|
|
|
|
// fixing the essential search and describe ones.
|
|
|
|
errch <- h.index.GetRecentPermanodes(ctx, ch, h.owner.BlobRef(), req.n(), before)
|
2011-03-14 00:14:48 +00:00
|
|
|
}()
|
2011-03-14 02:27:59 +00:00
|
|
|
|
2017-12-11 02:07:07 +00:00
|
|
|
dr := h.NewDescribeRequest()
|
2011-06-12 07:20:57 +00:00
|
|
|
|
2013-02-07 16:05:31 +00:00
|
|
|
var recent []*RecentItem
|
2011-03-14 00:14:48 +00:00
|
|
|
for res := range ch {
|
2016-08-27 00:01:24 +00:00
|
|
|
dr.StartDescribe(ctx, res.Permanode, 2)
|
2013-02-07 16:05:31 +00:00
|
|
|
recent = append(recent, &RecentItem{
|
2013-11-16 23:00:30 +00:00
|
|
|
BlobRef: res.Permanode,
|
2013-02-07 16:05:31 +00:00
|
|
|
Owner: res.Signer,
|
2013-11-16 23:00:30 +00:00
|
|
|
ModTime: types.Time3339(res.LastModTime),
|
2013-02-07 16:05:31 +00:00
|
|
|
})
|
Rename import paths from camlistore.org to perkeep.org.
Part of the project renaming, issue #981.
After this, users will need to mv their $GOPATH/src/camlistore.org to
$GOPATH/src/perkeep.org. Sorry.
This doesn't yet rename the tools like camlistored, camput, camget,
camtool, etc.
Also, this only moves the lru package to internal. More will move to
internal later.
Also, this doesn't yet remove the "/pkg/" directory. That'll likely
happen later.
This updates some docs, but not all.
devcam test now passes again, even with Go 1.10 (which requires vet
checks are clean too). So a bunch of vet tests are fixed in this CL
too, and a bunch of other broken tests are now fixed (introduced from
the past week of merging the CL backlog).
Change-Id: If580db1691b5b99f8ed6195070789b1f44877dd4
2018-01-01 22:41:41 +00:00
|
|
|
testHookBug121() // http://perkeep.org/issue/121
|
2011-03-14 00:14:48 +00:00
|
|
|
}
|
2011-03-14 02:27:59 +00:00
|
|
|
|
2013-02-08 05:34:25 +00:00
|
|
|
if err := <-errch; err != nil {
|
|
|
|
return nil, err
|
2011-03-14 02:27:59 +00:00
|
|
|
}
|
2011-06-12 07:20:57 +00:00
|
|
|
|
2015-06-16 16:14:08 +00:00
|
|
|
metaMap, err := dr.metaMap()
|
2013-02-07 17:33:00 +00:00
|
|
|
if err != nil {
|
2013-02-08 05:34:25 +00:00
|
|
|
return nil, err
|
2013-02-07 17:33:00 +00:00
|
|
|
}
|
|
|
|
|
2013-02-08 05:34:25 +00:00
|
|
|
res := &RecentResponse{
|
2013-02-07 17:33:00 +00:00
|
|
|
Recent: recent,
|
|
|
|
Meta: metaMap,
|
2013-02-08 05:34:25 +00:00
|
|
|
}
|
|
|
|
return res, nil
|
|
|
|
}
|
|
|
|
|
2017-12-11 02:07:07 +00:00
|
|
|
func (h *Handler) serveRecentPermanodes(rw http.ResponseWriter, req *http.Request) {
|
2013-02-11 22:35:19 +00:00
|
|
|
defer httputil.RecoverJSON(rw, req)
|
2013-02-12 23:11:59 +00:00
|
|
|
var rr RecentRequest
|
2013-02-11 22:35:19 +00:00
|
|
|
rr.fromHTTP(req)
|
2018-01-16 23:03:16 +00:00
|
|
|
res, err := h.GetRecentPermanodes(req.Context(), &rr)
|
2013-02-08 05:34:25 +00:00
|
|
|
if err != nil {
|
2013-02-09 06:21:09 +00:00
|
|
|
httputil.ServeJSONError(rw, err)
|
2013-02-08 05:34:25 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
httputil.ReturnJSON(rw, res)
|
2011-05-30 22:44:25 +00:00
|
|
|
}
|
|
|
|
|
2013-02-12 23:11:59 +00:00
|
|
|
// GetPermanodesWithAttr returns permanodes with attribute req.Attr
|
|
|
|
// having the req.Value as a value.
|
|
|
|
// See WithAttrRequest for more details about the query.
|
2017-12-11 02:07:07 +00:00
|
|
|
func (h *Handler) GetPermanodesWithAttr(req *WithAttrRequest) (*WithAttrResponse, error) {
|
2016-04-22 04:34:24 +00:00
|
|
|
ctx := context.TODO()
|
|
|
|
|
2017-12-11 02:07:07 +00:00
|
|
|
h.index.RLock()
|
|
|
|
defer h.index.RUnlock()
|
2016-04-22 04:34:24 +00:00
|
|
|
|
2013-08-04 02:54:30 +00:00
|
|
|
ch := make(chan blob.Ref, buffered)
|
2013-09-04 01:22:05 +00:00
|
|
|
errch := make(chan error, 1)
|
2011-08-25 13:15:38 +00:00
|
|
|
go func() {
|
2013-07-10 11:10:18 +00:00
|
|
|
signer := req.Signer
|
2013-08-04 02:54:30 +00:00
|
|
|
if !signer.Valid() {
|
2018-01-31 18:30:47 +00:00
|
|
|
signer = h.owner.BlobRef()
|
2013-07-10 11:10:18 +00:00
|
|
|
}
|
2017-12-11 02:07:07 +00:00
|
|
|
errch <- h.index.SearchPermanodesWithAttr(ctx, ch,
|
2013-11-16 23:00:30 +00:00
|
|
|
&camtypes.PermanodeByAttrRequest{
|
2013-09-04 01:22:05 +00:00
|
|
|
Attribute: req.Attr,
|
2013-02-08 17:27:23 +00:00
|
|
|
Query: req.Value,
|
2013-07-10 11:10:18 +00:00
|
|
|
Signer: signer,
|
2013-02-08 17:27:23 +00:00
|
|
|
FuzzyMatch: req.Fuzzy,
|
2013-09-04 01:22:05 +00:00
|
|
|
MaxResults: req.N,
|
2016-07-28 00:06:03 +00:00
|
|
|
At: req.At,
|
2013-09-04 01:22:05 +00:00
|
|
|
})
|
2011-08-25 13:15:38 +00:00
|
|
|
}()
|
|
|
|
|
2017-12-11 02:07:07 +00:00
|
|
|
dr := h.NewDescribeRequest()
|
2011-08-25 13:15:38 +00:00
|
|
|
|
2013-02-08 17:27:23 +00:00
|
|
|
var withAttr []*WithAttrItem
|
2011-08-25 13:15:38 +00:00
|
|
|
for res := range ch {
|
2016-08-27 00:01:24 +00:00
|
|
|
dr.StartDescribe(ctx, res, 2)
|
2013-02-08 17:27:23 +00:00
|
|
|
withAttr = append(withAttr, &WithAttrItem{
|
|
|
|
Permanode: res,
|
|
|
|
})
|
2011-08-25 13:15:38 +00:00
|
|
|
}
|
|
|
|
|
2015-06-16 16:14:08 +00:00
|
|
|
metaMap, err := dr.metaMap()
|
2011-08-25 13:15:38 +00:00
|
|
|
if err != nil {
|
2013-02-08 17:27:23 +00:00
|
|
|
return nil, err
|
2011-08-25 13:15:38 +00:00
|
|
|
}
|
|
|
|
|
2013-09-04 01:22:05 +00:00
|
|
|
if err := <-errch; err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2013-02-08 17:27:23 +00:00
|
|
|
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".
|
2017-12-11 02:07:07 +00:00
|
|
|
func (h *Handler) servePermanodesWithAttr(rw http.ResponseWriter, req *http.Request) {
|
2013-02-11 22:35:19 +00:00
|
|
|
defer httputil.RecoverJSON(rw, req)
|
2013-02-12 23:11:59 +00:00
|
|
|
var wr WithAttrRequest
|
2013-02-11 22:35:19 +00:00
|
|
|
wr.fromHTTP(req)
|
2017-12-11 02:07:07 +00:00
|
|
|
res, err := h.GetPermanodesWithAttr(&wr)
|
2013-02-08 17:27:23 +00:00
|
|
|
if err != nil {
|
2013-02-09 06:21:09 +00:00
|
|
|
httputil.ServeJSONError(rw, err)
|
2013-02-08 17:27:23 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
httputil.ReturnJSON(rw, res)
|
2011-08-25 13:15:38 +00:00
|
|
|
}
|
|
|
|
|
2017-12-11 02:07:07 +00:00
|
|
|
// GetClaims returns the claims on req.Permanode signed by h.owner.
|
|
|
|
func (h *Handler) GetClaims(req *ClaimsRequest) (*ClaimsResponse, error) {
|
2013-08-04 02:54:30 +00:00
|
|
|
if !req.Permanode.Valid() {
|
2017-12-10 09:13:00 +00:00
|
|
|
return nil, errors.New("error getting claims: nil permanode")
|
2013-02-12 15:46:16 +00:00
|
|
|
}
|
2017-12-11 02:07:07 +00:00
|
|
|
h.index.RLock()
|
|
|
|
defer h.index.RUnlock()
|
2016-04-22 04:34:24 +00:00
|
|
|
|
|
|
|
ctx := context.TODO()
|
2013-11-17 22:54:30 +00:00
|
|
|
var claims []camtypes.Claim
|
2018-01-31 18:30:47 +00:00
|
|
|
claims, err := h.index.AppendClaims(ctx, claims, req.Permanode, h.owner.KeyID(), req.AttrFilter)
|
2011-06-03 22:23:23 +00:00
|
|
|
if err != nil {
|
2013-02-11 22:35:19 +00:00
|
|
|
return nil, fmt.Errorf("Error getting claims of %s: %v", req.Permanode.String(), err)
|
|
|
|
}
|
2013-11-17 22:54:30 +00:00
|
|
|
sort.Sort(camtypes.ClaimsByDate(claims))
|
2013-02-11 22:35:19 +00:00
|
|
|
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,
|
2011-06-03 22:23:23 +00:00
|
|
|
}
|
2013-02-11 22:35:19 +00:00
|
|
|
jclaims = append(jclaims, jclaim)
|
2011-06-03 22:23:23 +00:00
|
|
|
}
|
|
|
|
|
2013-02-11 22:35:19 +00:00
|
|
|
res := &ClaimsResponse{
|
|
|
|
Claims: jclaims,
|
|
|
|
}
|
|
|
|
return res, nil
|
|
|
|
}
|
|
|
|
|
2017-12-11 02:07:07 +00:00
|
|
|
func (h *Handler) serveClaims(rw http.ResponseWriter, req *http.Request) {
|
2013-02-11 22:35:19 +00:00
|
|
|
defer httputil.RecoverJSON(rw, req)
|
2016-04-22 04:34:24 +00:00
|
|
|
|
2017-12-11 02:07:07 +00:00
|
|
|
h.index.RLock()
|
|
|
|
defer h.index.RUnlock()
|
2016-04-22 04:34:24 +00:00
|
|
|
|
2013-02-12 23:11:59 +00:00
|
|
|
var cr ClaimsRequest
|
2013-02-11 22:35:19 +00:00
|
|
|
cr.fromHTTP(req)
|
2017-12-11 02:07:07 +00:00
|
|
|
res, err := h.GetClaims(&cr)
|
2013-02-11 22:35:19 +00:00
|
|
|
if err != nil {
|
|
|
|
httputil.ServeJSONError(rw, err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
httputil.ReturnJSON(rw, res)
|
2011-06-03 22:23:23 +00:00
|
|
|
}
|
|
|
|
|
2017-12-11 02:07:07 +00:00
|
|
|
func (h *Handler) serveFiles(rw http.ResponseWriter, req *http.Request) {
|
2015-05-02 12:01:54 +00:00
|
|
|
var ret camtypes.FileSearchResponse
|
|
|
|
defer httputil.ReturnJSON(rw, &ret)
|
2011-06-09 19:55:38 +00:00
|
|
|
|
2017-12-11 02:07:07 +00:00
|
|
|
h.index.RLock()
|
|
|
|
defer h.index.RUnlock()
|
2016-04-22 04:34:24 +00:00
|
|
|
|
2018-01-13 21:40:52 +00:00
|
|
|
if err := req.ParseForm(); err != nil {
|
|
|
|
ret.Error = err.Error()
|
|
|
|
ret.ErrorType = "input"
|
|
|
|
return
|
|
|
|
}
|
|
|
|
values, ok := req.Form["wholedigest"]
|
2013-08-04 02:54:30 +00:00
|
|
|
if !ok {
|
2018-01-13 21:40:52 +00:00
|
|
|
ret.Error = "Missing 'wholedigest' param"
|
2015-05-02 12:01:54 +00:00
|
|
|
ret.ErrorType = "input"
|
2011-06-09 19:55:38 +00:00
|
|
|
return
|
|
|
|
}
|
2018-01-13 21:40:52 +00:00
|
|
|
var digests []blob.Ref
|
|
|
|
for _, v := range values {
|
|
|
|
br, ok := blob.Parse(v)
|
|
|
|
if !ok {
|
|
|
|
ret.Error = "Invalid 'wholedigest' param"
|
|
|
|
ret.ErrorType = "input"
|
|
|
|
return
|
|
|
|
}
|
|
|
|
digests = append(digests, br)
|
|
|
|
}
|
2011-06-09 19:55:38 +00:00
|
|
|
|
2018-01-13 21:40:52 +00:00
|
|
|
files, err := h.index.ExistingFileSchemas(digests...)
|
2011-06-09 19:55:38 +00:00
|
|
|
if err != nil {
|
2015-05-02 12:01:54 +00:00
|
|
|
ret.Error = err.Error()
|
|
|
|
ret.ErrorType = "server"
|
2011-06-09 19:55:38 +00:00
|
|
|
}
|
2015-05-27 12:03:41 +00:00
|
|
|
// the ui code expects an object
|
|
|
|
if files == nil {
|
2018-01-13 21:40:52 +00:00
|
|
|
files = make(index.WholeRefToFile)
|
2015-05-27 12:03:41 +00:00
|
|
|
}
|
2015-05-02 12:01:54 +00:00
|
|
|
ret.Files = files
|
2011-06-09 19:55:38 +00:00
|
|
|
}
|
|
|
|
|
2013-02-07 17:33:00 +00:00
|
|
|
// SignerAttrValueResponse is the JSON response to $search/camli/search/signerattrvalue
|
|
|
|
type SignerAttrValueResponse struct {
|
2013-08-04 02:54:30 +00:00
|
|
|
Permanode blob.Ref `json:"permanode"`
|
|
|
|
Meta MetaMap `json:"meta"`
|
2013-02-07 17:33:00 +00:00
|
|
|
}
|
|
|
|
|
2017-12-11 02:07:07 +00:00
|
|
|
func (h *Handler) serveSignerAttrValue(rw http.ResponseWriter, req *http.Request) {
|
2013-02-09 06:21:09 +00:00
|
|
|
defer httputil.RecoverJSON(rw, req)
|
2016-04-22 04:34:24 +00:00
|
|
|
ctx := context.TODO()
|
2013-02-09 06:21:09 +00:00
|
|
|
signer := httputil.MustGetBlobRef(req, "signer")
|
|
|
|
attr := httputil.MustGet(req, "attr")
|
|
|
|
value := httputil.MustGet(req, "value")
|
2011-06-23 05:21:18 +00:00
|
|
|
|
2017-12-11 02:07:07 +00:00
|
|
|
h.index.RLock()
|
|
|
|
defer h.index.RUnlock()
|
2016-04-22 04:34:24 +00:00
|
|
|
|
2017-12-11 02:07:07 +00:00
|
|
|
pn, err := h.index.PermanodeOfSignerAttrValue(ctx, signer, attr, value)
|
2011-06-23 05:21:18 +00:00
|
|
|
if err != nil {
|
2013-02-09 06:21:09 +00:00
|
|
|
httputil.ServeJSONError(rw, err)
|
2013-02-07 17:33:00 +00:00
|
|
|
return
|
|
|
|
}
|
2011-06-23 05:21:18 +00:00
|
|
|
|
2017-12-11 02:07:07 +00:00
|
|
|
dr := h.NewDescribeRequest()
|
2016-08-27 00:01:24 +00:00
|
|
|
dr.StartDescribe(ctx, pn, 2)
|
2013-02-10 20:59:05 +00:00
|
|
|
metaMap, err := dr.metaMap()
|
2013-02-07 17:33:00 +00:00
|
|
|
if err != nil {
|
2013-02-09 06:21:09 +00:00
|
|
|
httputil.ServeJSONError(rw, err)
|
2013-02-07 17:33:00 +00:00
|
|
|
return
|
2011-06-23 05:21:18 +00:00
|
|
|
}
|
2013-02-07 17:33:00 +00:00
|
|
|
|
|
|
|
httputil.ReturnJSON(rw, &SignerAttrValueResponse{
|
|
|
|
Permanode: pn,
|
|
|
|
Meta: metaMap,
|
|
|
|
})
|
2011-06-23 05:21:18 +00:00
|
|
|
}
|
|
|
|
|
2013-04-26 15:31:05 +00:00
|
|
|
// EdgesTo returns edges that reference req.RefTo.
|
|
|
|
// It filters out since-deleted permanode edges.
|
2017-12-11 02:07:07 +00:00
|
|
|
func (h *Handler) EdgesTo(req *EdgesRequest) (*EdgesResponse, error) {
|
2016-04-22 04:34:24 +00:00
|
|
|
ctx := context.TODO()
|
2017-12-11 02:07:07 +00:00
|
|
|
h.index.RLock()
|
|
|
|
defer h.index.RUnlock()
|
2016-04-22 04:34:24 +00:00
|
|
|
|
2013-04-26 15:31:05 +00:00
|
|
|
toRef := req.ToRef
|
2012-11-05 14:09:34 +00:00
|
|
|
toRefStr := toRef.String()
|
2013-04-26 15:31:05 +00:00
|
|
|
var edgeItems []*EdgeItem
|
2012-11-05 14:09:34 +00:00
|
|
|
|
2017-12-11 02:07:07 +00:00
|
|
|
edges, err := h.index.EdgesTo(toRef, nil)
|
2012-11-05 14:09:34 +00:00
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
2013-04-26 15:31:05 +00:00
|
|
|
type edgeOrError struct {
|
|
|
|
edge *EdgeItem // or nil
|
|
|
|
err error
|
2012-11-05 14:09:34 +00:00
|
|
|
}
|
2013-04-26 15:31:05 +00:00
|
|
|
resc := make(chan edgeOrError)
|
2013-11-16 23:00:30 +00:00
|
|
|
verify := func(edge *camtypes.Edge) {
|
2017-12-11 02:07:07 +00:00
|
|
|
db, err := h.NewDescribeRequest().DescribeSync(ctx, edge.From)
|
2012-11-05 14:09:34 +00:00
|
|
|
if err != nil {
|
2013-04-26 15:31:05 +00:00
|
|
|
resc <- edgeOrError{err: err}
|
2012-11-05 14:09:34 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
found := false
|
|
|
|
if db.Permanode != nil {
|
|
|
|
for attr, vv := range db.Permanode.Attr {
|
2013-11-16 23:00:30 +00:00
|
|
|
if index.IsBlobReferenceAttribute(attr) {
|
2012-11-05 14:09:34 +00:00
|
|
|
for _, v := range vv {
|
|
|
|
if v == toRefStr {
|
|
|
|
found = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2013-04-26 15:31:05 +00:00
|
|
|
var ei *EdgeItem
|
2012-11-05 14:09:34 +00:00
|
|
|
if found {
|
2013-04-26 15:31:05 +00:00
|
|
|
ei = &EdgeItem{
|
|
|
|
From: edge.From,
|
2021-01-17 03:05:35 +00:00
|
|
|
FromType: schema.TypePermanode,
|
2013-04-26 15:31:05 +00:00
|
|
|
}
|
2012-11-05 14:09:34 +00:00
|
|
|
}
|
2013-04-26 15:31:05 +00:00
|
|
|
resc <- edgeOrError{edge: ei}
|
2012-11-05 14:09:34 +00:00
|
|
|
}
|
|
|
|
verifying := 0
|
|
|
|
for _, edge := range edges {
|
2021-01-17 03:05:35 +00:00
|
|
|
if edge.FromType == schema.TypePermanode {
|
2012-11-05 14:09:34 +00:00
|
|
|
verifying++
|
|
|
|
go verify(edge)
|
|
|
|
continue
|
|
|
|
}
|
2013-04-26 15:31:05 +00:00
|
|
|
ei := &EdgeItem{
|
|
|
|
From: edge.From,
|
|
|
|
FromType: edge.FromType,
|
|
|
|
}
|
|
|
|
edgeItems = append(edgeItems, ei)
|
2012-11-05 14:09:34 +00:00
|
|
|
}
|
|
|
|
for i := 0; i < verifying; i++ {
|
|
|
|
res := <-resc
|
|
|
|
if res.err != nil {
|
2013-04-26 15:31:05 +00:00
|
|
|
return nil, res.err
|
2012-11-05 14:09:34 +00:00
|
|
|
}
|
2013-04-26 15:31:05 +00:00
|
|
|
if res.edge != nil {
|
|
|
|
edgeItems = append(edgeItems, res.edge)
|
2012-11-05 14:09:34 +00:00
|
|
|
}
|
|
|
|
}
|
2013-04-26 15:31:05 +00:00
|
|
|
|
|
|
|
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.
|
2017-12-11 02:07:07 +00:00
|
|
|
func (h *Handler) serveEdgesTo(rw http.ResponseWriter, req *http.Request) {
|
2013-04-26 15:31:05 +00:00
|
|
|
defer httputil.RecoverJSON(rw, req)
|
|
|
|
var er EdgesRequest
|
|
|
|
er.fromHTTP(req)
|
2017-12-11 02:07:07 +00:00
|
|
|
res, err := h.EdgesTo(&er)
|
2013-04-26 15:31:05 +00:00
|
|
|
if err != nil {
|
|
|
|
httputil.ServeJSONError(rw, err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
httputil.ReturnJSON(rw, res)
|
2012-11-05 14:09:34 +00:00
|
|
|
}
|
|
|
|
|
2017-12-11 02:07:07 +00:00
|
|
|
func (h *Handler) serveQuery(rw http.ResponseWriter, req *http.Request) {
|
2013-11-08 16:32:51 +00:00
|
|
|
defer httputil.RecoverJSON(rw, req)
|
|
|
|
|
|
|
|
var sq SearchQuery
|
pkg/server/app: proxy search requests for publisher
Some of the publisher features have moved from the server-side app to
the client-side app (the browser) thanks to gopherjs. Some of these
features imply doing some search queries against Camlistore, which
requires authentication. The server-side app receives the necessary
credentials on creation, from Camlistore. However, we can't just
communicate them to the client-side (as we do with the web UI) since the
publisher app itself does not require any auth and is supposed to be
exposed to the world.
Therefore, we need to allow some search queries to be done without
authentication.
To this end, the app handler on Camlistore now assumes a new role: it is
also a search proxy for the app. The app sends an unauthenticated search
query to the app handler (instead of directly to the search handler),
and it is the role of the app handler to verify that this query is
allowed for the app, and if yes, to forward the search to the Camlistore's
search handler.
We introduce a new mechanism to filter the search queries in the form of
a master query. Upon startup, the publisher registers, using the new
CAMLI_APP_MASTERQUERY_URL env var, a *search.SearchQuery with the app
handler. The app handler runs that query and caches all the blob refs
included in the response to that query. In the following, all incoming
search queries are run by the app handler, which checks that none of the
response blobs are out of the set defined by the aforementioned cached
blob refs. If that check fails, the search response is not forwarded to
the app/client.
The process can be improved in a subsequent CL (or patchset), with finer
grained domains, i.e. a master search query per published camliPath,
instead of one for the whole app handler.
Change-Id: I00d91ff73e0cbe78744bfae9878077dc3a8521f4
2016-07-20 15:54:29 +00:00
|
|
|
if err := sq.FromHTTP(req); err != nil {
|
2013-11-08 16:32:51 +00:00
|
|
|
httputil.ServeJSONError(rw, err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2018-01-16 23:03:16 +00:00
|
|
|
sr, err := h.Query(req.Context(), &sq)
|
2013-11-08 16:32:51 +00:00
|
|
|
if err != nil {
|
|
|
|
httputil.ServeJSONError(rw, err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
httputil.ReturnJSON(rw, sr)
|
|
|
|
}
|
|
|
|
|
2013-02-12 23:11:59 +00:00
|
|
|
// GetSignerPaths returns paths with a target of req.Target.
|
2017-12-11 02:07:07 +00:00
|
|
|
func (h *Handler) GetSignerPaths(req *SignerPathsRequest) (*SignerPathsResponse, error) {
|
2016-04-22 04:34:24 +00:00
|
|
|
ctx := context.TODO()
|
2013-08-04 02:54:30 +00:00
|
|
|
if !req.Signer.Valid() {
|
2017-12-10 09:13:00 +00:00
|
|
|
return nil, errors.New("error getting signer paths: nil signer")
|
2013-02-12 23:11:59 +00:00
|
|
|
}
|
2013-08-04 02:54:30 +00:00
|
|
|
if !req.Target.Valid() {
|
2017-12-10 09:13:00 +00:00
|
|
|
return nil, errors.New("error getting signer paths: nil target")
|
2013-02-12 23:11:59 +00:00
|
|
|
}
|
2017-12-11 02:07:07 +00:00
|
|
|
h.index.RLock()
|
|
|
|
defer h.index.RUnlock()
|
2016-04-22 04:34:24 +00:00
|
|
|
|
2017-12-11 02:07:07 +00:00
|
|
|
paths, err := h.index.PathsOfSignerTarget(ctx, req.Signer, req.Target)
|
2011-06-26 00:50:38 +00:00
|
|
|
if err != nil {
|
2013-02-12 23:11:59 +00:00
|
|
|
return nil, fmt.Errorf("Error getting paths of %s: %v", req.Target.String(), err)
|
2013-02-08 17:27:23 +00:00
|
|
|
}
|
|
|
|
var jpaths []*SignerPathsItem
|
|
|
|
for _, path := range paths {
|
|
|
|
jpaths = append(jpaths, &SignerPathsItem{
|
|
|
|
ClaimRef: path.Claim,
|
|
|
|
BaseRef: path.Base,
|
|
|
|
Suffix: path.Suffix,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2017-12-11 02:07:07 +00:00
|
|
|
dr := h.NewDescribeRequest()
|
2013-02-08 17:27:23 +00:00
|
|
|
for _, path := range paths {
|
2016-08-27 00:01:24 +00:00
|
|
|
dr.StartDescribe(ctx, path.Base, 2)
|
2011-06-26 00:50:38 +00:00
|
|
|
}
|
2013-02-10 20:59:05 +00:00
|
|
|
metaMap, err := dr.metaMap()
|
2013-02-08 17:27:23 +00:00
|
|
|
if err != nil {
|
2013-02-12 23:11:59 +00:00
|
|
|
return nil, err
|
2013-02-08 17:27:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
res := &SignerPathsResponse{
|
|
|
|
Paths: jpaths,
|
|
|
|
Meta: metaMap,
|
|
|
|
}
|
2013-02-12 23:11:59 +00:00
|
|
|
return res, nil
|
|
|
|
}
|
|
|
|
|
2017-12-11 02:07:07 +00:00
|
|
|
func (h *Handler) serveSignerPaths(rw http.ResponseWriter, req *http.Request) {
|
2013-02-12 23:11:59 +00:00
|
|
|
defer httputil.RecoverJSON(rw, req)
|
|
|
|
var sr SignerPathsRequest
|
|
|
|
sr.fromHTTP(req)
|
|
|
|
|
2017-12-11 02:07:07 +00:00
|
|
|
res, err := h.GetSignerPaths(&sr)
|
2013-02-12 23:11:59 +00:00
|
|
|
if err != nil {
|
|
|
|
httputil.ServeJSONError(rw, err)
|
|
|
|
return
|
|
|
|
}
|
2013-02-08 17:27:23 +00:00
|
|
|
httputil.ReturnJSON(rw, res)
|
2011-06-26 00:50:38 +00:00
|
|
|
}
|
|
|
|
|
2014-05-08 19:04:08 +00:00
|
|
|
// EvalSearchInput checks if its input is JSON. If so it returns a Constraint constructed from that JSON. Otherwise
|
|
|
|
// it assumes the input to be a search expression. It parses the expression and returns the parsed Constraint.
|
|
|
|
func evalSearchInput(in string) (*Constraint, error) {
|
|
|
|
if len(in) == 0 {
|
|
|
|
return nil, fmt.Errorf("empty expression")
|
|
|
|
}
|
|
|
|
if strings.HasPrefix(in, "{") && strings.HasSuffix(in, "}") {
|
|
|
|
cs := new(Constraint)
|
|
|
|
if err := json.NewDecoder(strings.NewReader(in)).Decode(&cs); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return cs, nil
|
|
|
|
} else {
|
|
|
|
sq, err := parseExpression(context.TODO(), in)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return sq.Constraint.Logical.B, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// getNamed displays the search expression or constraint json for the requested alias.
|
2018-01-16 23:03:16 +00:00
|
|
|
func (sh *Handler) getNamed(ctx context.Context, name string) (string, error) {
|
2014-05-08 19:04:08 +00:00
|
|
|
if sh.fetcher == nil {
|
|
|
|
return "", fmt.Errorf("GetNamed functionality not available")
|
|
|
|
}
|
2018-01-16 23:03:16 +00:00
|
|
|
sr, err := sh.Query(ctx, NamedSearch(name))
|
2014-05-08 19:04:08 +00:00
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(sr.Blobs) < 1 {
|
|
|
|
return "", fmt.Errorf("No named search found for: %s", name)
|
|
|
|
}
|
|
|
|
permaRef := sr.Blobs[0].Blob
|
|
|
|
substRefS := sr.Describe.Meta.Get(permaRef).Permanode.Attr.Get("camliContent")
|
|
|
|
br, ok := blob.Parse(substRefS)
|
|
|
|
if !ok {
|
|
|
|
return "", fmt.Errorf("Invalid blob ref: %s", substRefS)
|
|
|
|
}
|
|
|
|
|
2018-01-16 23:03:16 +00:00
|
|
|
reader, _, err := sh.fetcher.Fetch(ctx, br)
|
2014-05-08 19:04:08 +00:00
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
result, err := ioutil.ReadAll(reader)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
return string(result), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// NamedSearch returns a *SearchQuery to find the permanode of the search alias "name".
|
|
|
|
func NamedSearch(name string) *SearchQuery {
|
|
|
|
return &SearchQuery{
|
|
|
|
Constraint: &Constraint{
|
|
|
|
Permanode: &PermanodeConstraint{
|
|
|
|
Attr: "camliNamedSearch",
|
|
|
|
Value: name,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
Describe: &DescribeRequest{},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2011-05-30 22:44:25 +00:00
|
|
|
const camliTypePrefix = "application/json; camliType="
|