Skip to content
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

Add new storage backend: Dropbox (#103) #251

Merged
merged 49 commits into from
Aug 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
4e04cb9
Add new storage backend: Dropbox (#103)
MaxJa4 Aug 20, 2023
6362b32
Remove duplicate check
MaxJa4 Aug 21, 2023
05c7a15
Add concurrency level for parallel upload to dropbox.
MaxJa4 Aug 21, 2023
f29b4c0
Fixed some instabilites. Changed default concurrency to 6.
MaxJa4 Aug 21, 2023
c5d596d
Added some env config vars to readme. WIP
MaxJa4 Aug 21, 2023
ddca5c4
Wrap errors for storage backend creation.
MaxJa4 Aug 21, 2023
70275ea
Fixed token issue, added OAuth2 including recipe and docs.
MaxJa4 Aug 21, 2023
894608f
Readme typo fix
MaxJa4 Aug 21, 2023
cd49b8c
Test for dropbox integration
MaxJa4 Aug 22, 2023
2ea59a6
Update info and TOC
MaxJa4 Aug 22, 2023
c7b9ef7
Missed a file
MaxJa4 Aug 22, 2023
f60f633
Docker-compose fix
MaxJa4 Aug 22, 2023
5f6b547
Fix endpoint connection
MaxJa4 Aug 22, 2023
2db707c
Fix container names
MaxJa4 Aug 22, 2023
a64c6ab
Fix log fetching
MaxJa4 Aug 22, 2023
385d692
Fix log fetching (again)
MaxJa4 Aug 22, 2023
08ae3c7
Print command output to logs
MaxJa4 Aug 22, 2023
c304b4f
Addressing comments part 1
MaxJa4 Aug 23, 2023
5147753
Address comments part 2
MaxJa4 Aug 23, 2023
7ddeb99
Add OAuth2 mock server for CI testing
MaxJa4 Aug 23, 2023
0e46fc3
Fix env name of oauth2 endpoint
MaxJa4 Aug 23, 2023
a5ce440
Remove hostname
MaxJa4 Aug 23, 2023
955f98e
Add forgotten change to commit...
MaxJa4 Aug 23, 2023
396a75d
Fix oauth2 endpoint
MaxJa4 Aug 23, 2023
155f0e4
Try again
MaxJa4 Aug 23, 2023
4f7fb2c
Try suggested hostname again
MaxJa4 Aug 23, 2023
1b5e449
Fix docker internal DNS resolving issues (as suggested by oauth2 mock…
MaxJa4 Aug 23, 2023
84fc0c2
Add docker network, remove hostname
MaxJa4 Aug 23, 2023
f539240
Network not external
MaxJa4 Aug 23, 2023
569f74e
Last hostname try
MaxJa4 Aug 23, 2023
4d7af32
Add more delay, add oauth2 endpoint log
MaxJa4 Aug 23, 2023
4c4c431
Temp CI log output of command even when failing
MaxJa4 Aug 23, 2023
fe2ffa0
Try different config and method
MaxJa4 Aug 23, 2023
30159de
Add custom server-hostname. Rename test folder to accellerate debugging
MaxJa4 Aug 23, 2023
4dbfc76
Try that fix again
MaxJa4 Aug 23, 2023
bf73fa0
Adding quotes
MaxJa4 Aug 23, 2023
8b626c3
Port fix attempt
MaxJa4 Aug 23, 2023
3e32fe5
Try localhost
MaxJa4 Aug 23, 2023
1ba97e9
Try extra hosts
MaxJa4 Aug 23, 2023
58758dd
Change network mode
MaxJa4 Aug 23, 2023
cb0f79e
Undo some changes
MaxJa4 Aug 23, 2023
159a9d5
Use static IP
MaxJa4 Aug 23, 2023
4d415bc
Remove specific IP binding
MaxJa4 Aug 23, 2023
8654234
Change to default net driver
MaxJa4 Aug 23, 2023
32462de
Fix static IP
MaxJa4 Aug 23, 2023
e9b617b
Squash for revert
MaxJa4 Aug 23, 2023
b2b9e3a
Merge branch 'dropbox-storage-backend' of https://github.com/MaxJa4/d…
MaxJa4 Aug 23, 2023
3899afb
Revert "Squash for revert"
MaxJa4 Aug 23, 2023
1da61c5
Actual fix for CI testing from #257
MaxJa4 Aug 24, 2023
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
81 changes: 79 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@

# docker-volume-backup

Backup Docker volumes locally or to any S3, WebDAV, Azure Blob Storage or SSH compatible storage.
Backup Docker volumes locally or to any S3, WebDAV, Azure Blob Storage, Dropbox or SSH compatible storage.

The [offen/docker-volume-backup](https://hub.docker.com/r/offen/docker-volume-backup) Docker image can be used as a lightweight (below 15MB) sidecar container to an existing Docker setup.
It handles __recurring or one-off backups of Docker volumes__ to a __local directory__, __any S3, WebDAV, Azure Blob Storage or SSH compatible storage (or any combination) and rotates away old backups__ if configured. It also supports __encrypting your backups using GPG__ and __sending notifications for failed backup runs__.
It handles __recurring or one-off backups of Docker volumes__ to a __local directory__, __any S3, WebDAV, Azure Blob Storage, Dropbox or SSH compatible storage (or any combination) and rotates away old backups__ if configured. It also supports __encrypting your backups using GPG__ and __sending notifications for failed backup runs__.

<!-- MarkdownTOC -->

Expand Down Expand Up @@ -36,6 +36,7 @@ It handles __recurring or one-off backups of Docker volumes__ to a __local direc
- [Define different retention schedules](#define-different-retention-schedules)
- [Use special characters in notification URLs](#use-special-characters-in-notification-urls)
- [Handle file uploads using third party tools](#handle-file-uploads-using-third-party-tools)
- [Setup Dropbox storage backend](#setup-dropbox-storage-backend)
- [Recipes](#recipes)
- [Backing up to AWS S3](#backing-up-to-aws-s3)
- [Backing up to Filebase](#backing-up-to-filebase)
Expand All @@ -44,6 +45,7 @@ It handles __recurring or one-off backups of Docker volumes__ to a __local direc
- [Backing up to WebDAV](#backing-up-to-webdav)
- [Backing up to SSH](#backing-up-to-ssh)
- [Backing up to Azure Blob Storage](#backing-up-to-azure-blob-storage)
- [Backing up to Dropbox](#backing-up-to-dropbox)
- [Backing up locally](#backing-up-locally)
- [Backing up to AWS S3 as well as locally](#backing-up-to-aws-s3-as-well-as-locally)
- [Running on a custom cron schedule](#running-on-a-custom-cron-schedule)
Expand Down Expand Up @@ -356,6 +358,26 @@ You can populate below template according to your requirements and use it as you

# AZURE_STORAGE_ENDPOINT="https://{{ .AccountName }}.blob.core.windows.net/"

# Absolute remote path in your Dropbox where the backups shall be stored.
# Note: Use your app's subpath in Dropbox, if it doesn't have global access.
# Consulte the README for further information.

# DROPBOX_REMOTE_PATH="/my/directory"

# Number of concurrent chunked uploads for Dropbox.
# Values above 6 usually result in no enhancements.

# DROPBOX_CONCURRENCY_LEVEL="6"

# App key and app secret from your app created at https://www.dropbox.com/developers/apps/info

# DROPBOX_APP_KEY=""
# DROPBOX_APP_SECRET=""

# Refresh token to request new short-lived tokens (OAuth2). Consult README to see how to get one.

# DROPBOX_REFRESH_TOKEN=""

# In addition to storing backups remotely, you can also keep local copies.
# Pass a container-local path to store your backups if needed. You also need to
# mount a local folder or Docker volume into that location (`/archive`
Expand Down Expand Up @@ -1020,6 +1042,37 @@ volumes:

Commands will be invoked with the filepath of the tar archive passed as `COMMAND_RUNTIME_BACKUP_FILEPATH`.

### Setup Dropbox storage backend

#### Auth-Setup:

1. Create a new Dropbox App in the [App Console](https://www.dropbox.com/developers/apps)
2. Open your new Dropbox App and set `DROPBOX_APP_KEY` and `DROPBOX_APP_SECRET` in your environment (e.g. docker-compose.yml) accordingly
3. Click on `Permissions` in your app and make sure, that the following permissions are cranted (or more):
- `files.metadata.write`
- `files.metadata.read`
- `files.content.write`
- `files.content.read`
4. Replace APPKEY in `https://www.dropbox.com/oauth2/authorize?client_id=APPKEY&token_access_type=offline&response_type=code` with the app key from step 2
5. Visit the URL and confirm the access of your app. This gives you an `auth code` -> save it somewhere!
6. Replace AUTHCODE, APPKEY, APPSECRET accordingly and perform the request:
```
curl https://api.dropbox.com/oauth2/token \
-d code=AUTHCODE \
-d grant_type=authorization_code \
-d client_id=APPKEY \
-d client_secret=APPSECRET
```
7. Execute the request. You will get a JSON formatted reply. Use the value of the `refresh_token` for the last environment variable `DROPBOX_REFRESH_TOKEN`
8. You should now have `DROPBOX_APP_KEY`, `DROPBOX_APP_SECRET` and `DROPBOX_REFRESH_TOKEN` set. These don't expire.

Note: Using the "Generated access token" in the app console is not supported, as it is only very short lived and therefore not suitable for an automatic backup solution. The refresh token handles this automatically - the setup procedure above is only needed once.

#### Other parameters

Important: If you chose `App folder` access during the creation of your Dropbox app in step 1 above, you can only write in the app's directory!
This means, that `DROPBOX_REMOTE_PATH` must start with e.g. `/Apps/YOUR_APP_NAME` or `/Apps/YOUR_APP_NAME/some_sub_dir`

## Recipes

This section lists configuration for some real-world use cases that you can mix and match according to your needs.
Expand Down Expand Up @@ -1187,6 +1240,30 @@ volumes:
data:
```

### Backing up to Dropbox

See [Dropbox Setup](#setup-dropbox-storage-backend) on how to get the appropriate environment values.

```yml
version: '3'

services:
# ... define other services using the `data` volume here
backup:
image: offen/docker-volume-backup:v2
environment:
DROPBOX_REFRESH_TOKEN: REFRESH_KEY # replace
DROPBOX_APP_KEY: APP_KEY # replace
DROPBOX_APP_SECRET: APP_SECRET # replace
DROPBOX_REMOTE_PATH: /Apps/my-test-app/some_subdir # replace
volumes:
- data:/backup/my-app-backup:ro
- /var/run/docker.sock:/var/run/docker.sock:ro

volumes:
data:
```

### Backing up locally

```yml
Expand Down
26 changes: 26 additions & 0 deletions cmd/backup/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"io/ioutil"
"os"
"regexp"
"strconv"
"time"
)

Expand Down Expand Up @@ -70,6 +71,13 @@ type Config struct {
AzureStorageContainerName string `split_words:"true"`
AzureStoragePath string `split_words:"true"`
AzureStorageEndpoint string `split_words:"true" default:"https://{{ .AccountName }}.blob.core.windows.net/"`
DropboxEndpoint string `split_words:"true" default:"https://api.dropbox.com/"`
DropboxOAuth2Endpoint string `envconfig:"DROPBOX_OAUTH2_ENDPOINT" default:"https://api.dropbox.com/"`
DropboxRefreshToken string `split_words:"true"`
DropboxAppKey string `split_words:"true"`
DropboxAppSecret string `split_words:"true"`
DropboxRemotePath string `split_words:"true"`
DropboxConcurrencyLevel NaturalNumber `split_words:"true" default:"6"`
}

func (c *Config) resolveSecret(envVar string, secretPath string) (string, error) {
Expand Down Expand Up @@ -135,3 +143,21 @@ func (r *RegexpDecoder) Decode(v string) error {
*r = RegexpDecoder{Re: re}
return nil
}

type NaturalNumber int

func (n *NaturalNumber) Decode(v string) error {
asInt, err := strconv.Atoi(v)
if err != nil {
return fmt.Errorf("config: error converting %s to int", v)
}
if asInt <= 0 {
return fmt.Errorf("config: expected a natural number, got %d", asInt)
}
*n = NaturalNumber(asInt)
return nil
}

func (n *NaturalNumber) Int() int {
return int(*n)
}
37 changes: 28 additions & 9 deletions cmd/backup/script.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (

"github.com/offen/docker-volume-backup/internal/storage"
"github.com/offen/docker-volume-backup/internal/storage/azure"
"github.com/offen/docker-volume-backup/internal/storage/dropbox"
"github.com/offen/docker-volume-backup/internal/storage/local"
"github.com/offen/docker-volume-backup/internal/storage/s3"
"github.com/offen/docker-volume-backup/internal/storage/ssh"
Expand Down Expand Up @@ -70,11 +71,12 @@ func newScript() (*script, error) {
StartTime: time.Now(),
LogOutput: logBuffer,
Storages: map[string]StorageStats{
"S3": {},
"WebDAV": {},
"SSH": {},
"Local": {},
"Azure": {},
"S3": {},
"WebDAV": {},
"SSH": {},
"Local": {},
"Azure": {},
"Dropbox": {},
},
},
}
Expand Down Expand Up @@ -156,7 +158,7 @@ func newScript() (*script, error) {
PartSize: s.c.AwsPartSize,
}
if s3Backend, err := s3.NewStorageBackend(s3Config, logFunc); err != nil {
return nil, err
return nil, fmt.Errorf("newScript: error creating s3 storage backend: %w", err)
MaxJa4 marked this conversation as resolved.
Show resolved Hide resolved
} else {
s.storages = append(s.storages, s3Backend)
}
Expand All @@ -171,7 +173,7 @@ func newScript() (*script, error) {
RemotePath: s.c.WebdavPath,
}
if webdavBackend, err := webdav.NewStorageBackend(webDavConfig, logFunc); err != nil {
return nil, err
return nil, fmt.Errorf("newScript: error creating webdav storage backend: %w", err)
} else {
s.storages = append(s.storages, webdavBackend)
}
Expand All @@ -188,7 +190,7 @@ func newScript() (*script, error) {
RemotePath: s.c.SSHRemotePath,
}
if sshBackend, err := ssh.NewStorageBackend(sshConfig, logFunc); err != nil {
return nil, err
return nil, fmt.Errorf("newScript: error creating ssh storage backend: %w", err)
} else {
s.storages = append(s.storages, sshBackend)
}
Expand All @@ -213,11 +215,28 @@ func newScript() (*script, error) {
}
azureBackend, err := azure.NewStorageBackend(azureConfig, logFunc)
if err != nil {
return nil, err
return nil, fmt.Errorf("newScript: error creating azure storage backend: %w", err)
}
s.storages = append(s.storages, azureBackend)
}

if s.c.DropboxRefreshToken != "" && s.c.DropboxAppKey != "" && s.c.DropboxAppSecret != "" {
dropboxConfig := dropbox.Config{
Endpoint: s.c.DropboxEndpoint,
OAuth2Endpoint: s.c.DropboxOAuth2Endpoint,
RefreshToken: s.c.DropboxRefreshToken,
AppKey: s.c.DropboxAppKey,
AppSecret: s.c.DropboxAppSecret,
RemotePath: s.c.DropboxRemotePath,
ConcurrencyLevel: s.c.DropboxConcurrencyLevel.Int(),
}
dropboxBackend, err := dropbox.NewStorageBackend(dropboxConfig, logFunc)
if err != nil {
return nil, fmt.Errorf("newScript: error creating dropbox storage backend: %w", err)
}
s.storages = append(s.storages, dropboxBackend)
}

if s.c.EmailNotificationRecipient != "" {
emailURL := fmt.Sprintf(
"smtp://%s:%s@%s:%d/?from=%s&to=%s",
Expand Down
8 changes: 8 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ require (
golang.org/x/sync v0.3.0
)

require (
github.com/golang/protobuf v1.5.2 // indirect
golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.28.1 // indirect
)

require (
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 // indirect
Expand All @@ -28,6 +35,7 @@ require (
github.com/docker/distribution v2.8.2+incompatible // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.4.0 // indirect
github.com/dropbox/dropbox-sdk-go-unofficial/v6 v6.0.5
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,8 @@ github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKoh
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dropbox/dropbox-sdk-go-unofficial/v6 v6.0.5 h1:FT+t0UEDykcor4y3dMVKXIiWJETBpRgERYTGlmMd7HU=
github.com/dropbox/dropbox-sdk-go-unofficial/v6 v6.0.5/go.mod h1:rSS3kM9XMzSQ6pw91Qgd6yB5jdt70N4OdtrAf74As5M=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
Expand Down Expand Up @@ -785,6 +787,7 @@ golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7Lm
golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE=
golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 h1:nt+Q6cXKz4MosCSpnbMtqiQ8Oz0pxTef2B4Vca2lvfk=
golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
Expand Down Expand Up @@ -1046,6 +1049,7 @@ google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
Expand Down
Loading