chore: Merge pull request #992 from Xhofe/dev

Dev 2.4.3
pull/999/head v2.4.3
Xhofe 2022-04-24 17:40:26 +08:00 committed by GitHub
commit 6c0d54394f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 2285 additions and 30 deletions

View File

@ -30,10 +30,10 @@ jobs:
with: with:
path: alist path: alist
- name: Install upx - name: Install dependencies
run: | run: |
docker pull crazymax/xgo:latest docker pull techknowlogick/xgo:latest
go install github.com/crazy-max/xgo@latest go install src.techknowlogick.com/xgo@latest
sudo apt install upx sudo apt install upx
- name: Build - name: Build

View File

@ -29,8 +29,8 @@ jobs:
- name: Install upx - name: Install upx
run: | run: |
docker pull crazymax/xgo:latest docker pull techknowlogick/xgo:latest
go install github.com/crazy-max/xgo@latest go install src.techknowlogick.com/xgo@latest
sudo apt install upx sudo apt install upx
- name: Build - name: Build

View File

@ -61,6 +61,7 @@ BUILD() {
-X 'github.com/Xhofe/alist/conf.GitTag=$gitTag' \ -X 'github.com/Xhofe/alist/conf.GitTag=$gitTag' \
-X 'github.com/Xhofe/alist/conf.WebTag=$webTag' \ -X 'github.com/Xhofe/alist/conf.WebTag=$webTag' \
" "
rm -rf .git/
if [ "$1" == "release" ]; then if [ "$1" == "release" ]; then
xgo -out "$appName" -ldflags="$ldflags" -tags=jsoniter . xgo -out "$appName" -ldflags="$ldflags" -tags=jsoniter .
else else
@ -148,8 +149,8 @@ elif [ "$1" = "docker" ]; then
elif [ "$1" = "build" ]; then elif [ "$1" = "build" ]; then
BUILD build BUILD build
elif [ "$1" = "release" ]; then elif [ "$1" = "release" ]; then
BUILD release
BUILD_MUSL BUILD_MUSL
BUILD release
RELEASE RELEASE
else else
echo -e "${RED_COLOR} Parameter error ${RES}" echo -e "${RED_COLOR} Parameter error ${RES}"

View File

@ -19,8 +19,9 @@ type DriverConfig struct {
} }
type Args struct { type Args struct {
Path string Path string
IP string IP string
Header http.Header
} }
type Driver interface { type Driver interface {

View File

@ -3,6 +3,7 @@ package base
import ( import (
"errors" "errors"
"io" "io"
"net/http"
) )
var ( var (
@ -49,4 +50,6 @@ type Link struct {
Headers []Header `json:"headers"` Headers []Header `json:"headers"`
Data io.ReadCloser Data io.ReadCloser
FilePath string `json:"path"` // for native FilePath string `json:"path"` // for native
Status int
Header http.Header
} }

View File

@ -6,6 +6,7 @@ import (
"github.com/Xhofe/alist/drivers/webdav/odrvcookie" "github.com/Xhofe/alist/drivers/webdav/odrvcookie"
"github.com/Xhofe/alist/model" "github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils" "github.com/Xhofe/alist/utils"
"net/http"
"path/filepath" "path/filepath"
) )
@ -132,11 +133,27 @@ func (driver WebDav) Files(path string, account *model.Account) ([]model.File, e
func (driver WebDav) Link(args base.Args, account *model.Account) (*base.Link, error) { func (driver WebDav) Link(args base.Args, account *model.Account) (*base.Link, error) {
path := args.Path path := args.Path
c := driver.NewClient(account) c := driver.NewClient(account)
reader, err := c.ReadStream(driver.WebDavPath(path)) callback := func(r *http.Request) {
if args.Header.Get("Range") != "" {
r.Header.Set("Range", args.Header.Get("Range"))
}
if args.Header.Get("If-Range") != "" {
r.Header.Set("If-Range", args.Header.Get("If-Range"))
}
}
reader, header, err := c.ReadStream(driver.WebDavPath(path), callback)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &base.Link{Data: reader}, nil link := &base.Link{Data: reader}
if header.Get("Content-Range") != "" {
link.Status = 206
link.Header = http.Header{
"Content-Range": header.Values("Content-Range"),
"Content-Length": header.Values("Content-Length"),
}
}
return link, nil
} }
func (driver WebDav) Path(path string, account *model.Account) (*model.File, []model.File, error) { func (driver WebDav) Path(path string, account *model.Account) (*model.File, []model.File, error) {
@ -196,7 +213,11 @@ func (driver WebDav) Upload(file *model.FileStream, account *model.Account) erro
} }
c := driver.NewClient(account) c := driver.NewClient(account)
path := utils.Join(file.ParentPath, file.Name) path := utils.Join(file.ParentPath, file.Name)
err := c.WriteStream(driver.WebDavPath(path), file, 0644) callback := func(r *http.Request) {
r.Header.Set("Content-Type", file.GetMIMEType())
r.ContentLength = int64(file.GetSize())
}
err := c.WriteStream(driver.WebDavPath(path), file, 0644, callback)
return err return err
} }

View File

@ -2,6 +2,7 @@ package odrvcookie
import ( import (
"github.com/Xhofe/alist/utils/cookie" "github.com/Xhofe/alist/utils/cookie"
log "github.com/sirupsen/logrus"
"net/http" "net/http"
"sync" "sync"
"time" "time"
@ -13,7 +14,7 @@ type SpCookie struct {
} }
func (sp SpCookie) IsExpire() bool { func (sp SpCookie) IsExpire() bool {
return time.Now().Before(sp.expire) return time.Now().After(sp.expire)
} }
var cookiesMap = struct { var cookiesMap = struct {
@ -27,9 +28,11 @@ func GetCookie(username, password, siteUrl string) (string, error) {
spCookie, ok := cookiesMap.m[username] spCookie, ok := cookiesMap.m[username]
if ok { if ok {
if !spCookie.IsExpire() { if !spCookie.IsExpire() {
log.Debugln("sp use old cookie.")
return spCookie.Cookie, nil return spCookie.Cookie, nil
} }
} }
log.Debugln("fetch new cookie")
ca := New(username, password, siteUrl) ca := New(username, password, siteUrl)
tokenConf, err := ca.Cookies() tokenConf, err := ca.Cookies()
if err != nil { if err != nil {

View File

@ -4,9 +4,8 @@ import (
"github.com/Xhofe/alist/drivers/base" "github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/drivers/webdav/odrvcookie" "github.com/Xhofe/alist/drivers/webdav/odrvcookie"
"github.com/Xhofe/alist/model" "github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/pkg/gowebdav"
"github.com/Xhofe/alist/utils" "github.com/Xhofe/alist/utils"
log "github.com/sirupsen/logrus"
"github.com/studio-b12/gowebdav"
"net/http" "net/http"
"strings" "strings"
) )
@ -15,13 +14,10 @@ func (driver WebDav) NewClient(account *model.Account) *gowebdav.Client {
c := gowebdav.NewClient(account.SiteUrl, account.Username, account.Password) c := gowebdav.NewClient(account.SiteUrl, account.Username, account.Password)
if isSharePoint(account) { if isSharePoint(account) {
cookie, err := odrvcookie.GetCookie(account.Username, account.Password, account.SiteUrl) cookie, err := odrvcookie.GetCookie(account.Username, account.Password, account.SiteUrl)
log.Debugln(cookie, err)
if err == nil { if err == nil {
log.Debugln("set interceptor")
c.SetInterceptor(func(method string, rq *http.Request) { c.SetInterceptor(func(method string, rq *http.Request) {
rq.Header.Del("Authorization") rq.Header.Del("Authorization")
rq.Header.Set("Cookie", cookie) rq.Header.Set("Cookie", cookie)
log.Debugf("sp webdav req: %+v", rq)
}) })
} }
} }

3
go.mod
View File

@ -16,7 +16,6 @@ require (
github.com/pkg/sftp v1.13.4 github.com/pkg/sftp v1.13.4
github.com/robfig/cron/v3 v3.0.0 github.com/robfig/cron/v3 v3.0.0
github.com/sirupsen/logrus v1.8.1 github.com/sirupsen/logrus v1.8.1
github.com/studio-b12/gowebdav v0.0.0-20211109083228-3f8721cd4b6f
github.com/upyun/go-sdk/v3 v3.0.2 github.com/upyun/go-sdk/v3 v3.0.2
golang.org/x/text v0.3.7 golang.org/x/text v0.3.7
gorm.io/driver/mysql v1.3.2 gorm.io/driver/mysql v1.3.2
@ -77,7 +76,7 @@ require (
go.opentelemetry.io/otel/metric v0.20.0 // indirect go.opentelemetry.io/otel/metric v0.20.0 // indirect
go.opentelemetry.io/otel/trace v0.20.0 // indirect go.opentelemetry.io/otel/trace v0.20.0 // indirect
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29
golang.org/x/net v0.0.0-20211209124913-491a49abca63 // indirect golang.org/x/net v0.0.0-20211209124913-491a49abca63
golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f // indirect golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f // indirect
google.golang.org/protobuf v1.27.1 // indirect google.golang.org/protobuf v1.27.1 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect

8
go.sum
View File

@ -46,7 +46,6 @@ github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQ
github.com/caarlos0/env/v6 v6.9.1 h1:zOkkjM0F6ltnQ5eBX6IPI41UP/KDGEK7rRPwGCNos8k= github.com/caarlos0/env/v6 v6.9.1 h1:zOkkjM0F6ltnQ5eBX6IPI41UP/KDGEK7rRPwGCNos8k=
github.com/caarlos0/env/v6 v6.9.1/go.mod h1:hvp/ryKXKipEkcuYjs9mI4bBCg+UI0Yhgm5Zu0ddvwc= github.com/caarlos0/env/v6 v6.9.1/go.mod h1:hvp/ryKXKipEkcuYjs9mI4bBCg+UI0Yhgm5Zu0ddvwc=
github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/cenkalti/backoff/v4 v4.1.0 h1:c8LkOFQTzuO0WBM/ae5HdGQuZPfPxp7lqBRwQRm4fSc= github.com/cenkalti/backoff/v4 v4.1.0 h1:c8LkOFQTzuO0WBM/ae5HdGQuZPfPxp7lqBRwQRm4fSc=
github.com/cenkalti/backoff/v4 v4.1.0/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/cenkalti/backoff/v4 v4.1.0/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
@ -226,7 +225,6 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO
github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0=
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
@ -248,7 +246,6 @@ github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5W
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A=
github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
@ -509,11 +506,8 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/studio-b12/gowebdav v0.0.0-20211109083228-3f8721cd4b6f h1:L2NE7BXnSlSLoNYZ0lCwZDjdnYjCNYC71k9ClZUTFTs=
github.com/studio-b12/gowebdav v0.0.0-20211109083228-3f8721cd4b6f/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE=
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go v1.2.6 h1:tGiWC9HENWE2tqYycIqFTNorMmFRVhNwCpDOpWqnk8E=
github.com/ugorji/go v1.2.6/go.mod h1:anCg0y61KIhDlPZmnH+so+RQbysYVyDko0IMgJv0Nn0= github.com/ugorji/go v1.2.6/go.mod h1:anCg0y61KIhDlPZmnH+so+RQbysYVyDko0IMgJv0Nn0=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/ugorji/go/codec v1.2.6 h1:7kbGefxLoDBuYXOms4yD7223OpNMMPNPZxXk5TvFcyQ= github.com/ugorji/go/codec v1.2.6 h1:7kbGefxLoDBuYXOms4yD7223OpNMMPNPZxXk5TvFcyQ=
@ -600,7 +594,6 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211209124913-491a49abca63 h1:iocB37TsdFuN6IBRZ+ry36wrkoV51/tl5vOWqkcPGvY= golang.org/x/net v0.0.0-20211209124913-491a49abca63 h1:iocB37TsdFuN6IBRZ+ry36wrkoV51/tl5vOWqkcPGvY=
golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@ -649,7 +642,6 @@ golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

21
pkg/gowebdav/.gitignore vendored Normal file
View File

@ -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/

10
pkg/gowebdav/.travis.yml Normal file
View File

@ -0,0 +1,10 @@
language: go
go:
- "1.x"
install:
- go get ./...
script:
- go test -v --short ./...

27
pkg/gowebdav/LICENSE Normal file
View File

@ -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.

33
pkg/gowebdav/Makefile Normal file
View File

@ -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

564
pkg/gowebdav/README.md Normal file
View File

@ -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)
### <a name="pkg-overview">Overview</a>
Package gowebdav is a WebDAV client library with a command line tool
included.
### <a name="pkg-index">Index</a>
* [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)
##### <a name="pkg-examples">Examples</a>
* [PathEscape](#example_PathEscape)
##### <a name="pkg-files">Package files</a>
[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)
### <a name="FixSlash">func</a> [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
### <a name="FixSlashes">func</a> [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
### <a name="Join">func</a> [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
### <a name="PathEscape">func</a> [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
### <a name="ReadConfig">func</a> [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
### <a name="String">func</a> [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
### <a name="Authenticator">type</a> [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
### <a name="BasicAuth">type</a> [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
#### <a name="BasicAuth.Authorize">func</a> (\*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
#### <a name="BasicAuth.Pass">func</a> (\*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
#### <a name="BasicAuth.Type">func</a> (\*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
#### <a name="BasicAuth.User">func</a> (\*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
### <a name="Client">type</a> [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
#### <a name="NewClient">func</a> [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
#### <a name="Client.Connect">func</a> (\*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
#### <a name="Client.Copy">func</a> (\*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
#### <a name="Client.Mkdir">func</a> (\*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
#### <a name="Client.MkdirAll">func</a> (\*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
#### <a name="Client.Read">func</a> (\*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
#### <a name="Client.ReadDir">func</a> (\*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
#### <a name="Client.ReadStream">func</a> (\*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
#### <a name="Client.ReadStreamRange">func</a> (\*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`.
#### <a name="Client.Remove">func</a> (\*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
#### <a name="Client.RemoveAll">func</a> (\*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
#### <a name="Client.Rename">func</a> (\*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
#### <a name="Client.SetHeader">func</a> (\*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
#### <a name="Client.SetInterceptor">func</a> (\*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
#### <a name="Client.SetTimeout">func</a> (\*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
#### <a name="Client.SetTransport">func</a> (\*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
#### <a name="Client.Stat">func</a> (\*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
#### <a name="Client.Write">func</a> (\*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
#### <a name="Client.WriteStream">func</a> (\*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
### <a name="DigestAuth">type</a> [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
#### <a name="DigestAuth.Authorize">func</a> (\*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
#### <a name="DigestAuth.Pass">func</a> (\*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
#### <a name="DigestAuth.Type">func</a> (\*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
#### <a name="DigestAuth.User">func</a> (\*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
### <a name="File">type</a> [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
#### <a name="File.ContentType">func</a> (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
#### <a name="File.ETag">func</a> (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
#### <a name="File.IsDir">func</a> (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
#### <a name="File.ModTime">func</a> (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
#### <a name="File.Mode">func</a> (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
#### <a name="File.Name">func</a> (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
#### <a name="File.Path">func</a> (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
#### <a name="File.Size">func</a> (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
#### <a name="File.String">func</a> (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
#### <a name="File.Sys">func</a> (File) [Sys](https://github.com/studio-b12/gowebdav/blob/master/file.go?s=1095:1126#L66)
``` go
func (f File) Sys() interface{}
```
Sys ????
### <a name="NoAuth">type</a> [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
#### <a name="NoAuth.Authorize">func</a> (\*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
#### <a name="NoAuth.Pass">func</a> (\*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
#### <a name="NoAuth.Type">func</a> (\*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
#### <a name="NoAuth.User">func</a> (\*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)

34
pkg/gowebdav/basicAuth.go Normal file
View File

@ -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)
}

447
pkg/gowebdav/client.go Normal file
View File

@ -0,0 +1,447 @@
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
}
// 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,
`<d:propfind xmlns:d='DAV:'>
<d:prop>
<d:displayname/>
<d:resourcetype/>
<d:getcontentlength/>
<d:getcontenttype/>
<d:getetag/>
<d:getlastmodified/>
</d:prop>
</d:propfind>`,
&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,
`<d:propfind xmlns:d='DAV:'>
<d:prop>
<d:displayname/>
<d:resourcetype/>
<d:getcontentlength/>
<d:getcontenttype/>
<d:getetag/>
<d:getlastmodified/>
</d:prop>
</d:propfind>`,
&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
}
// 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)
}
}

View File

@ -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
```

View File

@ -0,0 +1,263 @@
package main
import (
"errors"
"flag"
"fmt"
"io"
"io/fs"
"os"
"os/user"
"path"
"path/filepath"
"runtime"
"strings"
d "github.com/Xhofe/alist/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 <PATH>
STAT <PATH>
MKDIR <PATH>
MKDIRALL <PATH>
GET <PATH> [<FILE>]
PUT <PATH> [<FILE>]
MV <OLD> <NEW>
CP <OLD> <NEW>
DEL <PATH>
`)
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,
}
}

