Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions drivers/webdav/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

"github.com/OpenListTeam/OpenList/v4/drivers/base"
"github.com/OpenListTeam/OpenList/v4/internal/driver"
"github.com/OpenListTeam/OpenList/v4/internal/errs"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/pkg/cron"
"github.com/OpenListTeam/OpenList/v4/pkg/gowebdav"
Expand Down Expand Up @@ -125,4 +126,22 @@ func (d *WebDav) Put(ctx context.Context, dstDir model.Obj, s model.FileStreamer
return err
}

func (d *WebDav) GetDetails(ctx context.Context) (*model.StorageDetails, error) {
quota, err := d.client.Quota("/")
if err != nil {
return nil, err
}
if quota == nil {
return nil, errs.NotImplement
}
total := quota.Available + quota.Used
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential arithmetic overflow when calculating total space. If quota.Available and quota.Used are both close to the maximum value of int64, their sum could overflow. According to RFC 4331, quota-available-bytes represents the available bytes remaining, not necessarily part of a fixed total. Consider checking if the server provides an explicit total space value, and handle cases where Available + Used might not accurately represent total capacity.

Copilot uses AI. Check for mistakes.
used := quota.Used
return &model.StorageDetails{
DiskUsage: model.DiskUsage{
TotalSpace: total,
UsedSpace: used,
},
}, nil
}

var _ driver.Driver = (*WebDav)(nil)
36 changes: 36 additions & 0 deletions pkg/gowebdav/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@ type props struct {
ContentType string `xml:"DAV: prop>getcontenttype,omitempty"`
ETag string `xml:"DAV: prop>getetag,omitempty"`
Modified string `xml:"DAV: prop>getlastmodified,omitempty"`

QuotaAvailableBytes int64 `xml:"DAV: prop>quota-available-bytes,omitempty"`
QuotaUsedBytes int64 `xml:"DAV: prop>quota-used-bytes,omitempty"`
}

