package plg_backend_webdav import ( "encoding/xml" . "github.com/mickael-kerjean/filestash/server/common" "io" "net/http" "net/url" "os" "path/filepath" "regexp" "strings" "time" ) type WebDav struct { params *WebDavParams } type WebDavParams struct { url string username string password string path string } func init() { Backend.Register("webdav", WebDav{}) } func (w WebDav) Init(params map[string]string, app *App) (IBackend, error) { params["url"] = regexp.MustCompile(`\/$`).ReplaceAllString(params["url"], "") if strings.HasPrefix(params["url"], "http://") == false && strings.HasPrefix(params["url"], "https://") == false { return nil, NewError("Malformed URL - missing http or https", 400) } backend := WebDav{ params: &WebDavParams{ strings.ReplaceAll(params["url"], "%{username}", url.PathEscape(params["username"])), params["username"], params["password"], params["path"], }, } return backend, nil } func (w WebDav) LoginForm() Form { return Form{ Elmnts: []FormElement{ FormElement{ Name: "type", Type: "hidden", Value: "webdav", }, FormElement{ Name: "url", Type: "text", Placeholder: "Address", }, FormElement{ Name: "username", Type: "text", Placeholder: "Username", }, FormElement{ Name: "password", Type: "password", Placeholder: "Password", }, FormElement{ Name: "advanced", Type: "enable", Placeholder: "Advanced", Target: []string{"webdav_path"}, }, FormElement{ Id: "webdav_path", Name: "path", Type: "text", Placeholder: "Path", }, }, } } func (w WebDav) Ls(path string) ([]os.FileInfo, error) { files := make([]os.FileInfo, 0) query := ` ` res, err := w.request("PROPFIND", w.params.url+encodeURL(path), strings.NewReader(query), func(req *http.Request) { req.Header.Add("Depth", "1") }) if err != nil { return nil, err } defer res.Body.Close() if res.StatusCode >= 400 { return nil, NewError(HTTPFriendlyStatus(res.StatusCode)+": can't get things in "+filepath.Base(path), res.StatusCode) } var r WebDavResp decoder := xml.NewDecoder(res.Body) decoder.Decode(&r) if len(r.Responses) == 0 { return nil, NewError("Server not found", 404) } LongURLDav := w.params.url + path ShortURLDav := regexp.MustCompile(`^http[s]?://[^/]*`).ReplaceAllString(LongURLDav, "") for _, tag := range r.Responses { decodedHref := decodeURL(tag.Href) if decodedHref == ShortURLDav || decodedHref == LongURLDav { continue } for i, prop := range tag.Props { if i > 0 { break } files = append(files, File{ FName: filepath.Base(decodedHref), FType: func(p string) string { if p == "collection" { return "directory" } return "file" }(prop.Type.Local), FTime: func() int64 { t, err := time.Parse(time.RFC1123, prop.Modified) if err != nil { return 0 } return t.Unix() }(), FSize: int64(prop.Size), }) } } return files, nil } func (w WebDav) Cat(path string) (io.ReadCloser, error) { res, err := w.request("GET", w.params.url+encodeURL(path), nil, nil) if err != nil { return nil, err } if res.StatusCode >= 400 { return nil, NewError(HTTPFriendlyStatus(res.StatusCode)+": can't fetch "+filepath.Base(path), res.StatusCode) } return res.Body, nil } func (w WebDav) Mkdir(path string) error { res, err := w.request("MKCOL", w.params.url+encodeURL(path), nil, func(req *http.Request) { req.Header.Add("Overwrite", "F") }) if err != nil { return err } res.Body.Close() if res.StatusCode >= 400 { return NewError(HTTPFriendlyStatus(res.StatusCode)+": can't create "+filepath.Base(path), res.StatusCode) } return nil } func (w WebDav) Rm(path string) error { res, err := w.request("DELETE", w.params.url+encodeURL(path), nil, nil) if err != nil { return err } res.Body.Close() if res.StatusCode >= 400 { return NewError(HTTPFriendlyStatus(res.StatusCode)+": can't remove "+filepath.Base(path), res.StatusCode) } return nil } func (w WebDav) Mv(from string, to string) error { res, err := w.request("MOVE", w.params.url+encodeURL(from), nil, func(req *http.Request) { req.Header.Add("Destination", w.params.url+encodeURL(to)) req.Header.Add("Overwrite", "T") }) if err != nil { return err } res.Body.Close() if res.StatusCode >= 400 { return NewError(HTTPFriendlyStatus(res.StatusCode)+": can't do that", res.StatusCode) } return nil } func (w WebDav) Touch(path string) error { return w.Save(path, strings.NewReader("")) } func (w WebDav) Save(path string, file io.Reader) error { res, err := w.request("PUT", w.params.url+encodeURL(path), file, nil) if err != nil { return err } res.Body.Close() if res.StatusCode >= 400 { return NewError(HTTPFriendlyStatus(res.StatusCode)+": can't do that", res.StatusCode) } return nil } func (w WebDav) request(method string, url string, body io.Reader, fn func(req *http.Request)) (*http.Response, error) { req, err := http.NewRequest(method, url, body) if err != nil { return nil, err } if w.params.username != "" { req.SetBasicAuth(w.params.username, w.params.password) } req.Header.Add("Content-Type", "text/xml;charset=UTF-8") req.Header.Add("Accept-Charset", "utf-8") switch method { case "GET": req.Header.Add("Accept", "*/*") default: req.Header.Add("Accept", "application/xml,text/xml") } if req.Body != nil { defer req.Body.Close() } if fn != nil { fn(req) } return HTTPClient.Do(req) } type WebDavResp struct { Responses []struct { Href string `xml:"href"` Props []struct { Name string `xml:"prop>displayname,omitempty"` Type xml.Name `xml:"prop>resourcetype>collection,omitempty"` Size int64 `xml:"prop>getcontentlength,omitempty"` Modified string `xml:"prop>getlastmodified,omitempty"` } `xml:"propstat"` } `xml:"response"` } func encodeURL(path string) string { p := url.PathEscape(path) return strings.Replace(p, "%2F", "/", -1) } func decodeURL(path string) string { str, err := url.PathUnescape(path) if err != nil { return path } return str }