From a477909f099e45398e307b1c9c21a40b2818aae2 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Tue, 5 Jul 2011 17:27:10 -0700 Subject: [PATCH] publish: support /mxxx subresources. aka HTML pages for permanode members. aka photo one-up pages from galleries. Change-Id: I404e836a728048631c6149ef76eb6d7014ec7978 --- lib/go/camli/blobref/blobref.go | 15 ++++ lib/go/camli/search/handler.go | 55 ++++++++++++- server/go/camlistored/publish.go | 132 +++++++++++++++++++++---------- 3 files changed, 161 insertions(+), 41 deletions(-) diff --git a/lib/go/camli/blobref/blobref.go b/lib/go/camli/blobref/blobref.go index dd819ff98..ae37e44e8 100644 --- a/lib/go/camli/blobref/blobref.go +++ b/lib/go/camli/blobref/blobref.go @@ -23,6 +23,7 @@ import ( "io" "os" "regexp" + "strings" ) var kBlobRefPattern *regexp.Regexp = regexp.MustCompile(`^([a-z0-9]+)-([a-f0-9]+)$`) @@ -71,6 +72,13 @@ func (b *BlobRef) Digest() string { return b.digest } +func (b *BlobRef) DigestPrefix(digits int) string { + if len(b.digest) < digits { + return b.digest + } + return b.digest[:digits] +} + func (b *BlobRef) String() string { if b == nil { return "" @@ -78,6 +86,13 @@ func (b *BlobRef) String() string { return b.strValue } +func (b *BlobRef) DomID() string { + if b == nil { + return "" + } + return "camli_" + strings.Replace(b.String(), "-", "_", 1) +} + func (o *BlobRef) Equals(other *BlobRef) bool { return o.hashName == other.hashName && o.digest == other.digest } diff --git a/lib/go/camli/search/handler.go b/lib/go/camli/search/handler.go index 7edc980c9..f5bb6cf2d 100644 --- a/lib/go/camli/search/handler.go +++ b/lib/go/camli/search/handler.go @@ -33,7 +33,7 @@ import ( "camli/httputil" ) -const buffered = 32 // arbitrary channel buffer size +const buffered = 32 // arbitrary channel buffer size const maxPermanodes = 50 // arbitrary limit on the number of permanodes fetched (by getTagged) func init() { @@ -287,6 +287,13 @@ func (b *DescribedBlob) PermanodeFile() (path []*blobref.BlobRef, fi *FileInfo, return } +func (b *DescribedBlob) DomID() string { + if b == nil { + return "" + } + return b.BlobRef.DomID() +} + func (b *DescribedBlob) Title() string { if b == nil { return "" @@ -330,6 +337,17 @@ func (b *DescribedBlob) Members() []*DescribedBlob { return m } +func (b *DescribedBlob) ContentRef() (br *blobref.BlobRef, ok bool) { + if b != nil && b.Permanode != nil { + if cref := b.Permanode.Attr.Get("camliContent"); cref != "" { + br = blobref.Parse(cref) + return br, br != nil + } + } + return +} + + func (b *DescribedBlob) PeerBlob(br *blobref.BlobRef) *DescribedBlob { if b.Request == nil { return &DescribedBlob{BlobRef: br, Stub: true} @@ -415,6 +433,32 @@ func (sh *Handler) NewDescribeRequest() *DescribeRequest { } } +func (sh *Handler) ResolveMemberPrefix(parent *blobref.BlobRef, prefix string) (child *blobref.BlobRef, err os.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 nil, 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 nil, fmt.Errorf("Failed to describe member %q in parent %q", prefix, parent) + } + for _, member := range des.Members() { + if strings.HasPrefix(member.BlobRef.Digest(), prefix) { + return member.BlobRef, nil + } + } + return nil, fmt.Errorf("Member prefix %q not found in %q", prefix, parent) +} + type DescribeError map[string]os.Error func (de DescribeError) String() string { @@ -467,6 +511,15 @@ func (dr *DescribeRequest) describedBlob(b *blobref.BlobRef) *DescribedBlob { return des } +func (dr *DescribeRequest) DescribeSync(br *blobref.BlobRef) (*DescribedBlob, os.Error) { + dr.Describe(br, 1) + res, err := dr.Result() + if err != nil { + return nil, err + } + return res[br.String()], nil +} + func (dr *DescribeRequest) Describe(br *blobref.BlobRef, depth int) { if depth <= 0 { return diff --git a/server/go/camlistored/publish.go b/server/go/camlistored/publish.go index 920bac016..39273ef78 100644 --- a/server/go/camlistored/publish.go +++ b/server/go/camlistored/publish.go @@ -24,6 +24,7 @@ import ( "json" "log" "os" + "regexp" "strconv" "strings" @@ -138,6 +139,21 @@ func (ph *PublishHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { preq.serveHTTP() } +// publishRequest is the state around a single HTTP request to the +// publish handler +type publishRequest struct { + ph *PublishHandler + rw http.ResponseWriter + req *http.Request + base, suffix, subres string + rootpn *blobref.BlobRef + subject *blobref.BlobRef + + // A describe request that we can reuse, sharing its map of + // blobs already described. + dr *search.DescribeRequest +} + func (ph *PublishHandler) NewRequest(rw http.ResponseWriter, req *http.Request) *publishRequest { // splits a path request into its suffix and subresource parts. // e.g. /blog/foo/camli/res/file/xxx -> ("foo", "file/xxx") @@ -156,20 +172,10 @@ func (ph *PublishHandler) NewRequest(rw http.ResponseWriter, req *http.Request) base: req.Header.Get("X-PrefixHandler-PathBase"), subres: res, rootpn: rootpn, + dr: ph.Search.NewDescribeRequest(), } } -// publishRequest is the state around a single HTTP request to the -// publish handler -type publishRequest struct { - ph *PublishHandler - rw http.ResponseWriter - req *http.Request - base, suffix, subres string - rootpn *blobref.BlobRef - subject *blobref.BlobRef -} - func (pr *publishRequest) Debug() bool { return pr.req.FormValue("debug") == "1" } @@ -202,6 +208,37 @@ func (pr *publishRequest) SubresThumbnailURL(path []*blobref.BlobRef, fileName s return buf.String() } +var memberRE = regexp.MustCompile(`^/?m([0-9a-f]+)`) + +func (pr *publishRequest) findSubject() os.Error { + subject, err := pr.ph.lookupPathTarget(pr.rootpn, pr.suffix) + if err != nil { + return err + } + + // Chase /m members in suffix. + for { + m := memberRE.FindStringSubmatch(pr.subres) + if m == nil { + break + } + match, memberPrefix := m[0], m[1] + + if err != nil { + return fmt.Errorf("Error looking up potential member %q in describe of subject %q: %v", + memberPrefix, subject, err) + } + subject, err = pr.ph.Search.ResolveMemberPrefix(subject, memberPrefix) + if err != nil { + return err + } + pr.subres = pr.subres[len(match):] + } + + pr.subject = subject + return nil +} + func (pr *publishRequest) serveHTTP() { if pr.rootpn == nil { pr.rw.WriteHeader(404) @@ -212,9 +249,8 @@ func (pr *publishRequest) serveHTTP() { pr.pf("I am publish handler at base %q, serving root %q (permanode=%s), suffix %q, subreq %q
", pr.base, pr.ph.RootName, pr.rootpn, html.EscapeString(pr.suffix), html.EscapeString(pr.subres)) } - var err os.Error - pr.subject, err = pr.ph.lookupPathTarget(pr.rootpn, pr.suffix) - if err != nil { + + if err := pr.findSubject(); err != nil { if err == os.ENOENT { pr.rw.WriteHeader(404) return @@ -231,7 +267,7 @@ func (pr *publishRequest) serveHTTP() { switch pr.SubresourceType() { case "": - pr.serve() + pr.serveSubject() case "blob": // TODO: download a raw blob case "file": @@ -255,7 +291,14 @@ func (pr *publishRequest) staticPath(fileName string) string { return pr.base + "-/static/" + fileName } -func (pr *publishRequest) serve() { +func (pr *publishRequest) memberPath(member *blobref.BlobRef) string { + if strings.Contains(pr.req.URL.Path, "/-/") { + return pr.req.URL.Path + "/m" + member.DigestPrefix(10) + } + return pr.req.URL.Path + "/-/m" + member.DigestPrefix(10) +} + +func (pr *publishRequest) serveSubject() { dr := pr.ph.Search.NewDescribeRequest() dr.Describe(pr.subject, 3) res, err := dr.Result() @@ -272,18 +315,13 @@ func (pr *publishRequest) serve() { return } - if subdes.Permanode != nil && subdes.Permanode.Attr.Get("camliContent") != "" { - pr.serveFileDownload(subdes) - return - } - title := subdes.Title() // HTML header + Javascript { jm := make(map[string]interface{}) dr.PopulateJSON(jm) - pr.pf("\n\n %s\n ",html.EscapeString(title)) + pr.pf("\n\n %s\n ", html.EscapeString(title)) pr.pf("\n", pr.staticPath("camli.js")) pr.pf("