This commit is contained in:
Brad Fitzpatrick 2013-11-19 15:28:20 -08:00
commit 838a59c56f
15 changed files with 529 additions and 228 deletions

1
config/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
flickr-credentials.json

View File

@ -24,6 +24,7 @@
"blobRoot": "/bs-and-maybe-also-index/",
"searchRoot": "/my-search/",
"cache": "/cache/",
"goTemplate": "blog.html",
"devBootstrapPermanodeUsing": "/sighelper/"
}
},
@ -38,6 +39,7 @@
"scaledImage": "lrucache",
"css": ["pics.css"],
"js": ["pics.js"],
"goTemplate": "gallery.html",
"devBootstrapPermanodeUsing": "/sighelper/"
}
},
@ -269,6 +271,14 @@
}
},
"/importer-flickr/": {
"handler": "importer-flickr",
"enabled": ["_env", "${CAMLI_FLICKR_ENABLED}", false],
"handlerArgs": {
"apiKey": ["_env", "${CAMLI_FLICKR_API_KEY}", ""]
}
},
"/share/": {
"handler": "share",
"handlerArgs": {

View File

@ -61,7 +61,8 @@ type serverCmd struct {
fullClosure bool
openBrowser bool
openBrowser bool
flickrAPIKey string
// end of flag vars
listen string // address + port to listen on
@ -94,6 +95,7 @@ func init() {
flags.BoolVar(&cmd.fullClosure, "fullclosure", false, "Use the ondisk closure library.")
flags.BoolVar(&cmd.openBrowser, "openbrowser", false, "Open the start page on startup.")
flags.StringVar(&cmd.flickrAPIKey, "flickrapikey", "", "The key and secret to use with the Flickr importer. Formatted as '<key>:<secret>'.")
return cmd
})
}
@ -143,6 +145,7 @@ func (c *serverCmd) setCamliRoot() error {
if err := os.RemoveAll(c.camliRoot); err != nil {
return fmt.Errorf("Could not wipe %v: %v", c.camliRoot, err)
}
os.Remove(filepath.Join("config", "flickr-credentials.json"))
}
return nil
}
@ -249,6 +252,11 @@ func (c *serverCmd) setEnvVars() error {
setenv("CAMLI_SECRET_RING", filepath.Join(camliSrcRoot,
filepath.FromSlash(defaultSecring)))
setenv("CAMLI_KEYID", defaultKeyID)
if c.flickrAPIKey != "" {
setenv("CAMLI_FLICKR_ENABLED", "true")
setenv("CAMLI_FLICKR_API_KEY", c.flickrAPIKey)
}
setenv("CAMLI_CONFIG_DIR", "config")
return nil
}

17
doc/publishing/README Normal file
View File

