Skip to content

Commit

Permalink
Merge pull request #36 from d0ugal/dev
Browse files Browse the repository at this point in the history
Working towards a 1.1.0
  • Loading branch information
d0ugal authored Nov 14, 2018
2 parents 69618f3 + 62f3cf2 commit b004ef1
Show file tree
Hide file tree
Showing 12 changed files with 162 additions and 15 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Dropbox_Upload 1.1.0 (2018-11-14)
=================================

Features
--------

- Add a new "filename" setting to customise the filenames saved in dropbox (#15)
- The dropbox_dir is now validated to ensure it isn't an empty string. (#16)
- Add a warning if snapshots are not "protected", adding a password is always a good idea (#31)
27 changes: 22 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,39 @@ Add this repository URL in Hass.io:

## Configuration

### Dropbox

You will need to create a [Dropbox app](https://www.dropbox.com/developers/apps)

1. Choose `Dropbox API`
2. Either type of Dropbox app should work (Full or App directory)
3. Give it a unique name, this can be anything
4. Click `Generate` under "Generated access token" and copy the token.

After that, the config is simple. You just need to specify the access token and
a directory name.

### All Configuration Options


| Options | Default | Description |
|---------------------- |--------------- |---------------------------------------------------------------------------------------------------------------------------------- |
| access_token | | Dropbox API Access Token. Required. |
| dropbox_dir | "/snapshots" | The directory name in dropbox to upload snapshots. Must start with a forward slash. |
| keep | 10 | The number of snapshots to keep. Once the limit is reached, older snapshots will be removed from Hass.io and Dropbox. |
| mins_between_backups | 60 | How often, in minutes, should the addon check for new backups. |
| filename | snapshot_slug | What filename strategy should be used in Dropbox? Can be either "snapshot_name" or "snapshot_slug". |
| debug | false | A flag to enable/disable verbose logging. If you are having issues, change this to True and include the output in bug reports. |


### Full Configuration Example

```
{
"access_token": "ACCESS TOKEN",
"dropbox_dir": "/hass-snapshots/"
"access_token": "<YOUR_ACCESS_TOKEN>",
"dropbox_dir": "/snapshots",
"keep": 10,
"mins_between_backups": 30
"mins_between_backups": 60,
"filename": "snapshot_name",
"debug": false
}
```

Expand Down
6 changes: 4 additions & 2 deletions dropbox-upload/config.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "Dropbox Upload",
"version": "1.0.13",
"version": "1.1.0",
"slug": "dropbox_upload",
"description": "Upload snapshots to Dropbox!",
"startup": "application",
Expand All @@ -17,14 +17,16 @@
"access_token": "<YOUR_ACCESS_TOKEN>",
"dropbox_dir": "/snapshots",
"keep": 10,
"mins_between_backups": 10,
"mins_between_backups": 60,
"filename": "snapshot_name",
"debug": false
},
"schema": {
"access_token": "str",
"dropbox_dir": "str",
"keep": "int?",
"mins_between_backups": "int?",
"filename": "str",
"debug": "bool?"
}
}
2 changes: 2 additions & 0 deletions dropbox-upload/dropbox_upload/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ def main(config_file, sleeper=time.sleep, DropboxAPI=dropbox.Dropbox):
LOG.debug(copy)
config.setup_logging(cfg)

config.validate(cfg)

try:
dbx = DropboxAPI(cfg["access_token"])
dbx.users_get_current_account()
Expand Down
17 changes: 16 additions & 1 deletion dropbox-upload/dropbox_upload/backup.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,16 @@ def local_path(snapshot):

def dropbox_path(config, snapshot):
dropbox_dir = pathlib.Path(config["dropbox_dir"])
name = snapshot["slug"]

if "filename" in config and config["filename"] == "snapshot_slug":
name = snapshot["slug"]
elif "filename" in config and config["filename"] == "snapshot_name":
name = snapshot["name"]
else:
raise ValueError(
"Unknown value for the filename config: {config.get('filename')}"
)

return dropbox_dir / f"{name}.tar"


Expand Down Expand Up @@ -57,6 +66,12 @@ def process_snapshot(config, dbx, snapshot):
if not os.path.isfile(path):
LOG.warning("The snapshot no longer exists")
return
if not snapshot.get("protected"):
LOG.warning(
f"Snapshot '{snapshot['name']}' is not password protected. Always "
"try to use passwords, particulary when uploading all your data "
"to a snapshot to a third party."
)
bytes_ = os.path.getsize(path)
size = util.bytes_to_human(bytes_)
target = str(dropbox_path(config, snapshot))
Expand Down
29 changes: 29 additions & 0 deletions dropbox-upload/dropbox_upload/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,43 @@
import logging
import sys

from dropbox_upload import exceptions

DEFAULT_CONFIG = "/data/options.json"
LOG = logging.getLogger(__name__)


def load_config(path=DEFAULT_CONFIG):
with open(path) as f:
return json.load(f)


def validate(cfg):
global errored
errored = False

def _e(message):
global errored
LOG.error(message)
errored = True

if not cfg["dropbox_dir"]:
_e("The dropbox_dir can't be an empty string, it must be at least '/'")

if "filename" not in cfg:
cfg["filename"] = "snapshot_slug"

if not cfg["filename"] in ["snapshot_name", "snapshot_slug"]:
_e(
"The `filename` config setting must equal either 'snapshot_name' "
"or 'snapshot_slug'. This is what it will use for the filename in "
"dropbox."
)

if errored:
raise exceptions.InvalidConfig()


def setup_logging(config):
log = logging.getLogger("dropbox_upload")
log.setLevel(logging.DEBUG if config.get("debug") else logging.INFO)
Expand Down
2 changes: 2 additions & 0 deletions dropbox-upload/dropbox_upload/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
class InvalidConfig(Exception):
pass
Empty file.
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[tool.towncrier]
package = "dropbox_upload"
package_dir = "dropbox-upload"
filename = "CHANGELOG.rst"
18 changes: 13 additions & 5 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,27 @@ def cfg():
"access_token": "token",
"debug": True,
"keep": 100,
"filename": "snapshot_slug",
}


