mirror of https://github.com/perkeep/perkeep.git
publisher app
http://camlistore.org/issue/365 Change-Id: I281fdcbbe6a2bdf15607e75a21bc93b453f82c85
This commit is contained in:
parent
173adebc19
commit
a34f9e2669
|
@ -22,6 +22,7 @@ bin/cam*
|
|||
bin/devcam
|
||||
bin/*_*
|
||||
bin/hello
|
||||
bin/publisher
|
||||
tmp
|
||||
server/camlistored/newui/all.js
|
||||
server/camlistored/newui/all.js.map
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
Copyright 2014 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.
|
||||
*/
|
||||
|
||||
/*
|
||||
#fileembed pattern .+\.(js|css|html|png|svg)$
|
||||
*/
|
||||
package main
|
||||
|
||||
import (
|
||||
"camlistore.org/pkg/fileembed"
|
||||
)
|
||||
|
||||
// TODO(mpl): appengine case
|
||||
|
||||
var Files = &fileembed.Files{}
|
|
@ -3,12 +3,6 @@
|
|||
{{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}}
|
||||
|
@ -54,14 +48,6 @@
|
|||
</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>
|
File diff suppressed because it is too large
Load Diff
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2011 Google Inc.
|
||||
Copyright 2014 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.
|
||||
|
@ -14,15 +14,17 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
package server
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
camliClient "camlistore.org/pkg/client"
|
||||
"camlistore.org/pkg/httputil"
|
||||
"camlistore.org/pkg/index"
|
||||
"camlistore.org/pkg/index/indextest"
|
||||
|
@ -113,6 +115,29 @@ func setupContent(rootName string) *indextest.IndexDeps {
|
|||
return idxd
|
||||
}
|
||||
|
||||
type fakeClient struct {
|
||||
*camliClient.Client // for blob.Fetcher
|
||||
sh *search.Handler
|
||||
}
|
||||
|
||||
func (fc *fakeClient) Search(req *search.SearchQuery) (*search.SearchResult, error) {
|
||||
return fc.sh.Query(req)
|
||||
}
|
||||
|
||||
func (fc *fakeClient) Describe(req *search.DescribeRequest) (*search.DescribeResponse, error) {
|
||||
return fc.sh.Describe(req)
|
||||
}
|
||||
|
||||
func (fc *fakeClient) GetJSON(url string, data interface{}) error {
|
||||
// no need to implement
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fc *fakeClient) Post(url string, bodyType string, body io.Reader) error {
|
||||
// no need to implement
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestPublishURLs(t *testing.T) {
|
||||
rootName := "foo"
|
||||
idxd := setupContent(rootName)
|
||||
|
@ -122,9 +147,14 @@ func TestPublishURLs(t *testing.T) {
|
|||
t.Fatalf("error slurping index to memory: %v", err)
|
||||
}
|
||||
sh.SetCorpus(corpus)
|
||||
ph := &PublishHandler{
|
||||
RootName: rootName,
|
||||
Search: sh,
|
||||
cl := camliClient.New("http://whatever.fake")
|
||||
fcl := &fakeClient{cl, sh}
|
||||
ph := &publishHandler{
|
||||
rootName: rootName,
|
||||
cl: fcl,
|
||||
}
|
||||
if err := ph.initRootNode(); err != nil {
|
||||
t.Fatalf("initRootNode: %v", err)
|
||||
}
|
||||
|
||||
for ti, tt := range publishURLTests {
|
||||
|
@ -137,9 +167,12 @@ func TestPublishURLs(t *testing.T) {
|
|||
pfxh := &httputil.PrefixHandler{
|
||||
Prefix: "/pics/",
|
||||
Handler: http.HandlerFunc(func(_ http.ResponseWriter, req *http.Request) {
|
||||
pr := ph.NewRequest(rw, req)
|
||||
pr, err := ph.NewRequest(rw, req)
|
||||
if err != nil {
|
||||
t.Fatalf("test #%d, NewRequest: %v", ti, err)
|
||||
}
|
||||
|
||||
err := pr.findSubject()
|
||||
err = pr.findSubject()
|
||||
if tt.subject != "" {
|
||||
if err != nil {
|
||||
t.Errorf("test #%d, findSubject: %v", ti, err)
|
||||
|
@ -168,9 +201,11 @@ func TestPublishMembers(t *testing.T) {
|
|||
t.Fatalf("error slurping index to memory: %v", err)
|
||||
}
|
||||
sh.SetCorpus(corpus)
|
||||
ph := &PublishHandler{
|
||||
RootName: rootName,
|
||||
Search: sh,
|
||||
cl := camliClient.New("http://whatever.fake")
|
||||
fcl := &fakeClient{cl, sh}
|
||||
ph := &publishHandler{
|
||||
rootName: rootName,
|
||||
cl: fcl,
|
||||
}
|
||||
|
||||
rw := httptest.NewRecorder()
|
||||
|
@ -179,17 +214,17 @@ func TestPublishMembers(t *testing.T) {
|
|||
pfxh := &httputil.PrefixHandler{
|
||||
Prefix: "/pics/",
|
||||
Handler: http.HandlerFunc(func(_ http.ResponseWriter, req *http.Request) {
|
||||
pr := ph.NewRequest(rw, req)
|
||||
|
||||
dr := pr.ph.Search.NewDescribeRequest()
|
||||
dr.Describe(pr.subject, 3)
|
||||
res, err := dr.Result()
|
||||
pr, err := ph.NewRequest(rw, req)
|
||||
if err != nil {
|
||||
t.Errorf("Result: %v", err)
|
||||
return
|
||||
t.Fatalf("NewRequest: %v", err)
|
||||
}
|
||||
|
||||
members, err := pr.subjectMembers(res)
|
||||
res, err := pr.ph.deepDescribe(pr.subject)
|
||||
if err != nil {
|
||||
t.Fatalf("deepDescribe: %v", err)
|
||||
}
|
||||
|
||||
members, err := pr.subjectMembers(res.Meta)
|
||||
if len(members.Members) != 2 {
|
||||
t.Errorf("Expected two members in publish root (one camlipath, one camlimember), got %d", len(members.Members))
|
||||
}
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
package server
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
|
@ -31,12 +31,13 @@ import (
|
|||
"camlistore.org/pkg/httputil"
|
||||
"camlistore.org/pkg/schema"
|
||||
"camlistore.org/pkg/search"
|
||||
"camlistore.org/pkg/types/camtypes"
|
||||
)
|
||||
|
||||
type zipHandler struct {
|
||||
fetcher blob.Fetcher
|
||||
search *search.Handler
|
||||
// the "parent" permanode of everything to zip.
|
||||
cl client // Used for search and describe requests.
|
||||
// root is the "parent" permanode of everything to zip.
|
||||
// Either a directory permanode, or a permanode with members.
|
||||
root blob.Ref
|
||||
// Optional name to use in the response header
|
||||
|
@ -58,17 +59,47 @@ func (s sortedFiles) Less(i, j int) bool { return s[i].path < s[j].path }
|
|||
func (s sortedFiles) Len() int { return len(s) }
|
||||
func (s sortedFiles) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
||||
|
||||
func (zh *zipHandler) describeMembers(br blob.Ref) (*search.DescribeResponse, error) {
|
||||
res, err := zh.cl.Search(&search.SearchQuery{
|
||||
Constraint: &search.Constraint{
|
||||
BlobRefPrefix: br.String(),
|
||||
CamliType: "permanode",
|
||||
},
|
||||
Describe: &search.DescribeRequest{
|
||||
ThumbnailSize: 1000,
|
||||
Depth: 1,
|
||||
Rules: []*search.DescribeRule{
|
||||
{
|
||||
Attrs: []string{"camliContent", "camliContentImage", "camliMember"},
|
||||
},
|
||||
},
|
||||
},
|
||||
Limit: -1,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Could not describe %v: %v", br, err)
|
||||
}
|
||||
if res == nil || res.Describe == nil {
|
||||
return nil, fmt.Errorf("no describe result for %v", br)
|
||||
}
|
||||
return res.Describe, nil
|
||||
}
|
||||
|
||||
// blobList returns the list of file blobs "under" dirBlob.
|
||||
// It traverses permanode directories and permanode with members (collections).
|
||||
func (zh *zipHandler) blobList(dirPath string, dirBlob blob.Ref) ([]*blobFile, error) {
|
||||
dr := zh.search.NewDescribeRequest()
|
||||
dr.Describe(dirBlob, 3)
|
||||
res, err := dr.Result()
|
||||
// dr := zh.search.NewDescribeRequest()
|
||||
// dr.Describe(dirBlob, 3)
|
||||
// res, err := dr.Result()
|
||||
// if err != nil {
|
||||
// return nil, fmt.Errorf("Could not describe %v: %v", dirBlob, err)
|
||||
// }
|
||||
res, err := zh.describeMembers(dirBlob)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Could not describe %v: %v", dirBlob, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
described := res[dirBlob.String()]
|
||||
described := res.Meta[dirBlob.String()]
|
||||
members := described.Members()
|
||||
dirBlobPath, _, isDir := described.PermanodeDir()
|
||||
if len(members) == 0 && !isDir {
|
||||
|
@ -85,13 +116,13 @@ func (zh *zipHandler) blobList(dirPath string, dirBlob blob.Ref) ([]*blobFile, e
|
|||
return list, nil
|
||||
}
|
||||
for _, member := range members {
|
||||
if fileBlobPath, fileInfo, ok := member.PermanodeFile(); ok {
|
||||
if fileBlobPath, fileInfo, ok := getFileInfo(member.BlobRef, res.Meta); ok {
|
||||
// file
|
||||
list = append(list,
|
||||
&blobFile{fileBlobPath[1], path.Join(dirPath, fileInfo.FileName)})
|
||||
continue
|
||||
}
|
||||
if dirBlobPath, dirInfo, ok := member.PermanodeDir(); ok {
|
||||
if dirBlobPath, dirInfo, ok := getDirInfo(member.BlobRef, res.Meta); ok {
|
||||
// directory
|
||||
newZipRoot := dirBlobPath[1]
|
||||
children, err := zh.blobsFromDir(
|
||||
|
@ -258,3 +289,21 @@ func (zh *zipHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
|||
return
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(mpl): refactor with getFileInfo
|
||||
func getDirInfo(item blob.Ref, peers map[string]*search.DescribedBlob) (path []blob.Ref, di *camtypes.FileInfo, ok bool) {
|
||||
described := peers[item.String()]
|
||||
if described == nil ||
|
||||
described.Permanode == nil ||
|
||||
described.Permanode.Attr == nil {
|
||||
return
|
||||
}
|
||||
contentRef := described.Permanode.Attr.Get("camliContent")
|
||||
if contentRef == "" {
|
||||
return
|
||||
}
|
||||
if cdes := peers[contentRef]; cdes != nil && cdes.Dir != nil {
|
||||
return []blob.Ref{described.BlobRef, cdes.BlobRef}, cdes.Dir, true
|
||||
}
|
||||
return
|
||||
}
|
|
@ -28,31 +28,17 @@
|
|||
}
|
||||
},
|
||||
|
||||
"/blog/": {
|
||||
"enabled": ["_env", "${CAMLI_PUBLISH_ENABLED}"],
|
||||
"handler": "publish",
|
||||
"handlerArgs": {
|
||||
"rootName": "dev-blog-root",
|
||||
"blobRoot": "/bs/",
|
||||
"searchRoot": "/my-search/",
|
||||
"cache": "/cache/",
|
||||
"goTemplate": "blog.html",
|
||||
"devBootstrapPermanodeUsing": "/sighelper/"
|
||||
}
|
||||
},
|
||||
|
||||
"/pics/": {
|
||||
"handler": "app",
|
||||
"enabled": ["_env", "${CAMLI_PUBLISH_ENABLED}"],
|
||||
"handler": "publish",
|
||||
"handlerArgs": {
|
||||
"rootName": "dev-pics-root",
|
||||
"blobRoot": "/bs/",
|
||||
"searchRoot": "/my-search/",
|
||||
"cache": "/cache/",
|
||||
"css": ["pics.css"],
|
||||
"js": ["pics.js"],
|
||||
"goTemplate": "gallery.html",
|
||||
"devBootstrapPermanodeUsing": "/sighelper/"
|
||||
"program": "publisher",
|
||||
"appConfig": {
|
||||
"camliRoot": "dev-pics-root",
|
||||
"sourceRoot": ["_env", "${CAMLI_DEV_CAMLI_ROOT}", ""],
|
||||
"cacheRoot": ["_env", "${CAMLI_ROOT_CACHE}"],
|
||||
"goTemplate": "gallery.html"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -72,8 +58,7 @@
|
|||
"scaledImage": {
|
||||
"type": "kv",
|
||||
"file": ["_env", "${CAMLI_ROOT_CACHE}/thumbnails.kv", ""]
|
||||
},
|
||||
"publishRoots": ["/blog/", "/pics/"]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -55,7 +55,7 @@ type serverCmd struct {
|
|||
|
||||
fullClosure bool
|
||||
mini bool
|
||||
publish bool // whether to start the publish handlers
|
||||
publish bool // whether to build and start the publisher app(s)
|
||||
hello bool // whether to build and start the hello demo app
|
||||
|
||||
openBrowser bool
|
||||
|
@ -82,7 +82,7 @@ func init() {
|
|||
flags.BoolVar(&cmd.tls, "tls", false, "Use TLS.")
|
||||
flags.BoolVar(&cmd.wipe, "wipe", false, "Wipe the blobs on disk and the indexer.")
|
||||
flags.BoolVar(&cmd.debug, "debug", false, "Enable http debugging.")
|
||||
flags.BoolVar(&cmd.publish, "publish", true, "Enable publish handlers")
|
||||
flags.BoolVar(&cmd.publish, "publish", true, "Enable publisher app(s)")
|
||||
flags.BoolVar(&cmd.hello, "hello", false, "Enable hello (demo) app")
|
||||
flags.BoolVar(&cmd.mini, "mini", false, "Enable minimal mode, where all optional features are disabled. (Currently just publishing)")
|
||||
|
||||
|
@ -384,6 +384,9 @@ func (c *serverCmd) RunCommand(args []string) error {
|
|||
if c.hello {
|
||||
targets = append(targets, filepath.Join("app", "hello"))
|
||||
}
|
||||
if c.publish {
|
||||
targets = append(targets, filepath.Join("app", "publisher"))
|
||||
}
|
||||
for _, name := range targets {
|
||||
err := build(name)
|
||||
if err != nil {
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
Camlistore uses Go html templates (http://golang.org/pkg/text/template/) to publish pages.
|
||||
Camlistore delegates publishing to the publisher server application, which 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.
|
||||
Resources for publishing, such as go templates, javascript and css files should be placed in the application source directory - app/publisher/ - so they can be served directly when using the dev server or automatically embedded in production.
|
||||
|
||||
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):
|
||||
You should then specify the Go template to be used through the configuration file. The CSS files are automatically all available to the app. For example, there already is a go template (gallery.html), 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 configure publishing for an image gallery in the server config ($HOME/.config/camlistore/server-config.json):
|
||||
|
||||
"publish": {
|
||||
"/pics/": {
|
||||
"rootPermanode": "sha1-6cbe9e1c35e854eab028cba43d099d35ceae0de8",
|
||||
"style": "pics.css",
|
||||
"js": "pics.js",
|
||||
"camliRoot": "mypics",
|
||||
"cacheRoot": "/home/joe/var/camlistore/blobs/cache",
|
||||
"goTemplate": "gallery.html"
|
||||
}
|
||||
}
|
||||
|
|
3
make.go
3
make.go
|
@ -169,6 +169,7 @@ func main() {
|
|||
"camlistore.org/cmd/camtool",
|
||||
"camlistore.org/server/camlistored",
|
||||
"camlistore.org/app/hello",
|
||||
"camlistore.org/app/publisher",
|
||||
}
|
||||
switch *targets {
|
||||
case "*":
|
||||
|
@ -345,7 +346,7 @@ func buildSrcPath(fromSrc string) string {
|
|||
// kept in between runs.
|
||||
func genEmbeds() error {
|
||||
cmdName := exeName(filepath.Join(buildGoPath, "bin", "genfileembed"))
|
||||
for _, embeds := range []string{"server/camlistored/ui", "pkg/server", "third_party/react", "third_party/glitch", "third_party/fontawesome"} {
|
||||
for _, embeds := range []string{"server/camlistored/ui", "pkg/server", "third_party/react", "third_party/glitch", "third_party/fontawesome", "app/publisher"} {
|
||||
embeds := buildSrcPath(embeds)
|
||||
args := []string{embeds}
|
||||
cmd := exec.Command(cmdName, args...)
|
||||
|
|
|
@ -785,6 +785,22 @@ func (c *Client) GetJSON(url string, data interface{}) error {
|
|||
return httputil.DecodeJSON(resp, data)
|
||||
}
|
||||
|
||||
// Post is like http://golang.org/pkg/net/http/#Client.Post
|
||||
// but with implementation details like gated requests. The
|
||||
// URL's host must match the client's configured server.
|
||||
func (c *Client) Post(url string, bodyType string, body io.Reader) error {
|
||||
if !strings.HasPrefix(url, c.discoRoot()) {
|
||||
return fmt.Errorf("wrong URL (%q) for this server", url)
|
||||
}
|
||||
req := c.newRequest("POST", url, body)
|
||||
req.Header.Set("Content-Type", bodyType)
|
||||
res, err := c.expect2XX(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return res.Body.Close()
|
||||
}
|
||||
|
||||
func (c *Client) newRequest(method, url string, body ...io.Reader) *http.Request {
|
||||
var bodyR io.Reader
|
||||
if len(body) > 0 {
|
||||
|
|
|
@ -456,32 +456,6 @@ func (b *DescribedBlob) peerBlob(br blob.Ref) *DescribedBlob {
|
|||
return &DescribedBlob{Request: b.Request, BlobRef: br, Stub: true}
|
||||
}
|
||||
|
||||
// HasSecureLinkTo returns true if there's a valid link from this blob
|
||||
// to the other blob. This is used in access control (hence the
|
||||
// somewhat redundant "Secure" in the name) and should be paranoid
|
||||
// against e.g. random user/attacker-control attributes making links
|
||||
// to other blobs.
|
||||
//
|
||||
// TODO: don't linear scan here. rewrite this in terms of ResolvePrefixHop,
|
||||
// passing down some policy perhaps? or maybe that's enough.
|
||||
func (b *DescribedBlob) HasSecureLinkTo(other blob.Ref) bool {
|
||||
if b == nil || !other.Valid() {
|
||||
return false
|
||||
}
|
||||
ostr := other.String()
|
||||
if b.Permanode != nil {
|
||||
if b.Permanode.Attr.Get("camliContent") == ostr {
|
||||
return true
|
||||
}
|
||||
for _, mstr := range b.Permanode.Attr["camliMember"] {
|
||||
if mstr == ostr {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (b *DescribedBlob) isPermanode() bool {
|
||||
return b.Permanode != nil
|
||||
}
|
||||
|
|
|
@ -639,53 +639,6 @@ func (sh *Handler) serveClaims(rw http.ResponseWriter, req *http.Request) {
|
|||
httputil.ReturnJSON(rw, res)
|
||||
}
|
||||
|
||||
// Given a blobref and a few hex characters of the digest of the next hop, return the complete
|
||||
// blobref of the prefix, if that's a valid next hop.
|
||||
func (sh *Handler) ResolvePrefixHop(parent blob.Ref, prefix string) (child blob.Ref, err 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 blob.Ref{}, 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 blob.Ref{}, fmt.Errorf("Failed to describe member %q in parent %q", prefix, parent)
|
||||
}
|
||||
if des.Permanode != nil {
|
||||
cr, ok := des.ContentRef()
|
||||
if ok && strings.HasPrefix(cr.Digest(), prefix) {
|
||||
return cr, nil
|
||||
}
|
||||
for _, member := range des.Members() {
|
||||
if strings.HasPrefix(member.BlobRef.Digest(), prefix) {
|
||||
return member.BlobRef, nil
|
||||
}
|
||||
}
|
||||
_, err := dr.DescribeSync(cr)
|
||||
if err != nil {
|
||||
return blob.Ref{}, fmt.Errorf("Failed to describe content %q of parent %q", cr, parent)
|
||||
}
|
||||
if _, _, ok := des.PermanodeDir(); ok {
|
||||
return sh.ResolvePrefixHop(cr, prefix)
|
||||
}
|
||||
} else if des.Dir != nil {
|
||||
for _, child := range des.DirChildren {
|
||||
if strings.HasPrefix(child.Digest(), prefix) {
|
||||
return child, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return blob.Ref{}, fmt.Errorf("Member prefix %q not found in %q", prefix, parent)
|
||||
}
|
||||
|
||||
func (sh *Handler) serveFiles(rw http.ResponseWriter, req *http.Request) {
|
||||
ret := jsonMap()
|
||||
defer httputil.ReturnJSON(rw, ret)
|
||||
|
|
|
@ -63,8 +63,8 @@ type ImageHandler struct {
|
|||
Cache blobserver.Storage // optional
|
||||
MaxWidth, MaxHeight int
|
||||
Square bool
|
||||
thumbMeta *thumbMeta // optional cache for scaled images
|
||||
resizeSem *syncutil.Sem
|
||||
ThumbMeta *ThumbMeta // optional cache index for scaled images
|
||||
ResizeSem *syncutil.Sem // Limit peak RAM used by concurrent image thumbnail calls.
|
||||
}
|
||||
|
||||
type subImager interface {
|
||||
|
@ -117,7 +117,7 @@ func (ih *ImageHandler) cacheScaled(thumbBytes []byte, name string) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ih.thumbMeta.Put(name, br)
|
||||
ih.ThumbMeta.Put(name, br)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -178,7 +178,7 @@ func cacheKey(bref string, width int, height int) string {
|
|||
// Almost all errors are not interesting. Real errors will be logged.
|
||||
func (ih *ImageHandler) scaledCached(buf *bytes.Buffer, file blob.Ref) (format string) {
|
||||
key := cacheKey(file.String(), ih.MaxWidth, ih.MaxHeight)
|
||||
br, err := ih.thumbMeta.Get(key)
|
||||
br, err := ih.ThumbMeta.Get(key)
|
||||
if err == errCacheMiss {
|
||||
return
|
||||
}
|
||||
|
@ -188,6 +188,9 @@ func (ih *ImageHandler) scaledCached(buf *bytes.Buffer, file blob.Ref) (format s
|
|||
}
|
||||
fr, err := ih.cached(br)
|
||||
if err != nil {
|
||||
if imageDebug {
|
||||
log.Printf("Could not get cached image %v: %v\n", br, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
defer fr.Close()
|
||||
|
@ -247,10 +250,10 @@ func (ih *ImageHandler) scaleImage(fileRef blob.Ref) (*formatAndImage, error) {
|
|||
// images being resized concurrently.
|
||||
ramSize := int64(conf.Width) * int64(conf.Height) * 3
|
||||
|
||||
if err = ih.resizeSem.Acquire(ramSize); err != nil {
|
||||
if err = ih.ResizeSem.Acquire(ramSize); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer ih.resizeSem.Release(ramSize)
|
||||
defer ih.ResizeSem.Release(ramSize)
|
||||
|
||||
i, imConfig, err := images.Decode(sr, &images.DecodeOpts{
|
||||
MaxWidth: ih.MaxWidth,
|
||||
|
@ -325,7 +328,7 @@ func (ih *ImageHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request, fil
|
|||
var imageData []byte
|
||||
format := ""
|
||||
cacheHit := false
|
||||
if ih.thumbMeta != nil && !disableThumbCache {
|
||||
if ih.ThumbMeta != nil && !disableThumbCache {
|
||||
var buf bytes.Buffer
|
||||
format = ih.scaledCached(&buf, file)
|
||||
if format != "" {
|
||||
|
@ -346,7 +349,7 @@ func (ih *ImageHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request, fil
|
|||
im := imi.(*formatAndImage)
|
||||
imageData = im.image
|
||||
format = im.format
|
||||
if ih.thumbMeta != nil {
|
||||
if ih.ThumbMeta != nil {
|
||||
err := ih.cacheScaled(imageData, key)
|
||||
if err != nil {
|
||||
log.Printf("image resize: %v", err)
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -143,7 +143,7 @@ func (rh *RootHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
|||
return
|
||||
}
|
||||
if req.URL.Path == "/favicon.ico" {
|
||||
serveStaticFile(rw, req, Files, "favicon.ico")
|
||||
ServeStaticFile(rw, req, Files, "favicon.ico")
|
||||
return
|
||||
}
|
||||
f := func(p string, a ...interface{}) {
|
||||
|
|
|
@ -27,30 +27,33 @@ import (
|
|||
|
||||
const memLRUSize = 1024 // arbitrary
|
||||
|
||||
// thumbMeta is a mapping from an image's scaling parameters (encoding
|
||||
var errCacheMiss = errors.New("not in cache")
|
||||
|
||||
// ThumbMeta is a mapping from an image's scaling parameters (encoding
|
||||
// as an opaque "key" string) and the blobref of the thumbnail
|
||||
// (currently it's file schema blob)
|
||||
// (currently its file schema blob).
|
||||
// ThumbMeta is safe for concurrent use by multiple goroutines.
|
||||
//
|
||||
// The key will be some string containing the original full-sized image's
|
||||
// blobref, its target dimensions, and any possible transformations on
|
||||
// it (e.g. cropping it to square).
|
||||
|
||||
var errCacheMiss = errors.New("not in cache")
|
||||
|
||||
type thumbMeta struct {
|
||||
mem *lru.Cache // string (see key format) -> blob.Ref
|
||||
type ThumbMeta struct {
|
||||
mem *lru.Cache // key -> blob.Ref
|
||||
kv sorted.KeyValue // optional
|
||||
}
|
||||
|
||||
// kv is optional
|
||||
func newThumbMeta(kv sorted.KeyValue) *thumbMeta {
|
||||
return &thumbMeta{
|
||||
// NewThumbMeta returns a new in-memory ThumbMeta, backed with the
|
||||
// optional kv.
|
||||
// If kv is nil, key/value pairs are stored in memory only.
|
||||
func NewThumbMeta(kv sorted.KeyValue) *ThumbMeta {
|
||||
return &ThumbMeta{
|
||||
mem: lru.New(memLRUSize),
|
||||
kv: kv,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *thumbMeta) Get(key string) (br blob.Ref, err error) {
|
||||
func (m *ThumbMeta) Get(key string) (blob.Ref, error) {
|
||||
var br blob.Ref
|
||||
if v, ok := m.mem.Get(key); ok {
|
||||
return v.(blob.Ref), nil
|
||||
}
|
||||
|
@ -72,7 +75,7 @@ func (m *thumbMeta) Get(key string) (br blob.Ref, err error) {
|
|||
return br, errCacheMiss
|
||||
}
|
||||
|
||||
func (m *thumbMeta) Put(key string, br blob.Ref) error {
|
||||
func (m *ThumbMeta) Put(key string, br blob.Ref) error {
|
||||
m.mem.Add(key, br)
|
||||
if m.kv != nil {
|
||||
return m.kv.Set(key, br.String())
|
||||
|
|
127
pkg/server/ui.go
127
pkg/server/ui.go
|
@ -38,6 +38,7 @@ import (
|
|||
"camlistore.org/pkg/jsonsign/signhandler"
|
||||
"camlistore.org/pkg/misc/closure"
|
||||
"camlistore.org/pkg/search"
|
||||
"camlistore.org/pkg/server/app"
|
||||
"camlistore.org/pkg/sorted"
|
||||
"camlistore.org/pkg/syncutil"
|
||||
uistatic "camlistore.org/server/camlistored/ui"
|
||||
|
@ -77,8 +78,7 @@ type UIHandler struct {
|
|||
// if we start having clients (like phones) that we want to upload
|
||||
// but don't trust to have private signing keys?
|
||||
JSONSignRoot string
|
||||
|
||||
publishRoots map[string]*PublishHandler
|
||||
publishRoots map[string]*publishRoot
|
||||
|
||||
prefix string // of the UI handler itself
|
||||
root *RootHandler
|
||||
|
@ -90,7 +90,7 @@ type UIHandler struct {
|
|||
|
||||
// Limit peak RAM used by concurrent image thumbnail calls.
|
||||
resizeSem *syncutil.Sem
|
||||
thumbMeta *thumbMeta // optional thumbnail key->blob.Ref cache
|
||||
thumbMeta *ThumbMeta // optional thumbnail key->blob.Ref cache
|
||||
|
||||
// sourceRoot optionally specifies the path to root of Camlistore's
|
||||
// source. If empty, the UI files must be compiled in to the
|
||||
|
@ -127,7 +127,6 @@ func uiFromConfig(ld blobserver.Loader, conf jsonconfig.Obj) (h http.Handler, er
|
|||
resizeSem: syncutil.NewSem(int64(conf.OptionalInt("maxResizeBytes",
|
||||
constants.DefaultMaxResizeMem))),
|
||||
}
|
||||
pubRoots := conf.OptionalList("publishRoots")
|
||||
cachePrefix := conf.OptionalString("cache", "")
|
||||
scaledImageConf := conf.OptionalObject("scaledImage")
|
||||
if err = conf.Validate(); err != nil {
|
||||
|
@ -141,24 +140,6 @@ func uiFromConfig(ld blobserver.Loader, conf jsonconfig.Obj) (h http.Handler, er
|
|||
}
|
||||
}
|
||||
|
||||
if os.Getenv("CAMLI_PUBLISH_ENABLED") == "false" {
|
||||
// Hack for dev server, to simplify its config with devcam server --publish=false.
|
||||
pubRoots = nil
|
||||
}
|
||||
|
||||
ui.publishRoots = make(map[string]*PublishHandler)
|
||||
for _, pubRoot := range pubRoots {
|
||||
h, err := ld.GetHandler(pubRoot)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("UI handler's publishRoots references invalid %q", pubRoot)
|
||||
}
|
||||
pubh, ok := h.(*PublishHandler)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("UI handler's publishRoots references invalid %q; not a PublishHandler", pubRoot)
|
||||
}
|
||||
ui.publishRoots[pubRoot] = pubh
|
||||
}
|
||||
|
||||
checkType := func(key string, htype string) {
|
||||
v := conf.OptionalString(key, "")
|
||||
if v == "" {
|
||||
|
@ -190,7 +171,7 @@ func uiFromConfig(ld blobserver.Loader, conf jsonconfig.Obj) (h http.Handler, er
|
|||
return nil, fmt.Errorf("UI handler's cache of %q error: %v", cachePrefix, err)
|
||||
}
|
||||
ui.Cache = bs
|
||||
ui.thumbMeta = newThumbMeta(scaledImageKV)
|
||||
ui.thumbMeta = NewThumbMeta(scaledImageKV)
|
||||
}
|
||||
|
||||
if ui.sourceRoot == "" {
|
||||
|
@ -259,6 +240,79 @@ func uiFromConfig(ld blobserver.Loader, conf jsonconfig.Obj) (h http.Handler, er
|
|||
return ui, nil
|
||||
}
|
||||
|
||||
type publishRoot struct {
|
||||
Name string
|
||||
Permanode blob.Ref
|
||||
Prefix string
|
||||
}
|
||||
|
||||
// InitHandler goes through all the other configured handlers to discover
|
||||
// the publisher ones, and uses them to populate ui.publishRoots.
|
||||
func (ui *UIHandler) InitHandler(hl blobserver.FindHandlerByTyper) error {
|
||||
searchPrefix, _, err := hl.FindHandlerByType("search")
|
||||
if err != nil {
|
||||
return errors.New("No search handler configured, which is necessary for the ui handler")
|
||||
}
|
||||
var sh *search.Handler
|
||||
htype, hi := hl.AllHandlers()
|
||||
if h, ok := hi[searchPrefix]; !ok {
|
||||
return errors.New("failed to find the \"search\" handler")
|
||||
} else {
|
||||
sh = h.(*search.Handler)
|
||||
}
|
||||
camliRootQuery := func(camliRoot string) (*search.SearchResult, error) {
|
||||
return sh.Query(&search.SearchQuery{
|
||||
Limit: 1,
|
||||
Constraint: &search.Constraint{
|
||||
Permanode: &search.PermanodeConstraint{
|
||||
Attr: "camliRoot",
|
||||
Value: camliRoot,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
for prefix, typ := range htype {
|
||||
if typ != "app" {
|
||||
continue
|
||||
}
|
||||
ah, ok := hi[prefix].(*app.Handler)
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("UI: handler for %v has type \"app\" but is not app.Handler", prefix))
|
||||
}
|
||||
if ah.ProgramName() != "publisher" {
|
||||
continue
|
||||
}
|
||||
appConfig := ah.AppConfig()
|
||||
if appConfig == nil {
|
||||
log.Printf("UI: app handler for %v has no appConfig", prefix)
|
||||
continue
|
||||
}
|
||||
camliRoot, ok := appConfig["camliRoot"].(string)
|
||||
if !ok {
|
||||
log.Printf("UI: camliRoot in appConfig is %T, want string, was %T", appConfig["camliRoot"])
|
||||
continue
|
||||
}
|
||||
result, err := camliRootQuery(camliRoot)
|
||||
if err != nil {
|
||||
log.Printf("UI: could not find permanode for camliRoot %v: %v", camliRoot, err)
|
||||
continue
|
||||
}
|
||||
if len(result.Blobs) == 0 || !result.Blobs[0].Blob.Valid() {
|
||||
log.Printf("UI: no valid permanode for camliRoot %v", camliRoot)
|
||||
continue
|
||||
}
|
||||
if ui.publishRoots == nil {
|
||||
ui.publishRoots = make(map[string]*publishRoot)
|
||||
}
|
||||
ui.publishRoots[prefix] = &publishRoot{
|
||||
Name: camliRoot,
|
||||
Prefix: prefix,
|
||||
Permanode: result.Blobs[0].Blob,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ui *UIHandler) makeClosureHandler(root string) (http.Handler, error) {
|
||||
return makeClosureHandler(root, "ui")
|
||||
}
|
||||
|
@ -415,11 +469,12 @@ func (ui *UIHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
|||
serveDepsJS(rw, req, ui.uiDir)
|
||||
return
|
||||
}
|
||||
serveStaticFile(rw, req, uistatic.Files, file)
|
||||
ServeStaticFile(rw, req, uistatic.Files, file)
|
||||
}
|
||||
}
|
||||
|
||||
func serveStaticFile(rw http.ResponseWriter, req *http.Request, root http.FileSystem, file string) {
|
||||
// ServeStaticFile serves file from the root virtual filesystem.
|
||||
func ServeStaticFile(rw http.ResponseWriter, req *http.Request, root http.FileSystem, file string) {
|
||||
f, err := root.Open("/" + file)
|
||||
if err != nil {
|
||||
http.NotFound(rw, req)
|
||||
|
@ -441,19 +496,13 @@ func serveStaticFile(rw http.ResponseWriter, req *http.Request, root http.FileSy
|
|||
|
||||
func (ui *UIHandler) populateDiscoveryMap(m map[string]interface{}) {
|
||||
pubRoots := map[string]interface{}{}
|
||||
for key, pubh := range ui.publishRoots {
|
||||
for _, v := range ui.publishRoots {
|
||||
m := map[string]interface{}{
|
||||
"name": pubh.RootName,
|
||||
"prefix": []string{key},
|
||||
// TODO: include gpg key id
|
||||
"name": v.Name,
|
||||
"prefix": []string{v.Prefix},
|
||||
"currentPermanode": v.Permanode.String(),
|
||||
}
|
||||
if sh, ok := ui.root.SearchHandler(); ok {
|
||||
pn, err := sh.Index().PermanodeOfSignerAttrValue(sh.Owner(), "camliRoot", pubh.RootName)
|
||||
if err == nil {
|
||||
m["currentPermanode"] = pn.String()
|
||||
}
|
||||
}
|
||||
pubRoots[pubh.RootName] = m
|
||||
pubRoots[v.Name] = m
|
||||
}
|
||||
|
||||
uiDisco := map[string]interface{}{
|
||||
|
@ -537,8 +586,8 @@ func (ui *UIHandler) serveThumbnail(rw http.ResponseWriter, req *http.Request) {
|
|||
Cache: ui.Cache,
|
||||
MaxWidth: width,
|
||||
MaxHeight: height,
|
||||
thumbMeta: ui.thumbMeta,
|
||||
resizeSem: ui.resizeSem,
|
||||
ThumbMeta: ui.thumbMeta,
|
||||
ResizeSem: ui.resizeSem,
|
||||
}
|
||||
th.ServeHTTP(rw, req, blobref)
|
||||
}
|
||||
|
@ -597,7 +646,7 @@ func (ui *UIHandler) serveFromDiskOrStatic(rw http.ResponseWriter, req *http.Req
|
|||
req.URL.Path = "/" + file
|
||||
disk.ServeHTTP(rw, req)
|
||||
} else {
|
||||
serveStaticFile(rw, req, static, file)
|
||||
ServeStaticFile(rw, req, static, file)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -63,43 +63,34 @@ func addPublishedConfig(prefixes jsonconfig.Obj,
|
|||
sourceRoot string) ([]string, error) {
|
||||
var pubPrefixes []string
|
||||
for k, v := range published {
|
||||
name := strings.Replace(k, "/", "", -1)
|
||||
rootName := name + "Root"
|
||||
if !v.Root.Valid() {
|
||||
return nil, fmt.Errorf("Invalid or missing \"rootPermanode\" key in configuration for %s.", k)
|
||||
if v.CamliRoot == "" {
|
||||
return nil, fmt.Errorf("Missing \"camliRoot\" key in configuration for %s.", k)
|
||||
}
|
||||
if v.GoTemplate == "" {
|
||||
return nil, fmt.Errorf("Missing \"goTemplate\" key in configuration for %s.", k)
|
||||
}
|
||||
ob := map[string]interface{}{}
|
||||
ob["handler"] = "publish"
|
||||
ob["handler"] = "app"
|
||||
|
||||
appConfig := map[string]interface{}{
|
||||
"camliRoot": v.CamliRoot,
|
||||
"cacheRoot": v.CacheRoot,
|
||||
"goTemplate": v.GoTemplate,
|
||||
}
|
||||
|
||||
handlerArgs := map[string]interface{}{
|
||||
"rootName": rootName,
|
||||
"blobRoot": "/bs-and-maybe-also-index/",
|
||||
"searchRoot": "/my-search/",
|
||||
"cache": "/cache/",
|
||||
"rootPermanode": []interface{}{"/sighelper/", v.Root.String()},
|
||||
"program": v.Program,
|
||||
"appConfig": appConfig,
|
||||
}
|
||||
if sourceRoot != "" {
|
||||
handlerArgs["sourceRoot"] = sourceRoot
|
||||
if v.BaseURL != "" {
|
||||
handlerArgs["baseURL"] = v.BaseURL
|
||||
}
|
||||
handlerArgs["goTemplate"] = v.GoTemplate
|
||||
if v.Style != "" {
|
||||
handlerArgs["css"] = []interface{}{v.Style}
|
||||
}
|
||||
if v.Javascript != "" {
|
||||
handlerArgs["js"] = []interface{}{v.Javascript}
|
||||
}
|
||||
// TODO(mpl): we'll probably want to use osutil.CacheDir() if thumbnails.kv
|
||||
// contains private info? same for some of the other "camli-cache" ones?
|
||||
thumbsCacheDir := filepath.Join(tempDir(), "camli-cache")
|
||||
handlerArgs["scaledImage"] = map[string]interface{}{
|
||||
"type": "kv",
|
||||
"file": filepath.Join(thumbsCacheDir, name+"-thumbnails.kv"),
|
||||
}
|
||||
if err := os.MkdirAll(thumbsCacheDir, 0700); err != nil {
|
||||
return nil, fmt.Errorf("Could not create cache dir %s: %v", thumbsCacheDir, err)
|
||||
program := "publisher"
|
||||
if v.Program != "" {
|
||||
program = v.Program
|
||||
}
|
||||
handlerArgs["program"] = program
|
||||
|
||||
ob["handlerArgs"] = handlerArgs
|
||||
prefixes[k] = ob
|
||||
pubPrefixes = append(pubPrefixes, k)
|
||||
|
@ -111,20 +102,12 @@ func addPublishedConfig(prefixes jsonconfig.Obj,
|
|||
func addUIConfig(params *configPrefixesParams,
|
||||
prefixes jsonconfig.Obj,
|
||||
uiPrefix string,
|
||||
published []string,
|
||||
sourceRoot string) {
|
||||
|
||||
args := map[string]interface{}{
|
||||
"jsonSignRoot": "/sighelper/",
|
||||
"cache": "/cache/",
|
||||
}
|
||||
if len(published) > 0 {
|
||||
var publishedAsList []interface{}
|
||||
for _, v := range published {
|
||||
publishedAsList = append(publishedAsList, v)
|
||||
}
|
||||
args["publishRoots"] = publishedAsList
|
||||
}
|
||||
if sourceRoot != "" {
|
||||
args["sourceRoot"] = sourceRoot
|
||||
}
|
||||
|
@ -674,19 +657,18 @@ func genLowLevelConfig(conf *serverconfig.Config) (lowLevelConf *Config, err err
|
|||
}
|
||||
}
|
||||
|
||||
var published []string
|
||||
if len(conf.Publish) > 0 {
|
||||
if !runIndex {
|
||||
return nil, fmt.Errorf("publishing requires an index")
|
||||
}
|
||||
published, err = addPublishedConfig(prefixes, conf.Publish, conf.SourceRoot)
|
||||
_, err = addPublishedConfig(prefixes, conf.Publish, conf.SourceRoot)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Could not generate config for published: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if runIndex {
|
||||
addUIConfig(prefixesParams, prefixes, "/ui/", published, conf.SourceRoot)
|
||||
addUIConfig(prefixesParams, prefixes, "/ui/", conf.SourceRoot)
|
||||
}
|
||||
|
||||
if conf.MySQL != "" {
|
||||
|
|
|
@ -60,27 +60,15 @@
|
|||
}
|
||||
},
|
||||
"/music/": {
|
||||
"handler": "publish",
|
||||
"handler": "app",
|
||||
"handlerArgs": {
|
||||
"blobRoot": "/bs-and-maybe-also-index/",
|
||||
"cache": "/cache/",
|
||||
"css": [
|
||||
"pics.css"
|
||||
],
|
||||
"goTemplate": "gallery.html",
|
||||
"js": [
|
||||
"pics.js"
|
||||
],
|
||||
"rootName": "musicRoot",
|
||||
"rootPermanode": [
|
||||
"/sighelper/",
|
||||
"sha1-999c6aae4ec8245dfe63edc4a2abb407824a4b5a"
|
||||
],
|
||||
"scaledImage": {
|
||||
"file": "/tmp/camli-cache/music-thumbnails.kv",
|
||||
"type": "kv"
|
||||
},
|
||||
"searchRoot": "/my-search/"
|
||||
"program": "publisher",
|
||||
"baseURL": "http://localhost:3178/",
|
||||
"appConfig": {
|
||||
"camliRoot": "musicRoot",
|
||||
"goTemplate": "music.html",
|
||||
"cacheRoot": "/tmp/blobs/cache"
|
||||
}
|
||||
}
|
||||
},
|
||||
"/my-search/": {
|
||||
|
@ -92,27 +80,14 @@
|
|||
}
|
||||
},
|
||||
"/pics/": {
|
||||
"handler": "publish",
|
||||
"handler": "app",
|
||||
"handlerArgs": {
|
||||
"blobRoot": "/bs-and-maybe-also-index/",
|
||||
"cache": "/cache/",
|
||||
"css": [
|
||||
"pics.css"
|
||||
],
|
||||
"goTemplate": "gallery.html",
|
||||
"js": [
|
||||
"pics.js"
|
||||
],
|
||||
"rootName": "picsRoot",
|
||||
"rootPermanode": [
|
||||
"/sighelper/",
|
||||
"sha1-046c6aae4ec8245dfe63edc4a2abb407824a4b5a"
|
||||
],
|
||||
"scaledImage": {
|
||||
"file": "/tmp/camli-cache/pics-thumbnails.kv",
|
||||
"type": "kv"
|
||||
},
|
||||
"searchRoot": "/my-search/"
|
||||
"program": "publisher",
|
||||
"appConfig": {
|
||||
"camliRoot": "picsRoot",
|
||||
"goTemplate": "gallery.html",
|
||||
"cacheRoot": "/tmp/blobs/cache"
|
||||
}
|
||||
}
|
||||
},
|
||||
"/setup/": {
|
||||
|
@ -151,10 +126,6 @@
|
|||
"handlerArgs": {
|
||||
"cache": "/cache/",
|
||||
"jsonSignRoot": "/sighelper/",
|
||||
"publishRoots": [
|
||||
"/music/",
|
||||
"/pics/"
|
||||
],
|
||||
"scaledImage": {
|
||||
"file": "/tmp/blobs/thumbmeta.kv",
|
||||
"type": "kv"
|
||||
|
|
|
@ -2,23 +2,22 @@
|
|||
"listen": "localhost:3179",
|
||||
"auth": "userpass:camlistore:pass3179",
|
||||
"blobPath": "/tmp/blobs",
|
||||
"kvIndexFile": "/path/to/indexkv.db",
|
||||
"kvIndexFile": "/path/to/indexkv.db",
|
||||
"identity": "26F5ABDA",
|
||||
"identitySecretRing": "/path/to/secring",
|
||||
"ownerName": "Alice",
|
||||
"shareHandlerPath": "/share/",
|
||||
"publish": {
|
||||
"/pics/": {
|
||||
"rootPermanode": "sha1-046c6aae4ec8245dfe63edc4a2abb407824a4b5a",
|
||||
"style": "pics.css",
|
||||
"js": "pics.js",
|
||||
"goTemplate": "gallery.html"
|
||||
},
|
||||
"/music/": {
|
||||
"rootPermanode": "sha1-999c6aae4ec8245dfe63edc4a2abb407824a4b5a",
|
||||
"style": "pics.css",
|
||||
"js": "pics.js",
|
||||
"goTemplate": "gallery.html"
|
||||
}
|
||||
}
|
||||
"publish": {
|
||||
"/pics/": {
|
||||
"camliRoot": "picsRoot",
|
||||
"cacheRoot": "/tmp/blobs/cache",
|
||||
"goTemplate": "gallery.html"
|
||||
},
|
||||
"/music/": {
|
||||
"camliRoot": "musicRoot",
|
||||
"baseURL": "http://localhost:3178/",
|
||||
"cacheRoot": "/tmp/blobs/cache",
|
||||
"goTemplate": "music.html"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,24 +13,15 @@
|
|||
}
|
||||
},
|
||||
"/blog/": {
|
||||
"handler": "publish",
|
||||
"handler": "app",
|
||||
"handlerArgs": {
|
||||
"blobRoot": "/bs-and-maybe-also-index/",
|
||||
"cache": "/cache/",
|
||||
"css": [
|
||||
"blog-purple.css"
|
||||
],
|
||||
"goTemplate": "blog.html",
|
||||
"rootName": "blogRoot",
|
||||
"rootPermanode": [
|
||||
"/sighelper/",
|
||||
"sha1-2790ec1ec6fd44b9620b21155c8738aa08d4e3a0"
|
||||
],
|
||||
"scaledImage": {
|
||||
"file": "/tmp/camli-cache/blog-thumbnails.kv",
|
||||
"type": "kv"
|
||||
},
|
||||
"searchRoot": "/my-search/"
|
||||
"program": "publisher",
|
||||
"baseURL": "http://localhost:3178/",
|
||||
"appConfig": {
|
||||
"camliRoot": "blogRoot",
|
||||
"goTemplate": "blog.html",
|
||||
"cacheRoot": "/tmp/blobs/cache"
|
||||
}
|
||||
}
|
||||
},
|
||||
"/bs-and-index/": {
|
||||
|
@ -123,9 +114,6 @@
|
|||
"handlerArgs": {
|
||||
"cache": "/cache/",
|
||||
"jsonSignRoot": "/sighelper/",
|
||||
"publishRoots": [
|
||||
"/blog/"
|
||||
],
|
||||
"scaledImage": {
|
||||
"file": "/tmp/blobs/thumbmeta.kv",
|
||||
"type": "kv"
|
||||
|
|
|
@ -9,9 +9,10 @@
|
|||
"s3": "",
|
||||
"publish": {
|
||||
"/blog/": {
|
||||
"rootPermanode": "sha1-2790ec1ec6fd44b9620b21155c8738aa08d4e3a0",
|
||||
"goTemplate": "blog.html",
|
||||
"style": "blog-purple.css"
|
||||
"camliRoot": "blogRoot",
|
||||
"baseURL": "http://localhost:3178/",
|
||||
"cacheRoot": "/tmp/blobs/cache",
|
||||
"goTemplate": "blog.html"
|
||||
}
|
||||
},
|
||||
"replicateTo": [],
|
||||
|
|
|
@ -67,27 +67,15 @@
|
|||
}
|
||||
},
|
||||
"/pics/": {
|
||||
"handler": "publish",
|
||||
"handler": "app",
|
||||
"handlerArgs": {
|
||||
"blobRoot": "/bs-and-maybe-also-index/",
|
||||
"cache": "/cache/",
|
||||
"css": [
|
||||
"pics.css"
|
||||
],
|
||||
"goTemplate": "gallery.html",
|
||||
"js": [
|
||||
"pics.js"
|
||||
],
|
||||
"rootName": "picsRoot",
|
||||
"rootPermanode": [
|
||||
"/sighelper/",
|
||||
"sha1-2790ec1ec6fd44b9620b21155c8738aa08d4e3a0"
|
||||
],
|
||||
"scaledImage": {
|
||||
"file": "/tmp/camli-cache/pics-thumbnails.kv",
|
||||
"type": "kv"
|
||||
},
|
||||
"searchRoot": "/my-search/"
|
||||
"program": "publisher",
|
||||
"baseURL": "http://localhost:3178/",
|
||||
"appConfig": {
|
||||
"camliRoot": "picsRoot",
|
||||
"goTemplate": "gallery.html",
|
||||
"cacheRoot": "/tmp/blobs/cache"
|
||||
}
|
||||
}
|
||||
},
|
||||
"/setup/": {
|
||||
|
@ -126,9 +114,6 @@
|
|||
"handlerArgs": {
|
||||
"cache": "/cache/",
|
||||
"jsonSignRoot": "/sighelper/",
|
||||
"publishRoots": [
|
||||
"/pics/"
|
||||
],
|
||||
"scaledImage": {
|
||||
"file": "/tmp/blobs/thumbmeta.kv",
|
||||
"type": "kv"
|
||||
|
|
|
@ -9,10 +9,10 @@
|
|||
"s3": "",
|
||||
"publish": {
|
||||
"/pics/": {
|
||||
"rootPermanode": "sha1-2790ec1ec6fd44b9620b21155c8738aa08d4e3a0",
|
||||
"goTemplate": "gallery.html",
|
||||
"js": "pics.js",
|
||||
"style": "pics.css"
|
||||
"camliRoot": "picsRoot",
|
||||
"baseURL": "http://localhost:3178/",
|
||||
"cacheRoot": "/tmp/blobs/cache",
|
||||
"goTemplate": "gallery.html"
|
||||
}
|
||||
},
|
||||
"replicateTo": [],
|
||||
|
|
|
@ -87,6 +87,9 @@ func (cl ClaimsByDate) String() string {
|
|||
type FileInfo struct {
|
||||
FileName string `json:"fileName"`
|
||||
|
||||
// TODO(mpl): I've noticed that Size is actually set to the
|
||||
// number of entries in the dir. fix the doc or the behaviour?
|
||||
|
||||
// Size is the size of files. It is not set for directories.
|
||||
Size int64 `json:"size"`
|
||||
|
||||
|
|
|
@ -18,7 +18,6 @@ limitations under the License.
|
|||
package serverconfig
|
||||
|
||||
import (
|
||||
"camlistore.org/pkg/blob"
|
||||
"camlistore.org/pkg/types"
|
||||
)
|
||||
|
||||
|
@ -70,14 +69,27 @@ type Config struct {
|
|||
Picasa string `json:"picasa,omitempty"` // picasa importer.
|
||||
}
|
||||
|
||||
// Publish holds the server configuration values specific to publishing, i.e. to a publish handler.
|
||||
// Publish holds the server configuration values specific to a publisher, i.e. to a publish prefix.
|
||||
type Publish struct {
|
||||
// Root is the permanode used as the root for all the paths served by this publish handler. The camliRoot value that is the root path for this handler is a property of this permanode.
|
||||
Root blob.Ref `json:"rootPermanode"`
|
||||
// GoTemplate is the name of the Go template file used by this publish handler to represent the data. This file should live in server/camlistored/ui/.
|
||||
// Program is the server app program to run as the publisher.
|
||||
// Defaults to "publisher".
|
||||
Program string `json:"program"`
|
||||
|
||||
// CamliRoot value that defines our root permanode for this
|
||||
// publisher. The root permanode is used as the root for all the
|
||||
// paths served by this publisher.
|
||||
CamliRoot string `json:"camliRoot"`
|
||||
|
||||
// Base URL the app will run at.
|
||||
BaseURL string `json:"baseURL,omitempty"`
|
||||
|
||||
// GoTemplate is the name of the Go template file used by this
|
||||
// publisher to represent the data. This file should live in
|
||||
// app/publisher/.
|
||||
GoTemplate string `json:"goTemplate"`
|
||||
// Javascript is the name of an optional javascript file used for additional features. This file should live in server/camlistored/ui/.
|
||||
Javascript string `json:"js,omitempty"`
|
||||
// Style is the name of an optional css file. This file should live in server/camlistored/ui/.
|
||||
Style string `json:"style,omitempty"`
|
||||
|
||||
// CacheRoot is the path that will be used as the root for the
|
||||
// caching blobserver (for images). No caching if empty.
|
||||
// An example value is Config.BlobPath + "/cache".
|
||||
CacheRoot string `json:"cacheRoot,omitempty"`
|
||||
}
|
||||
|
|
|
@ -1,150 +0,0 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
goog.provide('cam.GalleryPage');
|
||||
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.events.EventHandler');
|
||||
goog.require('goog.events.EventType');
|
||||
goog.require('goog.ui.Component');
|
||||
|
||||
goog.require('cam.ServerConnection');
|
||||
|
||||
// @param {cam.ServerType.DiscoveryDocument} config Global config of the current server this page is being rendered for.
|
||||
// @param {goog.dom.DomHelper=} opt_domHelper DOM helper to use.
|
||||
cam.GalleryPage = function(config, opt_domHelper) {
|
||||
goog.base(this, opt_domHelper);
|
||||
|
||||
this.config_ = config;
|
||||
this.connection_ = new cam.ServerConnection(config);
|
||||
};
|
||||
goog.inherits(cam.GalleryPage, goog.ui.Component);
|
||||
|
||||
cam.GalleryPage.prototype.decorateInternal = function(element) {
|
||||
cam.GalleryPage.superClass_.decorateInternal.call(this, element);
|
||||
};
|
||||
|
||||
cam.GalleryPage.prototype.disposeInternal = function() {
|
||||
cam.GalleryPage.superClass_.disposeInternal.call(this);
|
||||
};
|
||||
|
||||
cam.GalleryPage.prototype.enterDocument = function() {
|
||||
cam.GalleryPage.superClass_.enterDocument.call(this);
|
||||
|
||||
var members = goog.dom.getElement('members');
|
||||
if (!members) {
|
||||
return;
|
||||
}
|
||||
var children = goog.dom.getChildren(members);
|
||||
if (!children || children.length < 1) {
|
||||
return;
|
||||
}
|
||||
goog.array.forEach(children, function(li) {
|
||||
li.src = li.src + '&square=1';
|
||||
})
|
||||
|
||||
if (camliViewIsOwner) {
|
||||
var el = this.getElement();
|
||||
goog.dom.classes.add(el, 'camliadmin');
|
||||
|
||||
goog.array.forEach(children, function(li) {
|
||||
var lichild = goog.dom.getFirstElementChild(li);
|
||||
var titleSpan = goog.dom.getLastElementChild(lichild);
|
||||
var editLink = goog.dom.createElement('a', {'href': '#'});
|
||||
goog.dom.classes.add(editLink, 'hidden');
|
||||
goog.dom.setTextContent(editLink, 'edit title');
|
||||
|
||||
var titleInput = goog.dom.createElement('input');
|
||||
goog.dom.classes.add(titleInput, 'hidden');
|
||||
|
||||
goog.events.listen(editLink,
|
||||
goog.events.EventType.CLICK,
|
||||
function(e) {
|
||||
goog.dom.classes.remove(titleSpan, 'visible');
|
||||
goog.dom.classes.add(titleSpan, 'hidden');
|
||||
goog.dom.classes.remove(titleInput, 'hidden');
|
||||
goog.dom.classes.add(titleInput, 'visible');
|
||||
titleInput.focus();
|
||||
titleInput.select();
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
},
|
||||
false, this
|
||||
);
|
||||
goog.events.listen(li,
|
||||
goog.events.EventType.MOUSEOVER,
|
||||
function(e) {
|
||||
goog.dom.classes.remove(editLink, 'hidden');
|
||||
goog.dom.classes.add(editLink, 'title-edit');
|
||||
},
|
||||
false, this
|
||||
);
|
||||
goog.events.listen(li,
|
||||
goog.events.EventType.MOUSEOUT,
|
||||
function(e) {
|
||||
goog.dom.classes.remove(editLink, 'title-edit');
|
||||
goog.dom.classes.add(editLink, 'hidden');
|
||||
goog.dom.classes.remove(titleInput, 'visible');
|
||||
goog.dom.classes.add(titleInput, 'hidden');
|
||||
goog.dom.classes.remove(titleSpan, 'hidden');
|
||||
goog.dom.classes.add(titleSpan, 'visible');
|
||||
},
|
||||
false, this
|
||||
);
|
||||
goog.events.listen(titleInput,
|
||||
goog.events.EventType.KEYPRESS,
|
||||
goog.bind(function(e) {
|
||||
if (e.keyCode == 13) {
|
||||
this.saveImgTitle_(titleInput, titleSpan);
|
||||
}
|
||||
}, this),
|
||||
false, this
|
||||
);
|
||||
goog.dom.insertSiblingBefore(editLink, titleSpan);
|
||||
goog.dom.insertChildAt(li, titleInput, 1);
|
||||
}, this
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// @param {string} titleInput text field element for title
|
||||
// @param {string} titleSpan span element containing the title
|
||||
cam.GalleryPage.prototype.saveImgTitle_ = function (titleInput, titleSpan) {
|
||||
var spanText = goog.dom.getTextContent(titleSpan);
|
||||
var newVal = titleInput.value;
|
||||
if (newVal != "" && newVal != spanText) {
|
||||
goog.dom.setTextContent(titleSpan, newVal);
|
||||
var blobRef = goog.dom.getParentElement(titleInput).id.replace(/^camli-/, '');
|
||||
this.connection_.newSetAttributeClaim(
|
||||
blobRef,
|
||||
"title",
|
||||
newVal,
|
||||
function() {
|
||||
},
|
||||
function(msg) {
|
||||
alert(msg);
|
||||
}
|
||||
);
|
||||
}
|
||||
goog.dom.classes.remove(titleInput, 'visible');
|
||||
goog.dom.classes.add(titleInput, 'hidden');
|
||||
goog.dom.classes.remove(titleSpan, 'hidden');
|
||||
goog.dom.classes.add(titleSpan, 'visible');
|
||||
}
|
||||
|
||||
cam.GalleryPage.prototype.exitDocument = function() {
|
||||
cam.GalleryPage.superClass_.exitDocument.call(this);
|
||||
};
|
|
@ -72,14 +72,14 @@ config, as used by <code>devcam server</code>.</p>
|
|||
<pre>
|
||||
"publish": {
|
||||
"/pics/": {
|
||||
"rootPermanode": "sha1-6cbe9e1c35e854eab028cba43d099d35ceae0de8",
|
||||
"style": "pics.css",
|
||||
"js": "pics.js",
|
||||
"camliRoot": "mypics",
|
||||
"baseURL": "http://localhost:3178/",
|
||||
"cacheRoot": "/home/joe/var/camlistore/blobs/cache",
|
||||
"goTemplate": "gallery.html"
|
||||
}
|
||||
}
|
||||
</pre>
|
||||
<p>One can create any permanode with camput or the UI and use it as the rootPermanode.</p>
|
||||
<p>One can create any permanode with camput or the UI, and set its camliRoot attribute to the value set in the config, to use it as the root permanode for publishing.</p>
|
||||
|
||||
<p>Please see the <a href="/gw/doc/publishing/README"">publishing README</a> if you want to make/contribute more publishing views.</p>
|
||||
|
||||
|
|
Loading…
Reference in New Issue