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:
Robert Hencke 2011-08-06 20:46:37 -05:00
parent 999098b187
commit 52a15c7b91
8 changed files with 455 additions and 4 deletions

View File

@ -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

View File

@ -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)

View File

@ -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
}

View File

@ -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))
}

View File

@ -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))
}
}

3
dev-camwebdav Executable file
View File

@ -0,0 +1,3 @@
#!/bin/sh
./build.pl camwebdav && clients/go/camwebdav/camwebdav --blobserver=http://localhost:3179/bs --davaddr=localhost:8080 --password=pass3179 "$@"

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package main
package cacher
import (
"io"

View File

@ -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)
}