Files
2026-09-13 14:19:23 +01:00

269 lines
6.5 KiB
Go

package main
import (
"encoding/xml"
"fmt"
"io"
)
// Namespace prefixes used in the DAV XML
const (
NSDAV = "DAV:"
NSSabre = "http://sabredav.org/ns"
NSOwn = "http://owncloud.org/ns"
NSNextcl = "http://nextcloud.org/ns"
)
// Multistatus represents the root <d:multistatus> element
type Multistatus struct {
Responses []Response `xml:"DAV:response"`
}
// Response represents a <d:response> element containing path and status info
type Response struct {
Href string `xml:"DAV:href"`
Propstats []Propstat `xml:"DAV:propstat"`
}
// Propstat contains a set of properties and their HTTP status
type Propstat struct {
Prop Prop `xml:"DAV:prop"`
Status string `xml:"DAV:status"`
}
// Prop holds the various DAV properties (getlastmodified, getetag, etc.)
type Prop struct {
GetLastModified string `xml:"DAV:getlastmodified"`
GetContentLength *int64 `xml:"DAV:getcontentlength"`
GetContentType string `xml:"DAV:getcontenttype"`
GetETag string `xml:"DAV:getetag"`
QuotaUsedBytes *int64 `xml:"DAV:quota-used-bytes"`
QuotaAvailableBytes *int64 `xml:"DAV:quota-available-bytes"`
// ResourceType is special - it's a single element that may contain
// a <d:collection/> child (indicating a directory) or be empty (file)
ResourceType ResourceType `xml:"DAV:resourcetype"`
}
// ResourceType determines if a resource is a collection (directory) or a file
type ResourceType struct {
Collection bool `xml:"DAV:collection"`
}
// UnmarshalXML implements custom XML unmarshaling for Prop
// to handle properties that may not always be present
func (p *Prop) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
for {
tok, err := d.Token()
if err != nil {
if err == io.EOF {
return nil
}
return err
}
switch elem := tok.(type) {
case xml.StartElement:
switch elem.Name.Local {
case "getlastmodified":
var content string
if err := d.DecodeElement(&content, &elem); err != nil {
return err
}
p.GetLastModified = content
case "getcontentlength":
var content string
if err := d.DecodeElement(&content, &elem); err != nil {
return err
}
if val, err := parseInt64(content); err == nil {
p.GetContentLength = &val
}
case "getcontenttype":
var content string
if err := d.DecodeElement(&content, &elem); err != nil {
return err
}
p.GetContentType = content
case "getetag":
var content string
if err := d.DecodeElement(&content, &elem); err != nil {
return err
}
p.GetETag = content
case "quota-used-bytes":
var content string
if err := d.DecodeElement(&content, &elem); err != nil {
return err
}
if val, err := parseInt64(content); err == nil {
p.QuotaUsedBytes = &val
}
case "quota-available-bytes":
var content string
if err := d.DecodeElement(&content, &elem); err != nil {
return err
}
if val, err := parseInt64(content); err == nil {
p.QuotaAvailableBytes = &val
}
case "resourcetype":
// For resourcetype, we need to check if it contains <collection/>
// rather than just counting children
for {
innerTok, err := d.Token()
if err != nil {
return err
}
if innerTok, ok := innerTok.(xml.EndElement); ok && innerTok.Name.Local == "resourcetype" {
break
}
if innerTok, ok := innerTok.(xml.StartElement); ok && innerTok.Name.Local == "collection" {
p.ResourceType.Collection = true
// Skip past the collection element
for {
innerTok, err := d.Token()
if err != nil {
return err
}
if end, ok := innerTok.(xml.EndElement); ok && end.Name.Local == "collection" {
break
}
}
}
}
}
case xml.EndElement:
if elem.Name.Local == start.Name.Local {
return nil
}
}
}
}
// UnmarshalXML handles multistatus response parsing
func (m *Multistatus) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
m.Responses = []Response{}
for {
tok, err := d.Token()
if err != nil {
if err == io.EOF {
return nil
}
return err
}
if start, ok := tok.(xml.StartElement); ok && start.Name.Local == "response" {
var resp Response
if err := d.DecodeElement(&resp, &start); err != nil {
return err
}
m.Responses = append(m.Responses, resp)
}
if end, ok := tok.(xml.EndElement); ok && end.Name.Local == "multistatus" {
return nil
}
}
}
// UnmarshalXML handles response parsing
func (r *Response) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
for {
tok, err := d.Token()
if err != nil {
if err == io.EOF {
return nil
}
return err
}
switch elem := tok.(type) {
case xml.StartElement:
switch elem.Name.Local {
case "href":
if err := d.DecodeElement(&r.Href, &elem); err != nil {
return err
}
case "propstat":
var ps Propstat
if err := d.DecodeElement(&ps, &elem); err != nil {
return err
}
r.Propstats = append(r.Propstats, ps)
}
case xml.EndElement:
if elem.Name.Local == start.Name.Local {
return nil
}
}
}
}
// UnmarshalXML handles propstat parsing
func (p *Propstat) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
for {
tok, err := d.Token()
if err != nil {
if err == io.EOF {
return nil
}
return err
}
switch elem := tok.(type) {
case xml.StartElement:
switch elem.Name.Local {
case "prop":
if err := d.DecodeElement(&p.Prop, &elem); err != nil {
return err
}
case "status":
var content string
if err := d.DecodeElement(&content, &elem); err != nil {
return err
}
p.Status = content
}
case xml.EndElement:
if elem.Name.Local == start.Name.Local {
return nil
}
}
}
}
// RemoveQuotes removes surrounding double quotes from a string (for etags like `"abc123"`)
func RemoveQuotes(s string) string {
if len(s) >= 2 && s[0] == '"' && s[len(s)-1] == '"' {
return s[1 : len(s)-1]
}
return s
}
// parseInt64 converts a string to int64, returning 0 and error on failure
func parseInt64(s string) (int64, error) {
var val int64
_, err := fmt.Sscanf(s, "%d", &val)
return val, err
}
// IsCollection returns true if the resource is a directory
func (r *Response) IsCollection() bool {
for _, ps := range r.Propstats {
if ps.Prop.ResourceType.Collection {
return true
}
}
return false
}
func (r *Response) GetLastModified() string {
for _, ps := range r.Propstats {
if ps.Prop.GetLastModified != "" {
return ps.Prop.GetLastModified
}
}
return ""
}