Files
go-nc-files/webdav.go
T

377 lines
9.1 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)
}
// 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
}
}