publish: support /mxxx subresources. aka HTML pages for permanode members. aka photo one-up pages from galleries.

Change-Id: I404e836a728048631c6149ef76eb6d7014ec7978
This commit is contained in:
Brad Fitzpatrick 2011-07-05 17:27:10 -07:00
parent ebaf8b563e
commit a477909f09
3 changed files with 161 additions and 41 deletions

View File

@ -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 "<nil-BlobRef>"
@ -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
}

View File

@ -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

View File

@ -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<xxxxx> 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<hr>",
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("<html>\n<head>\n <title>%s</title>\n ",html.EscapeString(title))
pr.pf("<html>\n<head>\n <title>%s</title>\n ", html.EscapeString(title))
pr.pf("<script src='%s'></script>\n", pr.staticPath("camli.js"))
pr.pf("<script>\n")
pr.pf("var camliPagePermanode = %q;\n", pr.subject)
@ -298,6 +336,26 @@ func (pr *publishRequest) serve() {
pr.pf("<h1>%s</h1>\n", html.EscapeString(title))
}
if cref, ok := subdes.ContentRef(); ok {
des, err := pr.dr.DescribeSync(cref)
if err == nil && des.File != nil {
path := []*blobref.BlobRef{pr.subject, cref}
downloadURL := pr.SubresFileURL(path, des.File.FileName)
pr.pf("<div>File: %s, %d bytes, type %s</div>",
html.EscapeString(des.File.FileName),
des.File.Size,
des.File.MimeType)
if des.File.IsImage() {
pr.pf("<a href='%s'><img border=0 src='%s'></a>",
downloadURL,
pr.SubresThumbnailURL(path, des.File.FileName, 600))
}
pr.pf("<div id='%s' class='camlifile'>[<a href='%s'>download</a>]</div>",
cref.DomID(),
downloadURL)
}
}
if members := subdes.Members(); len(members) > 0 {
pr.pf("<ul>\n")
for _, member := range members {
@ -305,41 +363,35 @@ func (pr *publishRequest) serve() {
if des != "" {
des = " - " + des
}
link := "#"
thumbnail := ""
var fileLink, thumbnail string
if path, fileInfo, ok := member.PermanodeFile(); ok {
link = pr.SubresFileURL(path, fileInfo.FileName)
fileLink = fmt.Sprintf("<div id='%s' class='camlifile'><a href='%s'>file</a></div>",
path[len(path)-1].DomID(),
html.EscapeString(pr.SubresFileURL(path, fileInfo.FileName)),
)
if fileInfo.IsImage() {
thumbnail = fmt.Sprintf("<img src='%s'>", pr.SubresThumbnailURL(path, fileInfo.FileName, 200))
}
}
pr.pf(" <li><a href='%s'>%s%s</a>%s</li>\n",
link,
pr.pf(" <li id='%s'><a href='%s'>%s%s</a>%s%s</li>\n",
member.DomID(),
pr.memberPath(member.BlobRef),
thumbnail,
html.EscapeString(member.Title()),
des)
des,
fileLink)
}
pr.pf("</ul>\n")
}
}
func (pr *publishRequest) describeSingleBlob(b *blobref.BlobRef) (*search.DescribedBlob, os.Error) {
dr := pr.ph.Search.NewDescribeRequest()
dr.Describe(b, 1)
res, err := dr.Result()
if err != nil {
return nil, err
}
return res[b.String()], nil
}
func (pr *publishRequest) validPathChain(path []*blobref.BlobRef) bool {
bi := pr.subject
for len(path) > 0 {
var next *blobref.BlobRef
next, path = path[0], path[1:]
desi, err := pr.describeSingleBlob(bi)
desi, err := pr.dr.DescribeSync(bi)
if err != nil {
return false
}
@ -388,7 +440,7 @@ func (pr *publishRequest) describeSubresAndValidatePath() (des *search.Described
}
file := path[len(path)-1]
fileDes, err := pr.describeSingleBlob(file)
fileDes, err := pr.dr.DescribeSync(file)
if err != nil {
http.Error(pr.rw, "describe error", 500)
return