caldav, carddav: redirect on wrong path
it is much more user-friendly to redirect to the correct principalPath/homeSetPath when possible
This commit is contained in:
@@ -366,7 +366,7 @@ func (b *backend) HeadGet(w http.ResponseWriter, r *http.Request) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *backend) PropFind(r *http.Request, propfind *internal.PropFind, depth internal.Depth) (*internal.MultiStatus, error) {
|
func (b *backend) PropFind(w http.ResponseWriter, r *http.Request, propfind *internal.PropFind, depth internal.Depth) error {
|
||||||
resType := b.resourceTypeAtPath(r.URL.Path)
|
resType := b.resourceTypeAtPath(r.URL.Path)
|
||||||
|
|
||||||
var dataReq CalendarCompRequest
|
var dataReq CalendarCompRequest
|
||||||
@@ -376,86 +376,92 @@ func (b *backend) PropFind(r *http.Request, propfind *internal.PropFind, depth i
|
|||||||
case resourceTypeRoot:
|
case resourceTypeRoot:
|
||||||
resp, err := b.propFindRoot(r.Context(), propfind)
|
resp, err := b.propFindRoot(r.Context(), propfind)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
resps = append(resps, *resp)
|
resps = append(resps, *resp)
|
||||||
case resourceTypeUserPrincipal:
|
case resourceTypeUserPrincipal:
|
||||||
principalPath, err := b.Backend.CurrentUserPrincipal(r.Context())
|
principalPath, err := b.Backend.CurrentUserPrincipal(r.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
if r.URL.Path == principalPath {
|
if r.URL.Path == principalPath {
|
||||||
resp, err := b.propFindUserPrincipal(r.Context(), propfind)
|
resp, err := b.propFindUserPrincipal(r.Context(), propfind)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
resps = append(resps, *resp)
|
resps = append(resps, *resp)
|
||||||
if depth != internal.DepthZero {
|
if depth != internal.DepthZero {
|
||||||
resp, err := b.propFindHomeSet(r.Context(), propfind)
|
resp, err := b.propFindHomeSet(r.Context(), propfind)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
resps = append(resps, *resp)
|
resps = append(resps, *resp)
|
||||||
if depth == internal.DepthInfinity {
|
if depth == internal.DepthInfinity {
|
||||||
resps_, err := b.propFindAllCalendars(r.Context(), propfind, true)
|
resps_, err := b.propFindAllCalendars(r.Context(), propfind, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
resps = append(resps, resps_...)
|
resps = append(resps, resps_...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
http.Redirect(w, r, principalPath, http.StatusPermanentRedirect) // keep http method
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
case resourceTypeCalendarHomeSet:
|
case resourceTypeCalendarHomeSet:
|
||||||
homeSetPath, err := b.Backend.CalendarHomeSetPath(r.Context())
|
homeSetPath, err := b.Backend.CalendarHomeSetPath(r.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
if r.URL.Path == homeSetPath {
|
if r.URL.Path == homeSetPath {
|
||||||
resp, err := b.propFindHomeSet(r.Context(), propfind)
|
resp, err := b.propFindHomeSet(r.Context(), propfind)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
resps = append(resps, *resp)
|
resps = append(resps, *resp)
|
||||||
if depth != internal.DepthZero {
|
if depth != internal.DepthZero {
|
||||||
recurse := depth == internal.DepthInfinity
|
recurse := depth == internal.DepthInfinity
|
||||||
resps_, err := b.propFindAllCalendars(r.Context(), propfind, recurse)
|
resps_, err := b.propFindAllCalendars(r.Context(), propfind, recurse)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
resps = append(resps, resps_...)
|
resps = append(resps, resps_...)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
http.Redirect(w, r, homeSetPath, http.StatusPermanentRedirect) // keep http method
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
case resourceTypeCalendar:
|
case resourceTypeCalendar:
|
||||||
ab, err := b.Backend.GetCalendar(r.Context(), r.URL.Path)
|
ab, err := b.Backend.GetCalendar(r.Context(), r.URL.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
resp, err := b.propFindCalendar(r.Context(), propfind, ab)
|
resp, err := b.propFindCalendar(r.Context(), propfind, ab)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
resps = append(resps, *resp)
|
resps = append(resps, *resp)
|
||||||
if depth != internal.DepthZero {
|
if depth != internal.DepthZero {
|
||||||
resps_, err := b.propFindAllCalendarObjects(r.Context(), propfind, ab)
|
resps_, err := b.propFindAllCalendarObjects(r.Context(), propfind, ab)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
resps = append(resps, resps_...)
|
resps = append(resps, resps_...)
|
||||||
}
|
}
|
||||||
case resourceTypeCalendarObject:
|
case resourceTypeCalendarObject:
|
||||||
ao, err := b.Backend.GetCalendarObject(r.Context(), r.URL.Path, &dataReq)
|
ao, err := b.Backend.GetCalendarObject(r.Context(), r.URL.Path, &dataReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := b.propFindCalendarObject(r.Context(), propfind, ao)
|
resp, err := b.propFindCalendarObject(r.Context(), propfind, ao)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
resps = append(resps, *resp)
|
resps = append(resps, *resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
return internal.NewMultiStatus(resps...), nil
|
return internal.ServeMultiStatus(w, internal.NewMultiStatus(resps...))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *backend) propFindRoot(ctx context.Context, propfind *internal.PropFind) (*internal.Response, error) {
|
func (b *backend) propFindRoot(ctx context.Context, propfind *internal.PropFind) (*internal.Response, error) {
|
||||||
|
@@ -233,3 +233,35 @@ func (t testBackend) ListCalendarObjects(ctx context.Context, path string, req *
|
|||||||
func (t testBackend) QueryCalendarObjects(ctx context.Context, path string, query *CalendarQuery) ([]CalendarObject, error) {
|
func (t testBackend) QueryCalendarObjects(ctx context.Context, path string, query *CalendarQuery) ([]CalendarObject, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRedirections(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
calendars := []Calendar{{Path: "/user/calendars/a"}}
|
||||||
|
ts := httptest.NewServer(&Handler{&testBackend{
|
||||||
|
calendars: calendars,
|
||||||
|
}, ""})
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
client, err := NewClient(nil, ts.URL)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error creating client: %s", err)
|
||||||
|
}
|
||||||
|
hsp, err := client.FindCalendarHomeSet(ctx, "/must-be-redirected/")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error finding home set path: %s", err)
|
||||||
|
}
|
||||||
|
if want := "/user/calendars/"; hsp != want {
|
||||||
|
t.Fatalf("Found home set path '%s', expected '%s'", hsp, want)
|
||||||
|
}
|
||||||
|
abs, err := client.FindCalendars(ctx, "/must-be-redirected/again/")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error finding calendars: %s", err)
|
||||||
|
}
|
||||||
|
if len(abs) != 1 {
|
||||||
|
t.Fatalf("Found %d calendars, expected 1", len(abs))
|
||||||
|
}
|
||||||
|
if want := "/user/calendars/a"; abs[0].Path != want {
|
||||||
|
t.Fatalf("Found calendar at %s, expected %s", abs[0].Path, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -194,6 +194,43 @@ func TestAddressBookDiscovery(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRedirections(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
h := Handler{&testBackend{}, ""}
|
||||||
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
ctx = context.WithValue(ctx, currentUserPrincipalKey, "/principal/")
|
||||||
|
ctx = context.WithValue(ctx, homeSetPathKey, "/principal/contacts/")
|
||||||
|
ctx = context.WithValue(ctx, addressBookPathKey, "/principal/contacts/default")
|
||||||
|
r = r.WithContext(ctx)
|
||||||
|
(&h).ServeHTTP(w, r)
|
||||||
|
}))
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
client, err := NewClient(nil, ts.URL)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error creating client: %s", err)
|
||||||
|
}
|
||||||
|
hsp, err := client.FindAddressBookHomeSet(ctx, "/must-be-redirected/")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error finding home set path: %s", err)
|
||||||
|
}
|
||||||
|
if want := "/principal/contacts/"; hsp != want {
|
||||||
|
t.Fatalf("Found home set path '%s', expected '%s'", hsp, want)
|
||||||
|
}
|
||||||
|
abs, err := client.FindAddressBooks(ctx, "/must-be-redirected/again/")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error finding address books: %s", err)
|
||||||
|
}
|
||||||
|
if len(abs) != 1 {
|
||||||
|
t.Fatalf("Found %d address books, expected 1", len(abs))
|
||||||
|
}
|
||||||
|
if want := "/principal/contacts/default"; abs[0].Path != want {
|
||||||
|
t.Fatalf("Found address book at %s, expected %s", abs[0].Path, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var mkcolRequestBody = `
|
var mkcolRequestBody = `
|
||||||
<?xml version="1.0" encoding="utf-8" ?>
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
<D:mkcol xmlns:D="DAV:"
|
<D:mkcol xmlns:D="DAV:"
|
||||||
|
@@ -332,7 +332,7 @@ func (b *backend) HeadGet(w http.ResponseWriter, r *http.Request) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *backend) PropFind(r *http.Request, propfind *internal.PropFind, depth internal.Depth) (*internal.MultiStatus, error) {
|
func (b *backend) PropFind(w http.ResponseWriter, r *http.Request, propfind *internal.PropFind, depth internal.Depth) error {
|
||||||
resType := b.resourceTypeAtPath(r.URL.Path)
|
resType := b.resourceTypeAtPath(r.URL.Path)
|
||||||
|
|
||||||
var dataReq AddressDataRequest
|
var dataReq AddressDataRequest
|
||||||
@@ -342,86 +342,92 @@ func (b *backend) PropFind(r *http.Request, propfind *internal.PropFind, depth i
|
|||||||
case resourceTypeRoot:
|
case resourceTypeRoot:
|
||||||
resp, err := b.propFindRoot(r.Context(), propfind)
|
resp, err := b.propFindRoot(r.Context(), propfind)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
resps = append(resps, *resp)
|
resps = append(resps, *resp)
|
||||||
case resourceTypeUserPrincipal:
|
case resourceTypeUserPrincipal:
|
||||||
principalPath, err := b.Backend.CurrentUserPrincipal(r.Context())
|
principalPath, err := b.Backend.CurrentUserPrincipal(r.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
if r.URL.Path == principalPath {
|
if r.URL.Path == principalPath {
|
||||||
resp, err := b.propFindUserPrincipal(r.Context(), propfind)
|
resp, err := b.propFindUserPrincipal(r.Context(), propfind)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
resps = append(resps, *resp)
|
resps = append(resps, *resp)
|
||||||
if depth != internal.DepthZero {
|
if depth != internal.DepthZero {
|
||||||
resp, err := b.propFindHomeSet(r.Context(), propfind)
|
resp, err := b.propFindHomeSet(r.Context(), propfind)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
resps = append(resps, *resp)
|
resps = append(resps, *resp)
|
||||||
if depth == internal.DepthInfinity {
|
if depth == internal.DepthInfinity {
|
||||||
resps_, err := b.propFindAllAddressBooks(r.Context(), propfind, true)
|
resps_, err := b.propFindAllAddressBooks(r.Context(), propfind, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
resps = append(resps, resps_...)
|
resps = append(resps, resps_...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
http.Redirect(w, r, principalPath, http.StatusPermanentRedirect) // keep http method
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
case resourceTypeAddressBookHomeSet:
|
case resourceTypeAddressBookHomeSet:
|
||||||
homeSetPath, err := b.Backend.AddressBookHomeSetPath(r.Context())
|
homeSetPath, err := b.Backend.AddressBookHomeSetPath(r.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
if r.URL.Path == homeSetPath {
|
if r.URL.Path == homeSetPath {
|
||||||
resp, err := b.propFindHomeSet(r.Context(), propfind)
|
resp, err := b.propFindHomeSet(r.Context(), propfind)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
resps = append(resps, *resp)
|
resps = append(resps, *resp)
|
||||||
if depth != internal.DepthZero {
|
if depth != internal.DepthZero {
|
||||||
recurse := depth == internal.DepthInfinity
|
recurse := depth == internal.DepthInfinity
|
||||||
resps_, err := b.propFindAllAddressBooks(r.Context(), propfind, recurse)
|
resps_, err := b.propFindAllAddressBooks(r.Context(), propfind, recurse)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
resps = append(resps, resps_...)
|
resps = append(resps, resps_...)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
http.Redirect(w, r, homeSetPath, http.StatusPermanentRedirect) // keep http method
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
case resourceTypeAddressBook:
|
case resourceTypeAddressBook:
|
||||||
ab, err := b.Backend.GetAddressBook(r.Context(), r.URL.Path)
|
ab, err := b.Backend.GetAddressBook(r.Context(), r.URL.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
resp, err := b.propFindAddressBook(r.Context(), propfind, ab)
|
resp, err := b.propFindAddressBook(r.Context(), propfind, ab)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
resps = append(resps, *resp)
|
resps = append(resps, *resp)
|
||||||
if depth != internal.DepthZero {
|
if depth != internal.DepthZero {
|
||||||
resps_, err := b.propFindAllAddressObjects(r.Context(), propfind, ab)
|
resps_, err := b.propFindAllAddressObjects(r.Context(), propfind, ab)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
resps = append(resps, resps_...)
|
resps = append(resps, resps_...)
|
||||||
}
|
}
|
||||||
case resourceTypeAddressObject:
|
case resourceTypeAddressObject:
|
||||||
ao, err := b.Backend.GetAddressObject(r.Context(), r.URL.Path, &dataReq)
|
ao, err := b.Backend.GetAddressObject(r.Context(), r.URL.Path, &dataReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := b.propFindAddressObject(r.Context(), propfind, ao)
|
resp, err := b.propFindAddressObject(r.Context(), propfind, ao)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
resps = append(resps, *resp)
|
resps = append(resps, *resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
return internal.NewMultiStatus(resps...), nil
|
return internal.ServeMultiStatus(w, internal.NewMultiStatus(resps...))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *backend) propFindRoot(ctx context.Context, propfind *internal.PropFind) (*internal.Response, error) {
|
func (b *backend) propFindRoot(ctx context.Context, propfind *internal.PropFind) (*internal.Response, error) {
|
||||||
|
@@ -64,7 +64,7 @@ func ServeMultiStatus(w http.ResponseWriter, ms *MultiStatus) error {
|
|||||||
type Backend interface {
|
type Backend interface {
|
||||||
Options(r *http.Request) (caps []string, allow []string, err error)
|
Options(r *http.Request) (caps []string, allow []string, err error)
|
||||||
HeadGet(w http.ResponseWriter, r *http.Request) error
|
HeadGet(w http.ResponseWriter, r *http.Request) error
|
||||||
PropFind(r *http.Request, pf *PropFind, depth Depth) (*MultiStatus, error)
|
PropFind(w http.ResponseWriter, r *http.Request, pf *PropFind, depth Depth) error
|
||||||
PropPatch(r *http.Request, pu *PropertyUpdate) (*Response, error)
|
PropPatch(r *http.Request, pu *PropertyUpdate) (*Response, error)
|
||||||
Put(w http.ResponseWriter, r *http.Request) error
|
Put(w http.ResponseWriter, r *http.Request) error
|
||||||
Delete(r *http.Request) error
|
Delete(r *http.Request) error
|
||||||
@@ -152,12 +152,7 @@ func (h *Handler) handlePropfind(w http.ResponseWriter, r *http.Request) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ms, err := h.Backend.PropFind(r, &propfind, depth)
|
return h.Backend.PropFind(w, r, &propfind, depth)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return ServeMultiStatus(w, ms)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type PropFindFunc func(raw *RawXMLValue) (interface{}, error)
|
type PropFindFunc func(raw *RawXMLValue) (interface{}, error)
|
||||||
|
12
server.go
12
server.go
@@ -115,39 +115,39 @@ func (b *backend) HeadGet(w http.ResponseWriter, r *http.Request) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *backend) PropFind(r *http.Request, propfind *internal.PropFind, depth internal.Depth) (*internal.MultiStatus, error) {
|
func (b *backend) PropFind(w http.ResponseWriter, r *http.Request, propfind *internal.PropFind, depth internal.Depth) error {
|
||||||
// TODO: use partial error Response on error
|
// TODO: use partial error Response on error
|
||||||
|
|
||||||
fi, err := b.FileSystem.Stat(r.Context(), r.URL.Path)
|
fi, err := b.FileSystem.Stat(r.Context(), r.URL.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var resps []internal.Response
|
var resps []internal.Response
|
||||||
if depth != internal.DepthZero && fi.IsDir {
|
if depth != internal.DepthZero && fi.IsDir {
|
||||||
children, err := b.FileSystem.ReadDir(r.Context(), r.URL.Path, depth == internal.DepthInfinity)
|
children, err := b.FileSystem.ReadDir(r.Context(), r.URL.Path, depth == internal.DepthInfinity)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
resps = make([]internal.Response, len(children))
|
resps = make([]internal.Response, len(children))
|
||||||
for i, child := range children {
|
for i, child := range children {
|
||||||
resp, err := b.propFindFile(propfind, &child)
|
resp, err := b.propFindFile(propfind, &child)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
resps[i] = *resp
|
resps[i] = *resp
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
resp, err := b.propFindFile(propfind, fi)
|
resp, err := b.propFindFile(propfind, fi)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
resps = []internal.Response{*resp}
|
resps = []internal.Response{*resp}
|
||||||
}
|
}
|
||||||
|
|
||||||
return internal.NewMultiStatus(resps...), nil
|
return internal.ServeMultiStatus(w, internal.NewMultiStatus(resps...))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *backend) propFindFile(propfind *internal.PropFind, fi *FileInfo) (*internal.Response, error) {
|
func (b *backend) propFindFile(propfind *internal.PropFind, fi *FileInfo) (*internal.Response, error) {
|
||||||
|
Reference in New Issue
Block a user