CheckDNS needs that to Remove Domain without a second storage path. Co-authored-by: Cursor <cursoragent@cursor.com>
412 lines
10 KiB
Go
412 lines
10 KiB
Go
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(
|
|
`<?xml version="1.0"?><d:propfind xmlns:d="DAV:"><d:prop><d:resourcetype/></d:prop></d:propfind>`,
|
|
))
|
|
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(
|
|
`<?xml version="1.0"?><d:propfind xmlns:d="DAV:"><d:prop><d:resourcetype/></d:prop></d:propfind>`,
|
|
))
|
|
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)
|
|
}
|
|
|
|
// Remove implements Folder via WebDAV DELETE.
|
|
func (f webdavFolder) Remove(name string) error {
|
|
u, err := f.childURL(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req, err := http.NewRequest(http.MethodDelete, u, nil)
|
|
if err != nil {
|
|
return 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 err
|
|
}
|
|
defer res.Body.Close()
|
|
if _, err := io.Copy(io.Discard, res.Body); err != nil {
|
|
return err
|
|
}
|
|
switch res.StatusCode {
|
|
case http.StatusOK, http.StatusNoContent, http.StatusAccepted:
|
|
return nil
|
|
case http.StatusNotFound:
|
|
return fmt.Errorf("%w: %s", ErrNotExist, name)
|
|
case http.StatusForbidden, http.StatusUnauthorized:
|
|
return fmt.Errorf("%w", ErrForbidden)
|
|
default:
|
|
return fmt.Errorf("webdav DELETE %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
|
|
}
|
|
}
|