Commit 72bfdce4 authored by Nigel Tao's avatar Nigel Tao

webdav: make properties belong to the File(System), not a PropSystem.

This makes properties consistent even after a COPY, MOVE or DELETE.

A follow-up CL will eliminate the concept of a configurable PropSystem
entirely.

See discussion at
https://groups.google.com/forum/#!topic/golang-dev/2_LiN6sf93A

Litmust test before/after:
<- summary for `props': of 30 tests run: 28 passed, 2 failed. 93.3%
<- summary for `props': of 30 tests run: 29 passed, 1 failed. 96.7%

Change-Id: I9bd09d9fb73e40f87306eaec2679945874af023d
Reviewed-on: https://go-review.googlesource.com/10241Reviewed-by: 's avatarNigel Tao <nigeltao@golang.org>
parent 5273a78d
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
package webdav package webdav
import ( import (
"encoding/xml"
"io" "io"
"net/http" "net/http"
"os" "os"
...@@ -44,6 +45,9 @@ type FileSystem interface { ...@@ -44,6 +45,9 @@ type FileSystem interface {
// A File is returned by a FileSystem's OpenFile method and can be served by a // A File is returned by a FileSystem's OpenFile method and can be served by a
// Handler. // Handler.
//
// A File may optionally implement the DeadPropsHolder interface, if it can
// load and save dead properties.
type File interface { type File interface {
http.File http.File
io.Writer io.Writer
...@@ -405,6 +409,7 @@ type memFSNode struct { ...@@ -405,6 +409,7 @@ type memFSNode struct {
data []byte data []byte
mode os.FileMode mode os.FileMode
modTime time.Time modTime time.Time
deadProps map[xml.Name]Property
} }
func (n *memFSNode) stat(name string) *memFileInfo { func (n *memFSNode) stat(name string) *memFileInfo {
...@@ -418,6 +423,39 @@ func (n *memFSNode) stat(name string) *memFileInfo { ...@@ -418,6 +423,39 @@ func (n *memFSNode) stat(name string) *memFileInfo {
} }
} }
func (n *memFSNode) DeadProps() map[xml.Name]Property {
n.mu.Lock()
defer n.mu.Unlock()
if len(n.deadProps) == 0 {
return nil
}
ret := make(map[xml.Name]Property, len(n.deadProps))
for k, v := range n.deadProps {
ret[k] = v
}
return ret
}
func (n *memFSNode) Patch(patches []Proppatch) ([]Propstat, error) {
n.mu.Lock()
defer n.mu.Unlock()
pstat := Propstat{Status: http.StatusOK}
for _, patch := range patches {
for _, p := range patch.Props {
pstat.Props = append(pstat.Props, Property{XMLName: p.XMLName})
if patch.Remove {
delete(n.deadProps, p.XMLName)
continue
}
if n.deadProps == nil {
n.deadProps = map[xml.Name]Property{}
}
n.deadProps[p.XMLName] = p
}
}
return []Propstat{pstat}, nil
}
type memFileInfo struct { type memFileInfo struct {
name string name string
size int64 size int64
...@@ -443,6 +481,12 @@ type memFile struct { ...@@ -443,6 +481,12 @@ type memFile struct {
pos int pos int
} }
// A *memFile implements the optional DeadPropsHolder interface.
var _ DeadPropsHolder = (*memFile)(nil)
func (f *memFile) DeadProps() map[xml.Name]Property { return f.n.DeadProps() }
func (f *memFile) Patch(patches []Proppatch) ([]Propstat, error) { return f.n.Patch(patches) }
func (f *memFile) Close() error { func (f *memFile) Close() error {
return nil return nil
} }
......
...@@ -37,7 +37,7 @@ func main() { ...@@ -37,7 +37,7 @@ func main() {
http.Handle("/", &webdav.Handler{ http.Handle("/", &webdav.Handler{
FileSystem: fs, FileSystem: fs,
LockSystem: ls, LockSystem: ls,
PropSystem: webdav.NewMemPS(fs, ls, webdav.ReadWrite), PropSystem: webdav.NewMemPS(fs, ls),
Logger: func(r *http.Request, err error) { Logger: func(r *http.Request, err error) {
litmus := r.Header.Get("X-Litmus") litmus := r.Header.Get("X-Litmus")
if len(litmus) > 19 { if len(litmus) > 19 {
......
...@@ -13,9 +13,12 @@ import ( ...@@ -13,9 +13,12 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"strconv" "strconv"
"sync"
) )
// TODO(nigeltao): eliminate the concept of a configurable PropSystem, and the
// MemPS implementation. Properties are now the responsibility of a File
// implementation, not a PropSystem implementation.
// PropSystem manages the properties of named resources. It allows finding // PropSystem manages the properties of named resources. It allows finding
// and setting properties as defined in RFC 4918. // and setting properties as defined in RFC 4918.
// //
...@@ -54,8 +57,6 @@ type PropSystem interface { ...@@ -54,8 +57,6 @@ type PropSystem interface {
// //
// Note that the WebDAV RFC requires either all patches to succeed or none. // Note that the WebDAV RFC requires either all patches to succeed or none.
Patch(name string, patches []Proppatch) ([]Propstat, error) Patch(name string, patches []Proppatch) ([]Propstat, error)
// TODO(rost) COPY/MOVE/DELETE.
} }
// Proppatch describes a property update instruction as defined in RFC 4918. // Proppatch describes a property update instruction as defined in RFC 4918.
...@@ -91,46 +92,66 @@ type Propstat struct { ...@@ -91,46 +92,66 @@ type Propstat struct {
ResponseDescription string ResponseDescription string
} }
// makePropstats returns a slice containing those of x and y whose Props slice
// is non-empty. If both are empty, it returns a slice containing an otherwise
// zero Propstat whose HTTP status code is 200 OK.
func makePropstats(x, y Propstat) []Propstat {
pstats := make([]Propstat, 0, 2)
if len(x.Props) != 0 {
pstats = append(pstats, x)
}
if len(y.Props) != 0 {
pstats = append(pstats, y)
}
if len(pstats) == 0 {
pstats = append(pstats, Propstat{
Status: http.StatusOK,
})
}
return pstats
}
// DeadPropsHolder holds the dead properties of a resource.
//
// Dead properties are those properties that are explicitly defined. In
// comparison, live properties, such as DAV:getcontentlength, are implicitly
// defined by the underlying resource, and cannot be explicitly overridden or
// removed. See the Terminology section of
// http://www.webdav.org/specs/rfc4918.html#rfc.section.3
//
// There is a whitelist of the names of live properties. This package handles
// all live properties, and will only pass non-whitelisted names to the Patch
// method of DeadPropsHolder implementations.
type DeadPropsHolder interface {
// DeadProps returns a copy of the dead properties held.
DeadProps() map[xml.Name]Property
// Patch patches the dead properties held.
//
// Patching is atomic; either all or no patches succeed. It returns (nil,
// non-nil) if an internal server error occurred, otherwise the Propstats
// collectively contain one Property for each proposed patch Property. If
// all patches succeed, Patch returns a slice of length one and a Propstat
// element with a 200 OK HTTP status code. If none succeed, for reasons
// other than an internal server error, no Propstat has status 200 OK.
//
// For more details on when various HTTP status codes apply, see
// http://www.webdav.org/specs/rfc4918.html#PROPPATCH-status
Patch([]Proppatch) ([]Propstat, error)
}
// memPS implements an in-memory PropSystem. It supports all of the mandatory // memPS implements an in-memory PropSystem. It supports all of the mandatory
// live properties of RFC 4918. // live properties of RFC 4918.
type memPS struct { type memPS struct {
fs FileSystem fs FileSystem
ls LockSystem ls LockSystem
m Mutability
mu sync.RWMutex
nodes map[string]*memPSNode
}
// memPSNode stores the dead properties of a resource.
type memPSNode struct {
mu sync.RWMutex
deadProps map[xml.Name]Property
} }
// BUG(rost): In this development version, the in-memory property system does // NewMemPS returns a new in-memory PropSystem implementation.
// not handle COPY/MOVE/DELETE requests. As a result, dead properties are not func NewMemPS(fs FileSystem, ls LockSystem) PropSystem {
// released if the according DAV resource is deleted or moved. It is not
// recommended to use a read-writeable property system in production.
// Mutability indicates the mutability of a property system.
type Mutability bool
const (
ReadOnly = Mutability(false)
ReadWrite = Mutability(true)
)
// NewMemPS returns a new in-memory PropSystem implementation. A read-only
// property system rejects all patches. A read-writeable property system
// stores arbitrary properties but refuses to change any DAV: property
// specified in RFC 4918. It imposes no limit on the size of property values.
func NewMemPS(fs FileSystem, ls LockSystem, m Mutability) PropSystem {
return &memPS{ return &memPS{
fs: fs, fs: fs,
ls: ls, ls: ls,
m: m,
nodes: make(map[string]*memPSNode),
} }
} }
...@@ -185,79 +206,75 @@ var liveProps = map[xml.Name]struct { ...@@ -185,79 +206,75 @@ var liveProps = map[xml.Name]struct {
} }
func (ps *memPS) Find(name string, propnames []xml.Name) ([]Propstat, error) { func (ps *memPS) Find(name string, propnames []xml.Name) ([]Propstat, error) {
ps.mu.RLock() f, err := ps.fs.OpenFile(name, os.O_RDONLY, 0)
defer ps.mu.RUnlock() if err != nil {
return nil, err
fi, err := ps.fs.Stat(name) }
defer f.Close()
fi, err := f.Stat()
if err != nil { if err != nil {
return nil, err return nil, err
} }
isDir := fi.IsDir()
// Lookup the dead properties of this resource. It's OK if there are none. var deadProps map[xml.Name]Property
n, ok := ps.nodes[name] if dph, ok := f.(DeadPropsHolder); ok {
if ok { deadProps = dph.DeadProps()
n.mu.RLock()
defer n.mu.RUnlock()
} }
pm := make(map[int]Propstat) pstatOK := Propstat{Status: http.StatusOK}
pstatNotFound := Propstat{Status: http.StatusNotFound}
for _, pn := range propnames { for _, pn := range propnames {
// If this node has dead properties, check if they contain pn. // If this file has dead properties, check if they contain pn.
if n != nil { if dp, ok := deadProps[pn]; ok {
if dp, ok := n.deadProps[pn]; ok { pstatOK.Props = append(pstatOK.Props, dp)
pstat := pm[http.StatusOK]
pstat.Props = append(pstat.Props, dp)
pm[http.StatusOK] = pstat
continue continue
} }
}
// Otherwise, it must either be a live property or we don't know it. // Otherwise, it must either be a live property or we don't know it.
p := Property{XMLName: pn} if prop := liveProps[pn]; prop.findFn != nil && (prop.dir || !isDir) {
s := http.StatusNotFound innerXML, err := prop.findFn(ps, name, fi)
if prop := liveProps[pn]; prop.findFn != nil && (prop.dir || !fi.IsDir()) {
xmlvalue, err := prop.findFn(ps, name, fi)
if err != nil { if err != nil {
return nil, err return nil, err
} }
s = http.StatusOK pstatOK.Props = append(pstatOK.Props, Property{
p.InnerXML = []byte(xmlvalue) XMLName: pn,
} InnerXML: []byte(innerXML),
pstat := pm[s] })
pstat.Props = append(pstat.Props, p) } else {
pm[s] = pstat pstatNotFound.Props = append(pstatNotFound.Props, Property{
XMLName: pn,
})
} }
pstats := make([]Propstat, 0, len(pm))
for s, pstat := range pm {
pstat.Status = s
pstats = append(pstats, pstat)
} }
return pstats, nil return makePropstats(pstatOK, pstatNotFound), nil
} }
func (ps *memPS) Propnames(name string) ([]xml.Name, error) { func (ps *memPS) Propnames(name string) ([]xml.Name, error) {
fi, err := ps.fs.Stat(name) f, err := ps.fs.OpenFile(name, os.O_RDONLY, 0)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer f.Close()
fi, err := f.Stat()
if err != nil {
return nil, err
}
isDir := fi.IsDir()
var deadProps map[xml.Name]Property
if dph, ok := f.(DeadPropsHolder); ok {
deadProps = dph.DeadProps()
}
propnames := make([]xml.Name, 0, len(liveProps)) propnames := make([]xml.Name, 0, len(liveProps)+len(deadProps))
for pn, prop := range liveProps { for pn, prop := range liveProps {
if prop.findFn != nil && (prop.dir || !fi.IsDir()) { if prop.findFn != nil && (prop.dir || !isDir) {
propnames = append(propnames, pn) propnames = append(propnames, pn)
} }
} }
for pn := range deadProps {
ps.mu.RLock()
defer ps.mu.RUnlock()
if n, ok := ps.nodes[name]; ok {
n.mu.RLock()
defer n.mu.RUnlock()
for pn := range n.deadProps {
propnames = append(propnames, pn) propnames = append(propnames, pn)
} }
}
return propnames, nil return propnames, nil
} }
...@@ -280,61 +297,65 @@ func (ps *memPS) Allprop(name string, include []xml.Name) ([]Propstat, error) { ...@@ -280,61 +297,65 @@ func (ps *memPS) Allprop(name string, include []xml.Name) ([]Propstat, error) {
} }
func (ps *memPS) Patch(name string, patches []Proppatch) ([]Propstat, error) { func (ps *memPS) Patch(name string, patches []Proppatch) ([]Propstat, error) {
// A DELETE/COPY/MOVE might fly in, so we need to keep all nodes locked until conflict := false
// the end of this PROPPATCH. loop:
ps.mu.Lock()
defer ps.mu.Unlock()
n, ok := ps.nodes[name]
if !ok {
n = &memPSNode{deadProps: make(map[xml.Name]Property)}
}
n.mu.Lock()
defer n.mu.Unlock()
_, err := ps.fs.Stat(name)
if err != nil {
return nil, err
}
// Perform a dry-run to identify any patch conflicts. A read-only property
// system always fails at this stage.
pm := make(map[int]Propstat)
for _, patch := range patches { for _, patch := range patches {
for _, p := range patch.Props { for _, p := range patch.Props {
s := http.StatusOK if _, ok := liveProps[p.XMLName]; ok {
if _, ok := liveProps[p.XMLName]; ok || ps.m == ReadOnly { conflict = true
s = http.StatusForbidden break loop
}
} }
pstat := pm[s]
pstat.Props = append(pstat.Props, Property{XMLName: p.XMLName})
pm[s] = pstat
} }
if conflict {
pstatForbidden := Propstat{
Status: http.StatusForbidden,
XMLError: `<error xmlns="DAV:"><cannot-modify-protected-property/></error>`,
}
pstatFailedDep := Propstat{
Status: StatusFailedDependency,
} }
// Based on the dry-run, either apply the patches or handle conflicts.
if _, ok = pm[http.StatusOK]; ok {
if len(pm) == 1 {
for _, patch := range patches { for _, patch := range patches {
for _, p := range patch.Props { for _, p := range patch.Props {
if patch.Remove { if _, ok := liveProps[p.XMLName]; ok {
delete(n.deadProps, p.XMLName) pstatForbidden.Props = append(pstatForbidden.Props, Property{XMLName: p.XMLName})
} else { } else {
n.deadProps[p.XMLName] = p pstatFailedDep.Props = append(pstatFailedDep.Props, Property{XMLName: p.XMLName})
} }
} }
} }
ps.nodes[name] = n return makePropstats(pstatForbidden, pstatFailedDep), nil
} else {
pm[StatusFailedDependency] = pm[http.StatusOK]
delete(pm, http.StatusOK)
}
} }
pstats := make([]Propstat, 0, len(pm)) f, err := ps.fs.OpenFile(name, os.O_RDWR, 0)
for s, pstat := range pm { if err != nil {
pstat.Status = s return nil, err
pstats = append(pstats, pstat) }
defer f.Close()
if dph, ok := f.(DeadPropsHolder); ok {
ret, err := dph.Patch(patches)
if err != nil {
return nil, err
}
// http://www.webdav.org/specs/rfc4918.html#ELEMENT_propstat says that
// "The contents of the prop XML element must only list the names of
// properties to which the result in the status element applies."
for _, pstat := range ret {
for i, p := range pstat.Props {
pstat.Props[i] = Property{XMLName: p.XMLName}
}
}
return ret, nil
}
// The file doesn't implement the optional DeadPropsHolder interface, so
// all patches are forbidden.
pstat := Propstat{Status: http.StatusForbidden}
for _, patch := range patches {
for _, p := range patch.Props {
pstat.Props = append(pstat.Props, Property{XMLName: p.XMLName})
}
} }
return pstats, nil return []Propstat{pstat}, nil
} }
func (ps *memPS) findResourceType(name string, fi os.FileInfo) (string, error) { func (ps *memPS) findResourceType(name string, fi os.FileInfo) (string, error) {
......
...@@ -8,6 +8,7 @@ import ( ...@@ -8,6 +8,7 @@ import (
"encoding/xml" "encoding/xml"
"fmt" "fmt"
"net/http" "net/http"
"os"
"reflect" "reflect"
"sort" "sort"
"testing" "testing"
...@@ -50,7 +51,7 @@ func TestMemPS(t *testing.T) { ...@@ -50,7 +51,7 @@ func TestMemPS(t *testing.T) {
testCases := []struct { testCases := []struct {
desc string desc string
mutability Mutability noDeadProps bool
buildfs []string buildfs []string
propOp []propOp propOp []propOp
}{{ }{{
...@@ -238,9 +239,9 @@ func TestMemPS(t *testing.T) { ...@@ -238,9 +239,9 @@ func TestMemPS(t *testing.T) {
}}, }},
}}, }},
}, { }, {
desc: "proppatch property on read-only property system", desc: "proppatch property on no-dead-properties file system",
buildfs: []string{"mkdir /dir"}, buildfs: []string{"mkdir /dir"},
mutability: ReadOnly, noDeadProps: true,
propOp: []propOp{{ propOp: []propOp{{
op: "proppatch", op: "proppatch",
name: "/dir", name: "/dir",
...@@ -265,6 +266,7 @@ func TestMemPS(t *testing.T) { ...@@ -265,6 +266,7 @@ func TestMemPS(t *testing.T) {
}}, }},
wantPropstats: []Propstat{{ wantPropstats: []Propstat{{
Status: http.StatusForbidden, Status: http.StatusForbidden,
XMLError: `<error xmlns="DAV:"><cannot-modify-protected-property/></error>`,
Props: []Property{{ Props: []Property{{
XMLName: xml.Name{Space: "DAV:", Local: "getetag"}, XMLName: xml.Name{Space: "DAV:", Local: "getetag"},
}}, }},
...@@ -273,7 +275,6 @@ func TestMemPS(t *testing.T) { ...@@ -273,7 +275,6 @@ func TestMemPS(t *testing.T) {
}, { }, {
desc: "proppatch dead property", desc: "proppatch dead property",
buildfs: []string{"mkdir /dir"}, buildfs: []string{"mkdir /dir"},
mutability: ReadWrite,
propOp: []propOp{{ propOp: []propOp{{
op: "proppatch", op: "proppatch",
name: "/dir", name: "/dir",
...@@ -304,7 +305,6 @@ func TestMemPS(t *testing.T) { ...@@ -304,7 +305,6 @@ func TestMemPS(t *testing.T) {
}, { }, {
desc: "proppatch dead property with failed dependency", desc: "proppatch dead property with failed dependency",
buildfs: []string{"mkdir /dir"}, buildfs: []string{"mkdir /dir"},
mutability: ReadWrite,
propOp: []propOp{{ propOp: []propOp{{
op: "proppatch", op: "proppatch",
name: "/dir", name: "/dir",
...@@ -321,6 +321,7 @@ func TestMemPS(t *testing.T) { ...@@ -321,6 +321,7 @@ func TestMemPS(t *testing.T) {
}}, }},
wantPropstats: []Propstat{{ wantPropstats: []Propstat{{
Status: http.StatusForbidden, Status: http.StatusForbidden,
XMLError: `<error xmlns="DAV:"><cannot-modify-protected-property/></error>`,
Props: []Property{{ Props: []Property{{
XMLName: xml.Name{Space: "DAV:", Local: "displayname"}, XMLName: xml.Name{Space: "DAV:", Local: "displayname"},
}}, }},
...@@ -344,7 +345,6 @@ func TestMemPS(t *testing.T) { ...@@ -344,7 +345,6 @@ func TestMemPS(t *testing.T) {
}, { }, {
desc: "proppatch remove dead property", desc: "proppatch remove dead property",
buildfs: []string{"mkdir /dir"}, buildfs: []string{"mkdir /dir"},
mutability: ReadWrite,
propOp: []propOp{{ propOp: []propOp{{
op: "proppatch", op: "proppatch",
name: "/dir", name: "/dir",
...@@ -420,7 +420,6 @@ func TestMemPS(t *testing.T) { ...@@ -420,7 +420,6 @@ func TestMemPS(t *testing.T) {
}, { }, {
desc: "propname with dead property", desc: "propname with dead property",
buildfs: []string{"touch /file"}, buildfs: []string{"touch /file"},
mutability: ReadWrite,
propOp: []propOp{{ propOp: []propOp{{
op: "proppatch", op: "proppatch",
name: "/file", name: "/file",
...@@ -452,7 +451,6 @@ func TestMemPS(t *testing.T) { ...@@ -452,7 +451,6 @@ func TestMemPS(t *testing.T) {
}, { }, {
desc: "proppatch remove unknown dead property", desc: "proppatch remove unknown dead property",
buildfs: []string{"mkdir /dir"}, buildfs: []string{"mkdir /dir"},
mutability: ReadWrite,
propOp: []propOp{{ propOp: []propOp{{
op: "proppatch", op: "proppatch",
name: "/dir", name: "/dir",
...@@ -490,8 +488,11 @@ func TestMemPS(t *testing.T) { ...@@ -490,8 +488,11 @@ func TestMemPS(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("%s: cannot create test filesystem: %v", tc.desc, err) t.Fatalf("%s: cannot create test filesystem: %v", tc.desc, err)
} }
if tc.noDeadProps {
fs = noDeadPropsFS{fs}
}
ls := NewMemLS() ls := NewMemLS()
ps := NewMemPS(fs, ls, tc.mutability) ps := NewMemPS(fs, ls)
for _, op := range tc.propOp { for _, op := range tc.propOp {
desc := fmt.Sprintf("%s: %s %s", tc.desc, op.op, op.name) desc := fmt.Sprintf("%s: %s %s", tc.desc, op.op, op.name)
if err = calcProps(op.name, fs, op.wantPropstats); err != nil { if err = calcProps(op.name, fs, op.wantPropstats); err != nil {
...@@ -551,36 +552,43 @@ func cmpXMLName(a, b xml.Name) bool { ...@@ -551,36 +552,43 @@ func cmpXMLName(a, b xml.Name) bool {
type byXMLName []xml.Name type byXMLName []xml.Name
func (b byXMLName) Len() int { func (b byXMLName) Len() int { return len(b) }
return len(b) func (b byXMLName) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
} func (b byXMLName) Less(i, j int) bool { return cmpXMLName(b[i], b[j]) }
func (b byXMLName) Swap(i, j int) {
b[i], b[j] = b[j], b[i]
}
func (b byXMLName) Less(i, j int) bool {
return cmpXMLName(b[i], b[j])
}
type byPropname []Property type byPropname []Property
func (b byPropname) Len() int { func (b byPropname) Len() int { return len(b) }
return len(b) func (b byPropname) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
} func (b byPropname) Less(i, j int) bool { return cmpXMLName(b[i].XMLName, b[j].XMLName) }
func (b byPropname) Swap(i, j int) {
b[i], b[j] = b[j], b[i]
}
func (b byPropname) Less(i, j int) bool {
return cmpXMLName(b[i].XMLName, b[j].XMLName)
}
type byStatus []Propstat type byStatus []Propstat
func (b byStatus) Len() int { func (b byStatus) Len() int { return len(b) }
return len(b) func (b byStatus) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
func (b byStatus) Less(i, j int) bool { return b[i].Status < b[j].Status }
type noDeadPropsFS struct {
FileSystem
} }
func (b byStatus) Swap(i, j int) {
b[i], b[j] = b[j], b[i] func (fs noDeadPropsFS) OpenFile(name string, flag int, perm os.FileMode) (File, error) {
f, err := fs.FileSystem.OpenFile(name, flag, perm)
if err != nil {
return nil, err
}
return noDeadPropsFile{f}, nil
} }
func (b byStatus) Less(i, j int) bool {
return b[i].Status < b[j].Status // noDeadPropsFile wraps a File but strips any optional DeadPropsHolder methods
// provided by the underlying File implementation.
type noDeadPropsFile struct {
f File
} }
func (f noDeadPropsFile) Close() error { return f.f.Close() }
func (f noDeadPropsFile) Read(p []byte) (int, error) { return f.f.Read(p) }
func (f noDeadPropsFile) Readdir(count int) ([]os.FileInfo, error) { return f.f.Readdir(count) }
func (f noDeadPropsFile) Seek(off int64, whence int) (int64, error) { return f.f.Seek(off, whence) }
func (f noDeadPropsFile) Stat() (os.FileInfo, error) { return f.f.Stat() }
func (f noDeadPropsFile) Write(p []byte) (int, error) { return f.f.Write(p) }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment