Skip to content

feat(storage): add WebDAV provider support #649

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ jobs:
echo "ASSET_NAME=$_NAME" >> $GITHUB_ENV

- name: Set up Go
uses: actions/setup-go@v2
uses: actions/setup-go@v5
with:
go-version: ^1.18

Expand Down Expand Up @@ -153,14 +153,14 @@ jobs:
mv build_assets transfersh-${GITHUB_REF##*/}-${ASSET_NAME}

- name: Upload files to Artifacts
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: transfersh-${{ steps.get_filename.outputs.GIT_TAG }}-${{ steps.get_filename.outputs.ASSET_NAME }}
path: |
./transfersh-${{ steps.get_filename.outputs.GIT_TAG }}-${{ steps.get_filename.outputs.ASSET_NAME }}/*

- name: Upload binaries to release
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@v2
if: github.event_name == 'release'
with:
files: |
Expand Down
21 changes: 19 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Easy and fast file sharing from the command-line. This code contains the server with everything you need to create your own instance.

Transfer.sh currently supports the s3 (Amazon S3), gdrive (Google Drive), storj (Storj) providers, and local file system (local).
Transfer.sh currently supports the s3 (Amazon S3), gdrive (Google Drive), storj (Storj), webdav and local file system (local).

## Disclaimer

Expand Down Expand Up @@ -113,7 +113,7 @@ proxy-path | path prefix when service is run behind a proxy
proxy-port | port of the proxy when the service is run behind a proxy | | PROXY_PORT |
email-contact | email contact for the front end | | EMAIL_CONTACT |
ga-key | google analytics key for the front end | | GA_KEY |
provider | which storage provider to use | (s3, storj, gdrive or local) |
provider | which storage provider to use | (s3, storj, gdrive, webdav or local) |
uservoice-key | user voice key for the front end | | USERVOICE_KEY |
aws-access-key | aws access key | | AWS_ACCESS_KEY |
aws-secret-key | aws access key | | AWS_SECRET_KEY |
Expand All @@ -124,6 +124,9 @@ s3-no-multipart | disables s3 multipart upload
s3-path-style | Forces path style URLs, required for Minio. | false | S3_PATH_STYLE |
storj-access | Access for the project | | STORJ_ACCESS |
storj-bucket | Bucket to use within the project | | STORJ_BUCKET |
webdav-url | url of webdav server | | WEBDAV_URL |
webdav-username | username for webdav | | WEBDAV_USERNAME |
webdav-password | password for webdav | | WEBDAV_PASSWORD |
basedir | path storage for local/gdrive provider | | BASEDIR |
gdrive-client-json-filepath | path to oauth client json config for gdrive provider | | GDRIVE_CLIENT_JSON_FILEPATH |
gdrive-local-config-path | path to store local transfer.sh config cache for gdrive provider | | GDRIVE_LOCAL_CONFIG_PATH |
Expand Down Expand Up @@ -271,6 +274,20 @@ You need to create an OAuth Client id from console.cloud.google.com, download th

```go run main.go --provider gdrive --basedir /tmp/ --gdrive-client-json-filepath /[credential_dir] --gdrive-local-config-path [directory_to_save_config] ```

## WebDAV Usage

For WebDAV you need to specify the following options:
- provider `--provider webdav`
- webdav-url _(either via flag or environment variable `WEBDAV_URL`)_
- webdav-username _(either via flag or environment variable `WEBDAV_USERNAME`)_
- webdav-password _(either via flag or environment variable `WEBDAV_PASSWORD`)_
- basedir

Example:
```
go run main.go --provider webdav --basedir /remote/path --webdav-url https://dav.example.com --webdav-username user --webdav-password pass
```

## Shell functions

### Bash, ash and zsh (multiple files uploaded as zip archive)
Expand Down
32 changes: 32 additions & 0 deletions cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,24 @@ var globalFlags = []cli.Flag{
Value: "",
EnvVars: []string{"STORJ_BUCKET"},
},
&cli.StringFlag{
Name: "webdav-url",
Usage: "WebDAV server URL",
Value: "",
EnvVars: []string{"WEBDAV_URL"},
},
&cli.StringFlag{
Name: "webdav-username",
Usage: "WebDAV username",
Value: "",
EnvVars: []string{"WEBDAV_USERNAME"},
},
&cli.StringFlag{
Name: "webdav-password",
Usage: "WebDAV password",
Value: "",
EnvVars: []string{"WEBDAV_PASSWORD"},
},
&cli.IntFlag{
Name: "rate-limit",
Usage: "requests per minute",
Expand Down Expand Up @@ -519,6 +537,20 @@ func New() *Cmd {
} else {
options = append(options, server.UseStorage(store))
}
case "webdav":
if url := c.String("webdav-url"); url == "" {
return errors.New("webdav-url not set")
} else if user := c.String("webdav-username"); user == "" {
return errors.New("webdav-username not set")
} else if password := c.String("webdav-password"); password == "" {
return errors.New("webdav-password not set")
} else if basedir := c.String("basedir"); basedir == "" {
return errors.New("basedir not set")
} else if store, err := storage.NewWebDAVStorage(url, basedir, user, password, logger); err != nil {
return err
} else {
options = append(options, server.UseStorage(store))
}
case "local":
if v := c.String("basedir"); v == "" {
return errors.New("basedir not set.")
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ require (
github.com/microcosm-cc/bluemonday v1.0.23
github.com/russross/blackfriday/v2 v2.1.0
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/studio-b12/gowebdav v0.10.0
github.com/tg123/go-htpasswd v1.2.1
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce
github.com/urfave/cli/v2 v2.25.3
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,8 @@ 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/studio-b12/gowebdav v0.10.0 h1:Yewz8FFiadcGEu4hxS/AAJQlHelndqln1bns3hcJIYc=
github.com/studio-b12/gowebdav v0.10.0/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE=
github.com/tg123/go-htpasswd v1.2.1 h1:i4wfsX1KvvkyoMiHZzjS0VzbAPWfxzI8INcZAKtutoU=
github.com/tg123/go-htpasswd v1.2.1/go.mod h1:erHp1B86KXdwQf1X5ZrLb7erXZnWueEQezb2dql4q58=
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce h1:fb190+cK2Xz/dvi9Hv8eCYJYvIGUTN2/KLq1pT6CjEc=
Expand Down
148 changes: 148 additions & 0 deletions server/storage/webdav.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package storage

import (
"context"
"io"
"log"
"os"
"path"
"time"

"github.com/studio-b12/gowebdav"
)

// WebDAVStorage is a storage backed by a WebDAV server
// basePath is the root directory within the WebDAV server
// where files will be stored.
type WebDAVStorage struct {
client *gowebdav.Client
basePath string
logger *log.Logger
}

// NewWebDAVStorage creates a new WebDAVStorage
func NewWebDAVStorage(url, basePath, username, password string, logger *log.Logger) (*WebDAVStorage, error) {
c := gowebdav.NewClient(url, username, password)
if err := c.Connect(); err != nil {
if logger != nil {
logger.Printf("webdav connect error: %v", err)
}
return nil, err
}
if logger != nil {
logger.Printf("webdav connected to %s", url)
}
return &WebDAVStorage{client: c, basePath: basePath, logger: logger}, nil
}

// Type returns the storage type
func (s *WebDAVStorage) Type() string { return "webdav" }

func (s *WebDAVStorage) fullPath(token, filename string) string {
return path.Join(s.basePath, token, filename)
}

// Head retrieves content length of a file from storage
func (s *WebDAVStorage) Head(_ context.Context, token, filename string) (uint64, error) {
fi, err := s.client.Stat(s.fullPath(token, filename))
if err != nil {
if s.logger != nil {
s.logger.Printf("webdav head %s/%s error: %v", token, filename, err)
}
return 0, err
}
if s.logger != nil {
s.logger.Printf("webdav head %s/%s ok", token, filename)
}
return uint64(fi.Size()), nil
}

// Get retrieves a file from storage
func (s *WebDAVStorage) Get(_ context.Context, token, filename string, rng *Range) (io.ReadCloser, uint64, error) {
p := s.fullPath(token, filename)
var rc io.ReadCloser
var err error
if rng != nil {
rc, err = s.client.ReadStreamRange(p, int64(rng.Start), int64(rng.Limit))
} else {
rc, err = s.client.ReadStream(p)
}
if err != nil {
if s.logger != nil {
s.logger.Printf("webdav get %s/%s error: %v", token, filename, err)
}
return nil, 0, err
}
fi, err := s.client.Stat(p)
if err != nil {
if cerr := rc.Close(); cerr != nil && s.logger != nil {
s.logger.Printf("webdav close %s/%s error: %v", token, filename, cerr)
}
if s.logger != nil {
s.logger.Printf("webdav stat %s/%s error: %v", token, filename, err)
}
return nil, 0, err
}
size := uint64(fi.Size())
if rng != nil {
size = rng.AcceptLength(size)
}
if s.logger != nil {
s.logger.Printf("webdav get %s/%s ok", token, filename)
}
return rc, size, nil
}

// Delete removes a file from storage
func (s *WebDAVStorage) Delete(_ context.Context, token, filename string) error {
if err := s.client.Remove(s.fullPath(token, filename)); err != nil {
if s.logger != nil {
s.logger.Printf("webdav delete %s/%s error: %v", token, filename, err)
}
return err
}
if s.logger != nil {
s.logger.Printf("webdav delete %s/%s ok", token, filename)
}
return nil
}

// Purge cleans up the storage (noop for webdav)
func (s *WebDAVStorage) Purge(context.Context, time.Duration) error { return nil }

// Put saves a file on storage
func (s *WebDAVStorage) Put(_ context.Context, token, filename string, reader io.Reader, _ string, _ uint64) error {
dir := path.Join(s.basePath, token)
if err := s.client.MkdirAll(dir, 0755); err != nil {
if s.logger != nil {
s.logger.Printf("webdav mkdir %s error: %v", dir, err)
}
return err
}
if s.logger != nil {
s.logger.Printf("webdav mkdir %s ok", dir)
}
if err := s.client.WriteStream(s.fullPath(token, filename), reader, 0644); err != nil {
if s.logger != nil {
s.logger.Printf("webdav put %s/%s error: %v", token, filename, err)
}
return err
}
if s.logger != nil {
s.logger.Printf("webdav put %s/%s ok", token, filename)
}
return nil
}

// IsNotExist indicates if a file doesn't exist on storage
func (s *WebDAVStorage) IsNotExist(err error) bool {
if err == nil {
return false
}
if _, ok := err.(*os.PathError); ok {
return true
}
return false
}

func (s *WebDAVStorage) IsRangeSupported() bool { return true }