diff --git a/build.pl b/build.pl index ecf811d1b..e02b9da41 100755 --- a/build.pl +++ b/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 diff --git a/clients/go/cammount/main.go b/clients/go/cammount/main.go index fd03c71cc..eefa6f555 100644 --- a/clients/go/cammount/main.go +++ b/clients/go/cammount/main.go @@ -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) diff --git a/clients/go/camwebdav/main.go b/clients/go/camwebdav/main.go new file mode 100644 index 000000000..acd32fc84 --- /dev/null +++ b/clients/go/camwebdav/main.go @@ -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 ") + } + 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 +} diff --git a/clients/go/camwebdav/response.go b/clients/go/camwebdav/response.go new file mode 100644 index 000000000..d7f8677f2 --- /dev/null +++ b/clients/go/camwebdav/response.go @@ -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("" + template.HTMLEscapeString((*http.URL)(h).String()) + "") +} + +// 14.16 multistatus XML Element +type multistatus []*response + +func (m multistatus) XML(b *bytes.Buffer) { + b.WriteString("") + for _, r := range m { + r.XML(b) + } + b.WriteString("") +} + +// 14.24 response XML Element +type response struct { + href *href + body xmler // hrefsstatus OR propstats +} + +func (r *response) XML(b *bytes.Buffer) { + b.WriteString("") + r.href.XML(b) + r.body.XML(b) + b.WriteString("") +} + +// 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("") + for _, prop := range p { + prop.XML(b) + } + b.WriteString("") +} + +// 14.22 propstat XML Element +type propstat struct { + props []xmler + status status +} + +func (p *propstat) XML(b *bytes.Buffer) { + b.WriteString("") + for _, prop := range p.props { + prop.XML(b) + } + b.WriteString("") + p.status.XML(b) +} + +// 14.28 status XML element +type status int + +func (s status) XML(b *bytes.Buffer) { + b.WriteString(fmt.Sprintf("HTTP/1.1 %d %s", 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("") + b.WriteString(epochToXMLTime(int64(c))) + b.WriteString("") +} + +// 15.4 getcontentlength Property +type getcontentlength uint64 + +func (l getcontentlength) XML(b *bytes.Buffer) { + + b.WriteString("") + b.WriteString(fmt.Sprint(l)) + b.WriteString("") +} + +// 15.7 getlastmodified Property +type getlastmodified uint64 // seconds from unix epoch +func (g getlastmodified) XML(b *bytes.Buffer) { + b.WriteString("") + b.WriteString(epochToXMLTime(int64(g))) + b.WriteString("") +} + +// 15.9 resourcetype Property +type resourcetype bool // true if collection (directory), false otherwise + +func (r resourcetype) XML(b *bytes.Buffer) { + b.WriteString("") + if r { + b.WriteString("") + } + b.WriteString("") +} + +// helpers +func epochToXMLTime(sec int64) string { + return template.HTMLEscapeString(time.SecondsToUTC(sec).Format(time.RFC3339)) +} diff --git a/clients/go/camwebdav/xml.go b/clients/go/camwebdav/xml.go new file mode 100644 index 000000000..16018bd15 --- /dev/null +++ b/clients/go/camwebdav/xml.go @@ -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)) + } +} diff --git a/dev-camwebdav b/dev-camwebdav new file mode 100755 index 000000000..350bff49e --- /dev/null +++ b/dev-camwebdav @@ -0,0 +1,3 @@ +#!/bin/sh + +./build.pl camwebdav && clients/go/camwebdav/camwebdav --blobserver=http://localhost:3179/bs --davaddr=localhost:8080 --password=pass3179 "$@" \ No newline at end of file diff --git a/clients/go/cammount/cacher.go b/lib/go/camli/cacher/cacher.go similarity index 99% rename from clients/go/cammount/cacher.go rename to lib/go/camli/cacher/cacher.go index 722f7627a..b295d40dd 100644 --- a/clients/go/cammount/cacher.go +++ b/lib/go/camli/cacher/cacher.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package main +package cacher import ( "io" diff --git a/clients/go/cammount/fs.go b/lib/go/camli/fs/fs.go similarity index 98% rename from clients/go/cammount/fs.go rename to lib/go/camli/fs/fs.go index 089ed8682..8560eee5c 100644 --- a/clients/go/cammount/fs.go +++ b/lib/go/camli/fs/fs.go @@ -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) +}