146
pkg/gowebdav/digestAuth.go Normal file
View File

@ -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
}

3
pkg/gowebdav/doc.go Normal file
View File

@ -0,0 +1,3 @@
// Package gowebdav is a WebDAV client library with a command line tool
// included.
package gowebdav

49
pkg/gowebdav/errors.go Normal file
View File

@ -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,
}
}

77
pkg/gowebdav/file.go Normal file
View File

@ -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)
}

54
pkg/gowebdav/netrc.go Normal file
View File

@ -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 "", ""
}

214
pkg/gowebdav/requests.go Normal file
View File

@ -0,0 +1,214 @@
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
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 {
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.
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("PUT", 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)
}

118
pkg/gowebdav/utils.go Normal file
View File

@ -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()
}

View File

@ -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+"'", "expeted", "'"+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)
}
}

View File

@ -28,7 +28,16 @@ func Proxy(w http.ResponseWriter, r *http.Request, link *base.Link, file *model.
w.Header().Set("Content-Type", "application/octet-stream") w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename=%s`, url.QueryEscape(file.Name))) w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename=%s`, url.QueryEscape(file.Name)))
w.Header().Set("Content-Length", strconv.FormatInt(file.Size, 10)) w.Header().Set("Content-Length", strconv.FormatInt(file.Size, 10))
w.WriteHeader(http.StatusOK) if link.Header != nil {
for h, val := range link.Header {
w.Header()[h] = val
}
}
if link.Status == 0 {
w.WriteHeader(http.StatusOK)
} else {
w.WriteHeader(link.Status)
}
_, err = io.Copy(w, link.Data) _, err = io.Copy(w, link.Data)
if err != nil { if err != nil {
return err return err

View File

@ -55,7 +55,7 @@ func Proxy(c *gin.Context) {
return return
} }
// 对于中转不需要重设IP // 对于中转不需要重设IP
link, err := driver.Link(base.Args{Path: path}, account) link, err := driver.Link(base.Args{Path: path, Header: c.Request.Header}, account)
if err != nil { if err != nil {
common.ErrorResp(c, err, 500) common.ErrorResp(c, err, 500)
return return

View File

@ -133,7 +133,7 @@ func (fs *FileSystem) Link(w http.ResponseWriter, r *http.Request, rawPath strin
if err != nil { if err != nil {
return "", err return "", err
} }
link_, err := driver.Link(base.Args{Path: path_}, account) link_, err := driver.Link(base.Args{Path: path_, Header: r.Header}, account)
if err != nil { if err != nil {
return "", err return "", err
} }