mirror of https://github.com/perkeep/perkeep.git
first cut at a Camlistore WebDAV server
don't let it burn out your eyes too badly Change-Id: I7b1b1df0abbcafff411025d30bd32cd250d2221d
This commit is contained in:
parent
999098b187
commit
52a15c7b91
3
build.pl
3
build.pl
|
@ -633,6 +633,7 @@ TARGET: clients/go/camput
|
|||
TARGET: clients/go/cammount
|
||||
=only_os_linux
|
||||
TARGET: clients/go/camsync
|
||||
TARGET: clients/go/camwebdav
|
||||
TARGET: lib/go/camli/auth
|
||||
TARGET: lib/go/camli/blobref
|
||||
TARGET: lib/go/camli/blobserver
|
||||
|
@ -644,10 +645,12 @@ TARGET: lib/go/camli/blobserver/remote
|
|||
TARGET: lib/go/camli/blobserver/replica
|
||||
TARGET: lib/go/camli/blobserver/shard
|
||||
TARGET: lib/go/camli/blobserver/s3
|
||||
TARGET: lib/go/camli/cacher
|
||||
TARGET: lib/go/camli/client
|
||||
TARGET: lib/go/camli/db
|
||||
TARGET: lib/go/camli/db/dbimpl
|
||||
TARGET: lib/go/camli/errorutil
|
||||
TARGET: lib/go/camli/fs
|
||||
TARGET: lib/go/camli/httputil
|
||||
TARGET: lib/go/camli/jsonconfig
|
||||
TARGET: lib/go/camli/jsonsign
|
||||
|
|
|
@ -25,7 +25,9 @@ import (
|
|||
|
||||
"camli/blobref"
|
||||
"camli/blobserver/localdisk" // used for the blob cache
|
||||
"camli/cacher"
|
||||
"camli/client"
|
||||
"camli/fs"
|
||||
"camli/third_party/github.com/hanwen/go-fuse/fuse"
|
||||
)
|
||||
|
||||
|
@ -73,9 +75,9 @@ func main() {
|
|||
if err != nil {
|
||||
errorf("Error setting up local disk cache: %v", err)
|
||||
}
|
||||
fetcher := NewCachingFetcher(diskcache, client)
|
||||
fetcher := cacher.NewCachingFetcher(diskcache, client)
|
||||
|
||||
fs := NewCamliFileSystem(fetcher, root)
|
||||
fs := fs.NewCamliFileSystem(fetcher, root)
|
||||
timing := fuse.NewTimingPathFilesystem(fs)
|
||||
|
||||
conn := fuse.NewPathFileSystemConnector(timing)
|
||||
|
|
|
@ -0,0 +1,222 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"http"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"xml"
|
||||
|
||||
"camli/blobref"
|
||||
"camli/blobserver/localdisk"
|
||||
"camli/client"
|
||||
"camli/cacher"
|
||||
"camli/fs"
|
||||
"camli/third_party/github.com/hanwen/go-fuse/fuse"
|
||||
)
|
||||
|
||||
var (
|
||||
f *fs.CamliFileSystem
|
||||
davaddr = flag.String("davaddr", "", "WebDAV service address")
|
||||
)
|
||||
|
||||
// TODO(rh): tame copy/paste code from cammount
|
||||
func main() {
|
||||
flag.Parse()
|
||||
cacheDir, err := ioutil.TempDir("", "camlicache")
|
||||
if err != nil {
|
||||
log.Fatalf("Error creating temp cache directory: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(cacheDir)
|
||||
diskcache, err := localdisk.New(cacheDir)
|
||||
if err != nil {
|
||||
log.Fatalf("Error setting up local disk cache: %v", err)
|
||||
}
|
||||
if flag.NArg() != 1 {
|
||||
log.Fatal("usage: camwebdav <blobref>")
|
||||
}
|
||||
br := blobref.Parse(flag.Arg(0))
|
||||
if br == nil {
|
||||
log.Fatalf("%s was not a valid blobref.", flag.Arg(0))
|
||||
}
|
||||
client := client.NewOrFail()
|
||||
fetcher := cacher.NewCachingFetcher(diskcache, client)
|
||||
|
||||
f = fs.NewCamliFileSystem(fetcher, br)
|
||||
http.HandleFunc("/", webdav)
|
||||
err = http.ListenAndServe(*davaddr, nil)
|
||||
if err != nil {
|
||||
log.Fatalf("Error starting WebDAV server: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func webdav(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case "GET":
|
||||
get(w, r)
|
||||
case "OPTIONS":
|
||||
w.Header().Set("DAV", "1")
|
||||
case "PROPFIND":
|
||||
propfind(w, r)
|
||||
default:
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
// sendHTTPStatus is an HTTP status code.
|
||||
type sendHTTPStatus int
|
||||
|
||||
// senderr, when deferred, recovers panics of
|
||||
// type sendHTTPStatus and writes the corresponding
|
||||
// HTTP status to the response. If the value is not
|
||||
// of type sendHTTPStatus, it re-panics.
|
||||
func senderr(w http.ResponseWriter) {
|
||||
err := recover()
|
||||
if stat, ok := err.(sendHTTPStatus); ok {
|
||||
w.WriteHeader(int(stat))
|
||||
} else if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// GET Method
|
||||
func get(w http.ResponseWriter, r *http.Request) {
|
||||
defer senderr(w)
|
||||
file, stat := f.Open(url2path(r.URL), uint32(os.O_RDONLY))
|
||||
|
||||
checkerr(stat)
|
||||
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
ff, err := file.(*fs.CamliFile).GetReader()
|
||||
_, err = io.Copy(w, ff)
|
||||
if err != nil {
|
||||
log.Print("propfind: error writing response: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 9.1 PROPFIND Method
|
||||
func propfind(w http.ResponseWriter, r *http.Request) {
|
||||
defer senderr(w)
|
||||
depth := r.Header.Get("Depth")
|
||||
switch depth {
|
||||
case "0", "1":
|
||||
case /*implicit infinity*/ "", "infinity":
|
||||
log.Print("propfind: unsupported depth of infinity")
|
||||
panic(sendHTTPStatus(http.StatusForbidden))
|
||||
default:
|
||||
log.Print("propfind: invalid Depth of", depth)
|
||||
panic(sendHTTPStatus(http.StatusBadRequest))
|
||||
}
|
||||
|
||||
// TODO(rh) Empty body == allprop
|
||||
|
||||
// TODO(rh): allprop
|
||||
var propsToFind []string
|
||||
|
||||
x := parsexml(r.Body)
|
||||
x.muststart("propfind")
|
||||
switch {
|
||||
case x.start("propname"):
|
||||
x.mustend("propname")
|
||||
case x.start("allprop"):
|
||||
x.mustend("allprop")
|
||||
if x.start("include") {
|
||||
// TODO(rh) parse elements
|
||||
x.mustend("include")
|
||||
}
|
||||
case x.start("prop"):
|
||||
propsToFind = parseprop(x)
|
||||
x.mustend("prop")
|
||||
}
|
||||
x.mustend("propfind")
|
||||
var files = []string{url2path(r.URL)}
|
||||
if depth == "1" {
|
||||
// TODO(rh) handle bad stat
|
||||
files = append(files, ls(files[0])...)
|
||||
}
|
||||
|
||||
var ms multistatus
|
||||
for _, file := range files {
|
||||
resp := &response{href: (*href)(path2url(file))}
|
||||
attr, stat := f.GetAttr(file) // TODO(rh) better way?
|
||||
|
||||
checkerr(stat)
|
||||
|
||||
var props []xmler
|
||||
for _, p := range propsToFind {
|
||||
switch p {
|
||||
case "creationdate":
|
||||
props = append(props, creationdate(attr.Ctime))
|
||||
case "resourcetype":
|
||||
props = append(props, resourcetype(attr.Mode&fuse.S_IFDIR == fuse.S_IFDIR))
|
||||
case "getcontentlength":
|
||||
props = append(props, getcontentlength(attr.Size))
|
||||
case "getlastmodified":
|
||||
props = append(props, getlastmodified(attr.Mtime))
|
||||
}
|
||||
|
||||
resp.body = propstats{{props, 200}}
|
||||
}
|
||||
ms = append(ms, resp)
|
||||
}
|
||||
|
||||
var xmlbytes bytes.Buffer
|
||||
ms.XML(&xmlbytes)
|
||||
w.Header().Set("Content-Type", "application/xml; charset=UTF-8")
|
||||
w.WriteHeader(207) // 207 Multi-Status
|
||||
_, err := io.Copy(w, &xmlbytes)
|
||||
if err != nil {
|
||||
log.Print("propfind: error writing response: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func checkerr(stat fuse.Status) {
|
||||
switch stat {
|
||||
case fuse.ENOENT:
|
||||
panic(sendHTTPStatus(http.StatusNotFound))
|
||||
case fuse.OK:
|
||||
default:
|
||||
panic(sendHTTPStatus(http.StatusForbidden))
|
||||
}
|
||||
}
|
||||
|
||||
func ls(path string) (paths []string) {
|
||||
dirs, err := f.OpenDir(path)
|
||||
|
||||
checkerr(err)
|
||||
|
||||
for d := range dirs {
|
||||
// TODO(rh) determine a proper way to join paths
|
||||
if path != "" {
|
||||
d.Name = path + "/" + d.Name
|
||||
}
|
||||
paths = append(paths, d.Name)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// TODO(rh) settle on an internal format for paths, and a better way to translate between paths and URLs
|
||||
func url2path(url *http.URL) string {
|
||||
return strings.Trim(url.Path, "/") // TODO(rh) make not suck
|
||||
}
|
||||
|
||||
func path2url(path string) *http.URL {
|
||||
return &http.URL{Path: "/" + path} // TODO(rh) make not suck
|
||||
}
|
||||
|
||||
func parseprop(x *xmlparser) (props []string) {
|
||||
for {
|
||||
el, ok := x.cur.(xml.StartElement)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
props = append(props, el.Name.Local)
|
||||
x.muststart(el.Name.Local)
|
||||
x.mustend(el.Name.Local)
|
||||
}
|
||||
return
|
||||
}
|
|
@ -0,0 +1,137 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"exp/template"
|
||||
"fmt"
|
||||
"http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type xmler interface {
|
||||
XML(b *bytes.Buffer)
|
||||
}
|
||||
|
||||
// See: http://www.webdav.org/specs/rfc4918.html
|
||||
|
||||
// 14.7 href XML Element
|
||||
type href http.URL
|
||||
|
||||
func (h *href) XML(b *bytes.Buffer) {
|
||||
b.WriteString("<href>" + template.HTMLEscapeString((*http.URL)(h).String()) + "</href>")
|
||||
}
|
||||
|
||||
// 14.16 multistatus XML Element
|
||||
type multistatus []*response
|
||||
|
||||
func (m multistatus) XML(b *bytes.Buffer) {
|
||||
b.WriteString("<multistatus xmlns='DAV:'>")
|
||||
for _, r := range m {
|
||||
r.XML(b)
|
||||
}
|
||||
b.WriteString("</multistatus>")
|
||||
}
|
||||
|
||||
// 14.24 response XML Element
|
||||
type response struct {
|
||||
href *href
|
||||
body xmler // hrefsstatus OR propstats
|
||||
}
|
||||
|
||||
func (r *response) XML(b *bytes.Buffer) {
|
||||
b.WriteString("<response>")
|
||||
r.href.XML(b)
|
||||
r.body.XML(b)
|
||||
b.WriteString("</response>")
|
||||
}
|
||||
|
||||
// part of 14.24 response XML element
|
||||
|
||||
type hrefsstatus struct {
|
||||
hrefs []*href
|
||||
status status
|
||||
}
|
||||
|
||||
func (hs *hrefsstatus) XML(b *bytes.Buffer) {
|
||||
for _, h := range hs.hrefs {
|
||||
h.XML(b)
|
||||
}
|
||||
hs.status.XML(b)
|
||||
}
|
||||
|
||||
// part of 14.24 response element
|
||||
|
||||
type propstats []propstat
|
||||
|
||||
func (p propstats) XML(b *bytes.Buffer) {
|
||||
b.WriteString("<propstat>")
|
||||
for _, prop := range p {
|
||||
prop.XML(b)
|
||||
}
|
||||
b.WriteString("</propstat>")
|
||||
}
|
||||
|
||||
// 14.22 propstat XML Element
|
||||
type propstat struct {
|
||||
props []xmler
|
||||
status status
|
||||
}
|
||||
|
||||
func (p *propstat) XML(b *bytes.Buffer) {
|
||||
b.WriteString("<prop>")
|
||||
for _, prop := range p.props {
|
||||
prop.XML(b)
|
||||
}
|
||||
b.WriteString("</prop>")
|
||||
p.status.XML(b)
|
||||
}
|
||||
|
||||
// 14.28 status XML element
|
||||
type status int
|
||||
|
||||
func (s status) XML(b *bytes.Buffer) {
|
||||
b.WriteString(fmt.Sprintf("<status>HTTP/1.1 %d %s</status>", s, template.HTMLEscapeString(http.StatusText(int(s)))))
|
||||
}
|
||||
|
||||
// 15.1 creationdate Property
|
||||
type creationdate uint64 // seconds from unix epoch
|
||||
|
||||
func (c creationdate) XML(b *bytes.Buffer) {
|
||||
b.WriteString("<creationdate>")
|
||||
b.WriteString(epochToXMLTime(int64(c)))
|
||||
b.WriteString("</creationdate>")
|
||||
}
|
||||
|
||||
// 15.4 getcontentlength Property
|
||||
type getcontentlength uint64
|
||||
|
||||
func (l getcontentlength) XML(b *bytes.Buffer) {
|
||||
|
||||
b.WriteString("<getcontentlength>")
|
||||
b.WriteString(fmt.Sprint(l))
|
||||
b.WriteString("</getcontentlength>")
|
||||
}
|
||||
|
||||
// 15.7 getlastmodified Property
|
||||
type getlastmodified uint64 // seconds from unix epoch
|
||||
func (g getlastmodified) XML(b *bytes.Buffer) {
|
||||
b.WriteString("<getlastmodified>")
|
||||
b.WriteString(epochToXMLTime(int64(g)))
|
||||
b.WriteString("</getlastmodified>")
|
||||
}
|
||||
|
||||
// 15.9 resourcetype Property
|
||||
type resourcetype bool // true if collection (directory), false otherwise
|
||||
|
||||
func (r resourcetype) XML(b *bytes.Buffer) {
|
||||
b.WriteString("<resourcetype>")
|
||||
if r {
|
||||
b.WriteString("<collection/>")
|
||||
}
|
||||
b.WriteString("</resourcetype>")
|
||||
}
|
||||
|
||||
// helpers
|
||||
func epochToXMLTime(sec int64) string {
|
||||
return template.HTMLEscapeString(time.SecondsToUTC(sec).Format(time.RFC3339))
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"http"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"xml"
|
||||
)
|
||||
|
||||
func parsexml(r io.Reader) *xmlparser {
|
||||
x := &xmlparser{p: xml.NewParser(r)}
|
||||
x.next()
|
||||
return x
|
||||
}
|
||||
|
||||
type xmlparser struct {
|
||||
p *xml.Parser
|
||||
cur xml.Token
|
||||
}
|
||||
|
||||
// next moves to the next token,
|
||||
// skipping anything that is not an element
|
||||
// in the DAV: namespace
|
||||
func (x *xmlparser) next() xml.Token {
|
||||
var err os.Error
|
||||
for {
|
||||
x.cur, err = x.p.Token()
|
||||
if err == os.EOF {
|
||||
return x.cur
|
||||
} else if err != nil {
|
||||
panic(sendHTTPStatus(http.StatusBadRequest))
|
||||
}
|
||||
switch tok := x.cur.(type) {
|
||||
case xml.StartElement:
|
||||
if tok.Name.Space != "DAV:" {
|
||||
err = x.p.Skip()
|
||||
if err != nil && err != os.EOF {
|
||||
panic(sendHTTPStatus(http.StatusBadRequest))
|
||||
}
|
||||
} else {
|
||||
return x.cur
|
||||
}
|
||||
case xml.EndElement:
|
||||
return x.cur
|
||||
}
|
||||
}
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
func (x *xmlparser) start(name string) bool {
|
||||
el, ok := x.cur.(xml.StartElement)
|
||||
if !ok || el.Name.Local != name {
|
||||
return false
|
||||
}
|
||||
x.next()
|
||||
return true
|
||||
}
|
||||
|
||||
func (x *xmlparser) muststart(name string) {
|
||||
if !x.start(name) {
|
||||
log.Printf("expected start element %q", name)
|
||||
panic(sendHTTPStatus(http.StatusBadRequest))
|
||||
}
|
||||
}
|
||||
|
||||
func (x *xmlparser) end(name string) bool {
|
||||
if _, ok := x.cur.(xml.EndElement); !ok {
|
||||
return false
|
||||
}
|
||||
x.next()
|
||||
return true
|
||||
}
|
||||
|
||||
func (x *xmlparser) mustend(name string) {
|
||||
if !x.end(name) {
|
||||
log.Printf("expected end element %q", name)
|
||||
panic(sendHTTPStatus(http.StatusBadRequest))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
#!/bin/sh
|
||||
|
||||
./build.pl camwebdav && clients/go/camwebdav/camwebdav --blobserver=http://localhost:3179/bs --davaddr=localhost:8080 --password=pass3179 "$@"
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
package cacher
|
||||
|
||||
import (
|
||||
"io"
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
package fs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
@ -421,3 +421,7 @@ func (file *CamliFile) Read(ri *fuse.ReadIn, bp *fuse.BufferPool) (retbuf []byte
|
|||
retst = fuse.EIO
|
||||
return
|
||||
}
|
||||
|
||||
func (file *CamliFile) GetReader() (io.ReadCloser, os.Error) {
|
||||
return file.ss.NewFileReader(file.fs.fetcher)
|
||||
}
|
Loading…
Reference in New Issue