blob: b7767cd4b8c3deb43d648a231d883227d6bd11f7 [file] [log] [blame]
// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package webdav
import (
"encoding/xml"
"fmt"
"io"
"mime"
"net/http"
"os"
"path/filepath"
"strconv"
)
// PropSystem manages the properties of named resources. It allows finding
// and setting properties as defined in RFC 4918.
//
// The elements in a resource name are separated by slash ('/', U+002F)
// characters, regardless of host operating system convention.
type PropSystem interface {
// Find returns the status of properties named propnames for resource name.
//
// Each Propstat must have a unique status and each property name must
// only be part of one Propstat element.
Find(name string, propnames []xml.Name) ([]Propstat, error)
// TODO(nigeltao) merge Find and Allprop?
// Allprop returns the properties defined for resource name and the
// properties named in include. The returned Propstats are handled
// as in Find.
//
// Note that RFC 4918 defines 'allprop' to return the DAV: properties
// defined within the RFC plus dead properties. Other live properties
// should only be returned if they are named in 'include'.
//
// See http://www.webdav.org/specs/rfc4918.html#METHOD_PROPFIND
Allprop(name string, include []xml.Name) ([]Propstat, error)
// Propnames returns the property names defined for resource name.
Propnames(name string) ([]xml.Name, error)
// Patch patches the properties of resource name.
//
// If all patches can be applied without conflict, Patch returns a slice
// of length one and a Propstat element of status 200, naming all patched
// properties. In case of conflict, Patch returns an arbitrary long slice
// and no Propstat element must have status 200. In either case, properties
// in Propstat must not have values.
//
// Note that the WebDAV RFC requires either all patches to succeed or none.
Patch(name string, patches []Proppatch) ([]Propstat, error)
// TODO(rost) COPY/MOVE/DELETE.
}
// Proppatch describes a property update instruction as defined in RFC 4918.
// See http://www.webdav.org/specs/rfc4918.html#METHOD_PROPPATCH
type Proppatch struct {
// Remove specifies whether this patch removes properties. If it does not
// remove them, it sets them.
Remove bool
// Props contains the properties to be set or removed.
Props []Property
}
// Propstat describes a XML propstat element as defined in RFC 4918.
// See http://www.webdav.org/specs/rfc4918.html#ELEMENT_propstat
type Propstat struct {
// Props contains the properties for which Status applies.
Props []Property
// Status defines the HTTP status code of the properties in Prop.
// Allowed values include, but are not limited to the WebDAV status
// code extensions for HTTP/1.1.
// http://www.webdav.org/specs/rfc4918.html#status.code.extensions.to.http11
Status int
// XMLError contains the XML representation of the optional error element.
// XML content within this field must not rely on any predefined
// namespace declarations or prefixes. If empty, the XML error element
// is omitted.
XMLError string
// ResponseDescription contains the contents of the optional
// responsedescription field. If empty, the XML element is omitted.
ResponseDescription string
}
// memPS implements an in-memory PropSystem. It supports all of the mandatory
// live properties of RFC 4918.
type memPS struct {
fs FileSystem
ls LockSystem
}
// NewMemPS returns a new in-memory PropSystem implementation.
func NewMemPS(fs FileSystem, ls LockSystem) PropSystem {
return &memPS{fs: fs, ls: ls}
}
// davProps contains all supported DAV: properties and their optional
// propfind functions. A nil findFn indicates a hidden, protected property.
// The dir field indicates if the property applies to directories in addition
// to regular files.
var davProps = map[xml.Name]struct {
findFn func(*memPS, string, os.FileInfo) (string, error)
dir bool
}{
xml.Name{Space: "DAV:", Local: "resourcetype"}: {
findFn: (*memPS).findResourceType,
dir: true,
},
xml.Name{Space: "DAV:", Local: "displayname"}: {
findFn: (*memPS).findDisplayName,
dir: true,
},
xml.Name{Space: "DAV:", Local: "getcontentlength"}: {
findFn: (*memPS).findContentLength,
dir: true,
},
xml.Name{Space: "DAV:", Local: "getlastmodified"}: {
findFn: (*memPS).findLastModified,
dir: true,
},
xml.Name{Space: "DAV:", Local: "creationdate"}: {
findFn: nil,
dir: true,
},
xml.Name{Space: "DAV:", Local: "getcontentlanguage"}: {
findFn: nil,
dir: true,
},
xml.Name{Space: "DAV:", Local: "getcontenttype"}: {
findFn: (*memPS).findContentType,
dir: true,
},
// memPS implements ETag as the concatenated hex values of a file's
// modification time and size. This is not a reliable synchronization
// mechanism for directories, so we do not advertise getetag for
// DAV collections.
xml.Name{Space: "DAV:", Local: "getetag"}: {
findFn: (*memPS).findETag,
dir: false,
},
// TODO(nigeltao) Lock properties will be defined later.
// xml.Name{Space: "DAV:", Local: "lockdiscovery"}
// xml.Name{Space: "DAV:", Local: "supportedlock"}
}
func (ps *memPS) Find(name string, propnames []xml.Name) ([]Propstat, error) {
fi, err := ps.fs.Stat(name)
if err != nil {
return nil, err
}
pm := make(map[int]Propstat)
for _, pn := range propnames {
p := Property{XMLName: pn}
s := http.StatusNotFound
if prop := davProps[pn]; prop.findFn != nil && (prop.dir || !fi.IsDir()) {
xmlvalue, err := prop.findFn(ps, name, fi)
if err != nil {
return nil, err
}
s = http.StatusOK
p.InnerXML = []byte(xmlvalue)
}
pstat := pm[s]
pstat.Props = append(pstat.Props, p)
pm[s] = pstat
}
pstats := make([]Propstat, 0, len(pm))
for s, pstat := range pm {
pstat.Status = s
pstats = append(pstats, pstat)
}
return pstats, nil
}
func (ps *memPS) Propnames(name string) ([]xml.Name, error) {
fi, err := ps.fs.Stat(name)
if err != nil {
return nil, err
}
propnames := make([]xml.Name, 0, len(davProps))
for pn, prop := range davProps {
if prop.findFn != nil && (prop.dir || !fi.IsDir()) {
propnames = append(propnames, pn)
}
}
return propnames, nil
}
func (ps *memPS) Allprop(name string, include []xml.Name) ([]Propstat, error) {
propnames, err := ps.Propnames(name)
if err != nil {
return nil, err
}
// Add names from include if they are not already covered in propnames.
nameset := make(map[xml.Name]bool)
for _, pn := range propnames {
nameset[pn] = true
}
for _, pn := range include {
if !nameset[pn] {
propnames = append(propnames, pn)
}
}
return ps.Find(name, propnames)
}
func (ps *memPS) Patch(name string, patches []Proppatch) ([]Propstat, error) {
// TODO(rost): Support to patch "dead" DAV properties in the next CL.
pstat := Propstat{Status: http.StatusForbidden}
for _, patch := range patches {
for _, p := range patch.Props {
pstat.Props = append(pstat.Props, Property{XMLName: p.XMLName})
}
}
return []Propstat{pstat}, nil
}
func (ps *memPS) findResourceType(name string, fi os.FileInfo) (string, error) {
if fi.IsDir() {
return `<collection xmlns="DAV:"/>`, nil
}
return "", nil
}
func (ps *memPS) findDisplayName(name string, fi os.FileInfo) (string, error) {
if slashClean(name) == "/" {
// Hide the real name of a possibly prefixed root directory.
return "", nil
}
return fi.Name(), nil
}
func (ps *memPS) findContentLength(name string, fi os.FileInfo) (string, error) {
return strconv.FormatInt(fi.Size(), 10), nil
}
func (ps *memPS) findLastModified(name string, fi os.FileInfo) (string, error) {
return fi.ModTime().Format(http.TimeFormat), nil
}
func (ps *memPS) findContentType(name string, fi os.FileInfo) (string, error) {
f, err := ps.fs.OpenFile(name, os.O_RDONLY, 0)
if err != nil {
return "", err
}
defer f.Close()
// This implementation is based on serveContent's code in the standard net/http package.
ctype := mime.TypeByExtension(filepath.Ext(name))
if ctype == "" {
// Read a chunk to decide between utf-8 text and binary.
var buf [512]byte
n, _ := io.ReadFull(f, buf[:])
ctype = http.DetectContentType(buf[:n])
// Rewind file.
_, err = f.Seek(0, os.SEEK_SET)
}
return ctype, err
}
func (ps *memPS) findETag(name string, fi os.FileInfo) (string, error) {
return detectETag(fi), nil
}
// detectETag determines the ETag for the file described by fi.
func detectETag(fi os.FileInfo) string {
// The Apache http 2.4 web server by default concatenates the
// modification time and size of a file. We replicate the heuristic
// with nanosecond granularity.
return fmt.Sprintf(`"%x%x"`, fi.ModTime().UnixNano(), fi.Size())
}