diff --git a/README.md b/README.md
index d76ccfd..ae16d81 100644
--- a/README.md
+++ b/README.md
@@ -7,9 +7,10 @@
* 使用 `go` 开发的文件管理工具
* web 框架使用 `go` 框架 `echo`
-* 默认打包配置文件、静态文件和模板文件。可更改 `app/boot/boot.go` 文件内 `global.IsOnlyEmbed` 为 `false` 自定义配置文件和模板文件
* 模板库使用 `pongo2` 库,语法接近 `python` 的 `django` 框架
+* 默认打包配置文件、静态文件和模板文件。可更改 `app/boot/boot.go` 文件内 `global.IsOnlyEmbed` 为 `false` 自定义配置文件和模板文件
* 生成一个文件即可部署
+* 添加 WebDAV 支持
### 截图预览
@@ -42,7 +43,7 @@ go run main.go --config=config.toml
go run main.go --view=template
```
-3. 登录账号: `admin` / `123456`
+3. 登录账号: `admin` / `123456`, WebDAV 账号: `webnav` / `123456`
### 特别鸣谢
diff --git a/app/boot/boot.go b/app/boot/boot.go
index 3c10338..b12918b 100644
--- a/app/boot/boot.go
+++ b/app/boot/boot.go
@@ -4,12 +4,15 @@ import (
"fmt"
"time"
"flag"
+ "strings"
"net/http"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/labstack/gommon/log"
+ "github.com/labstack/gommon/color"
+ "github.com/deatil/doak-fs/pkg/utils"
"github.com/deatil/doak-fs/pkg/global"
"github.com/deatil/doak-fs/pkg/logger"
"github.com/deatil/doak-fs/pkg/session"
@@ -18,19 +21,41 @@ import (
"github.com/deatil/doak-fs/app/view"
"github.com/deatil/doak-fs/app/route"
+ "github.com/deatil/doak-fs/app/webdav"
"github.com/deatil/doak-fs/app/resources"
)
+const (
+ website = "https://github.com/deatil/doak-fs"
+ banner = `
+ .___ __ _____
+ __| _/_________ | | __ _/ ____\______
+ / __ |/ _ \__ \ | |/ / ______ \ __\/ ___/
+/ /_/ ( <_> ) __ \| < /_____/ | | \___ \
+\____ |\____(____ /__|_ \ |__| /____ >
+ \/ \/ \/ \/ %s
+Doak filesystem base on echo %s
+%s
+_____________________________________________________
+
+`
+)
+
+var makePass string
+
// 初始化
-func init() {
+func initServer() {
// 系统启动参数
- config := flag.String("config", "", "配置文件")
- view := flag.String("view", "", "是否导入模板")
+ config := flag.String("config", "", "config file")
+ view := flag.String("view", "", "view path")
+ pass := flag.String("pass", "", "make Pass")
flag.Parse()
global.ConfigFile = *config
global.ViewPath = *view
+ makePass = *pass
+
// 只使用打包文件
global.IsOnlyEmbed = true
@@ -43,6 +68,19 @@ func init() {
// 运行
func Start() {
+ initServer()
+
+ if makePass != "" {
+ newPass := utils.PasswordHash(makePass)
+ fmt.Println("new password: " + newPass)
+ return
+ }
+
+ runServer()
+}
+
+// 运行
+func runServer() {
// 初始化 echo
e := echo.New()
@@ -50,8 +88,12 @@ func Start() {
logger.SetLoggerFile(global.Conf.App.LogFile)
logger.SetLoggerLevel(global.Conf.App.LogLevel)
+ // 隐藏默认
+ e.HideBanner = true
+ e.HidePort = true
+
// 自定义错误处理
- e.HTTPErrorHandler = HTTPErrorHandler
+ e.HTTPErrorHandler = httpErrorHandler
// 调试状态
debug := global.Conf.App.Debug
@@ -108,7 +150,14 @@ func Start() {
// CSRF
e.Use(middleware.CSRFWithConfig(middleware.CSRFConfig{
- Skipper: middleware.DefaultSkipper,
+ Skipper: func(ctx echo.Context) bool {
+ path := ctx.Request().URL.String()
+ if strings.HasPrefix(path, "/dav") {
+ return true
+ }
+
+ return false
+ },
TokenLength: global.Conf.Server.CSRFTokenLength,
TokenLookup: "cookie:" + global.Conf.Server.CSRFCookieName,
ContextKey: global.Conf.Server.CSRFContextKey,
@@ -202,6 +251,9 @@ func Start() {
assetHandler := http.FileServer(resources.StaticAssets())
e.GET("/static/*", echo.WrapHandler(http.StripPrefix("/static/", assetHandler)))
+ // webdav
+ webdav.Route(e.Group("/dav"))
+
// 路由
route.Route(e)
@@ -214,12 +266,15 @@ func Start() {
return ctx.String(http.StatusNotFound, "not found")
})
+ // 显示信息
+ showBanner()
+
// 设置端口
e.Logger.Fatal(e.Start(global.Conf.Server.Address))
}
// 自定义错误
-func HTTPErrorHandler(err error, ctx echo.Context) {
+func httpErrorHandler(err error, ctx echo.Context) {
code := http.StatusInternalServerError
if he, ok := err.(*echo.HTTPError); ok {
code = he.Code
@@ -245,3 +300,19 @@ func HTTPErrorHandler(err error, ctx echo.Context) {
ctx.Logger().Error(repErr)
}
}
+
+// 控制台显示信息
+func showBanner() {
+ colorer := color.New()
+
+ colorer.Printf(
+ banner,
+ colorer.Red("v" + global.Conf.App.Version),
+ colorer.Green("v" + echo.Version),
+ colorer.Blue(website),
+ )
+
+ if global.Conf.App.Debug {
+ colorer.Printf("https server started on %s\n", colorer.Green(global.Conf.Server.Address))
+ }
+}
diff --git a/app/controller/file.go b/app/controller/file.go
index 784352f..df0449e 100644
--- a/app/controller/file.go
+++ b/app/controller/file.go
@@ -21,7 +21,11 @@ type File struct{
func (this *File) Index(ctx echo.Context) error {
path := ctx.QueryParam("path")
- list := global.Fs.Ls(path)
+ dirList := global.Fs.LsDir(path)
+ fileList := global.Fs.LsFile(path)
+
+ list := append(dirList, fileList...)
+
name := global.Fs.Basename(path)
parentPath := global.Fs.ParentPath(path)
@@ -316,10 +320,11 @@ func (this *File) PreviewFile(ctx echo.Context) error {
}
data := global.Fs.Read(file)
+ dataType := data["type"].(string)
- if data["type"] != "image" &&
- data["type"] != "audio" &&
- data["type"] != "video" {
+ if dataType != "image" &&
+ dataType != "audio" &&
+ dataType != "video" {
return response.String(ctx, "文件不存在")
}
diff --git a/app/controller/profile.go b/app/controller/profile.go
index 7dccee4..ca0b08a 100644
--- a/app/controller/profile.go
+++ b/app/controller/profile.go
@@ -75,3 +75,39 @@ func (this *Profile) PasswordSave(ctx echo.Context) error {
return response.ReturnSuccessJson(ctx, "更改密码成功", "")
}
+
+// Webdav 账号信息
+func (this *Profile) Webdav(ctx echo.Context) error {
+ username := global.Conf.Webdav.GetFirstUsername()
+
+ return response.Render(ctx, "profile_webdav.html", map[string]any{
+ "wusername": username,
+ })
+}
+
+// Webdav 账号信息 保存
+func (this *Profile) WebdavSave(ctx echo.Context) error {
+ pass := ctx.FormValue("pass")
+ if pass == "" {
+ return response.ReturnErrorJson(ctx, "密码不能为空")
+ }
+
+ // 新密码
+ newPass := utils.PasswordHash(pass)
+
+ username := global.Conf.Webdav.GetFirstUsername()
+
+ // 更改密码
+ global.Conf.Webdav = global.Conf.Webdav.UpdatePassword(username, newPass)
+
+ // 更改配置信息
+ if global.ConfigFile != "" && !global.IsOnlyEmbed {
+ // 写入配置
+ err := config.WriteConfig(global.ConfigFile, global.Conf)
+ if err != nil {
+ return response.ReturnErrorJson(ctx, "更改密码失败")
+ }
+ }
+
+ return response.ReturnSuccessJson(ctx, "更改密码成功", "")
+}
diff --git a/app/resources/config/config.toml b/app/resources/config/config.toml
index 0669741..78eda89 100644
--- a/app/resources/config/config.toml
+++ b/app/resources/config/config.toml
@@ -1,6 +1,6 @@
[app]
app_name = 'doak-fs'
-version = '1.0.2'
+version = '1.0.5'
debug = true
time_zone = 'Asia/Hong_Kong'
log_file = './fs-log.log'
@@ -26,6 +26,10 @@ driver = 'local'
[user]
names = ['admin:$2a$10$EnHCul2nbg0ZmvL0OEdoOOC4hkzniHO8zFS/vlGVSKYGlMX53qZNu']
+[webdav]
+users = ['webnav:$2a$10$EnHCul2nbg0ZmvL0OEdoOOC4hkzniHO8zFS/vlGVSKYGlMX53qZNu']
+path = './'
+
[session]
secret = 'secret'
key = 'session'
diff --git a/app/resources/view/common/top_nav.html b/app/resources/view/common/top_nav.html
index 9aa034e..8e128d0 100644
--- a/app/resources/view/common/top_nav.html
+++ b/app/resources/view/common/top_nav.html
@@ -31,6 +31,12 @@
+
+
+ 更改WebDAV
+
+
+
diff --git a/app/resources/view/file_index.html b/app/resources/view/file_index.html
index b44c34e..f145d45 100644
--- a/app/resources/view/file_index.html
+++ b/app/resources/view/file_index.html
@@ -20,12 +20,12 @@ 文件管理
-
文件列表 | {{ name }}
+
文件列表
{% if path == "" %}
-
/
+
/
{% else %}
-
{{ path }}
+
{{ path }}
{% endif %}
diff --git a/app/resources/view/profile_webdav.html b/app/resources/view/profile_webdav.html
new file mode 100644
index 0000000..7e933aa
--- /dev/null
+++ b/app/resources/view/profile_webdav.html
@@ -0,0 +1,97 @@
+{% extends "common/base.html" %}
+
+{% block title %}更改 Webdav - {{ block.Super }}{% endblock %}
+
+{% block content %}
+
+
+
+
+
+ - 控制台
+ - 我的信息
+
+
更改 Webdav
+
+
+
+
+
+
+
+{% endblock %}
+
+{% block script %}
+{{ block.Super }}
+
+
+{% endblock %}
diff --git a/app/route/route.go b/app/route/route.go
index 3d47e4c..9f23323 100644
--- a/app/route/route.go
+++ b/app/route/route.go
@@ -25,6 +25,9 @@ func Route(e *echo.Echo) {
profileController := new(controller.Profile)
profileGroup.GET("/password", profileController.Password)
profileGroup.POST("/password", profileController.PasswordSave)
+
+ profileGroup.GET("/webdav", profileController.Webdav)
+ profileGroup.POST("/webdav", profileController.WebdavSave)
}
// 文件管理
diff --git a/app/webdav/webdav.go b/app/webdav/webdav.go
new file mode 100644
index 0000000..fdf1dce
--- /dev/null
+++ b/app/webdav/webdav.go
@@ -0,0 +1,72 @@
+package webdav
+
+import (
+ "net/http"
+
+ "golang.org/x/net/webdav"
+ "github.com/labstack/echo/v4"
+
+ "github.com/deatil/doak-fs/pkg/utils"
+ "github.com/deatil/doak-fs/pkg/global"
+)
+
+var handler *webdav.Handler
+
+// webdav 路由
+func Route(dav *echo.Group) {
+ handler = &webdav.Handler{
+ Prefix: "/dav",
+ FileSystem: webdav.Dir(global.Conf.Webdav.Path),
+ LockSystem: webdav.NewMemLS(),
+ }
+
+ dav.Use(WebDAVAuth())
+ dav.Any("/*", ServeWebDAV)
+ dav.Any("", ServeWebDAV)
+ dav.Add("PROPFIND", "/*", ServeWebDAV)
+ dav.Add("PROPFIND", "", ServeWebDAV)
+ dav.Add("MKCOL", "/*", ServeWebDAV)
+ dav.Add("LOCK", "/*", ServeWebDAV)
+ dav.Add("UNLOCK", "/*", ServeWebDAV)
+ dav.Add("PROPPATCH", "/*", ServeWebDAV)
+ dav.Add("COPY", "/*", ServeWebDAV)
+ dav.Add("MOVE", "/*", ServeWebDAV)
+}
+
+// ServeWebDAV
+func ServeWebDAV(ctx echo.Context) error {
+ req := ctx.Request()
+ w := ctx.Response()
+
+ handler.ServeHTTP(w, req)
+
+ return nil
+}
+
+func WebDAVAuth() echo.MiddlewareFunc {
+ return func(next echo.HandlerFunc) echo.HandlerFunc {
+ return func(ctx echo.Context) error {
+ req := ctx.Request()
+ w := ctx.Response()
+
+ // 获取用户名/密码
+ username, password, ok := req.BasicAuth()
+
+ if !ok {
+ w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
+ w.WriteHeader(http.StatusUnauthorized)
+ return nil
+ }
+
+ userPassword := global.Conf.Webdav.GetUserPassword(username)
+
+ // 验证用户名/密码
+ if userPassword == "" || !utils.PasswordCheck(password, userPassword) {
+ http.Error(w, "WebDAV: need authorized!", http.StatusUnauthorized)
+ return nil
+ }
+
+ return next(ctx)
+ }
+ }
+}
diff --git a/go.mod b/go.mod
index 4324e10..280b35d 100644
--- a/go.mod
+++ b/go.mod
@@ -3,18 +3,18 @@ module github.com/deatil/doak-fs
go 1.18
require (
- github.com/deatil/lakego-filesystem v1.0.1006
+ github.com/deatil/lakego-filesystem v1.0.1008
github.com/flosch/pongo2/v6 v6.0.0
github.com/gorilla/sessions v1.2.1
github.com/jinzhu/now v1.1.5
- github.com/labstack/echo-contrib v0.13.1
- github.com/labstack/echo/v4 v4.10.0
+ github.com/labstack/echo-contrib v0.15.0
+ github.com/labstack/echo/v4 v4.11.1
github.com/labstack/gommon v0.4.0
- github.com/pelletier/go-toml/v2 v2.0.6
+ github.com/pelletier/go-toml/v2 v2.0.9
github.com/pkg/errors v0.9.1
github.com/steambap/captcha v1.4.1
- golang.org/x/crypto v0.6.0
- golang.org/x/image v0.5.0
+ golang.org/x/crypto v0.11.0
+ golang.org/x/image v0.9.0
)
require (
@@ -24,11 +24,11 @@ require (
github.com/gorilla/securecookie v1.1.1 // indirect
github.com/h2non/filetype v1.1.3 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
- github.com/mattn/go-isatty v0.0.17 // indirect
+ github.com/mattn/go-isatty v0.0.19 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
- golang.org/x/net v0.7.0 // indirect
- golang.org/x/sys v0.5.0 // indirect
- golang.org/x/text v0.7.0 // indirect
+ golang.org/x/net v0.12.0 // indirect
+ golang.org/x/sys v0.10.0 // indirect
+ golang.org/x/text v0.11.0 // indirect
golang.org/x/time v0.3.0 // indirect
)
diff --git a/go.sum b/go.sum
index 7d483f4..efbf6bd 100644
--- a/go.sum
+++ b/go.sum
@@ -5,6 +5,8 @@ github.com/deatil/lakego-filesystem v1.0.1003 h1:WmZccnCgOIaJ2iMHhKvUnvmklkXhv/V
github.com/deatil/lakego-filesystem v1.0.1003/go.mod h1:1xReBnNgi/Zik2RTcMGgBjndaJrBrILVP7DlXyC3Dk8=
github.com/deatil/lakego-filesystem v1.0.1006 h1:OaEbnKxIJ25cj6pV8X4rum5/kqYXLFnAPvmh5RL3oUQ=
github.com/deatil/lakego-filesystem v1.0.1006/go.mod h1:1xReBnNgi/Zik2RTcMGgBjndaJrBrILVP7DlXyC3Dk8=
+github.com/deatil/lakego-filesystem v1.0.1008 h1:MM2NnInL1hsr8211ZNmO6vpDwPvW+F4pq2MD8If/bmo=
+github.com/deatil/lakego-filesystem v1.0.1008/go.mod h1:1xReBnNgi/Zik2RTcMGgBjndaJrBrILVP7DlXyC3Dk8=
github.com/flosch/pongo2/v6 v6.0.0 h1:lsGru8IAzHgIAw6H2m4PCyleO58I40ow6apih0WprMU=
github.com/flosch/pongo2/v6 v6.0.0/go.mod h1:CuDpFm47R0uGGE7z13/tTlt1Y6zdxvr2RLT5LJhsHEU=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
@@ -27,10 +29,14 @@ github.com/labstack/echo-contrib v0.13.0 h1:bzSG0SpuZZd7BmJLvsWtPfU23W0Enh3K0tok
github.com/labstack/echo-contrib v0.13.0/go.mod h1:IF9+MJu22ADOZEHD+bAV67XMIO3vNXUy7Naz/ABPHEs=
github.com/labstack/echo-contrib v0.13.1 h1:9TktDom9FJKhkKO45YvV4klW8IedtSUp/k85gZVdZ28=
github.com/labstack/echo-contrib v0.13.1/go.mod h1:LdM7aOHAYLOPmAAGXXG9TuN4h5sh6dPEu4pb6W2HKuU=
+github.com/labstack/echo-contrib v0.15.0 h1:9K+oRU265y4Mu9zpRDv3X+DGTqUALY6oRHCSZZKCRVU=
+github.com/labstack/echo-contrib v0.15.0/go.mod h1:lei+qt5CLB4oa7VHTE0yEfQSEB9XTJI1LUqko9UWvo4=
github.com/labstack/echo/v4 v4.9.1 h1:GliPYSpzGKlyOhqIbG8nmHBo3i1saKWFOgh41AN3b+Y=
github.com/labstack/echo/v4 v4.9.1/go.mod h1:Pop5HLc+xoc4qhTZ1ip6C0RtP7Z+4VzRLWZZFKqbbjo=
github.com/labstack/echo/v4 v4.10.0 h1:5CiyngihEO4HXsz3vVsJn7f8xAlWwRr3aY6Ih280ZKA=
github.com/labstack/echo/v4 v4.10.0/go.mod h1:S/T/5fy/GigaXnHTkh0ZGe4LpkkQysvRjFMSUTkDRNQ=
+github.com/labstack/echo/v4 v4.11.1 h1:dEpLU2FLg4UVmvCGPuk/APjlH6GDpbEPti61srUUUs4=
+github.com/labstack/echo/v4 v4.11.1/go.mod h1:YuYRTSM3CHs2ybfrL8Px48bO6BAnYIN4l8wSTMP6BDQ=
github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
@@ -41,8 +47,12 @@ github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peK
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
+github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
+github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0=
+github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -57,6 +67,7 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
@@ -69,21 +80,30 @@ golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8=
golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80=
golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
+golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
+golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/image v0.2.0 h1:/DcQ0w3VHKCC5p0/P2B0JpAZ9Z++V2KOo2fyU89CXBQ=
golang.org/x/image v0.2.0/go.mod h1:la7oBXb9w3YFjBqaAwtynVioc1ZvOnNteUNrifGNmAI=
golang.org/x/image v0.5.0 h1:5JMiNunQeQw++mMOz48/ISeNu3Iweh/JaZU8ZLqHRrI=
golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4=
+golang.org/x/image v0.9.0 h1:QrzfX26snvCM20hIhBwuHI/ThTg18b/+kcKdXHvnR+g=
+golang.org/x/image v0.9.0/go.mod h1:jtrku+n79PfroUbvDdeUWMAI+heR786BofxrbiSF+J0=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU=
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
+golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -97,8 +117,12 @@ golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
+golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -107,11 +131,14 @@ golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
+golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
diff --git a/main.go b/main.go
index d7b6425..29e768c 100644
--- a/main.go
+++ b/main.go
@@ -6,13 +6,16 @@ import (
// 启动
// 打包配置文件
-// go run main.go
+// > go run main.go
// 使用自定义配置文件
-// go run main.go --config=config.toml
+// > go run main.go --config=config.toml
// 使用模板位置 './template'
-// go run main.go --view=template
+// > go run main.go --view=template
+
+// 生成密码
+// > go run main.go --pass=123456
func main() {
boot.Start()
}
diff --git a/pkg/config/config.go b/pkg/config/config.go
index 9e4337c..32bd043 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -13,6 +13,7 @@ type Conf struct {
Server `toml:"server"`
File `toml:"file"`
User `toml:"user"`
+ Webdav `toml:"webdav"`
Session `toml:"session"`
}
@@ -45,7 +46,6 @@ type User struct {
Names []string `toml:"names"`
}
-// 更改密码
func (this User) GetUsers() map[string]string {
users := make(map[string]string)
@@ -87,6 +87,62 @@ func (this User) UpdatePassword(name string, pass string) User {
return this
}
+type Webdav struct {
+ Users []string `toml:"users"`
+ Path string `toml:"path"`
+}
+
+func (this Webdav) GetUsers() map[string]string {
+ users := make(map[string]string)
+
+ for _, user := range this.Users {
+ newUser := strings.SplitN(user, ":", 2)
+ users[newUser[0]] = newUser[1]
+ }
+
+ return users
+}
+
+
+func (this Webdav) GetFirstUsername() string {
+ for _, user := range this.Users {
+ newUser := strings.SplitN(user, ":", 2)
+ return newUser[0]
+ }
+
+ return ""
+}
+
+// 账号密码
+func (this Webdav) GetUserPassword(name string) string {
+ users := this.GetUsers()
+
+ if password, ok := users[name]; ok {
+ return password
+ }
+
+ return ""
+}
+
+// 更改密码
+func (this Webdav) UpdatePassword(name, pass string) Webdav {
+ users := this.GetUsers()
+
+ _, ok := users[name]
+ if !ok {
+ return this
+ }
+
+ users[name] = pass
+
+ this.Users = make([]string, 0)
+ for name, pass := range users {
+ this.Users = append(this.Users, fmt.Sprintf("%s:%s", name, pass))
+ }
+
+ return this
+}
+
type Session struct {
Secret string `toml:"secret"`
Key string `toml:"key"`
diff --git a/pkg/fs/driver_base.go b/pkg/fs/driver_base.go
deleted file mode 100644
index d9ed0a7..0000000
--- a/pkg/fs/driver_base.go
+++ /dev/null
@@ -1,30 +0,0 @@
-package fs
-
-/**
- * 驱动公共类
- *
- * @create 2023-2-17
- * @author deatil
- */
-type DriverBase struct {}
-
-// 是否为文件夹
-func (this DriverBase) Basename(path string) string {
- return Filesystem.Basename(path)
-}
-
-// 是否为文件夹
-func (this DriverBase) ParentPath(path string) string {
- if path == "" || path == "/" {
- return ""
- }
-
- parentPath := Filesystem.Dirname(path)
- parentPath = Filesystem.ToSlash(parentPath)
-
- return parentPath
-}
-
-func (this DriverBase) Extension(path string) string {
- return Filesystem.Extension(path)
-}
diff --git a/pkg/fs/driver_local.go b/pkg/fs/driver_local.go
index c3e00e2..03b2070 100644
--- a/pkg/fs/driver_local.go
+++ b/pkg/fs/driver_local.go
@@ -21,14 +21,12 @@ func NewDriverLocal(rootPath string) DriverLocal {
* @author deatil
*/
type DriverLocal struct {
- DriverBase
-
// 根目录
rootPath string
}
// 列出文件及文件夹
-func (this DriverLocal) Ls(directory string) []map[string]any {
+func (this DriverLocal) LsFile(directory string) []map[string]any {
res := make([]map[string]any, 0)
directory = this.formatPath(directory)
@@ -37,9 +35,6 @@ func (this DriverLocal) Ls(directory string) []map[string]any {
return res
}
- directories, _ := Filesystem.Directories(directory)
- res = append(res, formatDirectories(directories, directory)...)
-
files, _ := Filesystem.Files(directory)
res = append(res, formatFiles(files, directory)...)
@@ -71,31 +66,19 @@ func (this DriverLocal) Read(path string) map[string]any {
}
size := int64(0)
- typ := "folder"
- ext := ""
- isDir := true
if Filesystem.IsFile(path) {
- typ = DetectFileType(path)
- size = Filesystem.Size(path)
- ext = Filesystem.Extension(path)
- isDir = false
+ size = Filesystem.Size(path)
}
- namesmall := Filesystem.Basename(path)
- time := Filesystem.LastModified(path)
-
+ time := Filesystem.LastModified(path)
perm, _ := Filesystem.PermString(path)
permInt, _ := Filesystem.Perm(path)
res := map[string]any{
"name": path,
- "namesmall": namesmall,
- "isDir": isDir,
"size": size,
- "time": FormatTime(time),
- "type": typ,
- "ext": ext,
+ "time": time,
"perm": perm,
"permInt": fmt.Sprintf("%o", permInt),
}
@@ -157,7 +140,7 @@ func (this DriverLocal) Rename(oldName string, newName string) error {
func (this DriverLocal) Move(oldName string, newName string) error {
oldName = this.formatPath(oldName)
- oldBasename := this.Basename(oldName)
+ oldBasename := Filesystem.Basename(oldName)
newName = this.formatPath(newName, oldBasename)
if !this.checkFilePath(oldName) {
@@ -188,7 +171,7 @@ func (this DriverLocal) Move(oldName string, newName string) error {
func (this DriverLocal) Copy(oldName string, newName string) error {
oldName = this.formatPath(oldName)
- oldBasename := this.Basename(oldName)
+ oldBasename := Filesystem.Basename(oldName)
newName = this.formatPath(newName, oldBasename)
if !this.checkFilePath(oldName) {
@@ -222,21 +205,6 @@ func (this DriverLocal) Copy(oldName string, newName string) error {
return nil
}
-// 判断
-func (this DriverLocal) Exists(path string) bool {
- return Filesystem.Exists(path)
-}
-
-// 是否为文件
-func (this DriverLocal) IsFile(path string) bool {
- return Filesystem.IsFile(path)
-}
-
-// 是否为文件夹
-func (this DriverLocal) IsDirectory(path string) bool {
- return Filesystem.IsDirectory(path)
-}
-
// 获取
func (this DriverLocal) Get(path string) (string, error) {
path = this.formatPath(path)
@@ -245,7 +213,7 @@ func (this DriverLocal) Get(path string) (string, error) {
return "", errors.New("访问错误")
}
- if !this.IsFile(path) {
+ if !Filesystem.IsFile(path) {
return "", errors.New("打开的不是文件")
}
@@ -265,7 +233,7 @@ func (this DriverLocal) Put(path string, contents string) error {
return errors.New("访问错误")
}
- if !this.IsFile(path) {
+ if !Filesystem.IsFile(path) {
return errors.New("要更新的不是文件")
}
@@ -285,7 +253,7 @@ func (this DriverLocal) CreateFile(path string) error {
return errors.New("访问错误")
}
- if this.IsFile(path) {
+ if Filesystem.IsFile(path) {
return errors.New("文件已经存在")
}
@@ -315,7 +283,7 @@ func (this DriverLocal) Upload(src io.Reader, path string, name string) error {
return errors.New("访问错误")
}
- if this.IsFile(path) {
+ if Filesystem.IsFile(path) {
return errors.New("文件已经存在")
}
@@ -334,6 +302,27 @@ func (this DriverLocal) Upload(src io.Reader, path string, name string) error {
return nil
}
+// 判断
+func (this DriverLocal) Exists(path string) bool {
+ path = this.formatPath(path)
+
+ return Filesystem.Exists(path)
+}
+
+// 是否为文件
+func (this DriverLocal) IsFile(path string) bool {
+ path = this.formatPath(path)
+
+ return Filesystem.IsFile(path)
+}
+
+// 是否为文件夹
+func (this DriverLocal) IsDirectory(path string) bool {
+ path = this.formatPath(path)
+
+ return Filesystem.IsDirectory(path)
+}
+
func (this DriverLocal) FormatFile(path string) (string, error) {
path = this.formatPath(path)
@@ -341,7 +330,7 @@ func (this DriverLocal) FormatFile(path string) (string, error) {
return "", errors.New("访问错误")
}
- if !this.IsFile(path) {
+ if !Filesystem.IsFile(path) {
return "", errors.New("打开的不是文件")
}
@@ -350,7 +339,6 @@ func (this DriverLocal) FormatFile(path string) (string, error) {
// 检测路径是否正常
func (this DriverLocal) checkFilePath(path string) bool {
- // 根目录
rootPath, _ := Filesystem.Realpath(this.rootPath)
if strings.HasPrefix(path, rootPath) {
@@ -378,24 +366,18 @@ func formatFiles(files []string, path string) []map[string]any {
for _, file := range files {
file = Filesystem.Join(path, file)
- namesmall := Filesystem.Basename(file)
size := Filesystem.Size(file)
time := Filesystem.LastModified(file)
- ext := Filesystem.Extension(file)
perm, _ := Filesystem.PermString(file)
permInt, _ := Filesystem.Perm(file)
res = append(res, map[string]any{
- "name": file,
- "namesmall": namesmall,
- "isDir": false,
- "size": size,
- "time": FormatTime(time),
- "type": DetectFileType(file),
- "ext": ext,
- "perm": perm,
- "permInt": fmt.Sprintf("%o", permInt),
+ "name": file,
+ "size": size,
+ "time": time,
+ "perm": perm,
+ "permInt": fmt.Sprintf("%o", permInt),
})
}
@@ -409,22 +391,17 @@ func formatDirectories(dirs []string, path string) []map[string]any {
for _, dir := range dirs {
dir = Filesystem.Join(path, dir)
- namesmall := Filesystem.Basename(dir)
time := Filesystem.LastModified(dir)
perm, _ := Filesystem.PermString(dir)
permInt, _ := Filesystem.Perm(dir)
res = append(res, map[string]any{
- "name": dir,
- "namesmall": namesmall,
- "isDir": true,
- "size": "-",
- "time": FormatTime(time),
- "type": "folder",
- "ext": "",
- "perm": perm,
- "permInt": fmt.Sprintf("%o", permInt),
+ "name": dir,
+ "size": "-",
+ "time": time,
+ "perm": perm,
+ "permInt": fmt.Sprintf("%o", permInt),
})
}
diff --git a/pkg/fs/fs.go b/pkg/fs/fs.go
index 0525bbb..c3c235f 100644
--- a/pkg/fs/fs.go
+++ b/pkg/fs/fs.go
@@ -6,32 +6,27 @@ import (
// 接口
type IFs interface {
- Ls(directory string) []map[string]any
+ LsFile(directory string) []map[string]any
LsDir(directory string) []map[string]any
+
Read(path string) map[string]any
Delete(paths ...string) error
- Exists(path string) bool
- IsFile(path string) bool
- IsDirectory(path string) bool
Get(path string) (string, error)
Put(path string, contents string) error
+
CreateFile(path string) error
CreateDir(path string) error
Upload(rd io.Reader, path string, name string) error
+
Rename(oldName string, newName string) error
Move(oldName string, newName string) error
Copy(oldName string, newName string) error
- Basename(path string) string
- ParentPath(path string) string
- Extension(path string) string
- FormatFile(path string) (string, error)
-}
+ Exists(path string) bool
+ IsFile(path string) bool
+ IsDirectory(path string) bool
-func New(driver IFs) Fs {
- return Fs{
- Driver: driver,
- }
+ FormatFile(path string) (string, error)
}
/**
@@ -44,16 +39,109 @@ type Fs struct {
Driver IFs
}
-func (this Fs) Ls(directory string) []map[string]any {
- return this.Driver.Ls(directory)
+func New(driver IFs) Fs {
+ return Fs{
+ Driver: driver,
+ }
}
+// 列出文件
+func (this Fs) LsFile(directory string) []map[string]any {
+ files := this.Driver.LsFile(directory)
+ if len(files) == 0 {
+ return files
+ }
+
+ res := make([]map[string]any, 0)
+ for _, file := range files {
+ fileName := file["name"].(string)
+ time := file["time"].(int64)
+
+ namesmall := Filesystem.Basename(fileName)
+ ext := Filesystem.Extension(fileName)
+
+ res = append(res, map[string]any{
+ "name": fileName,
+ "namesmall": namesmall,
+ "isDir": false,
+ "size": file["size"],
+ "time": FormatTime(time),
+ "type": DetectFileType(fileName),
+ "ext": ext,
+ "perm": file["perm"],
+ "permInt": file["permInt"],
+ })
+ }
+
+ return res
+}
+
+// 列出文件夹
func (this Fs) LsDir(directory string) []map[string]any {
- return this.Driver.LsDir(directory)
+ dirs := this.Driver.LsDir(directory)
+ if len(dirs) == 0 {
+ return dirs
+ }
+
+ res := make([]map[string]any, 0)
+ for _, dir := range dirs {
+ dirName := dir["name"].(string)
+ time := dir["time"].(int64)
+
+ namesmall := Filesystem.Basename(dirName)
+
+ res = append(res, map[string]any{
+ "name": dirName,
+ "namesmall": namesmall,
+ "isDir": true,
+ "size": dir["size"],
+ "time": FormatTime(time),
+ "type": "folder",
+ "ext": "",
+ "perm": dir["perm"],
+ "permInt": dir["permInt"],
+ })
+ }
+
+ return res
}
+// 读取数据
func (this Fs) Read(path string) map[string]any {
- return this.Driver.Read(path)
+ data := this.Driver.Read(path)
+
+ if len(data) == 0 {
+ return data
+ }
+
+ dataName := data["name"].(string)
+
+ typ := "folder"
+ ext := ""
+ isDir := true
+
+ if this.Driver.IsFile(path) {
+ typ = DetectFileType(dataName)
+ ext = Filesystem.Extension(dataName)
+ isDir = false
+ }
+
+ namesmall := Filesystem.Basename(dataName)
+ time := data["time"].(int64)
+
+ res := map[string]any{
+ "name": dataName,
+ "namesmall": namesmall,
+ "isDir": isDir,
+ "size": data["size"],
+ "time": FormatTime(time),
+ "type": typ,
+ "ext": ext,
+ "perm": data["perm"],
+ "permInt": data["permInt"],
+ }
+
+ return res
}
func (this Fs) Delete(paths ...string) error {
@@ -84,18 +172,6 @@ func (this Fs) CreateFile(path string) error {
return this.Driver.CreateFile(path)
}
-func (this Fs) Basename(path string) string {
- return this.Driver.Basename(path)
-}
-
-func (this Fs) ParentPath(path string) string {
- return this.Driver.ParentPath(path)
-}
-
-func (this Fs) Extension(path string) string {
- return this.Driver.Extension(path)
-}
-
func (this Fs) Upload(src io.Reader, path string, name string) error {
return this.Driver.Upload(src, path, name)
}
@@ -119,3 +195,25 @@ func (this Fs) FormatFile(path string) (string, error) {
func (this Fs) CreateDir(path string) error {
return this.Driver.CreateDir(path)
}
+
+// 名称
+func (this Fs) Basename(path string) string {
+ return Filesystem.Basename(path)
+}
+
+// 是否为文件夹
+func (this Fs) ParentPath(path string) string {
+ if path == "" || path == "/" {
+ return ""
+ }
+
+ parentPath := Filesystem.Dirname(path)
+ parentPath = Filesystem.ToSlash(parentPath)
+
+ return parentPath
+}
+
+// 后缀
+func (this Fs) Extension(path string) string {
+ return Filesystem.Extension(path)
+}
diff --git a/pkg/gowebdav/.gitignore b/pkg/gowebdav/.gitignore
new file mode 100644
index 0000000..394b2f5
--- /dev/null
+++ b/pkg/gowebdav/.gitignore
@@ -0,0 +1,21 @@
+# Folders to ignore
+/src
+/bin
+/pkg
+/gowebdav
+/.idea
+
+# Binaries for programs and plugins
+*.exe
+*.exe~
+*.dll
+*.so
+*.dylib
+
+# Test binary, build with `go test -c`
+*.test
+
+# Output of the go coverage tool, specifically when used with LiteIDE
+*.out
+
+.vscode/
\ No newline at end of file
diff --git a/pkg/gowebdav/.travis.yml b/pkg/gowebdav/.travis.yml
new file mode 100644
index 0000000..76bfb65
--- /dev/null
+++ b/pkg/gowebdav/.travis.yml
@@ -0,0 +1,10 @@
+language: go
+
+go:
+ - "1.x"
+
+install:
+ - go get ./...
+
+script:
+ - go test -v --short ./...
\ No newline at end of file
diff --git a/pkg/gowebdav/LICENSE b/pkg/gowebdav/LICENSE
new file mode 100644
index 0000000..a7cd442
--- /dev/null
+++ b/pkg/gowebdav/LICENSE
@@ -0,0 +1,27 @@
+Copyright (c) 2014, Studio B12 GmbH
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its contributors
+ may be used to endorse or promote products derived from this software without
+ specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/pkg/gowebdav/Makefile b/pkg/gowebdav/Makefile
new file mode 100644
index 0000000..c6a0062
--- /dev/null
+++ b/pkg/gowebdav/Makefile
@@ -0,0 +1,33 @@
+BIN := gowebdav
+SRC := $(wildcard *.go) cmd/gowebdav/main.go
+
+all: test cmd
+
+cmd: ${BIN}
+
+${BIN}: ${SRC}
+ go build -o $@ ./cmd/gowebdav
+
+test:
+ go test -v --short ./...
+
+api:
+ @sed '/^## API$$/,$$d' -i README.md
+ @echo '## API' >> README.md
+ @godoc2md github.com/studio-b12/gowebdav | sed '/^$$/N;/^\n$$/D' |\
+ sed '2d' |\
+ sed 's/\/src\/github.com\/studio-b12\/gowebdav\//https:\/\/github.com\/studio-b12\/gowebdav\/blob\/master\//g' |\
+ sed 's/\/src\/target\//https:\/\/github.com\/studio-b12\/gowebdav\/blob\/master\//g' |\
+ sed 's/^#/##/g' >> README.md
+
+check:
+ gofmt -w -s $(SRC)
+ @echo
+ gocyclo -over 15 .
+ @echo
+ golint ./...
+
+clean:
+ @rm -f ${BIN}
+
+.PHONY: all cmd clean test api check
diff --git a/pkg/gowebdav/README.md b/pkg/gowebdav/README.md
new file mode 100644
index 0000000..31d9fe7
--- /dev/null
+++ b/pkg/gowebdav/README.md
@@ -0,0 +1,564 @@
+# GoWebDAV
+
+[](https://travis-ci.org/studio-b12/gowebdav)
+[](https://godoc.org/github.com/studio-b12/gowebdav)
+[](https://goreportcard.com/report/github.com/studio-b12/gowebdav)
+
+A golang WebDAV client library.
+
+## Main features
+`gowebdav` library allows to perform following actions on the remote WebDAV server:
+* [create path](#create-path-on-a-webdav-server)
+* [get files list](#get-files-list)
+* [download file](#download-file-to-byte-array)
+* [upload file](#upload-file-from-byte-array)
+* [get information about specified file/folder](#get-information-about-specified-filefolder)
+* [move file to another location](#move-file-to-another-location)
+* [copy file to another location](#copy-file-to-another-location)
+* [delete file](#delete-file)
+
+## Usage
+
+First of all you should create `Client` instance using `NewClient()` function:
+
+```go
+root := "https://webdav.mydomain.me"
+user := "user"
+password := "password"
+
+c := gowebdav.NewClient(root, user, password)
+```
+
+After you can use this `Client` to perform actions, described below.
+
+**NOTICE:** we will not check errors in examples, to focus you on the `gowebdav` library's code, but you should do it in your code!
+
+### Create path on a WebDAV server
+```go
+err := c.Mkdir("folder", 0644)
+```
+In case you want to create several folders you can use `c.MkdirAll()`:
+```go
+err := c.MkdirAll("folder/subfolder/subfolder2", 0644)
+```
+
+### Get files list
+```go
+files, _ := c.ReadDir("folder/subfolder")
+for _, file := range files {
+ //notice that [file] has os.FileInfo type
+ fmt.Println(file.Name())
+}
+```
+
+### Download file to byte array
+```go
+webdavFilePath := "folder/subfolder/file.txt"
+localFilePath := "/tmp/webdav/file.txt"
+
+bytes, _ := c.Read(webdavFilePath)
+ioutil.WriteFile(localFilePath, bytes, 0644)
+```
+
+### Download file via reader
+Also you can use `c.ReadStream()` method:
+```go
+webdavFilePath := "folder/subfolder/file.txt"
+localFilePath := "/tmp/webdav/file.txt"
+
+reader, _ := c.ReadStream(webdavFilePath)
+
+file, _ := os.Create(localFilePath)
+defer file.Close()
+
+io.Copy(file, reader)
+```
+
+### Upload file from byte array
+```go
+webdavFilePath := "folder/subfolder/file.txt"
+localFilePath := "/tmp/webdav/file.txt"
+
+bytes, _ := ioutil.ReadFile(localFilePath)
+
+c.Write(webdavFilePath, bytes, 0644)
+```
+
+### Upload file via writer
+```go
+webdavFilePath := "folder/subfolder/file.txt"
+localFilePath := "/tmp/webdav/file.txt"
+
+file, _ := os.Open(localFilePath)
+defer file.Close()
+
+c.WriteStream(webdavFilePath, file, 0644)
+```
+
+### Get information about specified file/folder
+```go
+webdavFilePath := "folder/subfolder/file.txt"
+
+info := c.Stat(webdavFilePath)
+//notice that [info] has os.FileInfo type
+fmt.Println(info)
+```
+
+### Move file to another location
+```go
+oldPath := "folder/subfolder/file.txt"
+newPath := "folder/subfolder/moved.txt"
+isOverwrite := true
+
+c.Rename(oldPath, newPath, isOverwrite)
+```
+
+### Copy file to another location
+```go
+oldPath := "folder/subfolder/file.txt"
+newPath := "folder/subfolder/file-copy.txt"
+isOverwrite := true
+
+c.Copy(oldPath, newPath, isOverwrite)
+```
+
+### Delete file
+```go
+webdavFilePath := "folder/subfolder/file.txt"
+
+c.Remove(webdavFilePath)
+```
+
+## Links
+
+More details about WebDAV server you can read from following resources:
+
+* [RFC 4918 - HTTP Extensions for Web Distributed Authoring and Versioning (WebDAV)](https://tools.ietf.org/html/rfc4918)
+* [RFC 5689 - Extended MKCOL for Web Distributed Authoring and Versioning (WebDAV)](https://tools.ietf.org/html/rfc5689)
+* [RFC 2616 - HTTP/1.1 Status Code Definitions](http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html "HTTP/1.1 Status Code Definitions")
+* [WebDav: Next Generation Collaborative Web Authoring By Lisa Dusseaul](https://books.google.de/books?isbn=0130652083 "WebDav: Next Generation Collaborative Web Authoring By Lisa Dusseault")
+
+**NOTICE**: RFC 2518 is obsoleted by RFC 4918 in June 2007
+
+## Contributing
+All contributing are welcome. If you have any suggestions or find some bug - please create an Issue to let us make this project better. We appreciate your help!
+
+## License
+This library is distributed under the BSD 3-Clause license found in the [LICENSE](https://github.com/studio-b12/gowebdav/blob/master/LICENSE) file.
+## API
+
+`import "github.com/studio-b12/gowebdav"`
+
+* [Overview](#pkg-overview)
+* [Index](#pkg-index)
+* [Examples](#pkg-examples)
+* [Subdirectories](#pkg-subdirectories)
+
+### Overview
+Package gowebdav is a WebDAV client library with a command line tool
+included.
+
+### Index
+* [func FixSlash(s string) string](#FixSlash)
+* [func FixSlashes(s string) string](#FixSlashes)
+* [func Join(path0 string, path1 string) string](#Join)
+* [func PathEscape(path string) string](#PathEscape)
+* [func ReadConfig(uri, netrc string) (string, string)](#ReadConfig)
+* [func String(r io.Reader) string](#String)
+* [type Authenticator](#Authenticator)
+* [type BasicAuth](#BasicAuth)
+ * [func (b *BasicAuth) Authorize(req *http.Request, method string, path string)](#BasicAuth.Authorize)
+ * [func (b *BasicAuth) Pass() string](#BasicAuth.Pass)
+ * [func (b *BasicAuth) Type() string](#BasicAuth.Type)
+ * [func (b *BasicAuth) User() string](#BasicAuth.User)
+* [type Client](#Client)
+ * [func NewClient(uri, user, pw string) *Client](#NewClient)
+ * [func (c *Client) Connect() error](#Client.Connect)
+ * [func (c *Client) Copy(oldpath, newpath string, overwrite bool) error](#Client.Copy)
+ * [func (c *Client) Mkdir(path string, _ os.FileMode) error](#Client.Mkdir)
+ * [func (c *Client) MkdirAll(path string, _ os.FileMode) error](#Client.MkdirAll)
+ * [func (c *Client) Read(path string) ([]byte, error)](#Client.Read)
+ * [func (c *Client) ReadDir(path string) ([]os.FileInfo, error)](#Client.ReadDir)
+ * [func (c *Client) ReadStream(path string) (io.ReadCloser, error)](#Client.ReadStream)
+ * [func (c *Client) ReadStreamRange(path string, offset, length int64) (io.ReadCloser, error)](#Client.ReadStreamRange)
+ * [func (c *Client) Remove(path string) error](#Client.Remove)
+ * [func (c *Client) RemoveAll(path string) error](#Client.RemoveAll)
+ * [func (c *Client) Rename(oldpath, newpath string, overwrite bool) error](#Client.Rename)
+ * [func (c *Client) SetHeader(key, value string)](#Client.SetHeader)
+ * [func (c *Client) SetInterceptor(interceptor func(method string, rq *http.Request))](#Client.SetInterceptor)
+ * [func (c *Client) SetTimeout(timeout time.Duration)](#Client.SetTimeout)
+ * [func (c *Client) SetTransport(transport http.RoundTripper)](#Client.SetTransport)
+ * [func (c *Client) Stat(path string) (os.FileInfo, error)](#Client.Stat)
+ * [func (c *Client) Write(path string, data []byte, _ os.FileMode) error](#Client.Write)
+ * [func (c *Client) WriteStream(path string, stream io.Reader, _ os.FileMode) error](#Client.WriteStream)
+* [type DigestAuth](#DigestAuth)
+ * [func (d *DigestAuth) Authorize(req *http.Request, method string, path string)](#DigestAuth.Authorize)
+ * [func (d *DigestAuth) Pass() string](#DigestAuth.Pass)
+ * [func (d *DigestAuth) Type() string](#DigestAuth.Type)
+ * [func (d *DigestAuth) User() string](#DigestAuth.User)
+* [type File](#File)
+ * [func (f File) ContentType() string](#File.ContentType)
+ * [func (f File) ETag() string](#File.ETag)
+ * [func (f File) IsDir() bool](#File.IsDir)
+ * [func (f File) ModTime() time.Time](#File.ModTime)
+ * [func (f File) Mode() os.FileMode](#File.Mode)
+ * [func (f File) Name() string](#File.Name)
+ * [func (f File) Path() string](#File.Path)
+ * [func (f File) Size() int64](#File.Size)
+ * [func (f File) String() string](#File.String)
+ * [func (f File) Sys() interface{}](#File.Sys)
+* [type NoAuth](#NoAuth)
+ * [func (n *NoAuth) Authorize(req *http.Request, method string, path string)](#NoAuth.Authorize)
+ * [func (n *NoAuth) Pass() string](#NoAuth.Pass)
+ * [func (n *NoAuth) Type() string](#NoAuth.Type)
+ * [func (n *NoAuth) User() string](#NoAuth.User)
+
+##### Examples
+* [PathEscape](#example_PathEscape)
+
+##### Package files
+[basicAuth.go](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go) [client.go](https://github.com/studio-b12/gowebdav/blob/master/client.go) [digestAuth.go](https://github.com/studio-b12/gowebdav/blob/master/digestAuth.go) [doc.go](https://github.com/studio-b12/gowebdav/blob/master/doc.go) [file.go](https://github.com/studio-b12/gowebdav/blob/master/file.go) [netrc.go](https://github.com/studio-b12/gowebdav/blob/master/netrc.go) [requests.go](https://github.com/studio-b12/gowebdav/blob/master/requests.go) [utils.go](https://github.com/studio-b12/gowebdav/blob/master/utils.go)
+
+### func [FixSlash](https://github.com/studio-b12/gowebdav/blob/master/utils.go?s=707:737#L45)
+``` go
+func FixSlash(s string) string
+```
+FixSlash appends a trailing / to our string
+
+### func [FixSlashes](https://github.com/studio-b12/gowebdav/blob/master/utils.go?s=859:891#L53)
+``` go
+func FixSlashes(s string) string
+```
+FixSlashes appends and prepends a / if they are missing
+
+### func [Join](https://github.com/studio-b12/gowebdav/blob/master/utils.go?s=992:1036#L62)
+``` go
+func Join(path0 string, path1 string) string
+```
+Join joins two paths
+
+### func [PathEscape](https://github.com/studio-b12/gowebdav/blob/master/utils.go?s=506:541#L36)
+``` go
+func PathEscape(path string) string
+```
+PathEscape escapes all segments of a given path
+
+### func [ReadConfig](https://github.com/studio-b12/gowebdav/blob/master/netrc.go?s=428:479#L27)
+``` go
+func ReadConfig(uri, netrc string) (string, string)
+```
+ReadConfig reads login and password configuration from ~/.netrc
+machine foo.com login username password 123456
+
+### func [String](https://github.com/studio-b12/gowebdav/blob/master/utils.go?s=1166:1197#L67)
+``` go
+func String(r io.Reader) string
+```
+String pulls a string out of our io.Reader
+
+### type [Authenticator](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=388:507#L29)
+``` go
+type Authenticator interface {
+ Type() string
+ User() string
+ Pass() string
+ Authorize(*http.Request, string, string)
+}
+```
+Authenticator stub
+
+### type [BasicAuth](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=106:157#L9)
+``` go
+type BasicAuth struct {
+ // contains filtered or unexported fields
+}
+```
+BasicAuth structure holds our credentials
+
+#### func (\*BasicAuth) [Authorize](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=473:549#L30)
+``` go
+func (b *BasicAuth) Authorize(req *http.Request, method string, path string)
+```
+Authorize the current request
+
+#### func (\*BasicAuth) [Pass](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=388:421#L25)
+``` go
+func (b *BasicAuth) Pass() string
+```
+Pass holds the BasicAuth password
+
+#### func (\*BasicAuth) [Type](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=201:234#L15)
+``` go
+func (b *BasicAuth) Type() string
+```
+Type identifies the BasicAuthenticator
+
+#### func (\*BasicAuth) [User](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=297:330#L20)
+``` go
+func (b *BasicAuth) User() string
+```
+User holds the BasicAuth username
+
+### type [Client](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=172:364#L18)
+``` go
+type Client struct {
+ // contains filtered or unexported fields
+}
+```
+Client defines our structure
+
+#### func [NewClient](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1019:1063#L62)
+``` go
+func NewClient(uri, user, pw string) *Client
+```
+NewClient creates a new instance of client
+
+#### func (\*Client) [Connect](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1843:1875#L87)
+``` go
+func (c *Client) Connect() error
+```
+Connect connects to our dav server
+
+#### func (\*Client) [Copy](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=6702:6770#L313)
+``` go
+func (c *Client) Copy(oldpath, newpath string, overwrite bool) error
+```
+Copy copies a file from A to B
+
+#### func (\*Client) [Mkdir](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=5793:5849#L272)
+``` go
+func (c *Client) Mkdir(path string, _ os.FileMode) error
+```
+Mkdir makes a directory
+
+#### func (\*Client) [MkdirAll](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=6028:6087#L283)
+``` go
+func (c *Client) MkdirAll(path string, _ os.FileMode) error
+```
+MkdirAll like mkdir -p, but for webdav
+
+#### func (\*Client) [Read](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=6876:6926#L318)
+``` go
+func (c *Client) Read(path string) ([]byte, error)
+```
+Read reads the contents of a remote file
+
+#### func (\*Client) [ReadDir](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=2869:2929#L130)
+``` go
+func (c *Client) ReadDir(path string) ([]os.FileInfo, error)
+```
+ReadDir reads the contents of a remote directory
+
+#### func (\*Client) [ReadStream](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=7237:7300#L336)
+``` go
+func (c *Client) ReadStream(path string) (io.ReadCloser, error)
+```
+ReadStream reads the stream for a given path
+
+#### func (\*Client) [ReadStreamRange](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=8049:8139#L358)
+``` go
+func (c *Client) ReadStreamRange(path string, offset, length int64) (io.ReadCloser, error)
+```
+ReadStreamRange reads the stream representing a subset of bytes for a given path,
+utilizing HTTP Range Requests if the server supports it.
+The range is expressed as offset from the start of the file and length, for example
+offset=10, length=10 will return bytes 10 through 19.
+
+If the server does not support partial content requests and returns full content instead,
+this function will emulate the behavior by skipping `offset` bytes and limiting the result
+to `length`.
+
+#### func (\*Client) [Remove](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=5299:5341#L249)
+``` go
+func (c *Client) Remove(path string) error
+```
+Remove removes a remote file
+
+#### func (\*Client) [RemoveAll](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=5407:5452#L254)
+``` go
+func (c *Client) RemoveAll(path string) error
+```
+RemoveAll removes remote files
+
+#### func (\*Client) [Rename](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=6536:6606#L308)
+``` go
+func (c *Client) Rename(oldpath, newpath string, overwrite bool) error
+```
+Rename moves a file from A to B
+
+#### func (\*Client) [SetHeader](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1235:1280#L67)
+``` go
+func (c *Client) SetHeader(key, value string)
+```
+SetHeader lets us set arbitrary headers for a given client
+
+#### func (\*Client) [SetInterceptor](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1387:1469#L72)
+``` go
+func (c *Client) SetInterceptor(interceptor func(method string, rq *http.Request))
+```
+SetInterceptor lets us set an arbitrary interceptor for a given client
+
+#### func (\*Client) [SetTimeout](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1571:1621#L77)
+``` go
+func (c *Client) SetTimeout(timeout time.Duration)
+```
+SetTimeout exposes the ability to set a time limit for requests
+
+#### func (\*Client) [SetTransport](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1714:1772#L82)
+``` go
+func (c *Client) SetTransport(transport http.RoundTripper)
+```
+SetTransport exposes the ability to define custom transports
+
+#### func (\*Client) [Stat](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=4255:4310#L197)
+``` go
+func (c *Client) Stat(path string) (os.FileInfo, error)
+```
+Stat returns the file stats for a specified path
+
+#### func (\*Client) [Write](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=9051:9120#L388)
+``` go
+func (c *Client) Write(path string, data []byte, _ os.FileMode) error
+```
+Write writes data to a given path
+
+#### func (\*Client) [WriteStream](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=9476:9556#L411)
+``` go
+func (c *Client) WriteStream(path string, stream io.Reader, _ os.FileMode) error
+```
+WriteStream writes a stream
+
+### type [DigestAuth](https://github.com/studio-b12/gowebdav/blob/master/digestAuth.go?s=157:254#L14)
+``` go
+type DigestAuth struct {
+ // contains filtered or unexported fields
+}
+```
+DigestAuth structure holds our credentials
+
+#### func (\*DigestAuth) [Authorize](https://github.com/studio-b12/gowebdav/blob/master/digestAuth.go?s=577:654#L36)
+``` go
+func (d *DigestAuth) Authorize(req *http.Request, method string, path string)
+```
+Authorize the current request
+
+#### func (\*DigestAuth) [Pass](https://github.com/studio-b12/gowebdav/blob/master/digestAuth.go?s=491:525#L31)
+``` go
+func (d *DigestAuth) Pass() string
+```
+Pass holds the DigestAuth password
+
+#### func (\*DigestAuth) [Type](https://github.com/studio-b12/gowebdav/blob/master/digestAuth.go?s=299:333#L21)
+``` go
+func (d *DigestAuth) Type() string
+```
+Type identifies the DigestAuthenticator
+
+#### func (\*DigestAuth) [User](https://github.com/studio-b12/gowebdav/blob/master/digestAuth.go?s=398:432#L26)
+``` go
+func (d *DigestAuth) User() string
+```
+User holds the DigestAuth username
+
+### type [File](https://github.com/studio-b12/gowebdav/blob/master/file.go?s=93:253#L10)
+``` go
+type File struct {
+ // contains filtered or unexported fields
+}
+```
+File is our structure for a given file
+
+#### func (File) [ContentType](https://github.com/studio-b12/gowebdav/blob/master/file.go?s=476:510#L31)
+``` go
+func (f File) ContentType() string
+```
+ContentType returns the content type of a file
+
+#### func (File) [ETag](https://github.com/studio-b12/gowebdav/blob/master/file.go?s=929:956#L56)
+``` go
+func (f File) ETag() string
+```
+ETag returns the ETag of a file
+
+#### func (File) [IsDir](https://github.com/studio-b12/gowebdav/blob/master/file.go?s=1035:1061#L61)
+``` go
+func (f File) IsDir() bool
+```
+IsDir let us see if a given file is a directory or not
+
+#### func (File) [ModTime](https://github.com/studio-b12/gowebdav/blob/master/file.go?s=836:869#L51)
+``` go
+func (f File) ModTime() time.Time
+```
+ModTime returns the modified time of a file
+
+#### func (File) [Mode](https://github.com/studio-b12/gowebdav/blob/master/file.go?s=665:697#L41)
+``` go
+func (f File) Mode() os.FileMode
+```
+Mode will return the mode of a given file
+
+#### func (File) [Name](https://github.com/studio-b12/gowebdav/blob/master/file.go?s=378:405#L26)
+``` go
+func (f File) Name() string
+```
+Name returns the name of a file
+
+#### func (File) [Path](https://github.com/studio-b12/gowebdav/blob/master/file.go?s=295:322#L21)
+``` go
+func (f File) Path() string
+```
+Path returns the full path of a file
+
+#### func (File) [Size](https://github.com/studio-b12/gowebdav/blob/master/file.go?s=573:599#L36)
+``` go
+func (f File) Size() int64
+```
+Size returns the size of a file
+
+#### func (File) [String](https://github.com/studio-b12/gowebdav/blob/master/file.go?s=1183:1212#L71)
+``` go
+func (f File) String() string
+```
+String lets us see file information
+
+#### func (File) [Sys](https://github.com/studio-b12/gowebdav/blob/master/file.go?s=1095:1126#L66)
+``` go
+func (f File) Sys() interface{}
+```
+Sys ????
+
+### type [NoAuth](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=551:599#L37)
+``` go
+type NoAuth struct {
+ // contains filtered or unexported fields
+}
+```
+NoAuth structure holds our credentials
+
+#### func (\*NoAuth) [Authorize](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=894:967#L58)
+``` go
+func (n *NoAuth) Authorize(req *http.Request, method string, path string)
+```
+Authorize the current request
+
+#### func (\*NoAuth) [Pass](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=812:842#L53)
+``` go
+func (n *NoAuth) Pass() string
+```
+Pass returns the current password
+
+#### func (\*NoAuth) [Type](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=638:668#L43)
+``` go
+func (n *NoAuth) Type() string
+```
+Type identifies the authenticator
+
+#### func (\*NoAuth) [User](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=724:754#L48)
+``` go
+func (n *NoAuth) User() string
+```
+User returns the current user
+
+- - -
+Generated by [godoc2md](http://godoc.org/github.com/davecheney/godoc2md)
diff --git a/pkg/gowebdav/basicAuth.go b/pkg/gowebdav/basicAuth.go
new file mode 100644
index 0000000..bdb86da
--- /dev/null
+++ b/pkg/gowebdav/basicAuth.go
@@ -0,0 +1,34 @@
+package gowebdav
+
+import (
+ "encoding/base64"
+ "net/http"
+)
+
+// BasicAuth structure holds our credentials
+type BasicAuth struct {
+ user string
+ pw string
+}
+
+// Type identifies the BasicAuthenticator
+func (b *BasicAuth) Type() string {
+ return "BasicAuth"
+}
+
+// User holds the BasicAuth username
+func (b *BasicAuth) User() string {
+ return b.user
+}
+
+// Pass holds the BasicAuth password
+func (b *BasicAuth) Pass() string {
+ return b.pw
+}
+
+// Authorize the current request
+func (b *BasicAuth) Authorize(req *http.Request, method string, path string) {
+ a := b.user + ":" + b.pw
+ auth := "Basic " + base64.StdEncoding.EncodeToString([]byte(a))
+ req.Header.Set("Authorization", auth)
+}
diff --git a/pkg/gowebdav/client.go b/pkg/gowebdav/client.go
new file mode 100644
index 0000000..2fca0b7
--- /dev/null
+++ b/pkg/gowebdav/client.go
@@ -0,0 +1,484 @@
+package gowebdav
+
+import (
+ "bytes"
+ "encoding/xml"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "os"
+ pathpkg "path"
+ "strings"
+ "sync"
+ "time"
+)
+
+// Client defines our structure
+type Client struct {
+ root string
+ headers http.Header
+ interceptor func(method string, rq *http.Request)
+ c *http.Client
+
+ authMutex sync.Mutex
+ auth Authenticator
+}
+
+// Authenticator stub
+type Authenticator interface {
+ Type() string
+ User() string
+ Pass() string
+ Authorize(*http.Request, string, string)
+}
+
+// NoAuth structure holds our credentials
+type NoAuth struct {
+ user string
+ pw string
+}
+
+// Type identifies the authenticator
+func (n *NoAuth) Type() string {
+ return "NoAuth"
+}
+
+// User returns the current user
+func (n *NoAuth) User() string {
+ return n.user
+}
+
+// Pass returns the current password
+func (n *NoAuth) Pass() string {
+ return n.pw
+}
+
+// Authorize the current request
+func (n *NoAuth) Authorize(req *http.Request, method string, path string) {
+}
+
+// NewClient creates a new instance of client
+func NewClient(uri, user, pw string) *Client {
+ return &Client{FixSlash(uri), make(http.Header), nil, &http.Client{}, sync.Mutex{}, &NoAuth{user, pw}}
+}
+
+// SetHeader lets us set arbitrary headers for a given client
+func (c *Client) SetHeader(key, value string) {
+ c.headers.Add(key, value)
+}
+
+// SetInterceptor lets us set an arbitrary interceptor for a given client
+func (c *Client) SetInterceptor(interceptor func(method string, rq *http.Request)) {
+ c.interceptor = interceptor
+}
+
+// SetTimeout exposes the ability to set a time limit for requests
+func (c *Client) SetTimeout(timeout time.Duration) {
+ c.c.Timeout = timeout
+}
+
+// SetTransport exposes the ability to define custom transports
+func (c *Client) SetTransport(transport http.RoundTripper) {
+ c.c.Transport = transport
+}
+
+// SetJar exposes the ability to set a cookie jar to the client.
+func (c *Client) SetJar(jar http.CookieJar) {
+ c.c.Jar = jar
+}
+
+// Connect connects to our dav server
+func (c *Client) Connect() error {
+ rs, err := c.options("/")
+ if err != nil {
+ return err
+ }
+
+ err = rs.Body.Close()
+ if err != nil {
+ return err
+ }
+
+ if rs.StatusCode != 200 {
+ return newPathError("Connect", c.root, rs.StatusCode)
+ }
+
+ return nil
+}
+
+type props struct {
+ Status string `xml:"DAV: status"`
+ Name string `xml:"DAV: prop>displayname,omitempty"`
+ Type xml.Name `xml:"DAV: prop>resourcetype>collection,omitempty"`
+ Size string `xml:"DAV: prop>getcontentlength,omitempty"`
+ ContentType string `xml:"DAV: prop>getcontenttype,omitempty"`
+ ETag string `xml:"DAV: prop>getetag,omitempty"`
+ Modified string `xml:"DAV: prop>getlastmodified,omitempty"`
+}
+
+type response struct {
+ Href string `xml:"DAV: href"`
+ Props []props `xml:"DAV: propstat"`
+}
+
+func getProps(r *response, status string) *props {
+ for _, prop := range r.Props {
+ if strings.Contains(prop.Status, status) {
+ return &prop
+ }
+ }
+ return nil
+}
+
+// ReadDir reads the contents of a remote directory
+func (c *Client) ReadDir(path string) ([]os.FileInfo, error) {
+ path = FixSlashes(path)
+ files := make([]os.FileInfo, 0)
+ skipSelf := true
+ parse := func(resp interface{}) error {
+ r := resp.(*response)
+
+ if skipSelf {
+ skipSelf = false
+ if p := getProps(r, "200"); p != nil && p.Type.Local == "collection" {
+ r.Props = nil
+ return nil
+ }
+ return newPathError("ReadDir", path, 405)
+ }
+
+ if p := getProps(r, "200"); p != nil {
+ f := new(File)
+ if ps, err := url.PathUnescape(r.Href); err == nil {
+ f.name = pathpkg.Base(ps)
+ } else {
+ f.name = p.Name
+ }
+ f.path = path + f.name
+ f.modified = parseModified(&p.Modified)
+ f.etag = p.ETag
+ f.contentType = p.ContentType
+
+ if p.Type.Local == "collection" {
+ f.path += "/"
+ f.size = 0
+ f.isdir = true
+ } else {
+ f.size = parseInt64(&p.Size)
+ f.isdir = false
+ }
+
+ files = append(files, *f)
+ }
+
+ r.Props = nil
+ return nil
+ }
+
+ err := c.propfind(path, false,
+ `
+
+
+
+
+
+
+
+
+ `,
+ &response{},
+ parse)
+
+ if err != nil {
+ if _, ok := err.(*os.PathError); !ok {
+ err = newPathErrorErr("ReadDir", path, err)
+ }
+ }
+ return files, err
+}
+
+// Stat returns the file stats for a specified path
+func (c *Client) Stat(path string) (os.FileInfo, error) {
+ var f *File
+ parse := func(resp interface{}) error {
+ r := resp.(*response)
+ if p := getProps(r, "200"); p != nil && f == nil {
+ f = new(File)
+ f.name = p.Name
+ f.path = path
+ f.etag = p.ETag
+ f.contentType = p.ContentType
+
+ if p.Type.Local == "collection" {
+ if !strings.HasSuffix(f.path, "/") {
+ f.path += "/"
+ }
+ f.size = 0
+ f.modified = time.Unix(0, 0)
+ f.isdir = true
+ } else {
+ f.size = parseInt64(&p.Size)
+ f.modified = parseModified(&p.Modified)
+ f.isdir = false
+ }
+ }
+
+ r.Props = nil
+ return nil
+ }
+
+ err := c.propfind(path, true,
+ `
+
+
+
+
+
+
+
+
+ `,
+ &response{},
+ parse)
+
+ if err != nil {
+ if _, ok := err.(*os.PathError); !ok {
+ err = newPathErrorErr("ReadDir", path, err)
+ }
+ }
+ return f, err
+}
+
+// Remove removes a remote file
+func (c *Client) Remove(path string) error {
+ return c.RemoveAll(path)
+}
+
+// RemoveAll removes remote files
+func (c *Client) RemoveAll(path string) error {
+ rs, err := c.req("DELETE", path, nil, nil)
+ if err != nil {
+ return newPathError("Remove", path, 400)
+ }
+ err = rs.Body.Close()
+ if err != nil {
+ return err
+ }
+
+ if rs.StatusCode == 200 || rs.StatusCode == 204 || rs.StatusCode == 404 {
+ return nil
+ }
+
+ return newPathError("Remove", path, rs.StatusCode)
+}
+
+// Mkdir makes a directory
+func (c *Client) Mkdir(path string, _ os.FileMode) (err error) {
+ path = FixSlashes(path)
+ status, err := c.mkcol(path)
+ if err != nil {
+ return
+ }
+ if status == 201 {
+ return nil
+ }
+
+ return newPathError("Mkdir", path, status)
+}
+
+// MkdirAll like mkdir -p, but for webdav
+func (c *Client) MkdirAll(path string, _ os.FileMode) (err error) {
+ path = FixSlashes(path)
+ status, err := c.mkcol(path)
+ if err != nil {
+ return
+ }
+ if status == 201 {
+ return nil
+ }
+ if status == 409 {
+ paths := strings.Split(path, "/")
+ sub := "/"
+ for _, e := range paths {
+ if e == "" {
+ continue
+ }
+ sub += e + "/"
+ status, err = c.mkcol(sub)
+ if err != nil {
+ return
+ }
+ if status != 201 {
+ return newPathError("MkdirAll", sub, status)
+ }
+ }
+ return nil
+ }
+
+ return newPathError("MkdirAll", path, status)
+}
+
+// Rename moves a file from A to B
+func (c *Client) Rename(oldpath, newpath string, overwrite bool) error {
+ return c.copymove("MOVE", oldpath, newpath, overwrite)
+}
+
+// Copy copies a file from A to B
+func (c *Client) Copy(oldpath, newpath string, overwrite bool) error {
+ return c.copymove("COPY", oldpath, newpath, overwrite)
+}
+
+// Read reads the contents of a remote file
+func (c *Client) Read(path string) ([]byte, error) {
+ var stream io.ReadCloser
+ var err error
+
+ if stream, _, err = c.ReadStream(path, nil); err != nil {
+ return nil, err
+ }
+ defer stream.Close()
+
+ buf := new(bytes.Buffer)
+ _, err = buf.ReadFrom(stream)
+ if err != nil {
+ return nil, err
+ }
+ return buf.Bytes(), nil
+}
+
+func (c *Client) Link(path string) (string, http.Header, error) {
+ method := "GET"
+ u := PathEscape(Join(c.root, path))
+ r, err := http.NewRequest(method, u, nil)
+
+ if err != nil {
+ return "", nil, newPathErrorErr("Link", path, err)
+ }
+
+ if c.c.Jar != nil {
+ for _, cookie := range c.c.Jar.Cookies(r.URL) {
+ r.AddCookie(cookie)
+ }
+ }
+ for k, vals := range c.headers {
+ for _, v := range vals {
+ r.Header.Add(k, v)
+ }
+ }
+
+ c.authMutex.Lock()
+ auth := c.auth
+ c.authMutex.Unlock()
+
+ auth.Authorize(r, method, path)
+
+ if c.interceptor != nil {
+ c.interceptor(method, r)
+ }
+ return r.URL.String(), r.Header, nil
+}
+
+// ReadStream reads the stream for a given path
+func (c *Client) ReadStream(path string, callback func(rq *http.Request)) (io.ReadCloser, http.Header, error) {
+ rs, err := c.req("GET", path, nil, callback)
+ if err != nil {
+ return nil, nil, newPathErrorErr("ReadStream", path, err)
+ }
+
+ if rs.StatusCode < 400 {
+ return rs.Body, rs.Header, nil
+ }
+
+ rs.Body.Close()
+ return nil, nil, newPathError("ReadStream", path, rs.StatusCode)
+}
+
+// ReadStreamRange reads the stream representing a subset of bytes for a given path,
+// utilizing HTTP Range Requests if the server supports it.
+// The range is expressed as offset from the start of the file and length, for example
+// offset=10, length=10 will return bytes 10 through 19.
+//
+// If the server does not support partial content requests and returns full content instead,
+// this function will emulate the behavior by skipping `offset` bytes and limiting the result
+// to `length`.
+func (c *Client) ReadStreamRange(path string, offset, length int64) (io.ReadCloser, error) {
+ rs, err := c.req("GET", path, nil, func(r *http.Request) {
+ r.Header.Add("Range", fmt.Sprintf("bytes=%v-%v", offset, offset+length-1))
+ })
+ if err != nil {
+ return nil, newPathErrorErr("ReadStreamRange", path, err)
+ }
+
+ if rs.StatusCode == http.StatusPartialContent {
+ // server supported partial content, return as-is.
+ return rs.Body, nil
+ }
+
+ // server returned success, but did not support partial content, so we have the whole
+ // stream in rs.Body
+ if rs.StatusCode == 200 {
+ // discard first 'offset' bytes.
+ if _, err := io.Copy(io.Discard, io.LimitReader(rs.Body, offset)); err != nil {
+ return nil, newPathErrorErr("ReadStreamRange", path, err)
+ }
+
+ // return a io.ReadCloser that is limited to `length` bytes.
+ return &limitedReadCloser{rs.Body, int(length)}, nil
+ }
+
+ rs.Body.Close()
+ return nil, newPathError("ReadStream", path, rs.StatusCode)
+}
+
+// Write writes data to a given path
+func (c *Client) Write(path string, data []byte, _ os.FileMode) (err error) {
+ s, err := c.put(path, bytes.NewReader(data), nil)
+ if err != nil {
+ return
+ }
+
+ switch s {
+
+ case 200, 201, 204:
+ return nil
+
+ case 409:
+ err = c.createParentCollection(path)
+ if err != nil {
+ return
+ }
+
+ s, err = c.put(path, bytes.NewReader(data), nil)
+ if err != nil {
+ return
+ }
+ if s == 200 || s == 201 || s == 204 {
+ return
+ }
+ }
+
+ return newPathError("Write", path, s)
+}
+
+// WriteStream writes a stream
+func (c *Client) WriteStream(path string, stream io.Reader, _ os.FileMode, callback func(r *http.Request)) (err error) {
+
+ err = c.createParentCollection(path)
+ if err != nil {
+ return err
+ }
+
+ s, err := c.put(path, stream, callback)
+ if err != nil {
+ return err
+ }
+
+ switch s {
+ case 200, 201, 204:
+ return nil
+
+ default:
+ return newPathError("WriteStream", path, s)
+ }
+}
diff --git a/pkg/gowebdav/cmd/gowebdav/README.md b/pkg/gowebdav/cmd/gowebdav/README.md
new file mode 100644
index 0000000..30e1d4c
--- /dev/null
+++ b/pkg/gowebdav/cmd/gowebdav/README.md
@@ -0,0 +1,103 @@
+# Description
+Command line tool for [gowebdav](https://github.com/studio-b12/gowebdav) library.
+
+# Prerequisites
+## Software
+* **OS**: all, which are supported by `Golang`
+* **Golang**: version 1.x
+* **Git**: version 2.14.2 at higher (required to install via `go get`)
+
+# Install
+```sh
+go get -u github.com/studio-b12/gowebdav/cmd/gowebdav
+```
+
+# Usage
+It is recommended to set following environment variables to improve your experience with this tool:
+* `ROOT` is an URL of target WebDAV server (e.g. `https://webdav.mydomain.me/user_root_folder`)
+* `USER` is a login to connect to specified server (e.g. `user`)
+* `PASSWORD` is a password to connect to specified server (e.g. `p@s$w0rD`)
+
+In following examples we suppose that:
+* environment variable `ROOT` is set to `https://webdav.mydomain.me/ufolder`
+* environment variable `USER` is set to `user`
+* environment variable `PASSWORD` is set `p@s$w0rD`
+* folder `/ufolder/temp` exists on the server
+* file `/ufolder/temp/file.txt` exists on the server
+* file `/ufolder/temp/document.rtf` exists on the server
+* file `/tmp/webdav/to_upload.txt` exists on the local machine
+* folder `/tmp/webdav/` is used to download files from the server
+
+## Examples
+
+#### Get content of specified folder
+```sh
+gowebdav -X LS temp
+```
+
+#### Get info about file/folder
+```sh
+gowebdav -X STAT temp
+gowebdav -X STAT temp/file.txt
+```
+
+#### Create folder on the remote server
+```sh
+gowebdav -X MKDIR temp2
+gowebdav -X MKDIRALL all/folders/which-you-want/to_create
+```
+
+#### Download file
+```sh
+gowebdav -X GET temp/document.rtf /tmp/webdav/document.rtf
+```
+
+You may do not specify target local path, in this case file will be downloaded to the current folder with the
+
+#### Upload file
+```sh
+gowebdav -X PUT temp/uploaded.txt /tmp/webdav/to_upload.txt
+```
+
+#### Move file on the remote server
+```sh
+gowebdav -X MV temp/file.txt temp/moved_file.txt
+```
+
+#### Copy file to another location
+```sh
+gowebdav -X MV temp/file.txt temp/file-copy.txt
+```
+
+#### Delete file from the remote server
+```sh
+gowebdav -X DEL temp/file.txt
+```
+
+# Wrapper script
+
+You can create wrapper script for your server (via `$EDITOR ./dav && chmod a+x ./dav`) and add following content to it:
+```sh
+#!/bin/sh
+
+ROOT="https://my.dav.server/" \
+USER="foo" \
+PASSWORD="$(pass dav/foo@my.dav.server)" \
+gowebdav $@
+```
+
+It allows you to use [pass](https://www.passwordstore.org/ "the standard unix password manager") or similar tools to retrieve the password.
+
+## Examples
+
+Using the `dav` wrapper:
+
+```sh
+$ ./dav -X LS /
+
+$ echo hi dav! > hello && ./dav -X PUT /hello
+$ ./dav -X STAT /hello
+$ ./dav -X PUT /hello_dav hello
+$ ./dav -X GET /hello_dav
+$ ./dav -X GET /hello_dav hello.txt
+```
\ No newline at end of file
diff --git a/pkg/gowebdav/cmd/gowebdav/main.go b/pkg/gowebdav/cmd/gowebdav/main.go
new file mode 100644
index 0000000..0164496
--- /dev/null
+++ b/pkg/gowebdav/cmd/gowebdav/main.go
@@ -0,0 +1,263 @@
+package main
+
+import (
+ "errors"
+ "flag"
+ "fmt"
+ "io"
+ "io/fs"
+ "os"
+ "os/user"
+ "path"
+ "path/filepath"
+ "runtime"
+ "strings"
+
+ d "github.com/alist-org/alist/v3/pkg/gowebdav"
+)
+
+func main() {
+ root := flag.String("root", os.Getenv("ROOT"), "WebDAV Endpoint [ENV.ROOT]")
+ user := flag.String("user", os.Getenv("USER"), "User [ENV.USER]")
+ password := flag.String("pw", os.Getenv("PASSWORD"), "Password [ENV.PASSWORD]")
+ netrc := flag.String("netrc-file", filepath.Join(getHome(), ".netrc"), "read login from netrc file")
+ method := flag.String("X", "", `Method:
+ LS
+ STAT
+
+ MKDIR
+ MKDIRALL
+
+ GET []
+ PUT []
+
+ MV
+ CP
+
+ DEL
+ `)
+ flag.Parse()
+
+ if *root == "" {
+ fail("Set WebDAV ROOT")
+ }
+
+ if argsLength := len(flag.Args()); argsLength == 0 || argsLength > 2 {
+ fail("Unsupported arguments")
+ }
+
+ if *password == "" {
+ if u, p := d.ReadConfig(*root, *netrc); u != "" && p != "" {
+ user = &u
+ password = &p
+ }
+ }
+
+ c := d.NewClient(*root, *user, *password)
+
+ cmd := getCmd(*method)
+
+ if e := cmd(c, flag.Arg(0), flag.Arg(1)); e != nil {
+ fail(e)
+ }
+}
+
+func fail(err interface{}) {
+ if err != nil {
+ fmt.Println(err)
+ }
+ os.Exit(-1)
+}
+
+func getHome() string {
+ u, e := user.Current()
+ if e != nil {
+ return os.Getenv("HOME")
+ }
+
+ if u != nil {
+ return u.HomeDir
+ }
+
+ switch runtime.GOOS {
+ case "windows":
+ return ""
+ default:
+ return "~/"
+ }
+}
+
+func getCmd(method string) func(c *d.Client, p0, p1 string) error {
+ switch strings.ToUpper(method) {
+ case "LS", "LIST", "PROPFIND":
+ return cmdLs
+
+ case "STAT":
+ return cmdStat
+
+ case "GET", "PULL", "READ":
+ return cmdGet
+
+ case "DELETE", "RM", "DEL":
+ return cmdRm
+
+ case "MKCOL", "MKDIR":
+ return cmdMkdir
+
+ case "MKCOLALL", "MKDIRALL", "MKDIRP":
+ return cmdMkdirAll
+
+ case "RENAME", "MV", "MOVE":
+ return cmdMv
+
+ case "COPY", "CP":
+ return cmdCp
+
+ case "PUT", "PUSH", "WRITE":
+ return cmdPut
+
+ default:
+ return func(c *d.Client, p0, p1 string) (err error) {
+ return errors.New("Unsupported method: " + method)
+ }
+ }
+}
+
+func cmdLs(c *d.Client, p0, _ string) (err error) {
+ files, err := c.ReadDir(p0)
+ if err == nil {
+ fmt.Println(fmt.Sprintf("ReadDir: '%s' entries: %d ", p0, len(files)))
+ for _, f := range files {
+ fmt.Println(f)
+ }
+ }
+ return
+}
+
+func cmdStat(c *d.Client, p0, _ string) (err error) {
+ file, err := c.Stat(p0)
+ if err == nil {
+ fmt.Println(file)
+ }
+ return
+}
+
+func cmdGet(c *d.Client, p0, p1 string) (err error) {
+ bytes, err := c.Read(p0)
+ if err == nil {
+ if p1 == "" {
+ p1 = filepath.Join(".", p0)
+ }
+ err = writeFile(p1, bytes, 0644)
+ if err == nil {
+ fmt.Println(fmt.Sprintf("Written %d bytes to: %s", len(bytes), p1))
+ }
+ }
+ return
+}
+
+func cmdRm(c *d.Client, p0, _ string) (err error) {
+ if err = c.Remove(p0); err == nil {
+ fmt.Println("Remove: " + p0)
+ }
+ return
+}
+
+func cmdMkdir(c *d.Client, p0, _ string) (err error) {
+ if err = c.Mkdir(p0, 0755); err == nil {
+ fmt.Println("Mkdir: " + p0)
+ }
+ return
+}
+
+func cmdMkdirAll(c *d.Client, p0, _ string) (err error) {
+ if err = c.MkdirAll(p0, 0755); err == nil {
+ fmt.Println("MkdirAll: " + p0)
+ }
+ return
+}
+
+func cmdMv(c *d.Client, p0, p1 string) (err error) {
+ if err = c.Rename(p0, p1, true); err == nil {
+ fmt.Println("Rename: " + p0 + " -> " + p1)
+ }
+ return
+}
+
+func cmdCp(c *d.Client, p0, p1 string) (err error) {
+ if err = c.Copy(p0, p1, true); err == nil {
+ fmt.Println("Copy: " + p0 + " -> " + p1)
+ }
+ return
+}
+
+func cmdPut(c *d.Client, p0, p1 string) (err error) {
+ if p1 == "" {
+ p1 = path.Join(".", p0)
+ } else {
+ var fi fs.FileInfo
+ fi, err = c.Stat(p0)
+ if err != nil && !d.IsErrNotFound(err) {
+ return
+ }
+ if !d.IsErrNotFound(err) && fi.IsDir() {
+ p0 = path.Join(p0, p1)
+ }
+ }
+
+ stream, err := getStream(p1)
+ if err != nil {
+ return
+ }
+ defer stream.Close()
+
+ if err = c.WriteStream(p0, stream, 0644, nil); err == nil {
+ fmt.Println("Put: " + p1 + " -> " + p0)
+ }
+ return
+}
+
+func writeFile(path string, bytes []byte, mode os.FileMode) error {
+ parent := filepath.Dir(path)
+ if _, e := os.Stat(parent); os.IsNotExist(e) {
+ if e := os.MkdirAll(parent, os.ModePerm); e != nil {
+ return e
+ }
+ }
+
+ f, err := os.Create(path)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+
+ _, err = f.Write(bytes)
+ return err
+}
+
+func getStream(pathOrString string) (io.ReadCloser, error) {
+
+ fi, err := os.Stat(pathOrString)
+ if err != nil {
+ return nil, err
+ }
+
+ if fi.IsDir() {
+ return nil, &os.PathError{
+ Op: "Open",
+ Path: pathOrString,
+ Err: errors.New("Path: '" + pathOrString + "' is a directory"),
+ }
+ }
+
+ f, err := os.Open(pathOrString)
+ if err == nil {
+ return f, nil
+ }
+
+ return nil, &os.PathError{
+ Op: "Open",
+ Path: pathOrString,
+ Err: err,
+ }
+}
diff --git a/pkg/gowebdav/digestAuth.go b/pkg/gowebdav/digestAuth.go
new file mode 100644
index 0000000..4a5eb62
--- /dev/null
+++ b/pkg/gowebdav/digestAuth.go
@@ -0,0 +1,146 @@
+package gowebdav
+
+import (
+ "crypto/md5"
+ "crypto/rand"
+ "encoding/hex"
+ "fmt"
+ "io"
+ "net/http"
+ "strings"
+)
+
+// DigestAuth structure holds our credentials
+type DigestAuth struct {
+ user string
+ pw string
+ digestParts map[string]string
+}
+
+// Type identifies the DigestAuthenticator
+func (d *DigestAuth) Type() string {
+ return "DigestAuth"
+}
+
+// User holds the DigestAuth username
+func (d *DigestAuth) User() string {
+ return d.user
+}
+
+// Pass holds the DigestAuth password
+func (d *DigestAuth) Pass() string {
+ return d.pw
+}
+
+// Authorize the current request
+func (d *DigestAuth) Authorize(req *http.Request, method string, path string) {
+ d.digestParts["uri"] = path
+ d.digestParts["method"] = method
+ d.digestParts["username"] = d.user
+ d.digestParts["password"] = d.pw
+ req.Header.Set("Authorization", getDigestAuthorization(d.digestParts))
+}
+
+func digestParts(resp *http.Response) map[string]string {
+ result := map[string]string{}
+ if len(resp.Header["Www-Authenticate"]) > 0 {
+ wantedHeaders := []string{"nonce", "realm", "qop", "opaque", "algorithm", "entityBody"}
+ responseHeaders := strings.Split(resp.Header["Www-Authenticate"][0], ",")
+ for _, r := range responseHeaders {
+ for _, w := range wantedHeaders {
+ if strings.Contains(r, w) {
+ result[w] = strings.Trim(
+ strings.SplitN(r, `=`, 2)[1],
+ `"`,
+ )
+ }
+ }
+ }
+ }
+ return result
+}
+
+func getMD5(text string) string {
+ hasher := md5.New()
+ hasher.Write([]byte(text))
+ return hex.EncodeToString(hasher.Sum(nil))
+}
+
+func getCnonce() string {
+ b := make([]byte, 8)
+ io.ReadFull(rand.Reader, b)
+ return fmt.Sprintf("%x", b)[:16]
+}
+
+func getDigestAuthorization(digestParts map[string]string) string {
+ d := digestParts
+ // These are the correct ha1 and ha2 for qop=auth. We should probably check for other types of qop.
+
+ var (
+ ha1 string
+ ha2 string
+ nonceCount = 00000001
+ cnonce = getCnonce()
+ response string
+ )
+
+ // 'ha1' value depends on value of "algorithm" field
+ switch d["algorithm"] {
+ case "MD5", "":
+ ha1 = getMD5(d["username"] + ":" + d["realm"] + ":" + d["password"])
+ case "MD5-sess":
+ ha1 = getMD5(
+ fmt.Sprintf("%s:%v:%s",
+ getMD5(d["username"]+":"+d["realm"]+":"+d["password"]),
+ nonceCount,
+ cnonce,
+ ),
+ )
+ }
+
+ // 'ha2' value depends on value of "qop" field
+ switch d["qop"] {
+ case "auth", "":
+ ha2 = getMD5(d["method"] + ":" + d["uri"])
+ case "auth-int":
+ if d["entityBody"] != "" {
+ ha2 = getMD5(d["method"] + ":" + d["uri"] + ":" + getMD5(d["entityBody"]))
+ }
+ }
+
+ // 'response' value depends on value of "qop" field
+ switch d["qop"] {
+ case "":
+ response = getMD5(
+ fmt.Sprintf("%s:%s:%s",
+ ha1,
+ d["nonce"],
+ ha2,
+ ),
+ )
+ case "auth", "auth-int":
+ response = getMD5(
+ fmt.Sprintf("%s:%s:%v:%s:%s:%s",
+ ha1,
+ d["nonce"],
+ nonceCount,
+ cnonce,
+ d["qop"],
+ ha2,
+ ),
+ )
+ }
+
+ authorization := fmt.Sprintf(`Digest username="%s", realm="%s", nonce="%s", uri="%s", nc=%v, cnonce="%s", response="%s"`,
+ d["username"], d["realm"], d["nonce"], d["uri"], nonceCount, cnonce, response)
+
+ if d["qop"] != "" {
+ authorization += fmt.Sprintf(`, qop=%s`, d["qop"])
+ }
+
+ if d["opaque"] != "" {
+ authorization += fmt.Sprintf(`, opaque="%s"`, d["opaque"])
+ }
+
+ return authorization
+}
diff --git a/pkg/gowebdav/doc.go b/pkg/gowebdav/doc.go
new file mode 100644
index 0000000..e47d5ee
--- /dev/null
+++ b/pkg/gowebdav/doc.go
@@ -0,0 +1,3 @@
+// Package gowebdav is a WebDAV client library with a command line tool
+// included.
+package gowebdav
diff --git a/pkg/gowebdav/errors.go b/pkg/gowebdav/errors.go
new file mode 100644
index 0000000..bbf1e92
--- /dev/null
+++ b/pkg/gowebdav/errors.go
@@ -0,0 +1,49 @@
+package gowebdav
+
+import (
+ "fmt"
+ "os"
+)
+
+// StatusError implements error and wraps
+// an erroneous status code.
+type StatusError struct {
+ Status int
+}
+
+func (se StatusError) Error() string {
+ return fmt.Sprintf("%d", se.Status)
+}
+
+// IsErrCode returns true if the given error
+// is an os.PathError wrapping a StatusError
+// with the given status code.
+func IsErrCode(err error, code int) bool {
+ if pe, ok := err.(*os.PathError); ok {
+ se, ok := pe.Err.(StatusError)
+ return ok && se.Status == code
+ }
+ return false
+}
+
+// IsErrNotFound is shorthand for IsErrCode
+// for status 404.
+func IsErrNotFound(err error) bool {
+ return IsErrCode(err, 404)
+}
+
+func newPathError(op string, path string, statusCode int) error {
+ return &os.PathError{
+ Op: op,
+ Path: path,
+ Err: StatusError{statusCode},
+ }
+}
+
+func newPathErrorErr(op string, path string, err error) error {
+ return &os.PathError{
+ Op: op,
+ Path: path,
+ Err: err,
+ }
+}
diff --git a/pkg/gowebdav/file.go b/pkg/gowebdav/file.go
new file mode 100644
index 0000000..ae2303f
--- /dev/null
+++ b/pkg/gowebdav/file.go
@@ -0,0 +1,77 @@
+package gowebdav
+
+import (
+ "fmt"
+ "os"
+ "time"
+)
+
+// File is our structure for a given file
+type File struct {
+ path string
+ name string
+ contentType string
+ size int64
+ modified time.Time
+ etag string
+ isdir bool
+}
+
+// Path returns the full path of a file
+func (f File) Path() string {
+ return f.path
+}
+
+// Name returns the name of a file
+func (f File) Name() string {
+ return f.name
+}
+
+// ContentType returns the content type of a file
+func (f File) ContentType() string {
+ return f.contentType
+}
+
+// Size returns the size of a file
+func (f File) Size() int64 {
+ return f.size
+}
+
+// Mode will return the mode of a given file
+func (f File) Mode() os.FileMode {
+ // TODO check webdav perms
+ if f.isdir {
+ return 0775 | os.ModeDir
+ }
+
+ return 0664
+}
+
+// ModTime returns the modified time of a file
+func (f File) ModTime() time.Time {
+ return f.modified
+}
+
+// ETag returns the ETag of a file
+func (f File) ETag() string {
+ return f.etag
+}
+
+// IsDir let us see if a given file is a directory or not
+func (f File) IsDir() bool {
+ return f.isdir
+}
+
+// Sys ????
+func (f File) Sys() interface{} {
+ return nil
+}
+
+// String lets us see file information
+func (f File) String() string {
+ if f.isdir {
+ return fmt.Sprintf("Dir : '%s' - '%s'", f.path, f.name)
+ }
+
+ return fmt.Sprintf("File: '%s' SIZE: %d MODIFIED: %s ETAG: %s CTYPE: %s", f.path, f.size, f.modified.String(), f.etag, f.contentType)
+}
diff --git a/pkg/gowebdav/netrc.go b/pkg/gowebdav/netrc.go
new file mode 100644
index 0000000..df479b5
--- /dev/null
+++ b/pkg/gowebdav/netrc.go
@@ -0,0 +1,54 @@
+package gowebdav
+
+import (
+ "bufio"
+ "fmt"
+ "net/url"
+ "os"
+ "regexp"
+ "strings"
+)
+
+func parseLine(s string) (login, pass string) {
+ fields := strings.Fields(s)
+ for i, f := range fields {
+ if f == "login" {
+ login = fields[i+1]
+ }
+ if f == "password" {
+ pass = fields[i+1]
+ }
+ }
+ return login, pass
+}
+
+// ReadConfig reads login and password configuration from ~/.netrc
+// machine foo.com login username password 123456
+func ReadConfig(uri, netrc string) (string, string) {
+ u, err := url.Parse(uri)
+ if err != nil {
+ return "", ""
+ }
+
+ file, err := os.Open(netrc)
+ if err != nil {
+ return "", ""
+ }
+ defer file.Close()
+
+ re := fmt.Sprintf(`^.*machine %s.*$`, u.Host)
+ scanner := bufio.NewScanner(file)
+ for scanner.Scan() {
+ s := scanner.Text()
+
+ matched, err := regexp.MatchString(re, s)
+ if err != nil {
+ return "", ""
+ }
+ if matched {
+ return parseLine(s)
+ }
+ }
+
+ return "", ""
+}
diff --git a/pkg/gowebdav/requests.go b/pkg/gowebdav/requests.go
new file mode 100644
index 0000000..d623776
--- /dev/null
+++ b/pkg/gowebdav/requests.go
@@ -0,0 +1,218 @@
+package gowebdav
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "net/http"
+ "path"
+ "strings"
+)
+
+func (c *Client) req(method, path string, body io.Reader, intercept func(*http.Request)) (req *http.Response, err error) {
+ var r *http.Request
+ var retryBuf io.Reader
+ canRetry := true
+ if body != nil {
+ // If the authorization fails, we will need to restart reading
+ // from the passed body stream.
+ // When body is seekable, use seek to reset the streams
+ // cursor to the start.
+ // Otherwise, copy the stream into a buffer while uploading
+ // and use the buffers content on retry.
+ if sk, ok := body.(io.Seeker); ok {
+ if _, err = sk.Seek(0, io.SeekStart); err != nil {
+ return
+ }
+ retryBuf = body
+ } else if method == http.MethodPut {
+ canRetry = false
+ } else {
+ buff := &bytes.Buffer{}
+ retryBuf = buff
+ body = io.TeeReader(body, buff)
+ }
+ r, err = http.NewRequest(method, PathEscape(Join(c.root, path)), body)
+ } else {
+ r, err = http.NewRequest(method, PathEscape(Join(c.root, path)), nil)
+ }
+
+ if err != nil {
+ return nil, err
+ }
+
+ for k, vals := range c.headers {
+ for _, v := range vals {
+ r.Header.Add(k, v)
+ }
+ }
+
+ // make sure we read 'c.auth' only once since it will be substituted below
+ // and that is unsafe to do when multiple goroutines are running at the same time.
+ c.authMutex.Lock()
+ auth := c.auth
+ c.authMutex.Unlock()
+
+ auth.Authorize(r, method, path)
+
+ if intercept != nil {
+ intercept(r)
+ }
+
+ if c.interceptor != nil {
+ c.interceptor(method, r)
+ }
+
+ rs, err := c.c.Do(r)
+ if err != nil {
+ return nil, err
+ }
+
+ if rs.StatusCode == 401 && auth.Type() == "NoAuth" {
+ wwwAuthenticateHeader := strings.ToLower(rs.Header.Get("Www-Authenticate"))
+
+ if strings.Index(wwwAuthenticateHeader, "digest") > -1 {
+ c.authMutex.Lock()
+ c.auth = &DigestAuth{auth.User(), auth.Pass(), digestParts(rs)}
+ c.authMutex.Unlock()
+ } else if strings.Index(wwwAuthenticateHeader, "basic") > -1 {
+ c.authMutex.Lock()
+ c.auth = &BasicAuth{auth.User(), auth.Pass()}
+ c.authMutex.Unlock()
+ } else {
+ return rs, newPathError("Authorize", c.root, rs.StatusCode)
+ }
+
+ // retryBuf will be nil if body was nil initially so no check
+ // for body == nil is required here.
+ if canRetry {
+ return c.req(method, path, retryBuf, intercept)
+ }
+ } else if rs.StatusCode == 401 {
+ return rs, newPathError("Authorize", c.root, rs.StatusCode)
+ }
+
+ return rs, err
+}
+
+func (c *Client) mkcol(path string) (status int, err error) {
+ rs, err := c.req("MKCOL", path, nil, nil)
+ if err != nil {
+ return
+ }
+ defer rs.Body.Close()
+
+ status = rs.StatusCode
+ if status == 405 {
+ status = 201
+ }
+
+ return
+}
+
+func (c *Client) options(path string) (*http.Response, error) {
+ return c.req("OPTIONS", path, nil, func(rq *http.Request) {
+ rq.Header.Add("Depth", "0")
+ })
+}
+
+func (c *Client) propfind(path string, self bool, body string, resp interface{}, parse func(resp interface{}) error) error {
+ rs, err := c.req("PROPFIND", path, strings.NewReader(body), func(rq *http.Request) {
+ if self {
+ rq.Header.Add("Depth", "0")
+ } else {
+ rq.Header.Add("Depth", "1")
+ }
+ rq.Header.Add("Content-Type", "application/xml;charset=UTF-8")
+ rq.Header.Add("Accept", "application/xml,text/xml")
+ rq.Header.Add("Accept-Charset", "utf-8")
+ // TODO add support for 'gzip,deflate;q=0.8,q=0.7'
+ rq.Header.Add("Accept-Encoding", "")
+ })
+ if err != nil {
+ return err
+ }
+ defer rs.Body.Close()
+
+ if rs.StatusCode != 207 {
+ return newPathError("PROPFIND", path, rs.StatusCode)
+ }
+
+ return parseXML(rs.Body, resp, parse)
+}
+
+func (c *Client) doCopyMove(
+ method string,
+ oldpath string,
+ newpath string,
+ overwrite bool,
+) (
+ status int,
+ r io.ReadCloser,
+ err error,
+) {
+ rs, err := c.req(method, oldpath, nil, func(rq *http.Request) {
+ rq.Header.Add("Destination", PathEscape(Join(c.root, newpath)))
+ if overwrite {
+ rq.Header.Add("Overwrite", "T")
+ } else {
+ rq.Header.Add("Overwrite", "F")
+ }
+ })
+ if err != nil {
+ return
+ }
+ status = rs.StatusCode
+ r = rs.Body
+ return
+}
+
+func (c *Client) copymove(method string, oldpath string, newpath string, overwrite bool) (err error) {
+ s, data, err := c.doCopyMove(method, oldpath, newpath, overwrite)
+ if err != nil {
+ return
+ }
+ if data != nil {
+ defer data.Close()
+ }
+
+ switch s {
+ case 201, 204:
+ return nil
+
+ case 207:
+ // TODO handle multistat errors, worst case ...
+ log(fmt.Sprintf(" TODO handle %s - %s multistatus result %s", method, oldpath, String(data)))
+
+ case 409:
+ err := c.createParentCollection(newpath)
+ if err != nil {
+ return err
+ }
+
+ return c.copymove(method, oldpath, newpath, overwrite)
+ }
+
+ return newPathError(method, oldpath, s)
+}
+
+func (c *Client) put(path string, stream io.Reader, callback func(r *http.Request)) (status int, err error) {
+ rs, err := c.req(http.MethodPut, path, stream, callback)
+ if err != nil {
+ return
+ }
+ defer rs.Body.Close()
+ //all, _ := io.ReadAll(rs.Body)
+ //logrus.Debugln("put res: ", string(all))
+ status = rs.StatusCode
+ return
+}
+
+func (c *Client) createParentCollection(itemPath string) (err error) {
+ parentPath := path.Dir(itemPath)
+ if parentPath == "." || parentPath == "/" {
+ return nil
+ }
+
+ return c.MkdirAll(parentPath, 0755)
+}
diff --git a/pkg/gowebdav/utils.go b/pkg/gowebdav/utils.go
new file mode 100644
index 0000000..c7a65ad
--- /dev/null
+++ b/pkg/gowebdav/utils.go
@@ -0,0 +1,118 @@
+package gowebdav
+
+import (
+ "bytes"
+ "encoding/xml"
+ "fmt"
+ "io"
+ "net/url"
+ "strconv"
+ "strings"
+ "time"
+)
+
+func log(msg interface{}) {
+ fmt.Println(msg)
+}
+
+// PathEscape escapes all segments of a given path
+func PathEscape(path string) string {
+ s := strings.Split(path, "/")
+ for i, e := range s {
+ s[i] = url.PathEscape(e)
+ }
+ return strings.Join(s, "/")
+}
+
+// FixSlash appends a trailing / to our string
+func FixSlash(s string) string {
+ if !strings.HasSuffix(s, "/") {
+ s += "/"
+ }
+ return s
+}
+
+// FixSlashes appends and prepends a / if they are missing
+func FixSlashes(s string) string {
+ if !strings.HasPrefix(s, "/") {
+ s = "/" + s
+ }
+
+ return FixSlash(s)
+}
+
+// Join joins two paths
+func Join(path0 string, path1 string) string {
+ return strings.TrimSuffix(path0, "/") + "/" + strings.TrimPrefix(path1, "/")
+}
+
+// String pulls a string out of our io.Reader
+func String(r io.Reader) string {
+ buf := new(bytes.Buffer)
+ // TODO - make String return an error as well
+ _, _ = buf.ReadFrom(r)
+ return buf.String()
+}
+
+func parseUint(s *string) uint {
+ if n, e := strconv.ParseUint(*s, 10, 32); e == nil {
+ return uint(n)
+ }
+ return 0
+}
+
+func parseInt64(s *string) int64 {
+ if n, e := strconv.ParseInt(*s, 10, 64); e == nil {
+ return n
+ }
+ return 0
+}
+
+func parseModified(s *string) time.Time {
+ if t, e := time.Parse(time.RFC1123, *s); e == nil {
+ return t
+ }
+ return time.Unix(0, 0)
+}
+
+func parseXML(data io.Reader, resp interface{}, parse func(resp interface{}) error) error {
+ decoder := xml.NewDecoder(data)
+ for t, _ := decoder.Token(); t != nil; t, _ = decoder.Token() {
+ switch se := t.(type) {
+ case xml.StartElement:
+ if se.Name.Local == "response" {
+ if e := decoder.DecodeElement(resp, &se); e == nil {
+ if err := parse(resp); err != nil {
+ return err
+ }
+ }
+ }
+ }
+ }
+ return nil
+}
+
+// limitedReadCloser wraps a io.ReadCloser and limits the number of bytes that can be read from it.
+type limitedReadCloser struct {
+ rc io.ReadCloser
+ remaining int
+}
+
+func (l *limitedReadCloser) Read(buf []byte) (int, error) {
+ if l.remaining <= 0 {
+ return 0, io.EOF
+ }
+
+ if len(buf) > l.remaining {
+ buf = buf[0:l.remaining]
+ }
+
+ n, err := l.rc.Read(buf)
+ l.remaining -= n
+
+ return n, err
+}
+
+func (l *limitedReadCloser) Close() error {
+ return l.rc.Close()
+}
diff --git a/pkg/gowebdav/utils_test.go b/pkg/gowebdav/utils_test.go
new file mode 100644
index 0000000..db7b022
--- /dev/null
+++ b/pkg/gowebdav/utils_test.go
@@ -0,0 +1,67 @@
+package gowebdav
+
+import (
+ "fmt"
+ "net/url"
+ "testing"
+)
+
+func TestJoin(t *testing.T) {
+ eq(t, "/", "", "")
+ eq(t, "/", "/", "/")
+ eq(t, "/foo", "", "/foo")
+ eq(t, "foo/foo", "foo/", "/foo")
+ eq(t, "foo/foo", "foo/", "foo")
+}
+
+func eq(t *testing.T, expected string, s0 string, s1 string) {
+ s := Join(s0, s1)
+ if s != expected {
+ t.Error("For", "'"+s0+"','"+s1+"'", "expected", "'"+expected+"'", "got", "'"+s+"'")
+ }
+}
+
+func ExamplePathEscape() {
+ fmt.Println(PathEscape(""))
+ fmt.Println(PathEscape("/"))
+ fmt.Println(PathEscape("/web"))
+ fmt.Println(PathEscape("/web/"))
+ fmt.Println(PathEscape("/w e b/d a v/s%u&c#k:s/"))
+
+ // Output:
+ //
+ // /
+ // /web
+ // /web/
+ // /w%20e%20b/d%20a%20v/s%25u&c%23k:s/
+}
+
+func TestEscapeURL(t *testing.T) {
+ ex := "https://foo.com/w%20e%20b/d%20a%20v/s%25u&c%23k:s/"
+ u, _ := url.Parse("https://foo.com" + PathEscape("/w e b/d a v/s%u&c#k:s/"))
+ if ex != u.String() {
+ t.Error("expected: " + ex + " got: " + u.String())
+ }
+}
+
+func TestFixSlashes(t *testing.T) {
+ expected := "/"
+
+ if got := FixSlashes(""); got != expected {
+ t.Errorf("expected: %q, got: %q", expected, got)
+ }
+
+ expected = "/path/"
+
+ if got := FixSlashes("path"); got != expected {
+ t.Errorf("expected: %q, got: %q", expected, got)
+ }
+
+ if got := FixSlashes("/path"); got != expected {
+ t.Errorf("expected: %q, got: %q", expected, got)
+ }
+
+ if got := FixSlashes("path/"); got != expected {
+ t.Errorf("expected: %q, got: %q", expected, got)
+ }
+}