diff --git a/README.md b/README.md index a59dac1..52bdafa 100644 --- a/README.md +++ b/README.md @@ -357,6 +357,8 @@ 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" @@ -365,6 +367,15 @@ You can populate below template according to your requirements and use it as you # 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` @@ -1029,6 +1040,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 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. @@ -1196,6 +1238,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 diff --git a/cmd/backup/config.go b/cmd/backup/config.go index 4cced0b..b3ae44c 100644 --- a/cmd/backup/config.go +++ b/cmd/backup/config.go @@ -70,7 +70,9 @@ 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/"` - DropboxToken string `split_words:"true"` + DropboxRefreshToken string `split_words:"true"` + DropboxAppKey string `split_words:"true"` + DropboxAppSecret string `split_words:"true"` DropboxRemotePath string `split_words:"true"` DropboxConcurrencyLevel int `split_words:"true" default:"6"` } diff --git a/cmd/backup/script.go b/cmd/backup/script.go index 2fca193..8c26e5d 100644 --- a/cmd/backup/script.go +++ b/cmd/backup/script.go @@ -220,9 +220,11 @@ func newScript() (*script, error) { s.storages = append(s.storages, azureBackend) } - if s.c.DropboxToken != "" { + if s.c.DropboxRefreshToken != "" && s.c.DropboxAppKey != "" && s.c.DropboxAppSecret != "" { dropboxConfig := dropbox.Config{ - Token: s.c.DropboxToken, + RefreshToken: s.c.DropboxRefreshToken, + AppKey: s.c.DropboxAppKey, + AppSecret: s.c.DropboxAppSecret, RemotePath: s.c.DropboxRemotePath, ConcurrencyLevel: s.c.DropboxConcurrencyLevel, } diff --git a/internal/storage/dropbox/dropbox.go b/internal/storage/dropbox/dropbox.go index 823fc1f..00c55b2 100644 --- a/internal/storage/dropbox/dropbox.go +++ b/internal/storage/dropbox/dropbox.go @@ -2,6 +2,7 @@ package dropbox import ( "bytes" + "context" "fmt" "os" "path" @@ -14,6 +15,7 @@ import ( "github.com/dropbox/dropbox-sdk-go-unofficial/v6/dropbox" "github.com/dropbox/dropbox-sdk-go-unofficial/v6/dropbox/files" "github.com/offen/docker-volume-backup/internal/storage" + "golang.org/x/oauth2" ) type dropboxStorage struct { @@ -24,18 +26,33 @@ type dropboxStorage struct { // Config allows to configure a Dropbox storage backend. type Config struct { - Token string + RefreshToken string + AppKey string + AppSecret string RemotePath string ConcurrencyLevel int } // NewStorageBackend creates and initializes a new Dropbox storage backend. func NewStorageBackend(opts Config, logFunc storage.Log) (storage.Backend, error) { - config := dropbox.Config{ - Token: opts.Token, + conf := &oauth2.Config{ + ClientID: opts.AppKey, + ClientSecret: opts.AppSecret, + Endpoint: oauth2.Endpoint{ + TokenURL: "https://api.dropbox.com/oauth2/token", + }, } - client := files.New(config) + logFunc(storage.LogLevelInfo, "Dropbox", "Fetching fresh access token for Dropbox storage backend.") + tkSource := conf.TokenSource(context.TODO(), &oauth2.Token{RefreshToken: opts.RefreshToken}) + token, err := tkSource.Token() + if err != nil { + return nil, fmt.Errorf("(*dropboxStorage).NewStorageBackend: Error refreshing token: %w", err) + } + + client := files.New(dropbox.Config{ + Token: token.AccessToken, + }) if opts.ConcurrencyLevel < 1 { logFunc(storage.LogLevelWarning, "Dropbox", "Concurrency level must be at least 1! Using 1 instead of %d.", opts.ConcurrencyLevel)