@ -0,0 +1,17 @@
Camlistore uses Go html templates (http://golang.org/pkg/text/template/) to publish pages.
Resources for publishing, such as go templates, javascript and css files should be placed in server/camlistored/ui/, so they can be served directly when using the dev server or automatically embedded when using camlistored directly.
You can then specify those resources through the configuration file. For example, there already is a go template (gallery.html), javascript file (pics.js) and css file (pics.css) that work together to provide publishing for image galleries. The dev server config (config/dev-server-config.json) already uses them. Here is how one would use them in the server config ($HOME/.config/camlistore/server-config.json):
"publish": {
"/pics/": {
"rootPermanode": "sha1-6cbe9e1c35e854eab028cba43d099d35ceae0de8",
"style": "pics.css",
"js": "pics.js",
"goTemplate": "gallery.html"
}
}
If you want to provide your own (Go) template, see http://camlistore.org/pkg/publish for the data structures and functions available to the template.

View File

@ -61,8 +61,12 @@ func writeCredentials(user *userInfo) {
}
}
// This returns nil,nil if the file doesn't exist. Any other error bad.
func readCredentials() (*userInfo, error) {
fi, err := os.Open(userFile)
if os.IsNotExist(err) {
return nil, nil
}
if err != nil {
return nil, err
}

View File

@ -24,6 +24,7 @@ import (
"log"
"net/http"
"net/url"
"strings"
"camlistore.org/pkg/importer"
"camlistore.org/pkg/jsonconfig"
@ -45,14 +46,22 @@ type imp struct {
}
func newFromConfig(cfg jsonconfig.Obj, host *importer.Host) (importer.Importer, error) {
oauthClient.Credentials = oauth.Credentials{
Token: cfg.OptionalString("appKey", ""),
Secret: cfg.OptionalString("appSecret", ""),
}
apiKey := cfg.RequiredString("apiKey")
if err := cfg.Validate(); err != nil {
return nil, err
}
user, _ := readCredentials()
parts := strings.Split(apiKey, ":")
if len(parts) != 2 {
return nil, fmt.Errorf("Flickr importer: Invalid apiKey configuration: %q", apiKey)
}
oauthClient.Credentials = oauth.Credentials{
Token: parts[0],
Secret: parts[1],
}
user, err := readCredentials()
if err != nil {
return nil, err
}
return &imp{
host: host,
user: user,

View File

@ -74,7 +74,7 @@ func (id *IndexDeps) Set(key, value string) error {
return id.Index.Storage().Set(key, value)
}
func (id *IndexDeps) dumpIndex(t *testing.T) {
func (id *IndexDeps) DumpIndex(t *testing.T) {
t.Logf("Begin index dump:")
it := id.Index.Storage().Find("")
for it.Next() {
@ -341,7 +341,7 @@ func Index(t *testing.T, initIdx func() *index.Index) {
)
lastPermanodeMutation := id.lastTime()
id.dumpIndex(t)
id.DumpIndex(t)
key := "signerkeyid:sha1-ad87ca5c78bd0ce1195c46f7c98e6025abbaf007"
if g, e := id.Get(key), "2931A67C26F5ABDA"; g != e {
@ -610,7 +610,7 @@ func PathsOfSignerTarget(t *testing.T, initIdx func() *index.Index) {
claim2 := id.SetAttribute(pn, "camliPath:with|pipe", "targ-124")
t.Logf("made path claims %q and %q", claim1, claim2)
id.dumpIndex(t)
id.DumpIndex(t)
type test struct {
blobref string
@ -657,7 +657,7 @@ func Files(t *testing.T, initIdx func() *index.Index) {
fileTime := time.Unix(1361250375, 0)
fileRef, wholeRef := id.UploadFile("foo.html", "<html>I am an html file.</html>", fileTime)
t.Logf("uploaded fileref %q, wholeRef %q", fileRef, wholeRef)
id.dumpIndex(t)
id.DumpIndex(t)
// ExistingFileSchemas
{
@ -714,7 +714,7 @@ func EdgesTo(t *testing.T, initIdx func() *index.Index) {
t.Logf("edge %s --> %s", pn1, pn2)
id.dumpIndex(t)
id.DumpIndex(t)
// Look for pn1
{
@ -740,7 +740,7 @@ func IsDeleted(t *testing.T, initIdx func() *index.Index) {
idx := initIdx()
id := NewIndexDeps(idx)
id.Fataler = t
defer id.dumpIndex(t)
defer id.DumpIndex(t)
pn1 := id.NewPermanode()
// delete pn1
@ -790,7 +790,7 @@ func DeletedAt(t *testing.T, initIdx func() *index.Index) {
idx := initIdx()
id := NewIndexDeps(idx)
id.Fataler = t
defer id.dumpIndex(t)
defer id.DumpIndex(t)
pn1 := id.NewPermanode()
// Test the never, ever, deleted case

View File

@ -24,7 +24,7 @@ import (
// requiredSchemaVersion is incremented every time
// an index key type is added, changed, or removed.
const requiredSchemaVersion = 2
const requiredSchemaVersion = 3
// type of key returns the identifier in k before the first ":" or "|".
// (Originally we packed keys by hand and there are a mix of styles)

View File

@ -26,6 +26,7 @@ import (
_ "image/png"
"io"
"log"
"os"
"sort"
"strings"
"sync"
@ -126,6 +127,8 @@ func (ix *Index) commit(mm mutationMap) error {
// the blobref can be trusted at this point (it's been fully consumed
// and verified to match), and the sniffer has been populated.
func (ix *Index) populateMutationMap(br blob.Ref, sniffer *BlobSniffer) (mutationMap, error) {
// TODO(mpl): shouldn't we remove these two from the map (so they don't get committed) when
// e.g in populateClaim we detect a bogus claim (which does not yield an error)?
mm := mutationMap{
"have:" + br.String(): fmt.Sprintf("%d", sniffer.Size()),
"meta:" + br.String(): fmt.Sprintf("%d|%s", sniffer.Size(), sniffer.MIMEType()),
@ -137,10 +140,6 @@ func (ix *Index) populateMutationMap(br blob.Ref, sniffer *BlobSniffer) (mutatio
if err := ix.populateClaim(blob, mm); err != nil {
return nil, err
}
case "permanode":
//if err := mi.populatePermanode(blobRef, camli, mm); err != nil {
//return nil, err
//}
case "file":
if err := ix.populateFile(blob, mm); err != nil {
return nil, err
@ -311,6 +310,41 @@ func (ix *Index) populateDir(b *schema.Blob, mm mutationMap) error {
return nil
}
// populateDeleteClaim adds to mm the entries resulting from the delete claim cl.
// It is assumed cl is a valid claim, and vr has already been verified.
func (ix *Index) populateDeleteClaim(cl schema.Claim, vr *jsonsign.VerifyRequest, mm mutationMap) {
br := cl.Blob().BlobRef()
target := cl.Target()
if !target.Valid() {
log.Print(fmt.Errorf("no valid target for delete claim %v", br))
return
}
meta, err := ix.GetBlobMeta(target)
if err != nil {
if err == os.ErrNotExist {
// TODO: return a dependency error type, to schedule re-indexing in the future
}
log.Print(fmt.Errorf("Could not get mime type of target blob %v: %v", target, err))
return
}
// TODO(mpl): create consts somewhere for "claim" and "permanode" as camliTypes, and use them,
// instead of hardcoding. Unless they already exist ? (didn't find them).
if meta.CamliType != "permanode" && meta.CamliType != "claim" {
log.Print(fmt.Errorf("delete claim target in %v is neither a permanode nor a claim: %v", br, meta.CamliType))
return
}
mm.Set(keyDeleted.Key(target, cl.ClaimDateString(), br), "")
mm.Set(keyDeletes.Key(br, target), "")
if meta.CamliType == "claim" {
return
}
recentKey := keyRecentPermanode.Key(vr.SignerKeyId, cl.ClaimDateString(), br)
mm.Set(recentKey, target.String())
attr, value := cl.Attribute(), cl.Value()
claimKey := keyPermanodeClaim.Key(target, vr.SignerKeyId, cl.ClaimDateString(), br)
mm.Set(claimKey, keyPermanodeClaim.Val(cl.ClaimType(), attr, value, vr.CamliSigner))
}
func (ix *Index) populateClaim(b *schema.Blob, mm mutationMap) error {
br := b.BlobRef()
@ -320,13 +354,6 @@ func (ix *Index) populateClaim(b *schema.Blob, mm mutationMap) error {
return nil
}
pnbr := claim.ModifiedPermanode()
if !pnbr.Valid() {
// A different type of claim; not modifying a permanode.
return nil
}
attr, value := claim.Attribute(), claim.Value()
vr := jsonsign.NewVerificationRequest(b.JSON(), ix.KeyFetcher)
if !vr.Verify() {
// TODO(bradfitz): ask if the vr.Err.(jsonsign.Error).IsPermanent() and retry
@ -337,12 +364,22 @@ func (ix *Index) populateClaim(b *schema.Blob, mm mutationMap) error {
return errors.New("index: populateClaim verification failure")
}
verifiedKeyId := vr.SignerKeyId
mm.Set("signerkeyid:"+vr.CamliSigner.String(), verifiedKeyId)
if claim.ClaimType() == string(schema.DeleteClaim) {
ix.populateDeleteClaim(claim, vr, mm)
return nil
}
pnbr := claim.ModifiedPermanode()
if !pnbr.Valid() {
// A different type of claim; not modifying a permanode.
return nil
}
attr, value := claim.Attribute(), claim.Value()
recentKey := keyRecentPermanode.Key(verifiedKeyId, claim.ClaimDateString(), br)
mm.Set(recentKey, pnbr.String())
claimKey := keyPermanodeClaim.Key(pnbr, verifiedKeyId, claim.ClaimDateString(), br)
mm.Set(claimKey, keyPermanodeClaim.Val(claim.ClaimType(), attr, value, vr.CamliSigner))

89
pkg/publish/types.go Normal file
View File

@ -0,0 +1,89 @@
/*
Copyright 2013 The Camlistore Authors.
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 publish exposes the types and functions that can be used
// from a Go template, for publishing.
package publish
import (
"html/template"
"camlistore.org/pkg/search"
)
// SubjectPage is the data structure used when serving a
// publishing template. It contains the functions that can be called
// from the template.
type SubjectPage struct {
Header func() *PageHeader
File func() *PageFile
Members func() *PageMembers
}
// PageHeader contains the data available to the template,
// and relevant to the page header.
type PageHeader struct {
Title string // Page title.
CSSFiles []string // Available CSS files.
JSDeps []string // Dependencies (for e.g closure) that can/should be included as javascript files.
CamliClosure template.JS // Closure namespace defined in the provided js. e.g camlistore.GalleryPage from pics.js
Subject string // Subject of this page (i.e the object which is described and published).
Meta string // All the metadata describing the subject of this page.
ViewerIsOwner bool // Whether the viewer of the page is also the owner of the displayed subject. (localhost check for now.)
}
// PageFile contains the file related data available to the subject template,
// if the page describes some file contents.
type PageFile struct {
FileName string
Size int64
MIMEType string
IsImage bool
DownloadURL string
ThumbnailURL string
DomID string
Nav func() *Nav
}
// Nav holds links to the previous, next, and parent elements,
// when displaying members.
type Nav struct {
ParentPath string
PrevPath string
NextPath string
}
// PageMembers contains the data relevant to the members if the published subject
// is a permanode with members.
type PageMembers struct {
SubjectPath string // URL prefix path to the subject (i.e the permanode).
ZipName string // Name of the downloadable zip file which contains all the members.
Members []*search.DescribedBlob // List of the members.
Description func(*search.DescribedBlob) string // Returns the description of the given member.
Title func(*search.DescribedBlob) string // Returns the title for the given member.
Path func(*search.DescribedBlob) string // Returns the url prefix path to the given the member.
DomID func(*search.DescribedBlob) string // Returns the Dom ID of the given member.
FileInfo func(*search.DescribedBlob) *MemberFileInfo // Returns some file info if the given member is a file permanode.
}
// MemberFileInfo contains the file related data available for each member,
// if the member is the permanode for a file.
type MemberFileInfo struct {
FileName string
FileDomID string
FilePath string
FileThumbnailURL string
}

View File

@ -23,6 +23,8 @@ import (
"errors"
"fmt"
"html"
"html/template"
"io/ioutil"
"log"
"net/http"
"net/url"
@ -40,7 +42,7 @@ import (
"camlistore.org/pkg/httputil"
"camlistore.org/pkg/jsonconfig"
"camlistore.org/pkg/jsonsign/signhandler"
"camlistore.org/pkg/osutil"
"camlistore.org/pkg/publish"
"camlistore.org/pkg/schema"
"camlistore.org/pkg/search"
"camlistore.org/pkg/types/camtypes"
@ -56,7 +58,10 @@ type PublishHandler struct {
Cache blobserver.Storage // or nil
sc ScaledImage // cache of scaled images, optional
JSFiles, CSSFiles []string
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
@ -81,8 +86,9 @@ func newPublishFromConfig(ld blobserver.Loader, conf jsonconfig.Obj) (h http.Han
handlerFinder: ld,
}
ph.RootName = conf.RequiredString("rootName")
ph.JSFiles = conf.OptionalList("js")
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", "")
@ -180,9 +186,47 @@ func newPublishFromConfig(ld blobserver.Loader, conf jsonconfig.Obj) (h http.Han
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")
}
@ -413,7 +457,7 @@ func (pr *publishRequest) serveHTTP() {
switch pr.SubresourceType() {
case "":
pr.serveSubject()
pr.serveSubjectTemplate()
case "b":
// TODO: download a raw blob
case "f": // file download
@ -473,12 +517,7 @@ var provCamliRx = regexp.MustCompile(`^goog\.(provide)\(['"]camlistore\.(.*)['"]
// 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 ""
}
fullpath := filepath.Join(camliRootPath, "server", "camlistored", "ui", filename)
f, err := os.Open(fullpath)
f, err := uistatic.Files.Open(filename)
if err != nil {
return ""
}
@ -518,37 +557,6 @@ func (pr *publishRequest) serveZip() {
zh.ServeHTTP(pr.rw, pr.req)
}
// serveHeader serves the html header with the relevant title, javascript
// and css includes. It is meant to be called from serveSubject.
func (pr *publishRequest) serveHeader(title, camliClosurePage string) {
pr.pf("<!doctype html>\n<html>\n<head>\n <title>%s</title>\n", html.EscapeString(title))
if camliClosurePage != "" && pr.ViewerIsOwner() {
pr.pf(" <script src='%s'></script>\n", pr.staticPath("closure/goog/base.js"))
pr.pf(" <script src='%s'></script>\n", pr.staticPath("deps.js"))
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", camliClosurePage)
}
for _, filename := range pr.ph.CSSFiles {
pr.pf(" <link rel='stylesheet' type='text/css' href='%s'>\n", pr.staticPath(filename))
}
}
// serveMeta serves all the described meta data about the published items,
// within the html header. It is meant to be called from serveSubject.
func (pr *publishRequest) serveMeta(des map[string]*search.DescribedBlob) {
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(des, "", " ")
pr.rw.Write(json)
pr.pf(";\n </script>\n</head>\n")
}
// TODO(mpl): use those everywhere else
const (
resSeparator = "/-"
digestPrefix = "h"
@ -631,28 +639,115 @@ func (pr *publishRequest) parent() (parentPath string, parentBlobRef blob.Ref, e
return parentPath, parentBlobRef, nil
}
// serveNav serves some navigation links (prev, next, up) if the
// pr.subject is member of a collection (its parent has members).
// It is meant to be called from serveFile.
func (pr *publishRequest) serveNav() error {
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()
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 fmt.Errorf("Errors building nav links for %s: %v", pr.subject, err)
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,
}
parentNav := fmt.Sprintf("[<a href='%s'>up</a>]", strings.TrimSuffix(parentPath, resSeparator))
// 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 fmt.Errorf("Errors loading %s, permanode %s: %v, %#v", pr.req.URL, pr.subject, err, err)
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 {
pr.pf("<div class='camlifile'>[<a href='%s'>up</a>]</div>", parentNav)
return nil
return fileNav, nil
}
pos := 0
@ -669,68 +764,27 @@ func (pr *publishRequest) serveNav() error {
if pos < len(members)-1 {
next = members[pos+1].BlobRef
}
if prev.Valid() || next.Valid() {
var prevNav, nextNav string
if prev.Valid() {
prevNav = fmt.Sprintf("[<a href='%s/h%s'>prev</a>]",
parentPath, prev.DigestPrefix(10))
}
if next.Valid() {
nextNav = fmt.Sprintf("[<a href='%s/h%s'>next</a>]",
parentPath, next.DigestPrefix(10))
}
pr.pf("<div class='camlifile'>%s %s %s</div>", parentNav, prevNav, nextNav)
if !prev.Valid() && !next.Valid() {
return fileNav, nil
}
return 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
}
// serveFile serves the relevant view when the subject in serveSubject
// is a permanode with some content cref. It is meant to be called
// from serveSubject.
func (pr *publishRequest) serveFile(cref blob.Ref) error {
des, err := pr.dr.DescribeSync(cref)
if err != nil {
pr.pf("<p>Error serving file</p>")
return fmt.Errorf("Could not describe %v: %v", cref, err)
// 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
}
if des.File != nil {
path := []blob.Ref{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 strings.Contains(pr.subjectBasePath, resSeparator) {
// this permanode has a "parent" collection.
// so we send a deep request on the parent in order to get some info
// about the siblings and build some "prev" and "next" nav links.
// TODO(mpl): nav links everywhere, not just when showing a permanode
// with some content.
err := pr.serveNav()
if err != nil {
pr.pf("<p>Error building navs links</p>")
return err
}
}
return nil
}
// serveMembers serves the relevant view when the subject in serveSubject
// is a collection (permanode with members). It is meant to be called
// from serveSubject.
func (pr *publishRequest) serveMembers(title string, members []*search.DescribedBlob) {
zipName := ""
if title == "" {
if title := subdes.Title(); title == "" {
zipName = "download.zip"
} else {
zipName = title + ".zip"
@ -739,91 +793,95 @@ func (pr *publishRequest) serveMembers(title string, members []*search.Described
if !strings.Contains(subjectPath, "/-/") {
subjectPath += "/-"
}
pr.pf("<div><a href='%s/=z/%s'>%s</a></div>\n", subjectPath,
html.EscapeString(url.QueryEscape(zipName)), html.EscapeString(zipName))
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))
return &publish.PageMembers{
SubjectPath: subjectPath,
ZipName: zipName,
Members: subdes.Members(),
Description: func(member *search.DescribedBlob) string {
des := member.Description()
if des != "" {
des = " - " + des
}
}
memberTitle := member.Title()
if memberTitle == "" {
memberTitle = member.BlobRef.DigestPrefix(10)
}
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(memberTitle),
des,
fileLink)
}
pr.pf("</ul>\n")
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
}
func (pr *publishRequest) serveSubject() {
// 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)
pr.pf("<p>Errors loading.</p>")
http.Error(pr.rw, "Error loading describe request", http.StatusInternalServerError)
return
}
subdes := res[pr.subject.String()]
if subdes.CamliType == "file" {
pr.serveFileDownload(subdes)
return
}
title := subdes.Title()
// HTML header + Javascript
var camliPage string
// TODO(mpl): We are only using the first .js file, and expecting it to be
// using closure. We want to be more customizable in the long run and enable
// some sort of templating mechanism.
if len(pr.ph.JSFiles) > 0 {
camliPage = camliClosurePage(pr.ph.JSFiles[0])
headerFunc := func() *publish.PageHeader {
return pr.subjectHeader(res)
}
pr.serveHeader(title, camliPage)
pr.serveMeta(res)
pr.pf("<body>\n")
if title != "" {
pr.pf("<h1>%s</h1>\n", html.EscapeString(title))
}
defer pr.pf("</body>\n</html>\n")
if cref, ok := subdes.ContentRef(); ok {
err = pr.serveFile(cref)
fileFunc := func() *publish.PageFile {
file, err := pr.subjectFile(res)
if err != nil {
log.Print(err)
return
log.Printf("%v", err)
return nil
}
} else {
if members := subdes.Members(); len(members) > 0 {
pr.serveMembers(title, members)
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,
}
if camliPage != "" && pr.ViewerIsOwner() {
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")
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
}
}

View File

@ -44,7 +44,7 @@ type configPrefixesParams struct {
blobPath string
searchOwner blob.Ref
shareHandlerPath string
flickr map[string]interface{}
flickr string
}
var (
@ -62,7 +62,7 @@ func addPublishedConfig(prefixes jsonconfig.Obj,
return nil, fmt.Errorf("Wrong type for %s; was expecting map[string]interface{}, got %T", k, v)
}
rootName := strings.Replace(k, "/", "", -1) + "Root"
rootPermanode, template, style := "", "", ""
rootPermanode, goTemplate, style, js := "", "", "", ""
for pk, pv := range p {
val, ok := pv.(string)
if !ok {
@ -71,16 +71,18 @@ func addPublishedConfig(prefixes jsonconfig.Obj,
switch pk {
case "rootPermanode":
rootPermanode = val
case "template":
template = val
case "goTemplate":
goTemplate = val
case "style":
style = val
case "js":
js = val
default:
return nil, fmt.Errorf("Unexpected key %q in config for %s", pk, k)
}
}
if rootPermanode == "" || template == "" {
return nil, fmt.Errorf("Missing key in configuration for %s, need \"rootPermanode\" and \"template\"", k)
if rootPermanode == "" || goTemplate == "" {
return nil, fmt.Errorf("Missing key in configuration for %s, need \"rootPermanode\" and \"goTemplate\"", k)
}
ob := map[string]interface{}{}
ob["handler"] = "publish"
@ -94,19 +96,10 @@ func addPublishedConfig(prefixes jsonconfig.Obj,
if sourceRoot != "" {
handlerArgs["sourceRoot"] = sourceRoot
}
switch template {
case "gallery":
if style == "" {
style = "pics.css"
}
handlerArgs["css"] = []interface{}{style}
handlerArgs["js"] = []interface{}{"pics.js"}
handlerArgs["scaledImage"] = "lrucache"
case "blog":
if style != "" {
handlerArgs["css"] = []interface{}{style}
}
}
handlerArgs["goTemplate"] = goTemplate
handlerArgs["css"] = []interface{}{style}
handlerArgs["js"] = []interface{}{js}
handlerArgs["scaledImage"] = "lrucache"
ob["handlerArgs"] = handlerArgs
prefixes[k] = ob
pubPrefixes = append(pubPrefixes, k)
@ -445,10 +438,9 @@ func genLowLevelPrefixes(params *configPrefixesParams, ownerName string) (m json
}
}
if len(params.flickr) > 0 {
if params.flickr != "" {
m["/importer-flickr/"] = map[string]interface{}{
"handler": "importer-flickr",
"handlerArgs": params.flickr,
"apiKey": params.flickr,
}
}
@ -539,7 +531,7 @@ func genLowLevelConfig(conf *Config) (lowLevelConf *Config, err error) {
kvFile = conf.OptionalString("kvIndexFile", "")
// Importer options
flickr = conf.OptionalObject("flickr")
flickr = conf.OptionalString("flickr", "")
_ = conf.OptionalList("replicateTo")
publish = conf.OptionalObject("publish")

View File

@ -0,0 +1,6 @@
<!doctype html>
<html>
<body>
TODO
</body>
</html>

View File

@ -0,0 +1,67 @@
<!doctype html>
<html>
{{if $header := call .Header}}
<head>
<title>{{$header.Title}}</title>
{{range $js := $header.JSDeps}}
<script src='{{$js}}'></script>
{{end}}
{{if $header.CamliClosure}}
<script>goog.require('{{$header.CamliClosure}}');</script>
{{end}}
{{range $css := $header.CSSFiles}}
<link rel='stylesheet' type='text/css' href='{{$css}}'>
{{end}}
<script>
var camliViewIsOwner = {{$header.ViewerIsOwner}};
var camliPagePermanode = {{$header.Subject}};
var camliPageMeta =
{{$header.Meta}};
</script>
</head>
<body>
<h1>{{$header.Title}}</h1>
{{if $file := call .File}}
<div>File: {{$file.FileName}}, {{$file.Size}} bytes, type {{$file.MIMEType}}</div>
{{if $file.IsImage}}
<a href='{{$file.DownloadURL}}'><img src='{{$file.ThumbnailURL}}'></a>
{{end}}
<div id='{{$file.DomID}}' class='camlifile'>[<a href='{{$file.DownloadURL}}'>download</a>]</div>
{{if $nav := call $file.Nav}}
<div class='camlifile'>
{{if $prev := $nav.PrevPath}}[<a href='{{$prev}}'>prev</a>] {{end}}
{{if $up := $nav.ParentPath}}[<a href='{{$up}}'>up</a>] {{end}}
{{if $next := $nav.NextPath}}[<a href='{{$next}}'>next</a>] {{end}}
</div>
{{end}}
{{else}}
{{if $membersData := call .Members}}
<div><a href='{{$membersData.SubjectPath}}/=z/{{html $membersData.ZipName | urlquery}}'>{{html $membersData.ZipName}}</a></div>
<!-- TODO(mpl): something's messed up with the hidden edit title position, it should appear under the image. -->
<ul id='members'>
{{range $member := $membersData.Members}}
<li id='{{call $membersData.DomID $member}}'>
<a href='{{call $membersData.Path $member}}'>
{{$fileInfo := call $membersData.FileInfo $member}}
<img src='{{if $fileInfo}}{{$fileInfo.FileThumbnailURL}}{{end}}'>
<span>{{call $membersData.Title $member}}</span></a>
{{call $membersData.Description $member}}
<div id='{{if $fileInfo}}{{$fileInfo.FileDomID}}{{end}}' class='camlifile'>
<a href='{{if $fileInfo}}{{$fileInfo.FilePath}}{{end}}'>file</a>
</div>
</li>
{{end}}
</ul>
{{end}}
{{end}}
{{if $header.CamliClosure}}
{{if $header.ViewerIsOwner}}
<script>
var page = new {{$header.CamliClosure}}(CAMLISTORE_CONFIG);
page.decorate(document.body);
</script>
{{end}}
{{end}}
{{end}}
</body>
</html>

View File

@ -23,7 +23,7 @@ web browser and restart the server.</p>
<li><b><code>baseURL</code></b>: Optional. If non-empty, this is the root of your URL prefix for your Camlistore server. Useful for when running behind a reverse proxy. Should not end in a slash. e.g. <code>https://yourserver.example.com</code></li>
<li><b><code>flickr</code></b>: Optional, and doesn't do anything yet. Support for continuous import from Flickr. Enter two child keys: <code>appKey</code> and <code>appSecret</code>, which you can get by filling out <a href="http://www.flickr.com/services/apps/create/noncommercial/">this form</a>.</li>
<li><b><code>flickr</code></b>: Optional, and doesn't do anything yet. Support for continuous import from Flickr. Should be an API key and secret formatted as <code>key</code>:<code>secret</code>. Get yours by filling out <a href="http://www.flickr.com/services/apps/create/noncommercial/">this form</a>.</li>
<li><b><code>https</code></b>: if "true", HTTPS is used
<ul>
@ -67,18 +67,21 @@ to <code>pkg/genconfig</code> welcome.</p>
config, as used by <code>devcam server</code>.</p>
<h2 id="publishing">Publishing options</h2>
<p>Although limited, publishing can be configured through the <b><code>publish</code></b> key. There is only support for an image gallery view (even though it will display thumbnails for other kinds of items), which is not really customizable. Here is an example of a value if one wanted to publish some items under <code>/pics/</code>:</p>
<p>Camlistore uses Go html templates to publish pages, and publishing can be configured through the <b><code>publish</code></b> key. There is already support for an image gallery view, which can be enabled similarly to the example below (obviously, the rootPermanode will be different).</p>
<pre>
{
"/pics/": {
"rootPermanode": "sha1-09888624be84fcb3ae67e8aa2f29682b4ff515d7",
"style": "pics.css",
"template": "gallery"
}
"publish": {
"/pics/": {
"rootPermanode": "sha1-6cbe9e1c35e854eab028cba43d099d35ceae0de8",
"style": "pics.css",
"js": "pics.js",
"goTemplate": "gallery.html"
}
}
</pre>
<p>One can create any permanode with camput or the UI and use it as the rootPermanode.</p>
<p>Please see the <a href="/gw/doc/publishing/README"">publishing README</a> if you want to make/contribute more publishing views.</p>
<h2 id="windows">Windows</h2>
<p>