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 %} +
    +{% 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 + +[![Build Status](https://travis-ci.org/studio-b12/gowebdav.svg?branch=master)](https://travis-ci.org/studio-b12/gowebdav) +[![GoDoc](https://godoc.org/github.com/studio-b12/gowebdav?status.svg)](https://godoc.org/github.com/studio-b12/gowebdav) +[![Go Report Card](https://goreportcard.com/badge/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) + } +}