mirror of https://github.com/perkeep/perkeep.git
710 lines
20 KiB
Go
710 lines
20 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"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"camlistore.org/pkg/blobref"
|
|
"camlistore.org/pkg/blobserver"
|
|
"camlistore.org/pkg/client" // just for NewUploadHandleFromString. move elsewhere?
|
|
"camlistore.org/pkg/jsonconfig"
|
|
"camlistore.org/pkg/jsonsign/signhandler"
|
|
"camlistore.org/pkg/osutil"
|
|
"camlistore.org/pkg/schema"
|
|
"camlistore.org/pkg/search"
|
|
"net/url"
|
|
)
|
|
|
|
// 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
|
|
sc ScaledImage // cache of scaled images, optional
|
|
|
|
JSFiles, CSSFiles []string
|
|
|
|
handlerFinder blobserver.FindHandlerByTyper
|
|
staticHandler http.Handler
|
|
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")
|
|
ph.JSFiles = conf.OptionalList("js")
|
|
ph.CSSFiles = conf.OptionalList("css")
|
|
blobRoot := conf.RequiredString("blobRoot")
|
|
searchRoot := conf.RequiredString("searchRoot")
|
|
cachePrefix := conf.OptionalString("cache", "")
|
|
scType := conf.OptionalString("scaledImage", "")
|
|
bootstrapSignRoot := conf.OptionalString("devBootstrapPermanodeUsing", "")
|
|
rootNode := conf.OptionalList("rootPermanode")
|
|
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 := blobref.Parse(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)
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
switch scType {
|
|
case "lrucache":
|
|
ph.sc = NewScaledImageLRU()
|
|
case "":
|
|
default:
|
|
return nil, fmt.Errorf("unsupported publish handler's scType: %q ", scType)
|
|
}
|
|
}
|
|
|
|
camliRootPath, err := osutil.GoPackagePath("camlistore.org")
|
|
if err != nil {
|
|
error := "Package camlistore.org not found in $GOPATH (or $GOPATH not defined)." +
|
|
" Needed to find closure dir."
|
|
return nil, fmt.Errorf(error)
|
|
}
|
|
closureDir := filepath.Join(camliRootPath, "tmp", "closure-lib", "closure")
|
|
ph.closureHandler = http.FileServer(http.Dir(closureDir))
|
|
|
|
ph.staticHandler = http.FileServer(newuiFiles)
|
|
|
|
return ph, nil
|
|
}
|
|
|
|
func (ph *PublishHandler) rootPermanode() (*blobref.BlobRef, 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.
|
|
br, err := ph.Search.Index().PermanodeOfSignerAttrValue(ph.Search.Owner(), "camliRoot", ph.RootName)
|
|
if err != nil {
|
|
log.Printf("Error: publish handler at serving root name %q has no configured permanode: %v",
|
|
ph.RootName, err)
|
|
}
|
|
return br, err
|
|
}
|
|
|
|
func (ph *PublishHandler) lookupPathTarget(root *blobref.BlobRef, suffix string) (*blobref.BlobRef, error) {
|
|
if suffix == "" {
|
|
return root, nil
|
|
}
|
|
path, err := ph.Search.Index().PathLookup(ph.Search.Owner(), root, suffix, time.Time{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if path.Target == nil {
|
|
return nil, os.ErrNotExist
|
|
}
|
|
return path.Target, 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 *blobref.BlobRef
|
|
subject *blobref.BlobRef
|
|
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
|
|
}
|
|
|
|
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 := req.Header.Get("X-PrefixHandler-PathSuffix"), ""
|
|
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: req.Header.Get("X-PrefixHandler-PathBase"),
|
|
subres: res,
|
|
rootpn: rootpn,
|
|
dr: ph.Search.NewDescribeRequest(),
|
|
inSubjectChain: make(map[string]bool),
|
|
subjectBasePath: "",
|
|
}
|
|
}
|
|
|
|
func (ph *PublishHandler) ViewerIsOwner(req *http.Request) bool {
|
|
// TODO: better check later
|
|
return strings.HasPrefix(req.RemoteAddr, "127.") ||
|
|
strings.HasPrefix(req.RemoteAddr, "localhost:")
|
|
}
|
|
|
|
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 []*blobref.BlobRef, fileName string) string {
|
|
return pr.SubresThumbnailURL(path, fileName, -1)
|
|
}
|
|
|
|
func (pr *publishRequest) SubresThumbnailURL(path []*blobref.BlobRef, 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
|
|
}
|
|
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 == nil {
|
|
pr.rw.WriteHeader(404)
|
|
return
|
|
}
|
|
|
|
if pr.Debug() {
|
|
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.serveSubject()
|
|
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 {
|
|
if m := closurePattern.FindStringSubmatch(pr.req.URL.Path[1:]); m != nil {
|
|
pr.req.URL.Path = "/" + m[1]
|
|
pr.ph.closureHandler.ServeHTTP(pr.rw, pr.req)
|
|
return
|
|
}
|
|
}
|
|
if pr.req.URL.Path == "/deps.js" {
|
|
serveDepsJS(pr.rw, pr.req)
|
|
return
|
|
}
|
|
pr.ph.staticHandler.ServeHTTP(pr.rw, pr.req)
|
|
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 *blobref.BlobRef) 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 {
|
|
camliRootPath, err := osutil.GoPackagePath("camlistore.org")
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
// TODO(mpl): to change when we do the final move into ui
|
|
fullpath := filepath.Join(camliRootPath, "server", "camlistored", "newui", filename)
|
|
f, err := os.Open(fullpath)
|
|
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 ""
|
|
}
|
|
|
|
func (pr *publishRequest) serveSubject() {
|
|
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)
|
|
pr.pf("<p>Errors loading.</p>")
|
|
return
|
|
}
|
|
|
|
subdes := res[pr.subject.String()]
|
|
|
|
if subdes.CamliType == "file" {
|
|
pr.serveFileDownload(subdes)
|
|
return
|
|
}
|
|
|
|
title := subdes.Title()
|
|
|
|
// HTML header + Javascript
|
|
var camliPage string
|
|
{
|
|
pr.pf("<!doctype html>\n<html>\n<head>\n <title>%s</title>\n", html.EscapeString(title))
|
|
// TODO(mpl): We are only using the first .js file, and expecting it to be
|
|
// using closure. Do we want to be more open about this?
|
|
if len(pr.ph.JSFiles) > 0 {
|
|
camliPage = camliClosurePage(pr.ph.JSFiles[0])
|
|
}
|
|
|
|
if camliPage != "" {
|
|
pr.pf(" <script src='%s'></script>\n", pr.staticPath("closure/goog/base.js"))
|
|
pr.pf(" <script src='%s'></script>\n", pr.staticPath("deps.js"))
|
|
if pr.ViewerIsOwner() {
|
|
pr.pf(" <script src='%s'></script>\n", pr.base+"?camli.mode=config&var=CAMLISTORE_CONFIG")
|
|
}
|
|
pr.pf(" <script src='%s'></script>\n", pr.staticPath("base64.js"))
|
|
pr.pf(" <script src='%s'></script>\n", pr.staticPath("Crypto.js"))
|
|
pr.pf(" <script src='%s'></script>\n", pr.staticPath("SHA1.js"))
|
|
pr.pf("<script>\n goog.require('camlistore.%s');\n </script>\n", camliPage)
|
|
}
|
|
for _, filename := range pr.ph.CSSFiles {
|
|
pr.pf(" <link rel='stylesheet' type='text/css' href='%s'>\n", pr.staticPath(filename))
|
|
}
|
|
|
|
pr.pf(" <script>\n")
|
|
pr.pf("var camliViewIsOwner = %v;\n", pr.ViewerIsOwner())
|
|
pr.pf("var camliPagePermanode = %q;\n", pr.subject)
|
|
pr.pf("var camliPageMeta = \n")
|
|
json, _ := json.MarshalIndent(res, "", " ")
|
|
pr.rw.Write(json)
|
|
pr.pf(";\n </script>\n</head>\n<body>\n")
|
|
defer pr.pf("</body>\n</html>\n")
|
|
}
|
|
|
|
if title != "" {
|
|
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 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 id='members'>\n")
|
|
for _, member := range members {
|
|
des := member.Description()
|
|
if des != "" {
|
|
des = " - " + des
|
|
}
|
|
var fileLink, thumbnail string
|
|
if path, fileInfo, ok := member.PermanodeFile(); ok {
|
|
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 id='%s'><a href='%s'>%s<span>%s</span></a>%s%s</li>\n",
|
|
member.DomID(),
|
|
pr.memberPath(member.BlobRef),
|
|
thumbnail,
|
|
html.EscapeString(member.Title()),
|
|
des,
|
|
fileLink)
|
|
}
|
|
pr.pf("</ul>\n")
|
|
}
|
|
|
|
if camliPage != "" {
|
|
pr.pf("<script>\n")
|
|
pr.pf("var page = new camlistore.%s(CAMLISTORE_CONFIG);\n", camliPage)
|
|
pr.pf("page.decorate(document.body);\n")
|
|
pr.pf("</script>\n")
|
|
}
|
|
}
|
|
|
|
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.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,
|
|
sc: pr.ph.sc,
|
|
}
|
|
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 *blobref.BlobRef, fileinfo *search.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 := blobref.Parse(cref)
|
|
if cbr == nil {
|
|
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) (*blobref.BlobRef, error) {
|
|
signed, err := jsonSign.Sign(bb)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error signing %s: %v", name, err)
|
|
}
|
|
uh := client.NewUploadHandleFromString(signed)
|
|
_, err = ph.Storage.ReceiveBlob(uh.BlobRef, uh.Contents)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error uploading %s: %v", name, err)
|
|
}
|
|
return uh.BlobRef, nil
|
|
}
|
|
|
|
func (ph *PublishHandler) setRootNode(jsonSign *signhandler.Handler, pn *blobref.BlobRef) (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) {
|
|
if pn, err := ph.Search.Index().PermanodeOfSignerAttrValue(ph.Search.Owner(), "camliRoot", ph.RootName); err == nil {
|
|
log.Printf("Publish root %q using existing permanode %s", ph.RootName, pn)
|
|
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
|
|
}
|