package goncfiles import ( "bytes" "encoding/xml" "fmt" "io" "net/http" "net/url" "path" "strings" gonexapp "gitea.neitzel.de/konrad/go-nc-exapp" ) // WebDAVRoot is a User Files Root at /remote.php/dav/files/{user}/. type WebDAVRoot struct { Cred gonexapp.Credentials Client *http.Client } func (r WebDAVRoot) client() *http.Client { if r.Client != nil { return r.Client } return http.DefaultClient } func (r WebDAVRoot) filesBase() string { return strings.TrimRight(r.Cred.BaseURL, "/") + "/remote.php/dav/files/" + url.PathEscape(r.Cred.UserID) + "/" } func (r WebDAVRoot) urlFor(rel string) (string, error) { clean := CleanRel(rel) if clean == "" && rel != "" && rel != "." { return "", fmt.Errorf("invalid path") } base := r.filesBase() if clean == "" { return base, nil } parts := strings.Split(clean, "/") escaped := make([]string, len(parts)) for i, p := range parts { escaped[i] = url.PathEscape(p) } return base + strings.Join(escaped, "/"), nil } // OpenFolder opens an existing folder via WebDAV PROPFIND. func (r WebDAVRoot) OpenFolder(rel string) (Folder, error) { clean := CleanRel(rel) if clean == "" { return nil, fmt.Errorf("invalid Working Folder path") } u, err := r.urlFor(clean) if err != nil { return nil, err } if err := r.propfindExists(u); err != nil { return nil, err } return webdavFolder{root: r, rel: clean}, nil } // EnsureFolder creates the folder (and parents) with MKCOL, then opens it. func (r WebDAVRoot) EnsureFolder(rel string) (Folder, error) { clean := CleanRel(rel) if clean == "" { return nil, fmt.Errorf("invalid Working Folder path") } parts := strings.Split(clean, "/") var built []string for _, p := range parts { built = append(built, p) cur := strings.Join(built, "/") u, err := r.urlFor(cur) if err != nil { return nil, err } err = r.propfindExists(u) if err == nil { continue } if !isNotExist(err) { return nil, err } if err := r.mkcol(u); err != nil { return nil, err } } return webdavFolder{root: r, rel: clean}, nil } func isNotExist(err error) bool { return err != nil && strings.Contains(err.Error(), ErrNotExist.Error()) } func (r WebDAVRoot) propfindExists(u string) error { req, err := http.NewRequest("PROPFIND", u, strings.NewReader( ``, )) if err != nil { return err } req.Header.Set("Depth", "0") req.Header.Set("Content-Type", "application/xml") for k, vs := range r.Cred.AuthHeaders() { for _, v := range vs { req.Header.Set(k, v) } } res, err := r.client().Do(req) if err != nil { return err } defer res.Body.Close() body, err := io.ReadAll(res.Body) if err != nil { return err } switch res.StatusCode { case http.StatusMultiStatus, http.StatusOK: if !propfindIsCollection(body) { return fmt.Errorf("%w", ErrNotDir) } return nil case http.StatusNotFound: return fmt.Errorf("%w", ErrNotExist) case http.StatusForbidden, http.StatusUnauthorized: return fmt.Errorf("%w", ErrForbidden) default: return fmt.Errorf("webdav PROPFIND %s: %s", u, res.Status) } } func propfindIsCollection(body []byte) bool { return bytes.Contains(body, []byte("collection")) } func (r WebDAVRoot) mkcol(u string) error { req, err := http.NewRequest("MKCOL", u, nil) if err != nil { return err } for k, vs := range r.Cred.AuthHeaders() { for _, v := range vs { req.Header.Set(k, v) } } res, err := r.client().Do(req) if err != nil { return err } defer res.Body.Close() _, _ = io.Copy(io.Discard, res.Body) if res.StatusCode == http.StatusCreated || res.StatusCode == http.StatusMethodNotAllowed { return nil } if res.StatusCode == http.StatusForbidden || res.StatusCode == http.StatusUnauthorized { return fmt.Errorf("%w", ErrForbidden) } return fmt.Errorf("webdav MKCOL %s: %s", u, res.Status) } type webdavFolder struct { root WebDAVRoot rel string } func (f webdavFolder) childURL(name string) (string, error) { if err := RequireBasename(name); err != nil { return "", err } return f.root.urlFor(path.Join(f.rel, name)) } // List implements Folder via WebDAV PROPFIND Depth 1. func (f webdavFolder) List() ([]Entry, error) { u, err := f.root.urlFor(f.rel) if err != nil { return nil, err } req, err := http.NewRequest("PROPFIND", u, strings.NewReader( ``, )) if err != nil { return nil, err } req.Header.Set("Depth", "1") req.Header.Set("Content-Type", "application/xml") for k, vs := range f.root.Cred.AuthHeaders() { for _, v := range vs { req.Header.Set(k, v) } } res, err := f.root.client().Do(req) if err != nil { return nil, err } defer res.Body.Close() body, err := io.ReadAll(res.Body) if err != nil { return nil, err } if res.StatusCode == http.StatusForbidden || res.StatusCode == http.StatusUnauthorized { return nil, fmt.Errorf("%w", ErrForbidden) } if res.StatusCode != http.StatusMultiStatus && res.StatusCode != http.StatusOK { return nil, fmt.Errorf("webdav PROPFIND list: %s", res.Status) } return parsePropfind(body, f.rel) } type multiStatus struct { Responses []propResponse `xml:"response"` } type propResponse struct { Href string `xml:"href"` Propstat propStat `xml:"propstat"` } type propStat struct { Prop prop `xml:"prop"` } type prop struct { ResourceType resourceType `xml:"resourcetype"` } type resourceType struct { Collection *struct{} `xml:"collection"` } func parsePropfind(body []byte, folderRel string) ([]Entry, error) { var ms multiStatus dec := xml.NewDecoder(bytes.NewReader(body)) dec.CharsetReader = func(charset string, input io.Reader) (io.Reader, error) { return input, nil } if err := dec.Decode(&ms); err != nil { return nil, fmt.Errorf("webdav propfind xml: %w", err) } folderBase := path.Base(folderRel) var out []Entry for _, resp := range ms.Responses { href, err := url.PathUnescape(resp.Href) if err != nil { href = resp.Href } href = strings.TrimSuffix(href, "/") name := path.Base(href) if name == "" || name == folderBase || strings.HasSuffix(strings.TrimSuffix(href, "/"), "/"+folderRel) && name == path.Base(folderRel) { if selfHref(href, folderRel) { continue } } if selfHref(href, folderRel) { continue } isDir := resp.Propstat.Prop.ResourceType.Collection != nil out = append(out, Entry{Name: name, IsDir: isDir}) } return out, nil } func selfHref(href, folderRel string) bool { h := strings.TrimSuffix(href, "/") return strings.HasSuffix(h, "/"+folderRel) || strings.HasSuffix(h, folderRel) } // Read implements Folder via WebDAV GET. func (f webdavFolder) Read(name string) ([]byte, error) { u, err := f.childURL(name) if err != nil { return nil, err } req, err := http.NewRequest(http.MethodGet, u, nil) if err != nil { return nil, err } for k, vs := range f.root.Cred.AuthHeaders() { for _, v := range vs { req.Header.Set(k, v) } } res, err := f.root.client().Do(req) if err != nil { return nil, err } defer res.Body.Close() body, err := io.ReadAll(res.Body) if err != nil { return nil, err } switch res.StatusCode { case http.StatusOK: return body, nil case http.StatusNotFound: return nil, fmt.Errorf("%w: %s", ErrNotExist, name) case http.StatusForbidden, http.StatusUnauthorized: return nil, fmt.Errorf("%w", ErrForbidden) default: return nil, fmt.Errorf("webdav GET %s: %s", name, res.Status) } } // Write implements Folder via WebDAV PUT. func (f webdavFolder) Write(name string, data []byte) error { u, err := f.childURL(name) if err != nil { return err } req, err := http.NewRequest(http.MethodPut, u, bytes.NewReader(data)) if err != nil { return err } req.Header.Set("Content-Type", "application/octet-stream") for k, vs := range f.root.Cred.AuthHeaders() { for _, v := range vs { req.Header.Set(k, v) } } res, err := f.root.client().Do(req) if err != nil { return err } defer res.Body.Close() _, _ = io.Copy(io.Discard, res.Body) if res.StatusCode == http.StatusCreated || res.StatusCode == http.StatusOK || res.StatusCode == http.StatusNoContent { return nil } if res.StatusCode == http.StatusForbidden || res.StatusCode == http.StatusUnauthorized { return fmt.Errorf("%w", ErrForbidden) } return fmt.Errorf("webdav PUT %s: %s", name, res.Status) } // Exists implements Folder via WebDAV HEAD (falls back to GET when needed). func (f webdavFolder) Exists(name string) (bool, error) { u, err := f.childURL(name) if err != nil { return false, err } req, err := http.NewRequest(http.MethodHead, u, nil) if err != nil { return false, err } for k, vs := range f.root.Cred.AuthHeaders() { for _, v := range vs { req.Header.Set(k, v) } } res, err := f.root.client().Do(req) if err != nil { return false, err } defer res.Body.Close() _, _ = io.Copy(io.Discard, res.Body) switch res.StatusCode { case http.StatusOK: return true, nil case http.StatusNotFound: return false, nil case http.StatusForbidden, http.StatusUnauthorized: return false, fmt.Errorf("%w", ErrForbidden) default: _, err := f.Read(name) if err == nil { return true, nil } if isNotExist(err) { return false, nil } return false, err } }