type response struct {
Expand Down Expand Up @@ -252,6 +255,39 @@ func (c *Client) Stat(path string) (os.FileInfo, error) {
return f, err
}

// Quota returns the quota information about a specified path
func (c *Client) Quota(path string) (*Quota, error) {
var quota *Quota
parse := func(resp interface{}) error {
r := resp.(*response)
if p := getProps(r, "200"); p != nil && quota == nil {
quota = new(Quota)
quota.Available = p.QuotaAvailableBytes
quota.Used = p.QuotaUsedBytes
}

r.Props = nil
return nil
}
Comment on lines +259 to +271
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Quota method doesn't validate that both properties are present in the response before returning. If the server only returns one of the two quota properties (quota-available-bytes or quota-used-bytes), the zero value (0) will be used for the missing field, which could be misleading. Consider checking if both properties were actually returned by the server and return nil if essential quota information is missing or incomplete.

Copilot uses AI. Check for mistakes.

err := c.propfind(path, true,
`<d:propfind xmlns:d='DAV:'>
<d:prop>
<d:quota-available-bytes/>
<d:quota-used-bytes/>
</d:prop>
</d:propfind>`,
&response{},
parse)

if err != nil {
if _, ok := err.(*os.PathError); !ok {
err = newPathErrorErr("Quota", path, err)
}
}
return quota, err
}

// Remove removes a remote file
func (c *Client) Remove(path string) error {
return c.RemoveAll(path)
Expand Down
6 changes: 6 additions & 0 deletions pkg/gowebdav/quota.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package gowebdav

type Quota struct {
Available int64
Used int64
}
26 changes: 24 additions & 2 deletions server/webdav/prop.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,21 @@ func props(ctx context.Context, ls LockSystem, fi model.Obj, pnames []xml.Name)
XMLName: pn,
InnerXML: []byte(innerXML),
})
} else if prop := additionalLiveProps[pn]; prop.findFn != nil {
innerXML, show, err := prop.findFn(ctx, ls, fi.GetName(), fi)
if err != nil {
return nil, err
}
if !show {
pstatNotFound.Props = append(pstatNotFound.Props, Property{
XMLName: pn,
})
} else {
pstatOK.Props = append(pstatOK.Props, Property{
XMLName: pn,
InnerXML: []byte(innerXML),
})
}
} else {
pstatNotFound.Props = append(pstatNotFound.Props, Property{
XMLName: pn,
Expand All @@ -222,7 +237,7 @@ func props(ctx context.Context, ls LockSystem, fi model.Obj, pnames []xml.Name)
}

// Propnames returns the property names defined for resource name.
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The propnames function signature was changed to include a fromAllprop parameter, but the function documentation comment was not updated to explain this new parameter's purpose. Consider adding documentation to explain that the fromAllprop parameter controls whether additionalLiveProps should be included in the returned property names list, following RFC 4331 section 3 which specifies that quota properties should not be returned for allprop PROPFIND requests.

Suggested change
// Propnames returns the property names defined for resource name.
// Propnames returns the property names defined for the given resource.
// The fromAllprop parameter indicates whether this call is serving an
// "allprop" PROPFIND request. When fromAllprop is true, additionalLiveProps
// (such as quota-related properties) are omitted from the result, in
// accordance with RFC 4331 section 3, which specifies that quota properties
// are not returned for allprop requests.

Copilot uses AI. Check for mistakes.
func propnames(ctx context.Context, ls LockSystem, fi model.Obj) ([]xml.Name, error) {
func propnames(ctx context.Context, ls LockSystem, fi model.Obj, fromAllprop bool) ([]xml.Name, error) {
//f, err := fs.OpenFile(ctx, name, os.O_RDONLY, 0)
//if err != nil {
// return nil, err
Expand All @@ -249,6 +264,13 @@ func propnames(ctx context.Context, ls LockSystem, fi model.Obj) ([]xml.Name, er
pnames = append(pnames, pn)
}
}
if !fromAllprop {
for pn, prop := range additionalLiveProps {
if prop.findFn != nil && prop.showFn != nil && prop.showFn(fi) {
pnames = append(pnames, pn)
}
}
}
for pn := range deadProps {
pnames = append(pnames, pn)
}
Expand All @@ -264,7 +286,7 @@ func propnames(ctx context.Context, ls LockSystem, fi model.Obj) ([]xml.Name, er
//
// See http://www.webdav.org/specs/rfc4918.html#METHOD_PROPFIND
func allprop(ctx context.Context, ls LockSystem, fi model.Obj, include []xml.Name) ([]Propstat, error) {
pnames, err := propnames(ctx, ls, fi)
pnames, err := propnames(ctx, ls, fi, true)
if err != nil {
return nil, err
}
Expand Down
84 changes: 84 additions & 0 deletions server/webdav/prop_additional.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package webdav

import (
"context"
"encoding/xml"
"strconv"

"github.com/OpenListTeam/OpenList/v4/internal/conf"
"github.com/OpenListTeam/OpenList/v4/internal/fs"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/internal/op"
)

// additionalLiveProps are live properties that should not be returned for "allprop" PROPFIND requests.
var additionalLiveProps = map[xml.Name]struct {
// findFn implements the propfind function of this property. If nil,
// it indicates a hidden property.
findFn func(context.Context, LockSystem, string, model.Obj) (string, bool, error)
// showFn indicates whether the prop show be returned for the resource.
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spelling error in the comment. Change "show be" to "should be" for grammatical correctness.

Suggested change
// showFn indicates whether the prop show be returned for the resource.
// it indicates a hidden property.
// showFn indicates whether the prop should be returned for the resource.

Copilot uses AI. Check for mistakes.
showFn func(model.Obj) bool
}{
{Space: "DAV:", Local: "quota-available-bytes"}: {
findFn: findQuotaAvailableBytes,
showFn: showQuotaAvailableBytes,
},
{Space: "DAV:", Local: "quota-used-bytes"}: {
findFn: findQuotaUsedBytes,
showFn: showQuotaUsedBytes,
},
}

func getStorageUsage(ctx context.Context, fi model.Obj) (*model.StorageDetails, error) {
user := ctx.Value(conf.UserKey).(*model.User)
reqPath, err := user.JoinPath(fi.GetPath())
if err != nil {
return nil, err
}
storage, err := fs.GetStorage(reqPath, &fs.GetStoragesArgs{})
if err != nil {
return nil, err
}
details, err := op.GetStorageDetails(ctx, storage)
if err != nil {
return nil, err
}
return details, nil
}
Comment on lines +32 to +47
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error handling should be improved. When GetStorageDetails returns an error (especially errs.NotImplement or errs.StorageNotInit), the function propagates it as-is. According to RFC 4331, if quota information is unavailable, the properties should be omitted from the response rather than causing the entire PROPFIND to fail. Consider checking for NotImplement and StorageNotInit errors and returning (nil, nil) in those cases to gracefully handle storages that don't support quota reporting.

Copilot uses AI. Check for mistakes.

func showQuotaAvailableBytes(fi model.Obj) bool {
return fi.IsDir()
}

func findQuotaAvailableBytes(ctx context.Context, ls LockSystem, name string, fi model.Obj) (string, bool, error) {
if !fi.IsDir() {
return "", false, nil
}
usage, err := getStorageUsage(ctx, fi)
if err != nil {
return "", false, err
}
if usage == nil {
return "", false, nil
}
available := usage.TotalSpace - usage.UsedSpace
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Available space calculation can produce negative values if UsedSpace exceeds TotalSpace. While this may represent a valid over-quota situation, RFC 4331 states that quota-available-bytes should be the number of bytes available, which cannot be negative. Consider checking if the result is negative and returning 0 or an appropriate value in such cases to conform with the RFC specification.

Suggested change
available := usage.TotalSpace - usage.UsedSpace
available := usage.TotalSpace - usage.UsedSpace
if available < 0 {
available = 0
}

Copilot uses AI. Check for mistakes.
return strconv.FormatInt(available, 10), true, nil
}

func showQuotaUsedBytes(fi model.Obj) bool {
return fi.IsDir()
}

func findQuotaUsedBytes(ctx context.Context, ls LockSystem, name string, fi model.Obj) (string, bool, error) {
if !fi.IsDir() {
return "", false, nil
}
usage, err := getStorageUsage(ctx, fi)
if err != nil {
return "", false, err
}
if usage == nil {
return "", false, nil
}
return strconv.FormatInt(usage.UsedSpace, 10), true, nil
}
2 changes: 1 addition & 1 deletion server/webdav/webdav.go
Original file line number Diff line number Diff line change
Expand Up @@ -684,7 +684,7 @@ func (h *Handler) handlePropfind(w http.ResponseWriter, r *http.Request) (status
}
var pstats []Propstat
if pf.Propname != nil {
pnames, err := propnames(ctx, h.LockSystem, info)
pnames, err := propnames(ctx, h.LockSystem, info, false)
if err != nil {
return err
}
Expand Down
Loading