perkeep/pkg/server/publish.go

1074 lines
31 KiB
Go

/*
Copyright 2011 Google Inc.
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 server
import (
"bufio"
"bytes"
"encoding/json"
"errors"
"fmt"
"html"
"html/template"
"io/ioutil"
"log"
"net/http"
"net/url"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"camlistore.org/pkg/auth"
"camlistore.org/pkg/blob"
"camlistore.org/pkg/blobserver"
"camlistore.org/pkg/client"
"camlistore.org/pkg/constants" // just for NewUploadHandleFromString. move elsewhere?
"camlistore.org/pkg/fileembed"
"camlistore.org/pkg/httputil"
"camlistore.org/pkg/jsonconfig"
"camlistore.org/pkg/jsonsign/signhandler"
"camlistore.org/pkg/publish"
"camlistore.org/pkg/schema"
"camlistore.org/pkg/search"
"camlistore.org/pkg/syncutil"
"camlistore.org/pkg/types/camtypes"
uistatic "camlistore.org/server/camlistored/ui"
)
// PublishHandler publishes your info to the world, if permanodes have
// appropriate ACLs set. (everything is private by default)
type PublishHandler struct {
RootName string
Search *search.Handler
Storage blobserver.Storage // of blobRoot
Cache blobserver.Storage // or nil
// Limit peak RAM used by concurrent image thumbnail calls.
resizeSem *syncutil.Sem
thumbMeta *thumbMeta // optional cache of scaled images
CSSFiles []string
// goTemplate is the go html template used for publishing.
goTemplate *template.Template
closureName string // Name of the closure object used to decorate the published page.
handlerFinder blobserver.FindHandlerByTyper
// sourceRoot optionally specifies the path to root of Camlistore's
// source. If empty, the UI files must be compiled in to the
// binary (with go run make.go). This comes from the "sourceRoot"
// publish handler config option.
sourceRoot string
uiDir string // if sourceRoot != "", this is sourceRoot+"/server/camlistored/ui"
// closureHandler serves the Closure JS files.
closureHandler http.Handler
}
func init() {
blobserver.RegisterHandlerConstructor("publish", newPublishFromConfig)
}
func newPublishFromConfig(ld blobserver.Loader, conf jsonconfig.Obj) (h http.Handler, err error) {
ph := &PublishHandler{
handlerFinder: ld,
}
ph.RootName = conf.RequiredString("rootName")
jsFiles := conf.OptionalList("js")
ph.CSSFiles = conf.OptionalList("css")
goTemplateFile := conf.RequiredString("goTemplate")
blobRoot := conf.RequiredString("blobRoot")
searchRoot := conf.RequiredString("searchRoot")
cachePrefix := conf.OptionalString("cache", "")
scaledImageConf := conf.OptionalObject("scaledImage")
bootstrapSignRoot := conf.OptionalString("devBootstrapPermanodeUsing", "")
rootNode := conf.OptionalList("rootPermanode")
ph.sourceRoot = conf.OptionalString("sourceRoot", "")
ph.resizeSem = syncutil.NewSem(int64(conf.OptionalInt("maxResizeBytes", constants.DefaultMaxResizeMem)))
if err = conf.Validate(); err != nil {
return
}
if ph.RootName == "" {
return nil, errors.New("invalid empty rootName")
}
bs, err := ld.GetStorage(blobRoot)
if err != nil {
return nil, fmt.Errorf("publish handler's blobRoot of %q error: %v", blobRoot, err)
}
ph.Storage = bs
si, err := ld.GetHandler(searchRoot)
if err != nil {
return nil, fmt.Errorf("publish handler's searchRoot of %q error: %v", searchRoot, err)
}
var ok bool
ph.Search, ok = si.(*search.Handler)
if !ok {
return nil, fmt.Errorf("publish handler's searchRoot of %q is of type %T, expecting a search handler",
searchRoot, si)
}
if rootNode != nil {
if len(rootNode) != 2 {
return nil, fmt.Errorf("rootPermanode config must contain the jsonSignerHandler and the permanode hash")
}
if t := ld.GetHandlerType(rootNode[0]); t != "jsonsign" {
return nil, fmt.Errorf("publish handler's rootPermanode first value not a jsonsign")
}
h, _ := ld.GetHandler(rootNode[0])
jsonSign := h.(*signhandler.Handler)
pn, ok := blob.Parse(rootNode[1])
if !ok {
return nil, fmt.Errorf("Invalid \"rootPermanode\" value; was expecting a blobRef, got %q.", rootNode[1])
}
if err := ph.setRootNode(jsonSign, pn); err != nil {
return nil, fmt.Errorf("error setting publish root permanode: %v", err)
}
} else {
if bootstrapSignRoot != "" {
if t := ld.GetHandlerType(bootstrapSignRoot); t != "jsonsign" {
return nil, fmt.Errorf("publish handler's devBootstrapPermanodeUsing must be of type jsonsign")
}
h, _ := ld.GetHandler(bootstrapSignRoot)
jsonSign := h.(*signhandler.Handler)
if err := ph.bootstrapPermanode(jsonSign); err != nil {
return nil, fmt.Errorf("error bootstrapping permanode: %v", err)
}
}
}
scaledImageKV, err := newKVOrNil(scaledImageConf)
if err != nil {
return nil, fmt.Errorf("in publish handler's scaledImage: %v", err)
}
if scaledImageKV != nil && cachePrefix == "" {
return nil, fmt.Errorf("in publish handler, can't specify scaledImage without cache")
}
if cachePrefix != "" {
bs, err := ld.GetStorage(cachePrefix)
if err != nil {
return nil, fmt.Errorf("publish handler's cache of %q error: %v", cachePrefix, err)
}
ph.Cache = bs
ph.thumbMeta = newThumbMeta(scaledImageKV)
}
// TODO(mpl): check that it works on appengine too.
if ph.sourceRoot == "" {
ph.sourceRoot = os.Getenv("CAMLI_DEV_CAMLI_ROOT")
}
if ph.sourceRoot != "" {
ph.uiDir = filepath.Join(ph.sourceRoot, filepath.FromSlash("server/camlistored/ui"))
// Ignore any fileembed files:
Files = &fileembed.Files{
DirFallback: filepath.Join(ph.sourceRoot, filepath.FromSlash("pkg/server")),
}
uistatic.Files = &fileembed.Files{
DirFallback: ph.uiDir,
}
}
ph.closureHandler, err = ph.makeClosureHandler(ph.sourceRoot)
if err != nil {
return nil, fmt.Errorf(`Invalid "sourceRoot" value of %q: %v"`, ph.sourceRoot, err)
}
ph.goTemplate, err = goTemplate(goTemplateFile)
if err != nil {
return nil, err
}
ph.setClosureName(jsFiles)
return ph, nil
}
func goTemplate(templateFile string) (*template.Template, error) {
if filepath.Base(templateFile) != templateFile {
hint := fmt.Sprintf("The file should either be embedded or placed in %s.",
filepath.FromSlash("server/camlistored/ui"))
return nil, fmt.Errorf("Unsupported path %v for template. %s", templateFile, hint)
}
f, err := uistatic.Files.Open(templateFile)
if err != nil {
return nil, fmt.Errorf("Could not open template %v: %v", templateFile, err)
}
defer f.Close()
templateBytes, err := ioutil.ReadAll(f)
if err != nil {
return nil, fmt.Errorf("Could not read template %v: %v", templateFile, err)
}
return template.Must(template.New("subject").Parse(string(templateBytes))), nil
}
// setClosureName sets ph.closureName with the first found closure
// namespace provided in jsFiles.
func (ph *PublishHandler) setClosureName(jsFiles []string) {
for _, v := range jsFiles {
if ph.closureName == "" {
if cl := camliClosurePage(v); cl != "" {
ph.closureName = cl
break
}
}
}
}
func (ph *PublishHandler) makeClosureHandler(root string) (http.Handler, error) {
return makeClosureHandler(root, "publish")
}
func (ph *PublishHandler) camliRootQuery() (*search.SearchResult, error) {
// TODO(mpl): I've voluntarily omitted the owner because it's not clear to
// that we actually care about that. Same for signer in lookupPathTarget.
return ph.Search.Query(&search.SearchQuery{
Limit: 1,
Constraint: &search.Constraint{
Permanode: &search.PermanodeConstraint{
Attr: "camliRoot",
Value: ph.RootName,
},
},
})
}
func (ph *PublishHandler) rootPermanode() (blob.Ref, error) {
// TODO: caching, but this can change over time (though
// probably rare). might be worth a 5 second cache or
// something in-memory? better invalidation story first would
// be nice.
result, err := ph.camliRootQuery()
if err != nil {
return blob.Ref{}, fmt.Errorf("could not find permanode for root %q of publish handler: %v", ph.RootName, err)
}
if len(result.Blobs) == 0 || !result.Blobs[0].Blob.Valid() {
return blob.Ref{}, fmt.Errorf("could not find permanode for root %q of publish handler: %v", ph.RootName, os.ErrNotExist)
}
return result.Blobs[0].Blob, nil
}
func (ph *PublishHandler) lookupPathTarget(root blob.Ref, suffix string) (blob.Ref, error) {
if suffix == "" {
return root, nil
}
// TODO: verify it's optimized: http://camlistore.org/issue/405
result, err := ph.Search.Query(&search.SearchQuery{
Limit: 1,
Constraint: &search.Constraint{
Permanode: &search.PermanodeConstraint{
SkipHidden: true,
Relation: &search.RelationConstraint{
Relation: "parent",
EdgeType: "camliPath:" + suffix,
Any: &search.Constraint{
BlobRefPrefix: root.String(),
},
},
},
},
})
if err != nil {
return blob.Ref{}, err
}
if len(result.Blobs) == 0 || !result.Blobs[0].Blob.Valid() {
return blob.Ref{}, os.ErrNotExist
}
return result.Blobs[0].Blob, nil
}
func (ph *PublishHandler) serveDiscovery(rw http.ResponseWriter, req *http.Request) {
if !ph.ViewerIsOwner(req) {
discoveryHelper(rw, req, map[string]interface{}{
"error": "viewer isn't owner",
})
return
}
_, handler, err := ph.handlerFinder.FindHandlerByType("ui")
if err != nil {
discoveryHelper(rw, req, map[string]interface{}{
"error": "no admin handler running",
})
return
}
ui := handler.(*UIHandler)
ui.root.serveDiscovery(rw, req)
}
func (ph *PublishHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if req.URL.Query().Get("camli.mode") == "config" {
ph.serveDiscovery(rw, req)
return
}
preq := ph.NewRequest(rw, req)
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 blob.Ref
subject blob.Ref
inSubjectChain map[string]bool // blobref -> true
subjectBasePath string
// A describe request that we can reuse, sharing its map of
// blobs already described.
dr *search.DescribeRequest
// Limit peak RAM used by concurrent image thumbnail calls.
resizeSem *syncutil.Sem
}
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")
suffix, res := httputil.PathSuffix(req), ""
if strings.HasPrefix(suffix, "-/") {
suffix, res = "", suffix[2:]
} else if s := strings.SplitN(suffix, "/-/", 2); len(s) == 2 {
suffix, res = s[0], s[1]
}
rootpn, _ := ph.rootPermanode()
return &publishRequest{
ph: ph,
rw: rw,
req: req,
suffix: suffix,
base: httputil.PathBase(req),
subres: res,
rootpn: rootpn,
dr: ph.Search.NewDescribeRequest(),
inSubjectChain: make(map[string]bool),
subjectBasePath: "",
resizeSem: ph.resizeSem,
}
}
func (ph *PublishHandler) ViewerIsOwner(req *http.Request) bool {
// TODO: better check later
return auth.Allowed(req, auth.OpAll)
}
func (pr *publishRequest) ViewerIsOwner() bool {
return pr.ph.ViewerIsOwner(pr.req)
}
func (pr *publishRequest) Debug() bool {
return pr.req.FormValue("debug") == "1"
}
func (pr *publishRequest) SubresourceType() string {
if len(pr.subres) >= 3 && strings.HasPrefix(pr.subres, "/=") {
return pr.subres[2:3]
}
return ""
}
func (pr *publishRequest) SubresFileURL(path []blob.Ref, fileName string) string {
return pr.SubresThumbnailURL(path, fileName, -1)
}
func (pr *publishRequest) SubresThumbnailURL(path []blob.Ref, fileName string, maxDimen int) string {
var buf bytes.Buffer
resType := "i"
if maxDimen == -1 {
resType = "f"
}
fmt.Fprintf(&buf, "%s", pr.subjectBasePath)
if !strings.Contains(pr.subjectBasePath, "/-/") {
buf.Write([]byte("/-"))
}
for _, br := range path {
if pr.inSubjectChain[br.String()] {
continue
}
fmt.Fprintf(&buf, "/h%s", br.DigestPrefix(10))
}
fmt.Fprintf(&buf, "/=%s", resType)
fmt.Fprintf(&buf, "/%s", url.QueryEscape(fileName))
if maxDimen != -1 {
fmt.Fprintf(&buf, "?mw=%d&mh=%d", maxDimen, maxDimen)
}
return buf.String()
}
var memberRE = regexp.MustCompile(`^/?h([0-9a-f]+)`)
func (pr *publishRequest) findSubject() error {
if strings.HasPrefix(pr.suffix, "=s/") {
pr.subres = "/" + pr.suffix
return nil
}
subject, err := pr.ph.lookupPathTarget(pr.rootpn, pr.suffix)
if err != nil {
return err
}
if strings.HasPrefix(pr.subres, "=z/") {
// this happens when we are at the root of the published path,
// e.g /base/suffix/-/=z/foo.zip
// so we need to reset subres as fullpath so that it is detected
// properly when switching on pr.SubresourceType()
pr.subres = "/" + pr.subres
// since we return early, we set the subject because that is
// what is going to be used as a root node by the zip handler.
pr.subject = subject
return nil
}
pr.inSubjectChain[subject.String()] = true
pr.subjectBasePath = pr.base + pr.suffix
// Chase /h<xxxxx> hops 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.ResolvePrefixHop(subject, memberPrefix)
if err != nil {
return err
}
pr.inSubjectChain[subject.String()] = true
pr.subres = pr.subres[len(match):]
pr.subjectBasePath = addPathComponent(pr.subjectBasePath, match)
}
pr.subject = subject
return nil
}
func (pr *publishRequest) serveHTTP() {
if !pr.rootpn.Valid() {
pr.rw.WriteHeader(404)
return
}
if pr.Debug() {
pr.rw.Header().Set("Content-Type", "text/html")
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))
}
if err := pr.findSubject(); err != nil {
if err == os.ErrNotExist {
pr.rw.WriteHeader(404)
return
}
log.Printf("Error looking up %s/%q: %v", pr.rootpn, pr.suffix, err)
pr.rw.WriteHeader(500)
return
}
if pr.Debug() {
pr.pf("<p><b>Subject:</b> <a href='/ui/?p=%s'>%s</a></p>", pr.subject, pr.subject)
return
}
switch pr.SubresourceType() {
case "":
pr.serveSubjectTemplate()
case "b":
// TODO: download a raw blob
case "f": // file download
pr.serveSubresFileDownload()
case "i": // image, scaled
pr.serveSubresImage()
case "s": // static
pr.req.URL.Path = pr.subres[len("/=s"):]
if len(pr.req.URL.Path) <= 1 {
http.Error(pr.rw, "Illegal URL.", http.StatusNotFound)
return
}
file := pr.req.URL.Path[1:]
if m := closurePattern.FindStringSubmatch(file); m != nil {
pr.req.URL.Path = "/" + m[1]
pr.ph.closureHandler.ServeHTTP(pr.rw, pr.req)
return
}
if file == "deps.js" {
serveDepsJS(pr.rw, pr.req, pr.ph.uiDir)
return
}
serveStaticFile(pr.rw, pr.req, uistatic.Files, file)
case "z":
pr.serveZip()
default:
pr.rw.WriteHeader(400)
pr.pf("<p>Invalid or unsupported resource request.</p>")
}
}
func (pr *publishRequest) pf(format string, args ...interface{}) {
fmt.Fprintf(pr.rw, format, args...)
}
func (pr *publishRequest) staticPath(fileName string) string {
return pr.base + "=s/" + fileName
}
func addPathComponent(base, addition string) string {
if !strings.HasPrefix(addition, "/") {
addition = "/" + addition
}
if strings.Contains(base, "/-/") {
return base + addition
}
return base + "/-" + addition
}
func (pr *publishRequest) memberPath(member blob.Ref) string {
return addPathComponent(pr.subjectBasePath, "/h"+member.DigestPrefix(10))
}
var provCamliRx = regexp.MustCompile(`^goog\.(provide)\(['"]camlistore\.(.*)['"]\)`)
// camliClosurePage checks if filename is a .js file using closure
// and if yes, if it provides a page in the camlistore namespace.
// It returns that page name, or the empty string otherwise.
func camliClosurePage(filename string) string {
f, err := uistatic.Files.Open(filename)
if err != nil {
return ""
}
defer f.Close()
br := bufio.NewReader(f)
for {
l, err := br.ReadString('\n')
if err != nil {
return ""
}
if !strings.HasPrefix(l, "goog.") {
continue
}
m := provCamliRx.FindStringSubmatch(l)
if m != nil {
return m[2]
}
}
return ""
}
// serveZip streams a zip archive of all the files "under"
// pr.subject. That is, all the files pointed by file permanodes,
// which are directly members of pr.subject or recursively down
// directory permanodes and permanodes members.
func (pr *publishRequest) serveZip() {
filename := ""
if len(pr.subres) > len("/=z/") {
filename = pr.subres[4:]
}
zh := &zipHandler{
fetcher: pr.ph.Storage,
search: pr.ph.Search,
root: pr.subject,
filename: filename,
}
zh.ServeHTTP(pr.rw, pr.req)
}
const (
resSeparator = "/-"
digestPrefix = "h"
digestLen = 10
)
var hopRE = regexp.MustCompile(fmt.Sprintf("^/%s([0-9a-f]{%d})", digestPrefix, digestLen))
// publishedPath is a URL suffix path of the kind
// suffix + resSeparator + subresource(s), such as:
// /foo/bar/-/subres1/subres2
type publishedPath string
// splitHops returns a slice containing the subresource(s)
// digests. For example, with /foo/bar/-/he0917e5bcf/h5f46bb454d
// it will yield []string{"e0917e5bcf", "5f46bb454d"}
func (p publishedPath) splitHops() []string {
ps := string(p)
var hops []string
if idx := strings.Index(ps, resSeparator); idx != -1 {
ps = ps[idx+len(resSeparator):]
}
matchLen := 1 + len(digestPrefix) + digestLen
for {
m := memberRE.FindStringSubmatch(ps)
if m == nil {
break
}
hops = append(hops, m[1])
ps = ps[matchLen:]
}
return hops
}
// parent returns the base path and the blobRef of pr.subject's parent.
// It returns an error if pr.subject or pr.subjectBasePath were not set
// properly (with findSubject), or if the parent was not found.
func (pr *publishRequest) parent() (parentPath string, parentBlobRef blob.Ref, err error) {
if !pr.subject.Valid() {
return "", blob.Ref{}, errors.New("subject not set")
}
if pr.subjectBasePath == "" {
return "", blob.Ref{}, errors.New("subjectBasePath not set")
}
// TODO(mpl): this fails when the parent is the root. fix it.
hops := publishedPath(pr.subjectBasePath).splitHops()
if len(hops) == 0 {
return "", blob.Ref{}, errors.New("No subresource digest in subjectBasePath")
}
subjectDigest := hops[len(hops)-1]
if subjectDigest != pr.subject.DigestPrefix(digestLen) {
return "", blob.Ref{}, errors.New("subject digest not in subjectBasePath")
}
parentPath = strings.TrimSuffix(pr.subjectBasePath, "/"+digestPrefix+subjectDigest)
if len(hops) == 1 {
// the parent is the suffix, not one of the subresource hops
for br, _ := range pr.inSubjectChain {
if br != pr.subject.String() {
parentBlobRef = blob.ParseOrZero(br)
break
}
}
} else {
// nested collection(s)
parentDigest := hops[len(hops)-2]
for br, _ := range pr.inSubjectChain {
bref, ok := blob.Parse(br)
if !ok {
return "", blob.Ref{}, fmt.Errorf("Could not parse %q as blobRef", br)
}
if bref.DigestPrefix(10) == parentDigest {
parentBlobRef = bref
break
}
}
}
if !parentBlobRef.Valid() {
return "", blob.Ref{}, fmt.Errorf("No parent found for %v", pr.subjectBasePath)
}
return parentPath, parentBlobRef, nil
}
func (pr *publishRequest) cssFiles() []string {
files := []string{}
for _, filename := range pr.ph.CSSFiles {
files = append(files, pr.staticPath(filename))
}
return files
}
// jsDeps returns the list of paths that should be included
// as javascript files in the published page to enable and use
// additional javascript closure code.
func (pr *publishRequest) jsDeps() []string {
var js []string
closureDeps := []string{
"closure/goog/base.js",
"deps.js",
// TODO(mpl): fix the deps generator and/or the SHA1.js etc files so they get into deps.js and we
// do not even have to include them here. detection fails for them because the provide statement
// is not at the beginning of the line.
// Not doing it right away because it might have consequences for the rest of the ui I suppose.
"base64.js",
"Crypto.js",
"SHA1.js",
}
for _, v := range closureDeps {
js = append(js, pr.staticPath(v))
}
js = append(js, pr.base+"?camli.mode=config&var=CAMLISTORE_CONFIG")
return js
}
// subjectHeader returns the PageHeader corresponding to the described subject.
func (pr *publishRequest) subjectHeader(described map[string]*search.DescribedBlob) *publish.PageHeader {
subdes := described[pr.subject.String()]
header := &publish.PageHeader{
Title: html.EscapeString(subdes.Title()),
CSSFiles: pr.cssFiles(),
Meta: func() string {
jsonRes, _ := json.MarshalIndent(described, "", " ")
return string(jsonRes)
}(),
Subject: pr.subject.String(),
}
header.JSDeps = pr.jsDeps()
if pr.ph.closureName != "" {
header.CamliClosure = template.JS("camlistore." + pr.ph.closureName)
}
if pr.ViewerIsOwner() {
header.ViewerIsOwner = true
}
return header
}
// subjectFile returns the relevant PageFile if the described subject is a file permanode.
func (pr *publishRequest) subjectFile(described map[string]*search.DescribedBlob) (*publish.PageFile, error) {
subdes := described[pr.subject.String()]
contentRef, ok := subdes.ContentRef()
if !ok {
return nil, nil
}
fileDes, err := pr.dr.DescribeSync(contentRef)
if err != nil {
return nil, fmt.Errorf("Could not describe %v: %v", contentRef, err)
}
path := []blob.Ref{pr.subject, contentRef}
downloadURL := pr.SubresFileURL(path, fileDes.File.FileName)
thumbnailURL := ""
if fileDes.File.IsImage() {
thumbnailURL = pr.SubresThumbnailURL(path, fileDes.File.FileName, 600)
}
fileName := html.EscapeString(fileDes.File.FileName)
return &publish.PageFile{
FileName: fileName,
Size: fileDes.File.Size,
MIMEType: fileDes.File.MIMEType,
IsImage: fileDes.File.IsImage(),
DownloadURL: downloadURL,
ThumbnailURL: thumbnailURL,
DomID: contentRef.DomID(),
Nav: func() *publish.Nav {
nv, err := pr.fileNavigation()
if err != nil {
log.Print(err)
return nil
}
return nv
},
}, nil
}
func (pr *publishRequest) fileNavigation() (*publish.Nav, error) {
// first get the parent path and blob
parentPath, parentbr, err := pr.parent()
if err != nil {
return nil, fmt.Errorf("Could not get subject %v's parent's info: %v", pr.subject, err)
}
parentNav := strings.TrimSuffix(parentPath, resSeparator)
fileNav := &publish.Nav{
ParentPath: parentNav,
}
// describe the parent so we get the siblings (members of the parent)
dr := pr.ph.Search.NewDescribeRequest()
dr.Describe(parentbr, 3)
parentRes, err := dr.Result()
if err != nil {
return nil, fmt.Errorf("Could not \"deeply\" describe subject %v's parent %v: %v", pr.subject, parentbr, err)
}
members := parentRes[parentbr.String()].Members()
if len(members) == 0 {
return fileNav, nil
}
pos := 0
var prev, next blob.Ref
for k, member := range members {
if member.BlobRef.String() == pr.subject.String() {
pos = k
break
}
}
if pos > 0 {
prev = members[pos-1].BlobRef
}
if pos < len(members)-1 {
next = members[pos+1].BlobRef
}
if !prev.Valid() && !next.Valid() {
return fileNav, nil
}
if prev.Valid() {
fileNav.PrevPath = fmt.Sprintf("%s/%s%s", parentPath, digestPrefix, prev.DigestPrefix(10))
}
if next.Valid() {
fileNav.NextPath = fmt.Sprintf("%s/%s%s", parentPath, digestPrefix, next.DigestPrefix(10))
}
return fileNav, nil
}
// subjectMembers returns the relevant PageMembers if the described subject is a permanode with members.
func (pr *publishRequest) subjectMembers(resMap map[string]*search.DescribedBlob) (*publish.PageMembers, error) {
subdes := resMap[pr.subject.String()]
members := subdes.Members()
if len(members) == 0 {
return nil, nil
}
zipName := ""
if title := subdes.Title(); title == "" {
zipName = "download.zip"
} else {
zipName = title + ".zip"
}
subjectPath := pr.subjectBasePath
if !strings.Contains(subjectPath, "/-/") {
subjectPath += "/-"
}
return &publish.PageMembers{
SubjectPath: subjectPath,
ZipName: zipName,
Members: members,
Description: func(member *search.DescribedBlob) string {
des := member.Description()
if des != "" {
des = " - " + des
}
return des
},
Title: func(member *search.DescribedBlob) string {
memberTitle := member.Title()
if memberTitle == "" {
memberTitle = member.BlobRef.DigestPrefix(10)
}
return html.EscapeString(memberTitle)
},
Path: func(member *search.DescribedBlob) string {
return pr.memberPath(member.BlobRef)
},
DomID: func(member *search.DescribedBlob) string {
return member.DomID()
},
FileInfo: func(member *search.DescribedBlob) *publish.MemberFileInfo {
if path, fileInfo, ok := member.PermanodeFile(); ok {
info := &publish.MemberFileInfo{
FileName: fileInfo.FileName,
FileDomID: path[len(path)-1].DomID(),
FilePath: html.EscapeString(pr.SubresFileURL(path, fileInfo.FileName)),
}
if fileInfo.IsImage() {
info.FileThumbnailURL = pr.SubresThumbnailURL(path, fileInfo.FileName, 200)
}
return info
}
return nil
},
}, nil
}
// serveSubjectTemplate creates the funcs to generate the PageHeader, PageFile,
// and pageMembers that can be used by the subject template, and serves the template.
func (pr *publishRequest) serveSubjectTemplate() {
dr := pr.ph.Search.NewDescribeRequest()
dr.Describe(pr.subject, 3)
res, err := dr.Result()
if err != nil {
log.Printf("Errors loading %s, permanode %s: %v, %#v", pr.req.URL, pr.subject, err, err)
http.Error(pr.rw, "Error loading describe request", http.StatusInternalServerError)
return
}
subdes := res[pr.subject.String()]
if subdes.CamliType == "file" {
pr.serveFileDownload(subdes)
return
}
headerFunc := func() *publish.PageHeader {
return pr.subjectHeader(res)
}
fileFunc := func() *publish.PageFile {
file, err := pr.subjectFile(res)
if err != nil {
log.Printf("%v", err)
return nil
}
return file
}
membersFunc := func() *publish.PageMembers {
members, err := pr.subjectMembers(res)
if err != nil {
log.Printf("%v", err)
return nil
}
return members
}
page := &publish.SubjectPage{
Header: headerFunc,
File: fileFunc,
Members: membersFunc,
}
err = pr.ph.goTemplate.Execute(pr.rw, page)
if err != nil {
log.Printf("Error serving subject template: %v", err)
http.Error(pr.rw, "Error serving template", http.StatusInternalServerError)
return
}
}
func (pr *publishRequest) validPathChain(path []blob.Ref) bool {
bi := pr.subject
for len(path) > 0 {
var next blob.Ref
next, path = path[0], path[1:]
desi, err := pr.dr.DescribeSync(bi)
if err != nil {
return false
}
if !desi.HasSecureLinkTo(next) {
return false
}
bi = next
}
return true
}
func (pr *publishRequest) serveSubresImage() {
params := pr.req.URL.Query()
mw, _ := strconv.Atoi(params.Get("mw"))
mh, _ := strconv.Atoi(params.Get("mh"))
des, err := pr.dr.DescribeSync(pr.subject)
if err != nil {
log.Printf("error describing subject %q: %v", pr.subject, err)
return
}
pr.serveScaledImage(des, mw, mh, params.Get("square") == "1")
}
func (pr *publishRequest) serveSubresFileDownload() {
des, err := pr.dr.DescribeSync(pr.subject)
if err != nil {
log.Printf("error describing subject %q: %v", pr.subject, err)
return
}
pr.serveFileDownload(des)
}
func (pr *publishRequest) serveScaledImage(des *search.DescribedBlob, maxWidth, maxHeight int, square bool) {
fileref, _, ok := pr.fileSchemaRefFromBlob(des)
if !ok {
log.Printf("scaled image fail; failed to get file schema from des %q", des.BlobRef)
return
}
th := &ImageHandler{
Fetcher: pr.ph.Storage,
Cache: pr.ph.Cache,
MaxWidth: maxWidth,
MaxHeight: maxHeight,
Square: square,
thumbMeta: pr.ph.thumbMeta,
resizeSem: pr.resizeSem,
}
th.ServeHTTP(pr.rw, pr.req, fileref)
}
func (pr *publishRequest) serveFileDownload(des *search.DescribedBlob) {
fileref, fileinfo, ok := pr.fileSchemaRefFromBlob(des)
if !ok {
log.Printf("Didn't get file schema from described blob %q", des.BlobRef)
return
}
mime := ""
if fileinfo != nil && fileinfo.IsImage() {
mime = fileinfo.MIMEType
}
dh := &DownloadHandler{
Fetcher: pr.ph.Storage,
Cache: pr.ph.Cache,
ForceMime: mime,
}
dh.ServeHTTP(pr.rw, pr.req, fileref)
}
// Given a described blob, optionally follows a camliContent and
// returns the file's schema blobref and its fileinfo (if found).
func (pr *publishRequest) fileSchemaRefFromBlob(des *search.DescribedBlob) (fileref blob.Ref, fileinfo *camtypes.FileInfo, ok bool) {
if des == nil {
http.NotFound(pr.rw, pr.req)
return
}
if des.Permanode != nil {
// TODO: get "forceMime" attr out of the permanode? or
// fileName content-disposition?
if cref := des.Permanode.Attr.Get("camliContent"); cref != "" {
cbr, ok2 := blob.Parse(cref)
if !ok2 {
http.Error(pr.rw, "bogus camliContent", 500)
return
}
des = des.PeerBlob(cbr)
if des == nil {
http.Error(pr.rw, "camliContent not a peer in describe", 500)
return
}
}
}
if des.CamliType == "file" {
return des.BlobRef, des.File, true
}
http.Error(pr.rw, "failed to find fileSchemaRefFromBlob", 404)
return
}
func (ph *PublishHandler) signUpload(jsonSign *signhandler.Handler, name string, bb *schema.Builder) (blob.Ref, error) {
signed, err := jsonSign.Sign(bb)
if err != nil {
return blob.Ref{}, fmt.Errorf("error signing %s: %v", name, err)
}
uh := client.NewUploadHandleFromString(signed)
_, err = blobserver.Receive(ph.Storage, uh.BlobRef, uh.Contents)
if err != nil {
return blob.Ref{}, fmt.Errorf("error uploading %s: %v", name, err)
}
return uh.BlobRef, nil
}
func (ph *PublishHandler) setRootNode(jsonSign *signhandler.Handler, pn blob.Ref) (err error) {
_, err = ph.signUpload(jsonSign, "set-attr camliRoot", schema.NewSetAttributeClaim(pn, "camliRoot", ph.RootName))
if err != nil {
return err
}
_, err = ph.signUpload(jsonSign, "set-attr title", schema.NewSetAttributeClaim(pn, "title", "Publish root node for "+ph.RootName))
return err
}
func (ph *PublishHandler) bootstrapPermanode(jsonSign *signhandler.Handler) (err error) {
result, err := ph.camliRootQuery()
if err == nil && len(result.Blobs) > 0 && result.Blobs[0].Blob.Valid() {
log.Printf("Publish root %q using existing permanode %s", ph.RootName, result.Blobs[0].Blob)
return nil
}
log.Printf("Publish root %q needs a permanode + claim", ph.RootName)
pn, err := ph.signUpload(jsonSign, "permanode", schema.NewUnsignedPermanode())
if err != nil {
return err
}
err = ph.setRootNode(jsonSign, pn)
return err
}