feat: webdav handle

This commit is contained in:
Noah Hsu
2022-06-30 22:41:55 +08:00
parent dd013ac0b2
commit 2b1726614b
11 changed files with 344 additions and 3451 deletions

View File

@ -10,11 +10,10 @@ import (
"encoding/xml"
"errors"
"fmt"
"io"
"github.com/alist-org/alist/v3/internal/model"
"mime"
"net/http"
"os"
"path/filepath"
"path"
"strconv"
)
@ -103,7 +102,7 @@ type DeadPropsHolder interface {
var liveProps = map[xml.Name]struct {
// findFn implements the propfind function of this property. If nil,
// it indicates a hidden property.
findFn func(context.Context, FileSystem, LockSystem, string, os.FileInfo) (string, error)
findFn func(context.Context, LockSystem, string, model.Obj) (string, error)
// dir is true if the property applies to directories.
dir bool
}{
@ -166,25 +165,26 @@ var liveProps = map[xml.Name]struct {
//
// Each Propstat has a unique status and each property name will only be part
// of one Propstat element.
func props(ctx context.Context, fs FileSystem, ls LockSystem, name string, pnames []xml.Name) ([]Propstat, error) {
f, err := fs.OpenFile(ctx, name, os.O_RDONLY, 0)
if err != nil {
return nil, err
}
defer f.Close()
fi, err := f.Stat()
if err != nil {
return nil, err
}
func props(ctx context.Context, ls LockSystem, fi model.Obj, pnames []xml.Name) ([]Propstat, error) {
//f, err := fs.OpenFile(ctx, name, os.O_RDONLY, 0)
//if err != nil {
// 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, err = dph.DeadProps()
if err != nil {
return nil, err
}
}
// ??? what is this for?
//if dph, ok := f.(DeadPropsHolder); ok {
// deadProps, err = dph.DeadProps()
// if err != nil {
// return nil, err
// }
//}
pstatOK := Propstat{Status: http.StatusOK}
pstatNotFound := Propstat{Status: http.StatusNotFound}
@ -196,7 +196,7 @@ func props(ctx context.Context, fs FileSystem, ls LockSystem, name string, pname
}
// Otherwise, it must either be a live property or we don't know it.
if prop := liveProps[pn]; prop.findFn != nil && (prop.dir || !isDir) {
innerXML, err := prop.findFn(ctx, fs, ls, name, fi)
innerXML, err := prop.findFn(ctx, ls, fi.GetName(), fi)
if err != nil {
return nil, err
}
@ -214,25 +214,26 @@ func props(ctx context.Context, fs FileSystem, ls LockSystem, name string, pname
}
// Propnames returns the property names defined for resource name.
func propnames(ctx context.Context, fs FileSystem, ls LockSystem, name string) ([]xml.Name, error) {
f, err := fs.OpenFile(ctx, name, os.O_RDONLY, 0)
if err != nil {
return nil, err
}
defer f.Close()
fi, err := f.Stat()
if err != nil {
return nil, err
}
func propnames(ctx context.Context, ls LockSystem, fi model.Obj) ([]xml.Name, error) {
//f, err := fs.OpenFile(ctx, name, os.O_RDONLY, 0)
//if err != nil {
// 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, err = dph.DeadProps()
if err != nil {
return nil, err
}
}
// ??? what is this for?
//if dph, ok := f.(DeadPropsHolder); ok {
// deadProps, err = dph.DeadProps()
// if err != nil {
// return nil, err
// }
//}
pnames := make([]xml.Name, 0, len(liveProps)+len(deadProps))
for pn, prop := range liveProps {
@ -254,8 +255,8 @@ func propnames(ctx context.Context, fs FileSystem, ls LockSystem, name string) (
// returned if they are named in 'include'.
//
// See http://www.webdav.org/specs/rfc4918.html#METHOD_PROPFIND
func allprop(ctx context.Context, fs FileSystem, ls LockSystem, name string, include []xml.Name) ([]Propstat, error) {
pnames, err := propnames(ctx, fs, ls, name)
func allprop(ctx context.Context, ls LockSystem, fi model.Obj, include []xml.Name) ([]Propstat, error) {
pnames, err := propnames(ctx, ls, fi)
if err != nil {
return nil, err
}
@ -269,12 +270,12 @@ func allprop(ctx context.Context, fs FileSystem, ls LockSystem, name string, inc
pnames = append(pnames, pn)
}
}
return props(ctx, fs, ls, name, pnames)
return props(ctx, ls, fi, pnames)
}
// Patch patches the properties of resource name. The return values are
// constrained in the same manner as DeadPropsHolder.Patch.
func patch(ctx context.Context, fs FileSystem, ls LockSystem, name string, patches []Proppatch) ([]Propstat, error) {
func patch(ctx context.Context, ls LockSystem, name string, patches []Proppatch) ([]Propstat, error) {
conflict := false
loop:
for _, patch := range patches {
@ -305,26 +306,29 @@ loop:
return makePropstats(pstatForbidden, pstatFailedDep), nil
}
f, err := fs.OpenFile(ctx, name, os.O_RDWR, 0)
if err != nil {
return nil, err
}
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
}
// ------------------------------------------------------------
//f, err := fs.OpenFile(ctx, name, os.O_RDWR, 0)
//if err != nil {
// return nil, err
//}
//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}
@ -356,26 +360,26 @@ func escapeXML(s string) string {
return s
}
func findResourceType(ctx context.Context, fs FileSystem, ls LockSystem, name string, fi os.FileInfo) (string, error) {
func findResourceType(ctx context.Context, ls LockSystem, name string, fi model.Obj) (string, error) {
if fi.IsDir() {
return `<D:collection xmlns:D="DAV:"/>`, nil
}
return "", nil
}
func findDisplayName(ctx context.Context, fs FileSystem, ls LockSystem, name string, fi os.FileInfo) (string, error) {
func findDisplayName(ctx context.Context, ls LockSystem, name string, fi model.Obj) (string, error) {
if slashClean(name) == "/" {
// Hide the real name of a possibly prefixed root directory.
return "", nil
}
return escapeXML(fi.Name()), nil
return escapeXML(fi.GetName()), nil
}
func findContentLength(ctx context.Context, fs FileSystem, ls LockSystem, name string, fi os.FileInfo) (string, error) {
return strconv.FormatInt(fi.Size(), 10), nil
func findContentLength(ctx context.Context, ls LockSystem, name string, fi model.Obj) (string, error) {
return strconv.FormatInt(fi.GetSize(), 10), nil
}
func findLastModified(ctx context.Context, fs FileSystem, ls LockSystem, name string, fi os.FileInfo) (string, error) {
func findLastModified(ctx context.Context, ls LockSystem, name string, fi model.Obj) (string, error) {
return fi.ModTime().UTC().Format(http.TimeFormat), nil
}
@ -400,33 +404,34 @@ type ContentTyper interface {
ContentType(ctx context.Context) (string, error)
}
func findContentType(ctx context.Context, fs FileSystem, ls LockSystem, name string, fi os.FileInfo) (string, error) {
if do, ok := fi.(ContentTyper); ok {
ctype, err := do.ContentType(ctx)
if err != ErrNotImplemented {
return ctype, err
}
}
f, err := fs.OpenFile(ctx, name, os.O_RDONLY, 0)
if err != nil {
return "", err
}
defer f.Close()
func findContentType(ctx context.Context, ls LockSystem, name string, fi model.Obj) (string, error) {
//if do, ok := fi.(ContentTyper); ok {
// ctype, err := do.ContentType(ctx)
// if err != ErrNotImplemented {
// return ctype, err
// }
//}
//f, err := fs.OpenFile(ctx, 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))
ctype := mime.TypeByExtension(path.Ext(name))
if ctype != "" {
return ctype, nil
}
return "application/octet-stream", nil
// Read a chunk to decide between utf-8 text and binary.
var buf [512]byte
n, err := io.ReadFull(f, buf[:])
if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
return "", err
}
ctype = http.DetectContentType(buf[:n])
// Rewind file.
_, err = f.Seek(0, os.SEEK_SET)
return ctype, err
//var buf [512]byte
//n, err := io.ReadFull(f, buf[:])
//if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
// return "", err
//}
//ctype = http.DetectContentType(buf[:n])
//// Rewind file.
//_, err = f.Seek(0, os.SEEK_SET)
//return ctype, err
}
// ETager is an optional interface for the os.FileInfo objects
@ -447,7 +452,7 @@ type ETager interface {
ETag(ctx context.Context) (string, error)
}
func findETag(ctx context.Context, fs FileSystem, ls LockSystem, name string, fi os.FileInfo) (string, error) {
func findETag(ctx context.Context, ls LockSystem, name string, fi model.Obj) (string, error) {
if do, ok := fi.(ETager); ok {
etag, err := do.ETag(ctx)
if err != ErrNotImplemented {
@ -457,10 +462,10 @@ func findETag(ctx context.Context, fs FileSystem, ls LockSystem, name string, fi
// 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()), nil
return fmt.Sprintf(`"%x%x"`, fi.ModTime().UnixNano(), fi.GetSize()), nil
}
func findSupportedLock(ctx context.Context, fs FileSystem, ls LockSystem, name string, fi os.FileInfo) (string, error) {
func findSupportedLock(ctx context.Context, ls LockSystem, name string, fi model.Obj) (string, error) {
return `` +
`<D:lockentry xmlns:D="DAV:">` +
`<D:lockscope><D:exclusive/></D:lockscope>` +