@pytest.fixture
def snapshot(requests_mock):
def snapshot():
return {
"slug": "dbaa2add",
"name": "Automated Backup 2018-09-14",
"date": "2018-09-14T01:00:00.873481+00:00",
"type": "full",
"protected": True,
}


@pytest.fixture
def snapshot_unprotected(snapshot):
snapshot["protected"] = False
return snapshot


@pytest.fixture
def snapshots(requests_mock):

Expand All @@ -34,10 +42,10 @@ def snapshots(requests_mock):

snapshots = [
# Intentionally out of order.
{"date": days_ago(3), "slug": "slug3", "name": "name3"},
{"date": days_ago(1), "slug": "slug1", "name": "name1"},
{"date": days_ago(2), "slug": "slug2", "name": "name2"},
{"date": days_ago(0), "slug": "slug0", "name": "name0"},
{"date": days_ago(3), "slug": "slug3", "name": "name3", "protected": True},
{"date": days_ago(1), "slug": "slug1", "name": "name1", "protected": True},
{"date": days_ago(2), "slug": "slug2", "name": "name2", "protected": False},
{"date": days_ago(0), "slug": "slug0", "name": "name0", "protected": True},
]
data = {"data": {"snapshots": snapshots}}
requests_mock.get("http://hassio/snapshots", text=json.dumps(data))
Expand Down
38 changes: 36 additions & 2 deletions tests/test_backup.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import pathlib
from unittest import mock

import pytest

from dropbox_upload import backup


Expand All @@ -10,12 +12,27 @@ def test_local_path():
assert backup.local_path({"slug": "SLUG"}) == expected


def test_dropbox_path(cfg):
def test_dropbox_path_invalid_config():
cfg = {"dropbox_dir": "/"}
with pytest.raises(ValueError):
backup.dropbox_path(cfg, {"slug": "SLUG"})


def test_dropbox_path_slug(cfg):
cfg["dropbox_dir"] = "/dropbox_dir"
cfg["filename"] = "snapshot_slug"
expected = pathlib.Path("/dropbox_dir/SLUG.tar")
assert backup.dropbox_path(cfg, {"slug": "SLUG"}) == expected


def test_dropbox_path_name(cfg):
cfg["dropbox_dir"] = "/dropbox_dir"
cfg["filename"] = "snapshot_name"
snapshot = {"name": "Automated Backup 2018-11-14"}
expected = pathlib.Path("/dropbox_dir/") / f"{snapshot['name']}.tar"
assert backup.dropbox_path(cfg, snapshot) == expected


def test_backup_no_snapshots(cfg, caplog):
backup.backup(None, cfg, [])

Expand All @@ -35,7 +52,7 @@ def test_snapshot_deleted(cfg, snapshot, caplog):
) in caplog.record_tuples


def test_snapshot_stats(cfg, snapshot, caplog, tmpdir, dropbox_fake):
def test_snapshot_stats(cfg, snapshot, tmpdir, dropbox_fake):
file_ = tmpdir.mkdir("sub").join("hello.txt")
file_.write("testing content 24 bytes" * 1000)
with mock.patch("dropbox_upload.backup.local_path") as local_path:
Expand Down Expand Up @@ -74,3 +91,20 @@ def test_backup_file_exists(cfg, dropbox_fake, snapshot, caplog):
logging.INFO,
"Already found in Dropbox with the same hash",
) in caplog.record_tuples


def test_backup_password_warning(cfg, dropbox_fake, snapshot_unprotected, caplog):
caplog.set_level(logging.WARNING)
with mock.patch("dropbox_upload.dropbox.file_exists") as file_exists:
with mock.patch("dropbox_upload.backup.local_path"):
file_exists.return_value = True
backup.process_snapshot(cfg, None, snapshot_unprotected)
assert (
"dropbox_upload.backup",
logging.WARNING,
(
f"Snapshot '{snapshot_unprotected['name']}' is not password "
"protected. Always try to use passwords, particulary when "
"uploading all your data to a snapshot to a third party."
),
) in caplog.record_tuples
25 changes: 25 additions & 0 deletions tests/test_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import json

import pytest

from dropbox_upload import config, exceptions


def test_config_dropbox_dir(tmpdir):

p = tmpdir.join("config.json")
p.write(json.dumps({"dropbox_dir": "/"}))

cfg = config.load_config(p.strpath)
assert cfg["dropbox_dir"] == "/"


def test_config_dropbox_dir_invalid(tmpdir):

p = tmpdir.join("config.json")
p.write(json.dumps({"dropbox_dir": ""}))

cfg = config.load_config(p.strpath)

with pytest.raises(exceptions.InvalidConfig):
config.validate(cfg)

0 comments on commit b004ef1

Please sign in to comment.