diff --git a/pkg/index/corpus.go b/pkg/index/corpus.go index f5bc4e650..12c6b95e2 100644 --- a/pkg/index/corpus.go +++ b/pkg/index/corpus.go @@ -659,6 +659,60 @@ func (c *Corpus) isDeletedLocked(br blob.Ref) bool { return false } +// PermanodeTimeLocked returns the time of the content in permanode. +func (c *Corpus) PermanodeTimeLocked(pn blob.Ref) (t time.Time, ok bool) { + // TODO(bradfitz): keep this time property cached on the permanode / files + + // TODO(bradfitz): finish implmenting all these + + // Priorities: + // -- Permanode explicit "camliTime" property + // -- EXIF GPS time + // -- Exif camera time + // -- File time + // -- File modtime + // -- camliContent claim set time + ccRef, ccTime, ok := c.pnCamliContentLocked(pn) + if !ok { + return + } + + fi, ok := c.files[ccRef] + if ok { + if fi.Time != nil { + return time.Time(*fi.Time), true + } + if fi.ModTime != nil { + return time.Time(*fi.ModTime), true + } + } + return ccTime, true +} + +func (c *Corpus) pnCamliContentLocked(pn blob.Ref) (cc blob.Ref, t time.Time, ok bool) { + // TODO(bradfitz): keep this property cached + pm, ok := c.permanodes[pn] + if !ok { + return + } + for _, cl := range pm.Claims { + if cl.Attr != "camliContent" { + continue + } + // TODO: pass down the 'PermanodeConstraint.At' parameter, and then do: if cl.Date.After(at) { continue } + switch cl.Type { + case string(schema.DelAttributeClaim): + cc = blob.Ref{} + t = time.Time{} + case string(schema.SetAttributeClaim): + cc = blob.ParseOrZero(cl.Value) + t = cl.Date + } + } + return cc, t, cc.Valid() + +} + // PermanodeModtime returns the latest modification time of the given // permanode. // diff --git a/pkg/search/expr.go b/pkg/search/expr.go index 8b1e97258..263c79e8e 100644 --- a/pkg/search/expr.go +++ b/pkg/search/expr.go @@ -23,9 +23,11 @@ import ( "regexp" "strconv" "strings" + "time" "camlistore.org/pkg/context" "camlistore.org/pkg/geocode" + "camlistore.org/pkg/types" ) var ( @@ -183,6 +185,36 @@ func parseExpression(ctx *context.Context, exp string) (*SearchQuery, error) { }) continue } + if strings.HasPrefix(word, "before:") || strings.HasPrefix(word, "after:") { + before := false + when := "" + if strings.HasPrefix(word, "before:") { + before = true + when = strings.TrimPrefix(word, "before:") + } else { + when = strings.TrimPrefix(word, "after:") + } + base := "0000-01-01T00:00:00Z" + if len(when) < len(base) { + when += base[len(when):] + } + t, err := time.Parse(time.RFC3339, when) + if err != nil { + return nil, err + } + tc := &TimeConstraint{} + if before { + tc.Before = types.Time3339(t) + } else { + tc.After = types.Time3339(t) + } + and(&Constraint{ + Permanode: &PermanodeConstraint{ + Time: tc, + }, + }) + continue + } if strings.HasPrefix(word, "loc:") { where := strings.TrimPrefix(word, "loc:") rects, err := geocode.Lookup(ctx, where) diff --git a/pkg/search/query.go b/pkg/search/query.go index 9517d1a72..b1f9001a3 100644 --- a/pkg/search/query.go +++ b/pkg/search/query.go @@ -521,6 +521,12 @@ type PermanodeConstraint struct { // ModTime optionally matches on the last modtime of the permanode. ModTime *TimeConstraint `json:"modTime,omitempty"` + // Time optionally matches the permanode's time. A Permanode + // may not have a known time. If the permanode does not have a + // known time, one may be guessed if the top-level search + // parameters request so. + Time *TimeConstraint `json:"time,omitempty"` + // Attr optionally specifies the attribute to match. // e.g. "camliContent", "camliMember", "tag" // This is required if any of the items below are used. @@ -1032,6 +1038,17 @@ func (c *PermanodeConstraint) blobMatches(s *search, br blob.Ref, bm camtypes.Bl } } + if c.Time != nil { + if corpus != nil { + t, ok := corpus.PermanodeTimeLocked(br) + if !ok || !c.Time.timeMatches(t) { + return false, nil + } + } else { + panic("TODO: not yet supported") + } + } + if cc := c.Continue; cc != nil { if corpus == nil { // Requires an in-memory index for infinite