From 702d080a1ea8ca650dec07e18f035f03e052eb72 Mon Sep 17 00:00:00 2001 From: Smart Home Cambodia Date: Sun, 23 Apr 2023 18:17:03 +0700 Subject: [PATCH] New Addon Google BK --- hassio-google-drive-backup/AUTHENTICATION.md | 44 + .../BACKUP_AND_SNAPSHOT.md | 123 + hassio-google-drive-backup/CHANGELOG.md | 33 + hassio-google-drive-backup/DOCS.md | 198 + hassio-google-drive-backup/Dockerfile | 12 + hassio-google-drive-backup/Dockerfile-server | 16 + .../GENERATIONAL_BACKUP.md | 41 + hassio-google-drive-backup/README.md | 34 + hassio-google-drive-backup/addon_deps.sh | 7 + hassio-google-drive-backup/backup/__init__.py | 0 hassio-google-drive-backup/backup/__main__.py | 22 + .../backup/config/__init__.py | 11 + .../backup/config/boolvalidator.py | 18 + .../backup/config/byteformatter.py | 56 + .../config/bytesizeasstringvalidator.py | 30 + .../backup/config/config.py | 307 + .../backup/config/createoptions.py | 13 + .../config/durationasstringvalidator.py | 37 + .../backup/config/durationparser.py | 80 + .../backup/config/floatvalidator.py | 25 + .../backup/config/intvalidator.py | 25 + .../backup/config/listvalidator.py | 15 + .../backup/config/regexvalidator.py | 19 + .../backup/config/settings.py | 514 + .../backup/config/startable.py | 11 + .../backup/config/stringvalidator.py | 14 + .../backup/config/validator.py | 21 + .../backup/config/version.py | 84 + hassio-google-drive-backup/backup/const.py | 128 + .../backup/creds/__init__.py | 5 + .../backup/creds/creds.py | 90 + .../backup/creds/driverequester.py | 113 + .../backup/creds/exchanger.py | 160 + .../backup/debug/__init__.py | 2 + .../backup/debug/debug_server.py | 18 + .../backup/debugworker.py | 216 + .../backup/drive/__init__.py | 5 + .../backup/drive/authcodequery.py | 107 + .../backup/drive/driverequests.py | 373 + .../backup/drive/drivesource.py | 282 + .../backup/drive/folderfinder.py | 204 + .../backup/drive/thumbnail.py | 2 + .../backup/exceptions/__init__.py | 2 + .../backup/exceptions/exceptions.py | 446 + .../backup/file/__init__.py | 2 + .../backup/file/file.py | 76 + .../backup/file/jsonfilesaver.py | 71 + .../backup/ha/__init__.py | 8 + .../backup/ha/addon_stopper.py | 146 + .../backup/ha/backupname.py | 39 + .../backup/ha/harequests.py | 329 + .../backup/ha/hasource.py | 537 + .../backup/ha/haupdater.py | 189 + .../backup/ha/password.py | 31 + hassio-google-drive-backup/backup/logger.py | 217 + .../backup/model/__init__.py | 13 + .../backup/model/backups.py | 318 + .../backup/model/backupscheme.py | 236 + .../backup/model/coordinator.py | 354 + .../backup/model/destinationprecache.py | 81 + .../backup/model/drivebackup.py | 74 + .../backup/model/dummybackup.py | 26 + .../backup/model/dummybackupsource.py | 20 + .../backup/model/habackup.py | 72 + .../backup/model/model.py | 397 + .../backup/model/precache.py | 15 + .../backup/model/simulatedsource.py | 129 + .../backup/model/simulation.py | 0 .../backup/model/syncer.py | 36 + hassio-google-drive-backup/backup/module.py | 87 + .../backup/server/__init__.py | 4 + .../backup/server/__main__.py | 27 + .../backup/server/cloudlogger.py | 28 + .../backup/server/errorstore.py | 32 + .../backup/server/server.py | 218 + hassio-google-drive-backup/backup/starter.py | 24 + .../backup/static/authorize.jinja2 | 73 + .../backup/static/css/colpick.css | 439 + .../css/iconfont/MaterialIcons-Regular.eot | Bin 0 -> 143258 bytes .../css/iconfont/MaterialIcons-Regular.ijmap | 1 + .../css/iconfont/MaterialIcons-Regular.svg | 2373 +++ .../css/iconfont/MaterialIcons-Regular.ttf | Bin 0 -> 128180 bytes .../css/iconfont/MaterialIcons-Regular.woff | Bin 0 -> 57620 bytes .../css/iconfont/MaterialIcons-Regular.woff2 | Bin 0 -> 44300 bytes .../backup/static/css/iconfont/codepoints | 932 + .../static/css/iconfont/material-icons.css | 36 + .../backup/static/css/icons.css | 36 + .../backup/static/css/materialize.css | 9058 ++++++++++ .../backup/static/css/materialize.min.css | 13 + .../backup/static/css/spinner.css | 216 + .../backup/static/css/static.css | 590 + .../backup/static/images/bitcoin-button.svg | 20 + .../backup/static/images/bitcoin-logo.svg | 12 + .../backup/static/images/bmc-button.svg | 22 + .../backup/static/images/bmc.svg | 26 + .../backup/static/images/drive_logo.png | Bin 0 -> 312335 bytes .../backup/static/images/ethereum-button.svg | 37 + .../backup/static/images/ethereum-logo.svg | 18 + .../backup/static/images/favicon.png | Bin 0 -> 7258 bytes .../static/images/github-sponsors-button.svg | 32 + .../backup/static/images/hassio_logo.png | Bin 0 -> 4215 bytes .../static/images/hassio_logo_white.png | Bin 0 -> 6336 bytes .../backup/static/images/logo.png | Bin 0 -> 9567 bytes .../backup/static/images/monero-button.svg | 25 + .../backup/static/images/monero-logo.svg | 1 + .../backup/static/images/patreon-button.svg | 15 + .../backup/static/images/patreon.png | Bin 0 -> 3364 bytes .../backup/static/images/paypal-button.svg | 15 + .../backup/static/images/pp.png | Bin 0 -> 1730 bytes .../backup/static/index.jinja2 | 390 + .../backup/static/js/URI.js | 93 + .../backup/static/js/about.js | 113 + .../backup/static/js/colpick.js | 605 + .../js/iconfont/MaterialIcons-Regular.eot | Bin 0 -> 143258 bytes .../js/iconfont/MaterialIcons-Regular.ijmap | 1 + .../js/iconfont/MaterialIcons-Regular.svg | 2373 +++ .../js/iconfont/MaterialIcons-Regular.ttf | Bin 0 -> 128180 bytes .../js/iconfont/MaterialIcons-Regular.woff | Bin 0 -> 57620 bytes .../js/iconfont/MaterialIcons-Regular.woff2 | Bin 0 -> 44300 bytes .../backup/static/js/iconfont/README.md | 9 + .../backup/static/js/iconfont/codepoints | 932 + .../static/js/iconfont/material-icons.css | 36 + .../backup/static/js/icons.css | 36 + .../backup/static/js/init.js | 7 + .../backup/static/js/jquery.js | 2 + .../backup/static/js/jquery.qrcode.min.js | 28 + .../backup/static/js/markdown-it.min.js | 1 + .../backup/static/js/materialize.js | 12337 +++++++++++++ .../backup/static/js/materialize.min.js | 6 + .../backup/static/js/moment.js | 14765 ++++++++++++++++ .../backup/static/js/scripts.js | 894 + .../backup/static/js/settings.js | 353 + .../backup/static/js/theme.js | 247 + .../backup/static/layouts/base-server.jinja2 | 41 + .../backup/static/layouts/base.jinja2 | 51 + .../backup/static/layouts/macros.jinja2 | 36 + .../layouts/partials/error-messages.jinja2 | 693 + .../static/layouts/partials/footer.jinja2 | 40 + .../static/layouts/partials/icons.jinja2 | 80 + .../layouts/partials/modals/about.jinja2 | 76 + .../partials/modals/backup/delete.jinja2 | 106 + .../partials/modals/backup/details.jinja2 | 610 + .../layouts/partials/modals/backup/new.jinja2 | 62 + .../partials/modals/backup/upload.jinja2 | 16 + .../static/layouts/partials/modals/bug.jinja2 | 51 + .../partials/modals/crypto_donate.jinja2 | 138 + .../layouts/partials/modals/debug.jinja2 | 26 + .../layouts/partials/modals/help.jinja2 | 78 + .../layouts/partials/modals/settings.jinja2 | 632 + .../static/layouts/partials/navbar.jinja2 | 129 + .../backup/static/logs.jinja2 | 171 + .../backup/static/picker.jinja2 | 178 + .../backup/static/privacy_policy.jinja2 | 240 + .../backup/static/redirect.jinja2 | 49 + .../backup/static/server-index.jinja2 | 52 + .../backup/static/server-redirect.jinja2 | 45 + .../backup/static/terms_of_service.jinja2 | 86 + .../backup/static/working.jinja2 | 332 + hassio-google-drive-backup/backup/time.py | 175 + .../backup/tracing_session.py | 72 + .../backup/ui/__init__.py | 3 + hassio-google-drive-backup/backup/ui/debug.py | 75 + .../backup/ui/restarter.py | 40 + .../backup/ui/uiserver.py | 908 + .../backup/util/__init__.py | 8 + .../backup/util/asynchttpgetter.py | 241 + .../backup/util/backoff.py | 42 + .../backup/util/data_cache.py | 149 + .../backup/util/error_analyzer.py | 97 + .../backup/util/estimator.py | 105 + .../backup/util/globalinfo.py | 125 + .../backup/util/rangelookup.py | 56 + .../backup/util/resolver.py | 58 + hassio-google-drive-backup/backup/watcher.py | 96 + .../backup/worker/__init__.py | 3 + .../backup/worker/trigger.py | 26 + .../backup/worker/worker.py | 63 + .../cloudbuild-dev.yaml | 17 + .../cloudbuild-server.yaml | 22 + hassio-google-drive-backup/config.json | 106 + hassio-google-drive-backup/dev/__init__.py | 0 hassio-google-drive-backup/dev/apiingress.py | 404 + .../dev/backup/keep_me_here.txt | 1 + hassio-google-drive-backup/dev/base_server.py | 56 + .../dev/data/dev_addon_config.yaml | 3 + .../dev/data/dev_options.json | 28 + .../dev/data/drive_dev_options.json | 20 + .../dev/data/drive_options.json | 17 + .../dev/data/options.json | 11 + .../dev/data/secrets.yaml | 2 + hassio-google-drive-backup/dev/deploy.sh | 6 + .../dev/deploy_addon.py | 19 + .../dev/deploy_dev_addon.py | 20 + .../dev/deploy_dev_server.py | 8 + .../dev/deploy_server.py | 8 + .../dev/http_exception.py | 6 + hassio-google-drive-backup/dev/ports.py | 5 + .../dev/request_interceptor.py | 135 + .../dev/simulated_google.py | 521 + .../dev/simulated_supervisor.py | 410 + .../dev/simulationserver.py | 165 + .../dev/ssl/fullchain.pem | 18 + .../dev/ssl/localhost-ca-bundle.csr | 19 + .../dev/ssl/localhost.crt | 18 + .../dev/ssl/localhost.key | 28 + .../dev/ssl/privkey.pem | 28 + hassio-google-drive-backup/icon.png | Bin 0 -> 7258 bytes hassio-google-drive-backup/logo.png | Bin 0 -> 9567 bytes .../requirements-addon.txt | 19 + .../requirements-server.txt | 20 + hassio-google-drive-backup/setup.py | 8 + hassio-google-drive-backup/snapshot.json | 0 hassio-google-drive-backup/tests/__init__.py | 0 hassio-google-drive-backup/tests/conftest.py | 415 + hassio-google-drive-backup/tests/faketime.py | 50 + hassio-google-drive-backup/tests/helpers.py | 219 + .../tests/test_addon_stopper.py | 355 + .../tests/test_asynchttpgetter.py | 117 + .../tests/test_authcodequery.py | 104 + .../tests/test_backoff.py | 104 + .../tests/test_bytesizeasstring.py | 125 + .../tests/test_config.py | 307 + .../tests/test_coordinator.py | 552 + .../tests/test_data_cache.py | 210 + .../tests/test_debugworker.py | 142 + .../tests/test_destinationprecache.py | 119 + .../tests/test_drivesource.py | 991 ++ .../tests/test_duration_parser.py | 89 + .../tests/test_durationasstringvalidator.py | 28 + .../tests/test_estimator.py | 13 + .../tests/test_exceptions.py | 49 + .../tests/test_exchanger.py | 186 + hassio-google-drive-backup/tests/test_file.py | 60 + .../tests/test_hasource.py | 1087 ++ .../tests/test_haupdater.py | 416 + .../tests/test_jsonfilesaver.py | 63 + .../tests/test_model.py | 1349 ++ .../tests/test_rangelookup.py | 26 + .../tests/test_resolver.py | 46 + .../tests/test_scheme.py | 442 + .../tests/test_server.py | 50 + .../tests/test_settings.py | 37 + .../tests/test_starter.py | 22 + .../tests/test_timezone.py | 59 + .../tests/test_uiserver.py | 1176 ++ .../tests/test_version.py | 47 + .../tests/test_watcher.py | 119 + .../tests/test_worker.py | 46 + 248 files changed, 72745 insertions(+) create mode 100644 hassio-google-drive-backup/AUTHENTICATION.md create mode 100644 hassio-google-drive-backup/BACKUP_AND_SNAPSHOT.md create mode 100644 hassio-google-drive-backup/CHANGELOG.md create mode 100644 hassio-google-drive-backup/DOCS.md create mode 100644 hassio-google-drive-backup/Dockerfile create mode 100644 hassio-google-drive-backup/Dockerfile-server create mode 100644 hassio-google-drive-backup/GENERATIONAL_BACKUP.md create mode 100644 hassio-google-drive-backup/README.md create mode 100644 hassio-google-drive-backup/addon_deps.sh create mode 100644 hassio-google-drive-backup/backup/__init__.py create mode 100644 hassio-google-drive-backup/backup/__main__.py create mode 100644 hassio-google-drive-backup/backup/config/__init__.py create mode 100644 hassio-google-drive-backup/backup/config/boolvalidator.py create mode 100644 hassio-google-drive-backup/backup/config/byteformatter.py create mode 100644 hassio-google-drive-backup/backup/config/bytesizeasstringvalidator.py create mode 100644 hassio-google-drive-backup/backup/config/config.py create mode 100644 hassio-google-drive-backup/backup/config/createoptions.py create mode 100644 hassio-google-drive-backup/backup/config/durationasstringvalidator.py create mode 100644 hassio-google-drive-backup/backup/config/durationparser.py create mode 100644 hassio-google-drive-backup/backup/config/floatvalidator.py create mode 100644 hassio-google-drive-backup/backup/config/intvalidator.py create mode 100644 hassio-google-drive-backup/backup/config/listvalidator.py create mode 100644 hassio-google-drive-backup/backup/config/regexvalidator.py create mode 100644 hassio-google-drive-backup/backup/config/settings.py create mode 100644 hassio-google-drive-backup/backup/config/startable.py create mode 100644 hassio-google-drive-backup/backup/config/stringvalidator.py create mode 100644 hassio-google-drive-backup/backup/config/validator.py create mode 100644 hassio-google-drive-backup/backup/config/version.py create mode 100644 hassio-google-drive-backup/backup/const.py create mode 100644 hassio-google-drive-backup/backup/creds/__init__.py create mode 100644 hassio-google-drive-backup/backup/creds/creds.py create mode 100644 hassio-google-drive-backup/backup/creds/driverequester.py create mode 100644 hassio-google-drive-backup/backup/creds/exchanger.py create mode 100644 hassio-google-drive-backup/backup/debug/__init__.py create mode 100644 hassio-google-drive-backup/backup/debug/debug_server.py create mode 100644 hassio-google-drive-backup/backup/debugworker.py create mode 100644 hassio-google-drive-backup/backup/drive/__init__.py create mode 100644 hassio-google-drive-backup/backup/drive/authcodequery.py create mode 100644 hassio-google-drive-backup/backup/drive/driverequests.py create mode 100644 hassio-google-drive-backup/backup/drive/drivesource.py create mode 100644 hassio-google-drive-backup/backup/drive/folderfinder.py create mode 100644 hassio-google-drive-backup/backup/drive/thumbnail.py create mode 100644 hassio-google-drive-backup/backup/exceptions/__init__.py create mode 100644 hassio-google-drive-backup/backup/exceptions/exceptions.py create mode 100644 hassio-google-drive-backup/backup/file/__init__.py create mode 100644 hassio-google-drive-backup/backup/file/file.py create mode 100644 hassio-google-drive-backup/backup/file/jsonfilesaver.py create mode 100644 hassio-google-drive-backup/backup/ha/__init__.py create mode 100644 hassio-google-drive-backup/backup/ha/addon_stopper.py create mode 100644 hassio-google-drive-backup/backup/ha/backupname.py create mode 100644 hassio-google-drive-backup/backup/ha/harequests.py create mode 100644 hassio-google-drive-backup/backup/ha/hasource.py create mode 100644 hassio-google-drive-backup/backup/ha/haupdater.py create mode 100644 hassio-google-drive-backup/backup/ha/password.py create mode 100644 hassio-google-drive-backup/backup/logger.py create mode 100644 hassio-google-drive-backup/backup/model/__init__.py create mode 100644 hassio-google-drive-backup/backup/model/backups.py create mode 100644 hassio-google-drive-backup/backup/model/backupscheme.py create mode 100644 hassio-google-drive-backup/backup/model/coordinator.py create mode 100644 hassio-google-drive-backup/backup/model/destinationprecache.py create mode 100644 hassio-google-drive-backup/backup/model/drivebackup.py create mode 100644 hassio-google-drive-backup/backup/model/dummybackup.py create mode 100644 hassio-google-drive-backup/backup/model/dummybackupsource.py create mode 100644 hassio-google-drive-backup/backup/model/habackup.py create mode 100644 hassio-google-drive-backup/backup/model/model.py create mode 100644 hassio-google-drive-backup/backup/model/precache.py create mode 100644 hassio-google-drive-backup/backup/model/simulatedsource.py create mode 100644 hassio-google-drive-backup/backup/model/simulation.py create mode 100644 hassio-google-drive-backup/backup/model/syncer.py create mode 100644 hassio-google-drive-backup/backup/module.py create mode 100644 hassio-google-drive-backup/backup/server/__init__.py create mode 100644 hassio-google-drive-backup/backup/server/__main__.py create mode 100644 hassio-google-drive-backup/backup/server/cloudlogger.py create mode 100644 hassio-google-drive-backup/backup/server/errorstore.py create mode 100644 hassio-google-drive-backup/backup/server/server.py create mode 100644 hassio-google-drive-backup/backup/starter.py create mode 100644 hassio-google-drive-backup/backup/static/authorize.jinja2 create mode 100644 hassio-google-drive-backup/backup/static/css/colpick.css create mode 100644 hassio-google-drive-backup/backup/static/css/iconfont/MaterialIcons-Regular.eot create mode 100644 hassio-google-drive-backup/backup/static/css/iconfont/MaterialIcons-Regular.ijmap create mode 100644 hassio-google-drive-backup/backup/static/css/iconfont/MaterialIcons-Regular.svg create mode 100644 hassio-google-drive-backup/backup/static/css/iconfont/MaterialIcons-Regular.ttf create mode 100644 hassio-google-drive-backup/backup/static/css/iconfont/MaterialIcons-Regular.woff create mode 100644 hassio-google-drive-backup/backup/static/css/iconfont/MaterialIcons-Regular.woff2 create mode 100644 hassio-google-drive-backup/backup/static/css/iconfont/codepoints create mode 100644 hassio-google-drive-backup/backup/static/css/iconfont/material-icons.css create mode 100644 hassio-google-drive-backup/backup/static/css/icons.css create mode 100644 hassio-google-drive-backup/backup/static/css/materialize.css create mode 100644 hassio-google-drive-backup/backup/static/css/materialize.min.css create mode 100644 hassio-google-drive-backup/backup/static/css/spinner.css create mode 100644 hassio-google-drive-backup/backup/static/css/static.css create mode 100644 hassio-google-drive-backup/backup/static/images/bitcoin-button.svg create mode 100644 hassio-google-drive-backup/backup/static/images/bitcoin-logo.svg create mode 100644 hassio-google-drive-backup/backup/static/images/bmc-button.svg create mode 100644 hassio-google-drive-backup/backup/static/images/bmc.svg create mode 100644 hassio-google-drive-backup/backup/static/images/drive_logo.png create mode 100644 hassio-google-drive-backup/backup/static/images/ethereum-button.svg create mode 100644 hassio-google-drive-backup/backup/static/images/ethereum-logo.svg create mode 100644 hassio-google-drive-backup/backup/static/images/favicon.png create mode 100644 hassio-google-drive-backup/backup/static/images/github-sponsors-button.svg create mode 100644 hassio-google-drive-backup/backup/static/images/hassio_logo.png create mode 100644 hassio-google-drive-backup/backup/static/images/hassio_logo_white.png create mode 100644 hassio-google-drive-backup/backup/static/images/logo.png create mode 100644 hassio-google-drive-backup/backup/static/images/monero-button.svg create mode 100644 hassio-google-drive-backup/backup/static/images/monero-logo.svg create mode 100644 hassio-google-drive-backup/backup/static/images/patreon-button.svg create mode 100644 hassio-google-drive-backup/backup/static/images/patreon.png create mode 100644 hassio-google-drive-backup/backup/static/images/paypal-button.svg create mode 100644 hassio-google-drive-backup/backup/static/images/pp.png create mode 100644 hassio-google-drive-backup/backup/static/index.jinja2 create mode 100644 hassio-google-drive-backup/backup/static/js/URI.js create mode 100644 hassio-google-drive-backup/backup/static/js/about.js create mode 100644 hassio-google-drive-backup/backup/static/js/colpick.js create mode 100644 hassio-google-drive-backup/backup/static/js/iconfont/MaterialIcons-Regular.eot create mode 100644 hassio-google-drive-backup/backup/static/js/iconfont/MaterialIcons-Regular.ijmap create mode 100644 hassio-google-drive-backup/backup/static/js/iconfont/MaterialIcons-Regular.svg create mode 100644 hassio-google-drive-backup/backup/static/js/iconfont/MaterialIcons-Regular.ttf create mode 100644 hassio-google-drive-backup/backup/static/js/iconfont/MaterialIcons-Regular.woff create mode 100644 hassio-google-drive-backup/backup/static/js/iconfont/MaterialIcons-Regular.woff2 create mode 100644 hassio-google-drive-backup/backup/static/js/iconfont/README.md create mode 100644 hassio-google-drive-backup/backup/static/js/iconfont/codepoints create mode 100644 hassio-google-drive-backup/backup/static/js/iconfont/material-icons.css create mode 100644 hassio-google-drive-backup/backup/static/js/icons.css create mode 100644 hassio-google-drive-backup/backup/static/js/init.js create mode 100644 hassio-google-drive-backup/backup/static/js/jquery.js create mode 100644 hassio-google-drive-backup/backup/static/js/jquery.qrcode.min.js create mode 100644 hassio-google-drive-backup/backup/static/js/markdown-it.min.js create mode 100644 hassio-google-drive-backup/backup/static/js/materialize.js create mode 100644 hassio-google-drive-backup/backup/static/js/materialize.min.js create mode 100644 hassio-google-drive-backup/backup/static/js/moment.js create mode 100644 hassio-google-drive-backup/backup/static/js/scripts.js create mode 100644 hassio-google-drive-backup/backup/static/js/settings.js create mode 100644 hassio-google-drive-backup/backup/static/js/theme.js create mode 100644 hassio-google-drive-backup/backup/static/layouts/base-server.jinja2 create mode 100644 hassio-google-drive-backup/backup/static/layouts/base.jinja2 create mode 100644 hassio-google-drive-backup/backup/static/layouts/macros.jinja2 create mode 100644 hassio-google-drive-backup/backup/static/layouts/partials/error-messages.jinja2 create mode 100644 hassio-google-drive-backup/backup/static/layouts/partials/footer.jinja2 create mode 100644 hassio-google-drive-backup/backup/static/layouts/partials/icons.jinja2 create mode 100644 hassio-google-drive-backup/backup/static/layouts/partials/modals/about.jinja2 create mode 100644 hassio-google-drive-backup/backup/static/layouts/partials/modals/backup/delete.jinja2 create mode 100644 hassio-google-drive-backup/backup/static/layouts/partials/modals/backup/details.jinja2 create mode 100644 hassio-google-drive-backup/backup/static/layouts/partials/modals/backup/new.jinja2 create mode 100644 hassio-google-drive-backup/backup/static/layouts/partials/modals/backup/upload.jinja2 create mode 100644 hassio-google-drive-backup/backup/static/layouts/partials/modals/bug.jinja2 create mode 100644 hassio-google-drive-backup/backup/static/layouts/partials/modals/crypto_donate.jinja2 create mode 100644 hassio-google-drive-backup/backup/static/layouts/partials/modals/debug.jinja2 create mode 100644 hassio-google-drive-backup/backup/static/layouts/partials/modals/help.jinja2 create mode 100644 hassio-google-drive-backup/backup/static/layouts/partials/modals/settings.jinja2 create mode 100644 hassio-google-drive-backup/backup/static/layouts/partials/navbar.jinja2 create mode 100644 hassio-google-drive-backup/backup/static/logs.jinja2 create mode 100644 hassio-google-drive-backup/backup/static/picker.jinja2 create mode 100644 hassio-google-drive-backup/backup/static/privacy_policy.jinja2 create mode 100644 hassio-google-drive-backup/backup/static/redirect.jinja2 create mode 100644 hassio-google-drive-backup/backup/static/server-index.jinja2 create mode 100644 hassio-google-drive-backup/backup/static/server-redirect.jinja2 create mode 100644 hassio-google-drive-backup/backup/static/terms_of_service.jinja2 create mode 100644 hassio-google-drive-backup/backup/static/working.jinja2 create mode 100644 hassio-google-drive-backup/backup/time.py create mode 100644 hassio-google-drive-backup/backup/tracing_session.py create mode 100644 hassio-google-drive-backup/backup/ui/__init__.py create mode 100644 hassio-google-drive-backup/backup/ui/debug.py create mode 100644 hassio-google-drive-backup/backup/ui/restarter.py create mode 100644 hassio-google-drive-backup/backup/ui/uiserver.py create mode 100644 hassio-google-drive-backup/backup/util/__init__.py create mode 100644 hassio-google-drive-backup/backup/util/asynchttpgetter.py create mode 100644 hassio-google-drive-backup/backup/util/backoff.py create mode 100644 hassio-google-drive-backup/backup/util/data_cache.py create mode 100644 hassio-google-drive-backup/backup/util/error_analyzer.py create mode 100644 hassio-google-drive-backup/backup/util/estimator.py create mode 100644 hassio-google-drive-backup/backup/util/globalinfo.py create mode 100644 hassio-google-drive-backup/backup/util/rangelookup.py create mode 100644 hassio-google-drive-backup/backup/util/resolver.py create mode 100644 hassio-google-drive-backup/backup/watcher.py create mode 100644 hassio-google-drive-backup/backup/worker/__init__.py create mode 100644 hassio-google-drive-backup/backup/worker/trigger.py create mode 100644 hassio-google-drive-backup/backup/worker/worker.py create mode 100644 hassio-google-drive-backup/cloudbuild-dev.yaml create mode 100644 hassio-google-drive-backup/cloudbuild-server.yaml create mode 100644 hassio-google-drive-backup/config.json create mode 100644 hassio-google-drive-backup/dev/__init__.py create mode 100644 hassio-google-drive-backup/dev/apiingress.py create mode 100644 hassio-google-drive-backup/dev/backup/keep_me_here.txt create mode 100644 hassio-google-drive-backup/dev/base_server.py create mode 100644 hassio-google-drive-backup/dev/data/dev_addon_config.yaml create mode 100644 hassio-google-drive-backup/dev/data/dev_options.json create mode 100644 hassio-google-drive-backup/dev/data/drive_dev_options.json create mode 100644 hassio-google-drive-backup/dev/data/drive_options.json create mode 100644 hassio-google-drive-backup/dev/data/options.json create mode 100644 hassio-google-drive-backup/dev/data/secrets.yaml create mode 100755 hassio-google-drive-backup/dev/deploy.sh create mode 100644 hassio-google-drive-backup/dev/deploy_addon.py create mode 100644 hassio-google-drive-backup/dev/deploy_dev_addon.py create mode 100644 hassio-google-drive-backup/dev/deploy_dev_server.py create mode 100644 hassio-google-drive-backup/dev/deploy_server.py create mode 100644 hassio-google-drive-backup/dev/http_exception.py create mode 100644 hassio-google-drive-backup/dev/ports.py create mode 100644 hassio-google-drive-backup/dev/request_interceptor.py create mode 100644 hassio-google-drive-backup/dev/simulated_google.py create mode 100644 hassio-google-drive-backup/dev/simulated_supervisor.py create mode 100644 hassio-google-drive-backup/dev/simulationserver.py create mode 100644 hassio-google-drive-backup/dev/ssl/fullchain.pem create mode 100644 hassio-google-drive-backup/dev/ssl/localhost-ca-bundle.csr create mode 100644 hassio-google-drive-backup/dev/ssl/localhost.crt create mode 100644 hassio-google-drive-backup/dev/ssl/localhost.key create mode 100644 hassio-google-drive-backup/dev/ssl/privkey.pem create mode 100644 hassio-google-drive-backup/icon.png create mode 100644 hassio-google-drive-backup/logo.png create mode 100644 hassio-google-drive-backup/requirements-addon.txt create mode 100644 hassio-google-drive-backup/requirements-server.txt create mode 100644 hassio-google-drive-backup/setup.py create mode 100644 hassio-google-drive-backup/snapshot.json create mode 100644 hassio-google-drive-backup/tests/__init__.py create mode 100644 hassio-google-drive-backup/tests/conftest.py create mode 100644 hassio-google-drive-backup/tests/faketime.py create mode 100644 hassio-google-drive-backup/tests/helpers.py create mode 100644 hassio-google-drive-backup/tests/test_addon_stopper.py create mode 100644 hassio-google-drive-backup/tests/test_asynchttpgetter.py create mode 100644 hassio-google-drive-backup/tests/test_authcodequery.py create mode 100644 hassio-google-drive-backup/tests/test_backoff.py create mode 100644 hassio-google-drive-backup/tests/test_bytesizeasstring.py create mode 100644 hassio-google-drive-backup/tests/test_config.py create mode 100644 hassio-google-drive-backup/tests/test_coordinator.py create mode 100644 hassio-google-drive-backup/tests/test_data_cache.py create mode 100644 hassio-google-drive-backup/tests/test_debugworker.py create mode 100644 hassio-google-drive-backup/tests/test_destinationprecache.py create mode 100644 hassio-google-drive-backup/tests/test_drivesource.py create mode 100644 hassio-google-drive-backup/tests/test_duration_parser.py create mode 100644 hassio-google-drive-backup/tests/test_durationasstringvalidator.py create mode 100644 hassio-google-drive-backup/tests/test_estimator.py create mode 100644 hassio-google-drive-backup/tests/test_exceptions.py create mode 100644 hassio-google-drive-backup/tests/test_exchanger.py create mode 100644 hassio-google-drive-backup/tests/test_file.py create mode 100644 hassio-google-drive-backup/tests/test_hasource.py create mode 100644 hassio-google-drive-backup/tests/test_haupdater.py create mode 100644 hassio-google-drive-backup/tests/test_jsonfilesaver.py create mode 100644 hassio-google-drive-backup/tests/test_model.py create mode 100644 hassio-google-drive-backup/tests/test_rangelookup.py create mode 100644 hassio-google-drive-backup/tests/test_resolver.py create mode 100644 hassio-google-drive-backup/tests/test_scheme.py create mode 100644 hassio-google-drive-backup/tests/test_server.py create mode 100644 hassio-google-drive-backup/tests/test_settings.py create mode 100644 hassio-google-drive-backup/tests/test_starter.py create mode 100644 hassio-google-drive-backup/tests/test_timezone.py create mode 100644 hassio-google-drive-backup/tests/test_uiserver.py create mode 100644 hassio-google-drive-backup/tests/test_version.py create mode 100644 hassio-google-drive-backup/tests/test_watcher.py create mode 100644 hassio-google-drive-backup/tests/test_worker.py diff --git a/hassio-google-drive-backup/AUTHENTICATION.md b/hassio-google-drive-backup/AUTHENTICATION.md new file mode 100644 index 0000000..098c3be --- /dev/null +++ b/hassio-google-drive-backup/AUTHENTICATION.md @@ -0,0 +1,44 @@ +# Authentication with Google Drive +This document describes how the addon (Home Assistant Google Drive Backup) authenticates with Google Drive and stores your credentials. It's geared toward those who wish to know more detail and is not necessary to take advantage of the full features of the addon. The document is provided in the interest of providing full transparency into how the add-on works. I've tried to describe this as plainly as possible, but it is technical and therefore may not be understandable to everyone. Feedback on its clarity is appreciated. + + > This document describes how authentication works if you use the big blue "AUTHENTICATE WITH GOOGLE DRIVE" button in the addon. If you're using [your own Google Drive credentials](https://github.com/sabeechen/hassio-google-drive-backup/blob/master/LOCAL_AUTH.md), then none of this applies. + +## Your Credentials and the Needed Permission +To have access to any information in Google Drive, Google's authentication servers must be told that the add-on has the permission. The add-on uses [Google Drive's Rest API (v3)](https://developers.google.com/drive/api/v3/about-sdk) for communication and requests the [drive.file](https://developers.google.com/drive/api/v3/about-auth) permission *scope*. This *scope* means the add-on has access to files and folders that the add-on created, but nothing else. It can't see files you've added to Google Drive through their web interface or anywhere else. Google Drive's Rest API allows the addon to periodically check what backups are uploaded and upload new ones if necessary by making requests over the internet. + +## Authentication with Google Services +For reference, Google's documentation for how to authenticate users with the Google Drive REST API is [here](https://developers.google.com/drive/api/v3/about-auth). Authentication is handled through [OAuth 2.0](https://developers.google.com/identity/protocols/OAuth2), which means that the add-on never actually sees your Google username and password, only an opaque [security token](https://en.wikipedia.org/wiki/Access_token) used to verify that the addon has been given permission. More detail is provided about what that token is and where it is stored later in this document. + +The way a web-based application would normally authenticate with a Google service (eg Google Drive) looks something like this: +1. User navigates to the app's webpage, eg http://examplegoogleapp.com +2. The app generates a URL to Google's servers (https://accounts.google.com) used to grant the app permission. +3. User navigates there, enters their Google username and password, and confirms the intention to give the app some permission (eg one or more *scopes*). +4. Google redirects the user back to the app's webpage with an access token appended to the URL (eg http://examplegoogleapp.com/authenticate?token=0x12345678) +5. The app stores the access token (0x12345678 in this example), and then passes it back to Google whenever it wishes to make access the API on behalf of the user who logged in. + +This access token allows the app to act as if it is the user who created it. In the case of this add-on, the permission granted by the drive.file scope allows it to create folders, upload backups, and retrieve the previously created folders. Because the add-on only ever sees the access token (not the username/password), and the access token only grants limited permissions, the add-on doesn't have a way to elevate its permission further to access other information in Google Drive or your Google account. + +## Authentication for the Add-on + +Google puts some limitations on how the access token must be generated that will be important for understanding how the add-on authenticates in reality: +* When the user is redirected to https://accounts.google.com (step 2), the redirect must be from a known public website associated with the app. +* When the user is redirected back to the app after authorization (step 4), the redirect must be a statically addressed and publicly accessible website. + +These limitations make a technical problem for the addon because most people's Home Assistant instances aren't publicly accessible and the address is different for each one. Performing the authentication workflow exactly as described above won't work. To get around this, I (the developer of this addon) set up a website, https://habackup.io, which serves as the known public and statically addressable website that Google redirects from/to. The source code for this server is available within the add-on's GitHub repository. + +So when you authenticate the add-on, the workflow looks like this: +1. You start at the add-on's web interface, something like https://homeassistant.local:8123/ingress/hassio_google_drive_backup +2. You click the "Authenticate With Google Drive" button, which takes note of the address of your Home Assistant installation (https://homeassistant.local:8123 in this case) and sends you to https://habackup.io/drive/authorize +3. https://habackup.io immediately generates the Google login URL for you and redirects you to https://accounts.google.com +4. You log in with your Google credentials on Google's domain, and confirm you want to give the add-on permission to see files and folders it creates (the drive.file scope) +5. Google redirects you back to https://habackup.io, along with the access token that will be used for future authentication. +6. https://habackup.io redirects you back to your add-on web-UI (which is kept track of in step 2) along with the access token. +7. The addon (on your local Home Assistant installation) persists the access token and uses it in the future any time it needs to talk to Google Drive. + +Notably, your access token isn't persisted at https://habackup.io, it is only passed through back to your local add-on installation. I do this because: +- It ensures your information is only ever stored on your machine, which is reassuring from the user's perspective (eg you). +- If my server (https://habackup.io) ever gets compromised, there isn't any valuable information stored there that compromises you as well. +- This is practicing a form of [defense-in-depth](https://en.wikipedia.org/wiki/Defense_in_depth_%28computing%29) security, where-in [personal data](https://en.wikipedia.org/wiki/Personal_data) is only stored in the places where it is strictly critical. +- It makes the server more simple since it is a stateless machine that doesn't require a database (eg to store your token). + +After your token is generated and stored on your machine, it needs to be *refreshed* periodically with Google Drive. To do this, the addon will again ask https://habackup.io who will relay the request with Google Drive. diff --git a/hassio-google-drive-backup/BACKUP_AND_SNAPSHOT.md b/hassio-google-drive-backup/BACKUP_AND_SNAPSHOT.md new file mode 100644 index 0000000..0a82ab5 --- /dev/null +++ b/hassio-google-drive-backup/BACKUP_AND_SNAPSHOT.md @@ -0,0 +1,123 @@ +# 'Snapshot' vs 'Backup' +In August 2021 [the Home Assistant team announced](https://www.home-assistant.io/blog/2021/08/24/supervisor-update/) that 'snapshots' will be called 'backups' moving forward. This addon exposes a binary sensor to indicate if snapshots are stale and a another sensor that publishes details about backups. Both of the sensors used 'snapshot' in their names and values, so they had to be changed to match the new language. To prevent breaking any existing automations you might have, the addon will only start using the new names and values when you upgrade if you tell it to. + +This can be controlled by using the configuration option ```call_backup_snapshot```, which will use the old names and values for sensors when it is true. If you updated the addon from a version that used to use 'snapshot' in it names, this option will be automatically added when you update to make sure it doesn't break any existing automations. + +Here is a breakdown of what the new and old sensor values mean: + +## Old sensor name/values +These will be the sensor values used when ```call_backup_snapshot: True``` or if the addon is below version 0.105.1. The addon sets ```call_backup_snapshot: True``` automatically if you upgrade the addon from an older version. +### Backup Stale Binary Sensor +#### Entity Id: +```yaml +binary_sensor.snapshots_stale +``` +#### Possible states: +```yaml +on +off +``` +#### Example Attributes: +```yaml +friendly_name: Snapshots Stale +device_class: problem +``` +### Backup State Sensor +#### Entity Id: +```yaml +sensor.snapshot_backup +``` +#### Possible States: +```yaml +error +waiting +backed_up +``` +#### Example Attributes: +```yaml +friendly_name: Snapshots State +last_snapshot: 2021-09-01T20:26:49.100376+00:00 +snapshots_in_google_drive: 2 +snapshots_in_hassio: 2 +snapshots_in_home_assistant: 2 +size_in_google_drive: 2.5 GB +size_in_home_assistant: 2.5 GB +snapshots: +- name: Full Snapshot 2021-02-06 11:37:00 + date: '2021-02-06T18:37:00.916510+00:00' + state: Backed Up + slug: DFG123 +- name: Full Snapshot 2021-02-07 11:00:00 + date: '2021-02-07T18:00:00.916510+00:00' + state: Backed Up + slug: DFG124 +``` + +## New Sensor Names/Values +These will be the sensor values used when ```call_backup_snapshot: False``` or if the configuration option is un-set. New installations of the addon will default to this. +### Backup Stale Binary Sensor +#### Entity Id +```yaml +binary_sensor.backups_stale +``` +#### Possible States +```yaml +on +off +``` +#### Example Attributes: +```yaml +friendly_name: Backups Stale +device_class: problem +``` +### Backup State Sensor +#### Entity Id +```yaml +sensor.backup_state +``` +#### Possible States +```yaml +error +waiting +backed_up +``` +#### Example Attributes: +```yaml +friendly_name: Backup State +last_backup: 2021-09-01T20:26:49.100376+00:00 +last_upload: 2021-09-01T20:26:49.100376+00:00 +backups_in_google_drive: 2 +backups_in_home_assistant: 2 +size_in_google_drive: 2.5 GB +size_in_home_assistant: 2.5 GB +backups: +- name: Full Snapshot 2021-02-06 11:37:00 + date: '2021-02-06T18:37:00.916510+00:00 + state: Backed Up + slug: DFG123 +- name: Full Snapshot 2021-02-07 11:00:00 + date: '2021-02-07T18:00:00.916510+00:00' + state: Backed Up + slug: DFG124 +``` + +### What do the values mean? +```binary_sensor.backups_stale``` is "on" when backups are stale and "off"" otherwise. Backups are stale when the addon is 6 hours past a scheduled backup and no new backup has been made. This delay is in place to avoid triggerring on transient errors (eg internet connectivity problems or one-off problems in Home Assistant). + +```sensor.backup_state``` is: +- ```waiting``` when the addon is first booted up or hasn't been connected to Google Drive yet. +- ```error``` immediately after any error is encountered, even transient ones. +- ```backed_up``` when everything is running fine without errors. + +It's attributes are: +- ```last_backup``` The UTC ISO-8601 date of the most recent backup in Home Assistant or Google Drive. +- ```last_upload``` The UTC ISO-8601 date of the most recent backup uploaded to Google Drive. +- ```backups_in_google_drive``` The number of backups in Google Drive. +- ```backups_in_home_assistant``` The number of backups in Home Assistant. +- ```size_in_google_drive``` A string representation of the space used by backups in Google Drive. +- ```size_in_home_assistant``` A string representation of the space used by backups in Home Assistant. +- ```backups``` The list of each snapshot in decending order of date. Each snapshot includes its ```name```, ```date```, ```slug```, and ```state```. ```state``` can be one of: + - ```Backed Up``` if its in Home Assistant and Google Drive. + - ```HA Only``` if its only in Home Assistant. + - ```Drive Only``` if its only in Google Drive. + - ```Pending``` if the snapshot was requested but not yet complete. diff --git a/hassio-google-drive-backup/CHANGELOG.md b/hassio-google-drive-backup/CHANGELOG.md new file mode 100644 index 0000000..1bc4f1d --- /dev/null +++ b/hassio-google-drive-backup/CHANGELOG.md @@ -0,0 +1,33 @@ +## v0.110.3 [2023-03-24] +- Fix an error causing "Days Between Backups" to be ignored when "Time of Day" for a backup is set. +- Fix a bug causing some timezones to make the addon to fail to start. + +## v0.110.2 [2023-03-24] +- Fix a potential cause of SSL errors when communicating with Google Drive +- Fix a bug causing backups to be requested indefinitely if scheduled during DST transitions. + +## v0.110.1 [2023-01-09] +- Adds some additional options for donating +- Mitgigates SD card corruption by redundantly storing config files needed for addon startup. +- Avoid global throttling of Google Drive API calls by: + - Making sync intervals more spread out and a little random. + - Syncing more selectively when there are modifications to the /backup directory. + - Caching data from Google Drive for short periods during periodic syncing. + - Backing off for a longer time (2 hours) when the addon hits permanent errors. +- Fixes CSS issues that made the logs page hard to use. + +## v0.109.2 [2022-11-15] +* Fixed a bug where disabling deletion from Google Drive and enabling deltes after upload could cause backups in Google Drive to be deleted. + +## v0.109.1 [2022-11-07] +* If configured from the browser, defaults to a "dark" theme if haven't already configured custom colors +* Makes the interval at which the addon publishes sensors to Home Assistant configurable (see the "Uncommon Options" settings) +* "Free space in Google Drive" is now published as an attribute of the "sensor.backup_state" sensor. +* The "binary_sensor.backups_stale" sensor will now report a problem if creating a backup hangs for more than a day. +* Fixes potential whitespace errors when copy-pasting Google Drive credentials. +* Fixes an exception when using generational backup and no backups are present. + +## v0.108.4 [2022-08-22] +* Fixed an error causing "Undefined" to show up for addon descriptions. +* Fixed an error preventing addon thumbnails from showing up. +* Fixed an error causing username/password authentication to fail. diff --git a/hassio-google-drive-backup/DOCS.md b/hassio-google-drive-backup/DOCS.md new file mode 100644 index 0000000..8b79267 --- /dev/null +++ b/hassio-google-drive-backup/DOCS.md @@ -0,0 +1,198 @@ +# Home Assistant Add-on: Google Assistant SDK + +## Installation + +To install the add-on, first follow the installation steps from the [README on GitHub](https://github.com/sabeechen/hassio-google-drive-backup#installation). + +## Configuration + +_Note_: The configuration can be changed easily by starting the add-on and clicking `Settings` in the web UI. +The UI explains what each setting is and you don't need to modify anything before clicking `Start`. +If you would still prefer to modify the settings in yaml, the options are detailed below. + +Add-on configuration example. Don't use this directly, the addon has a lot of configuration options that most users don't need or want: + +```yaml +# Keep 10 backups in Home Assistant +max_backups_in_ha: 10 + +# Keep 10 backups in Google Drive +max_backups_in_google_drive: 10 + +# Ignore backups the add-on hasn't created +ignore_other_backups: True + +# Ignore backups that look like they were created by Home Assistant automatic backup option during upgrades +ignore_upgrade_backups: True + +# Automatically delete "ignored" snapshots after this many days +delete_ignored_after_days: 7 + +# Take a backup every 3 days +days_between_backups: 3 + +# Create backups at 1:30pm exactly +backup_time_of_day: "13:30" + +# Delete backups from Home Assistant immediately after uploading them to Google Drive +delete_after_upload: True + +# Manually specify the backup folder used in Google Drive +specify_backup_folder: true + +# Use a dark and red theme +background_color: "#242424" +accent_color: "#7D0034" + +# Use a password for backup archives. Use "!secret secret_name" to use a password form your secrets file +backup_password: "super_secret" + +# Create backup names like 'Full Backup HA 0.92.0' +backup_name: "{type} Backup HA {version_ha}" + +# Keep a backup once every day for 3 days and once a week for 4 weeks +generational_days: 3 +generational_weeks: 4 + +# Create partial backups with no folders and no configurator add-on +exclude_folders: "homeassistant,ssl,share,addons/local,media" +exclude_addons: "core_configurator" + +# Turn off notifications and staleness sensor +enable_backup_stale_sensor: false +notify_for_stale_backups: false + +# Enable server directly on port 1627 +expose_extra_server: true + +# Allow sending error reports +send_error_reports: true + +# Delete backups after they're uploaded to Google Drive +delete_after_upload: true +``` + +### Option: `max_backups_in_ha` (default: 4) + +The number of backups the add-on will allow Home Assistant to store locally before old ones are deleted. + +### Option: `max_backups_in_google_drive` (default: 4) + +The number of backups the add-on will keep in Google Drive before old ones are deleted. Google Drive gives you 15GB of free storage (at the time of writing) so plan accordingly if you know how big your backups are. + +### Option: `ignore_other_backups` (default: False) +Make the addon ignore any backups it didn't directly create. Any backup already uploaded to Google Drive will not be ignored until you delete it from Google Drive. + +### Option: `ignore_upgrade_backups` (default: False) +Ignores backups that look like they were automatically created from updating an add-on or Home Assistant itself. This will make the add-on ignore any partial backup that has only one add-on or folder in it. + +### Option: `days_between_backups` (default: 3) + +How often a new backup should be scheduled, eg `1` for daily and `7` for weekly. + +### Option: `backup_time_of_day` + +The time of day (local time) that new backups should be created in 24-hour ("HH:MM") format. When not specified backups are created at (roughly) the same time of day as the most recent backup. + + +### Options: `delete_after_upload` (default: False) + +Deletes backups from Home Assistant immediately after uploading them to Google Drive. This is useful if you have very limited space inside Home Assistant since you only need to have available space for a single backup locally. + +### Option: `specify_backup_folder` (default: False) + +When true, you must select the folder in Google Drive where backups are stored. Once you turn this on, restart the add-on and visit the Web-UI to be prompted to select the backup folder. + +### Option: `background_color` and `accent_color` + +The background and accent colors for the web UI. You can use this to make the UI fit in with whatever color scheme you use in Home Assistant. When unset, the interface matches Home Assistant's default blue/white style. + +### Option: `backup_password` + +When set, backups are created with a password. You can use a value from your secrets.yaml by prefixing the password with "!secret". You'll need to remember this password when restoring a backup. + +> Example: Use a password for backup archives +> +> ```yaml +> backup_password: "super_secret" +> ``` +> +> Example: Use a password from secrets.yaml +> +> ```yaml +> backup_password: "!secret backup_password" +> ``` + +### Option: `backup_name` (default: "{type} Backup {year}-{month}-{day} {hr24}:{min}:{sec}") + +Sets the name for new backups. Variable parameters of the form `{variable_name}` can be used to modify the name to your liking. A list of available variables is available [here](https://github.com/sabeechen/hassio-google-drive-backup#can-i-give-backups-a-different-name). + +### Option: `generational_*` + +When set, older backups will be kept longer using a [generational backup scheme](https://en.wikipedia.org/wiki/Backup_rotation_scheme). See the [question here](https://github.com/sabeechen/hassio-google-drive-backup#can-i-keep-older-backups-for-longer) for configuration options. + +### Option: `exclude_folders` + +When set, excludes the comma-separated list of folders by creating a partial backup. + +### Option: `exclude_addons` + +When set, excludes the comma-separated list of addons by creating a partial backup. + +_Note_: Folders and add-ons must be identified by their "slug" name. It is recommended to use the `Settings` dialog within the add-on web UI to configure partial backups since these names are esoteric and hard to find. + +### Option: `enable_backup_stale_sensor` (default: True) + +When false, the add-on will not publish the [binary_sensor.backups_stale](https://github.com/sabeechen/hassio-google-drive-backup#how-will-i-know-this-will-be-there-when-i-need-it) stale sensor. + +### Option: `enable_backup_state_sensor` (default: True) + +When false, the add-on will not publish the [sensor.backup_state](https://github.com/sabeechen/hassio-google-drive-backup#how-will-i-know-this-will-be-there-when-i-need-it) sensor. + +### Option: `notify_for_stale_backups` (default: True) + +When false, the add-on will send a [persistent notification](https://github.com/sabeechen/hassio-google-drive-backup#how-will-i-know-this-will-be-there-when-i-need-it) in Home Assistant when backups are stale. + +--- + +### UI Server Options + +The UI is available through Home Assistant [ingress](https://www.home-assistant.io/blog/2019/04/15/hassio-ingress/). + +It can also be exposed through a web server on port `1627`, which you can map to an externally visible port from the add-on `Network` panel. You can configure a few more options to add SSL or require your Home Assistant username/password. + +#### Option: `expose_extra_server` (default: False) + +Expose the webserver on port `1627`. This is optional, as the add-on is already available with Home Assistant ingress. + +#### Option: `require_login` (default: False) + +When true, requires your home assistant username and password to access the Web UI. + +#### Option: `use_ssl` (default: False) + +When true, requires your home assistant username and password to access the Web UI. + +#### Option: `certfile` (default: `/ssl/certfile.pem`) + +Required when `use_ssl: True`. The path to your SSL key file + +#### Option: `keyfile` (default: `/ssl/keyfile.pem`) + +Required when `use_ssl: True`. The path to your SSL cert file. + +#### Option: `verbose` (default: False) + +If true, enable additional debug logging. Useful if you start seeing errors and need to file a bug with me. + +#### Option: `send_error_reports` (default: False) + +When true, the text of unexpected errors will be sent to a database maintained by the developer. This helps identify problems with new releases and provide better context messages when errors come up. + +#### Option: `delete_after_upload` (default: False) + +When true, backups are always deleted after they've been uploaded to Google Drive. 'max_backups_in_ha' is ignored when this option is True, since a backup is always deleted from Home Assistant after it gets uploaded to Google Drive. Some find this useful if they only have enough space on their Home Assistant machine for one backup. + +## FAQ + +Read the [FAQ on GitHub](https://github.com/sabeechen/hassio-google-drive-backup#faq). diff --git a/hassio-google-drive-backup/Dockerfile b/hassio-google-drive-backup/Dockerfile new file mode 100644 index 0000000..e2d8b42 --- /dev/null +++ b/hassio-google-drive-backup/Dockerfile @@ -0,0 +1,12 @@ +ARG BUILD_FROM +FROM $BUILD_FROM +WORKDIR /app +COPY . /app +RUN chmod +x addon_deps.sh +RUN ./addon_deps.sh +RUN pip3 install . +COPY config.json /usr/local/lib/python3.8/site-packages/config.json + +EXPOSE 1627 +EXPOSE 8099 +ENTRYPOINT ["python3", "-m", "backup"] \ No newline at end of file diff --git a/hassio-google-drive-backup/Dockerfile-server b/hassio-google-drive-backup/Dockerfile-server new file mode 100644 index 0000000..57b414d --- /dev/null +++ b/hassio-google-drive-backup/Dockerfile-server @@ -0,0 +1,16 @@ +# Use the official lightweight Python image. +# https://hub.docker.com/_/python +FROM python:3.9-buster + +# Copy local code to the container image. +ENV APP_HOME /server +WORKDIR $APP_HOME +COPY . ./ +COPY config.json /usr/local/lib/python3.9/site-packages/config.json + +# Install server python requirements +RUN pip3 install --trusted-host pypi.python.org -r requirements-server.txt +RUN pip3 install . + +WORKDIR / +ENTRYPOINT ["python3", "-m", "backup.server"] \ No newline at end of file diff --git a/hassio-google-drive-backup/GENERATIONAL_BACKUP.md b/hassio-google-drive-backup/GENERATIONAL_BACKUP.md new file mode 100644 index 0000000..770ee53 --- /dev/null +++ b/hassio-google-drive-backup/GENERATIONAL_BACKUP.md @@ -0,0 +1,41 @@ +# Generational Backup +Generational backup lets you keep a longer history of backups on daily, weekly, monthly, and yearly cycles. This is in contrast to the "regular" scheme for keeping history backups, which will always just delete the oldest backup when needed. This has the effect of keeping older backups around for a longer time, which is particularly useful if you've made a bad configuration change but didn't notice until several days later. + +## Configuration +The generational backup will be used when any one of `generational_days`, `generational_weeks`, `generational_months`, or `generational_years` is greater than zero. All of the available configuration options are given below, but utes much easier to configure from the Settings dialog accessible from the "Settings" menu at the top of the web UI. +* `generational_days` (int): The number of days to keep +* `generational_weeks` (int): The number of weeks to keep +* `generational_months` (int): The number of months to keep +* `generational_years` (int): The number of years to keep +* `generational_day_of_week` (str): The day of the week when weekly backups will be kept. It can be one of 'mon', 'tue', 'wed', 'thu', 'fri', 'sat' or 'sun'. The default is 'mon'. +* `generational_day_of_month` (int): The day of the month when monthly backups will be kept, from 1 to 31. If a month has less than the configured number of days, the latest day of that month is used. +* `generational_day_of_year` (int): The day of the year that yearly backups are kept, from 1 to 365. + +## Some Details to Consider +* Generational backup assumes that a backup is available for every day to work properly, so it's recommended that you set `days_between_backups`=1 if you're using the feature. Otherwise, a backup may not be available to be saved for a given day. +* The backups maintained by generational backup will still never exceed the number you permit to be maintained in Google Drive or Home Assistant. For example, if `max_backups_in_google_drive`=3 and `generational_weeks`=4, then only 3 weeks of backups will be kept in Google Drive. +* Generational backup will only delete older backups when it has to. For example, if you've configured it to keep 5 weekly backups on Monday, you've been running it for a week (so you have 7 backups), and `max_backups_in_google_drive`=7, then your backups on Tuesday, Wednesday, etc won't get deleted yet. They won't get deleted until doing so is necessary to keep older backups around without violating the maximum allowed in Google Drive. +>Note: You can configure the addon to delete backups more aggressively by setting `generational_delete_early`=true. With this, the addon will delete old backups that don't match a daily, weekly, monthly, or yearly configured cycle even if you aren't yet at risk of exceeding `max_backups_in_ha` or `max_backups_in_google_drive`. Careful though! You can accidentally delete all your backups this way if you don't have all your settings configured just the way you want them. +* If more than one backup is created for a day (for example if you create one manually) then only the latest backup from that day will be kept. + +## Schedule +Figuring out date math in your head is hard, so it's useful to see a concrete example. Consider you have the following configuration. Two backups for each day, week, month, and year along with a limit in Google drive large enough to accommodate them all: +```json +"days_between_backups": 1, +"generational_days": 2, +"generational_weeks": 2, +"generational_months": 2 +"generational_years": 2 +"max_backups_in_google_drive": 8 +``` +Imagine you've been running the add-on for 2 years now, diligently making a backup every day with no interruptions. On 19 May 2021, you could expect your list of backups in Google Drive to look like this: +- May 19, 2021 <-- 1st Daily backup +- May 18, 2021 <-- 2nd Daily backup +- May 13, 2021 <-- 1st Weekly backup +- May 06, 2021 <-- 2nd Weekly backup +- May 01, 2021 <-- 1st Monthly backup +- April 01, 2021 <-- 2nd Monthly backup +- January 01, 2021 <-- 1st Yearly backup +- January 01, 2020 <-- 2nd Yearly backup + +Note that sometimes a day might overlap more than one schedule. For example, a backup on January 1st could satisfy the constraints for both a yearly and monthly backup. In this case, the add-on will only delete older backups when it *must* to keep from exceeding `max_backups_in_ha` or `max_backups_in_google_drive`. Thus, the most recent backup that would otherwise be deleted will be kept until space is needed somewhere else in the schedule. diff --git a/hassio-google-drive-backup/README.md b/hassio-google-drive-backup/README.md new file mode 100644 index 0000000..94489ae --- /dev/null +++ b/hassio-google-drive-backup/README.md @@ -0,0 +1,34 @@ +# Home Assistant Add-on: Google Drive Backup + +A complete and easy way to upload your Home Assistant backups to Google Drive. + +## About + +Quickly set up a backup strategy without much fuss. It doesn't require much familiarity with Home Assistant, its architecture, or Google Drive. Detailed install instructions are provided below but you can just add the repo, click install and open the Web UI. It will tell you what to do and only takes a few simple clicks. + +>This project requires financial support to make the Google Drive integration work, but it is free for you to use. You can join those helping to keep the lights on at: +> +>[](https://www.buymeacoffee.com/sabeechen) +>[](https://www.paypal.com/paypalme/stephenbeechen) +>[](https://www.patreon.com/bePatron?u=4064183) +>[](https://github.com/sponsors/sabeechen) +>[](https://github.com/sabeechen/hassio-google-drive-backup/blob/master/donate-crypto.md) +>[](https://github.com/sabeechen/hassio-google-drive-backup/blob/master/donate-crypto.md) +>[](https://github.com/sabeechen/hassio-google-drive-backup/blob/master/donate-crypto.md) + + +### Features + +- Creates backups on a configurable schedule. +- Uploads backups to Drive, even the ones it didn't create. +- Clean up old backups in Home Assistant and Google Drive, so you don't run out of space. +- Restore from a fresh install or recover quickly from disaster by uploading your backups directly from Google Drive. +- Integrates with Home Assistant Notifications, and provides sensors you can trigger off of. +- Notifies you when something goes wrong with your backups. +- Super easy installation and configuration. +- Privacy-centric design philosophy. +- Comprehensive documentation. +- _Most certainly_ doesn't mine bitcoin on your home automation server. Definitely no. + +See the [README on GitHub](https://github.com/sabeechen/hassio-google-drive-backup) for all the details, or just install the add-on and open the Web UI. +The Web-UI explains everything you have to do. diff --git a/hassio-google-drive-backup/addon_deps.sh b/hassio-google-drive-backup/addon_deps.sh new file mode 100644 index 0000000..2b781f6 --- /dev/null +++ b/hassio-google-drive-backup/addon_deps.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +apk add python3 fping linux-headers libc-dev libffi-dev python3-dev gcc py3-pip +pip3 install --upgrade pip wheel setuptools +pip3 install --trusted-host pypi.python.org -r requirements-addon.txt +# Remove packages we only needed for installation +apk del linux-headers libc-dev libffi-dev python3-dev gcc \ No newline at end of file diff --git a/hassio-google-drive-backup/backup/__init__.py b/hassio-google-drive-backup/backup/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/hassio-google-drive-backup/backup/__main__.py b/hassio-google-drive-backup/backup/__main__.py new file mode 100644 index 0000000..fdee209 --- /dev/null +++ b/hassio-google-drive-backup/backup/__main__.py @@ -0,0 +1,22 @@ +import platform +import asyncio +from aiorun import run +from injector import Injector + +from backup.module import MainModule, BaseModule +from backup.starter import Starter + + +async def main(): + await Injector([BaseModule(), MainModule()]).get(Starter).start() + while True: + await asyncio.sleep(1) + + +if __name__ == '__main__': + if platform.system() == "Windows": + # Needed for dev on windows machines + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) + asyncio.run(main()) + else: + run(main()) diff --git a/hassio-google-drive-backup/backup/config/__init__.py b/hassio-google-drive-backup/backup/config/__init__.py new file mode 100644 index 0000000..49ad36c --- /dev/null +++ b/hassio-google-drive-backup/backup/config/__init__.py @@ -0,0 +1,11 @@ +# flake8: noqa +from .config import Config, GenConfig, UPGRADE_OPTIONS +from .settings import Setting, _DEFAULTS, _VALIDATORS, _LOOKUP, VERSION, PRIVATE, isStaging, addon_config, _CONFIG +from .createoptions import CreateOptions +from .boolvalidator import BoolValidator +from .startable import Startable +from .listvalidator import ListValidator +from .durationasstringvalidator import DurationAsStringValidator +from .bytesizeasstringvalidator import BytesizeAsStringValidator +from .version import Version +from .durationparser import DurationParser diff --git a/hassio-google-drive-backup/backup/config/boolvalidator.py b/hassio-google-drive-backup/backup/config/boolvalidator.py new file mode 100644 index 0000000..20a5882 --- /dev/null +++ b/hassio-google-drive-backup/backup/config/boolvalidator.py @@ -0,0 +1,18 @@ +from .validator import Validator +from ..logger import getLogger + +logger = getLogger(__name__) + + +class BoolValidator(Validator): + def __init__(self, name): + super().__init__(name) + + def validate(self, value): + if value is None or (type(value) == str and len(value) == 0): + return None + return BoolValidator.strToBool(value) + + @classmethod + def strToBool(cls, value) -> bool: + return str(value).lower() in ['true', 't', 'on', 'yes', 'y', '1', 'hai', 'si', 'omgyesplease'] diff --git a/hassio-google-drive-backup/backup/config/byteformatter.py b/hassio-google-drive-backup/backup/config/byteformatter.py new file mode 100644 index 0000000..241baec --- /dev/null +++ b/hassio-google-drive-backup/backup/config/byteformatter.py @@ -0,0 +1,56 @@ +import re +from injector import inject, singleton + +SECOND_IDENTIFIERS = ["s", "sec", "secs", "second", "seconds"] +MINUTE_IDENTIFIERS = ["m", "min", "mins", "minute", "minutes"] +HOUR_IDENTIFIERS = ["h", "hr", "hour", "hours"] +DAY_IDENTIFIERS = ["d", "day", "days"] +NUMBER_REGEX = "^([0-9]*[.])?[0-9]+" +VALID_REGEX = "^[ ]*([0-9,]*\\.?[0-9]*)[ ]*(b|B|k|K|m|M|g|G|t|T|p|P|e|E|z|Z|y|Y)[a-zA-Z ]*[ ]*$" +BYTES_BASE = 1024 +PREFIX_VALUES = { + "b": 1, + "k": BYTES_BASE, + "m": pow(BYTES_BASE, 2), + "g": pow(BYTES_BASE, 3), + "t": pow(BYTES_BASE, 4), + "p": pow(BYTES_BASE, 5), + "e": pow(BYTES_BASE, 6), + "z": pow(BYTES_BASE, 7), + "y": pow(BYTES_BASE, 8) +} + +PREFIX_CANONICAL = ["", "K", "M", "G", "T", "P", "E", "Z", "Y"] + + +@singleton +class ByteFormatter(): + @inject + def __init__(self): + pass + + def parse(self, source: str): + source = source.lower() + match = re.match(VALID_REGEX, source.lower()) + if not match: + raise ValueError() + number, prefix = match.group(1, 2) + if prefix not in PREFIX_VALUES: + raise ValueError() + + return float(number) * PREFIX_VALUES[prefix] + + def format(self, bytes): + for prefix in PREFIX_CANONICAL: + if bytes < BYTES_BASE: + if int(bytes) == bytes: + return f"{int(bytes)} {prefix}B" + else: + return f"{bytes} {prefix}B" + bytes /= BYTES_BASE + + bytes *= BYTES_BASE + if int(bytes) == bytes: + return f"{int(bytes)} YB" + else: + return f"{bytes} YB" diff --git a/hassio-google-drive-backup/backup/config/bytesizeasstringvalidator.py b/hassio-google-drive-backup/backup/config/bytesizeasstringvalidator.py new file mode 100644 index 0000000..8fe35dc --- /dev/null +++ b/hassio-google-drive-backup/backup/config/bytesizeasstringvalidator.py @@ -0,0 +1,30 @@ +from .byteformatter import ByteFormatter +from .validator import Validator + + +class BytesizeAsStringValidator(Validator): + def __init__(self, name, minimum=None, maximum=None): + super().__init__(name) + self.min = minimum + self.max = maximum + + def validate(self, value): + if type(value) is str: + value = value.strip() + if value is None or (type(value) == str and len(value) == 0): + return None + try: + if type(value) == str: + value = ByteFormatter().parse(value) + value = float(value) + except ValueError: + self.raiseForValue(value) + + if self.max is not None and value > self.max: + self.raiseForValue(value) + if self.min is not None and value < self.min: + self.raiseForValue(value) + return value + + def formatForUi(self, value): + return ByteFormatter().format(value) diff --git a/hassio-google-drive-backup/backup/config/config.py b/hassio-google-drive-backup/backup/config/config.py new file mode 100644 index 0000000..c2ced2f --- /dev/null +++ b/hassio-google-drive-backup/backup/config/config.py @@ -0,0 +1,307 @@ +import json +import os +import os.path +import uuid +from typing import Any, Dict, List, Optional +from yarl import URL + +from .settings import _LOOKUP, Setting, _VALIDATORS +from ..logger import getLogger +from backup.file import JsonFileSaver + +logger = getLogger(__name__) + +ALWAYS_KEEP = { + Setting.DAYS_BETWEEN_BACKUPS, + Setting.MAX_BACKUPS_IN_HA, + Setting.MAX_BACKUPS_IN_GOOGLE_DRIVE, +} + +KEEP_DEFAULT = { + Setting.SEND_ERROR_REPORTS, + Setting.IGNORE_UPGRADE_BACKUPS +} + +# these are the options that should trigger a restart of the server +SERVER_OPTIONS = { + Setting.USE_SSL, + Setting.REQUIRE_LOGIN, + Setting.CERTFILE, + Setting.KEYFILE, + Setting.EXPOSE_EXTRA_SERVER +} + +NON_UI_SETTING = { + Setting.SUPERVISOR_URL, + Setting.TOKEN_SERVER_HOSTS, + Setting.DRIVE_AUTHORIZE_URL, + Setting.DRIVE_DEVICE_CODE_URL, + Setting.DEFAULT_DRIVE_CLIENT_ID, + Setting.NEW_BACKUP_TIMEOUT_SECONDS, + Setting.LOG_LEVEL, + Setting.CONSOLE_LOG_LEVEL, + Setting.DEFAULT_SYNC_INTERVAL_VARIATION, + Setting.CACHE_WARMUP_MAX_SECONDS, + Setting.CACHE_WARMUP_ERROR_TIMEOUT_SECONDS, + Setting.WATCH_BACKUP_DIRECTORY, + Setting.TRACE_REQUESTS, + Setting.MAX_BACKOFF_SECONDS +} + +UPGRADE_OPTIONS = { + Setting.DEPRECTAED_MAX_BACKUPS_IN_HA: Setting.MAX_BACKUPS_IN_HA, + Setting.DEPRECTAED_MAX_BACKUPS_IN_GOOGLE_DRIVE: Setting.MAX_BACKUPS_IN_GOOGLE_DRIVE, + Setting.DEPRECATED_DAYS_BETWEEN_BACKUPS: Setting.DAYS_BETWEEN_BACKUPS, + Setting.DEPRECTAED_IGNORE_OTHER_BACKUPS: Setting.IGNORE_OTHER_BACKUPS, + Setting.DEPRECTAED_IGNORE_UPGRADE_BACKUPS: Setting.IGNORE_UPGRADE_BACKUPS, + Setting.DEPRECTAED_DELETE_BEFORE_NEW_BACKUP: Setting.DELETE_BEFORE_NEW_BACKUP, + Setting.DEPRECTAED_BACKUP_NAME: Setting.BACKUP_NAME, + Setting.DEPRECTAED_BACKUP_TIME_OF_DAY: Setting.BACKUP_TIME_OF_DAY, + Setting.DEPRECTAED_SPECIFY_BACKUP_FOLDER: Setting.SPECIFY_BACKUP_FOLDER, + Setting.DEPRECTAED_NOTIFY_FOR_STALE_BACKUPS: Setting.NOTIFY_FOR_STALE_BACKUPS, + Setting.DEPRECTAED_ENABLE_BACKUP_STALE_SENSOR: Setting.ENABLE_BACKUP_STALE_SENSOR, + Setting.DEPRECTAED_ENABLE_BACKUP_STATE_SENSOR: Setting.ENABLE_BACKUP_STATE_SENSOR, + Setting.DEPRECATED_BACKUP_PASSWORD: Setting.BACKUP_PASSWORD +} + +EMPTY_IS_DEFAULT = { + Setting.ACCENT_COLOR, + Setting.BACKGROUND_COLOR, +} + + +class GenConfig(): + def __init__(self, days=0, weeks=0, months=0, years=0, day_of_week='mon', day_of_month=1, day_of_year=1, aggressive=False): + self.days = days + self.weeks = weeks + self.months = months + self.years = years + self.day_of_week = day_of_week + self.day_of_month = day_of_month + self.day_of_year = day_of_year + self.aggressive = aggressive + self._config_was_upgraded = False + + def __eq__(self, other): + """Overrides the default implementation""" + if isinstance(other, GenConfig): + return self.__dict__ == other.__dict__ + return NotImplemented + + def __hash__(self): + """Overrides the default implementation""" + return hash(tuple(sorted(self.__dict__.items()))) + + +class Config(): + @classmethod + def fromFile(cls, config_path): + return Config(JsonFileSaver.read(config_path)) + + @classmethod + def withOverrides(cls, overrides): + config = Config() + for key in overrides.keys(): + config.override(key, overrides[key]) + return config + + @classmethod + def withFileOverrides(cls, override_path): + data = JsonFileSaver.read(override_path) + overrides = {} + for key in data.keys(): + overrides[_LOOKUP[key]] = data[key] + return Config.withOverrides(overrides) + + @classmethod + def fromEnvironment(cls): + config = {} + for key in os.environ: + if key in _LOOKUP: + config[_LOOKUP[key]] = _VALIDATORS[_LOOKUP[key]].validate(os.environ[key]) + elif str.lower(key) in _LOOKUP: + config[_LOOKUP[str.lower(key)]] = _VALIDATORS[_LOOKUP[str.lower(key)]].validate(os.environ[key]) + return Config(config) + + def __init__(self, data=None): + self.overrides = {} + if data is None: + self.config = {} + else: + self.config = data + self._legacy_ignored_behavior = False + self._subscriptions = [] + self._clientIdentifier = None + self.retained = self._loadRetained() + self._gen_config_cache = self.getGenerationalConfig() + + # Tracks when hosts have been seen to be offline to retry on different hosts. + self._commFailure = {} + + def getConfigFor(self, options): + new_config = Config() + new_config.overrides = self.overrides.copy() + new_config.update(options) + return new_config + + def validateUpdate(self, additions): + new_config = self.config.copy() + new_config.update(additions) + validated, upgraded = self.validate(new_config) + return validated + + def validate(self, new_config) -> Dict[str, Any]: + final_config = {} + + upgraded = False + # validate each item + for key in new_config: + if type(key) == str: + if key not in _LOOKUP: + # its not in the schema, just ignore it + continue + setting = _LOOKUP[key] + else: + setting = key + + value = setting.validator().validate(new_config[key]) + if setting in UPGRADE_OPTIONS: + upgraded = True + if isinstance(value, str) and len(value) == 0 and setting in EMPTY_IS_DEFAULT: + value = setting.default() + if value is not None and (setting in KEEP_DEFAULT or value != setting.default()): + if setting in UPGRADE_OPTIONS and (UPGRADE_OPTIONS[setting] not in new_config or new_config[UPGRADE_OPTIONS[setting]] == UPGRADE_OPTIONS[setting].default()): + upgraded = True + final_config[UPGRADE_OPTIONS[setting]] = value + elif setting not in UPGRADE_OPTIONS: + final_config[setting] = value + + if upgraded: + final_config[Setting.CALL_BACKUP_SNAPSHOT] = True + + # add in non-ui settings + for setting in NON_UI_SETTING: + if self.get(setting) != setting.default() and not (setting in new_config or setting.key in new_config) and setting not in self.overrides: + final_config[setting] = self.get(setting) + + # add defaults + for key in ALWAYS_KEEP: + if key not in final_config: + final_config[key] = key.default() + + if not final_config.get(Setting.USE_SSL, False): + for key in [Setting.CERTFILE, Setting.KEYFILE]: + if key in final_config: + del final_config[key] + + return final_config, upgraded + + def update(self, new_config): + validated, upgraded = self.validate(new_config) + self._config_was_upgraded = upgraded + self.config = validated + self._gen_config_cache = self.getGenerationalConfig() + for sub in self._subscriptions: + sub() + + def getServerOptions(self): + ret = {} + for setting in SERVER_OPTIONS: + ret[setting] = self.get(setting) + return ret + + def subscribe(self, func): + self._subscriptions.append(func) + + def clientIdentifier(self) -> str: + if self._clientIdentifier is None: + try: + if JsonFileSaver.exists(self.get(Setting.ID_FILE_PATH)): + self._clientIdentifier = JsonFileSaver.read(self.get(Setting.ID_FILE_PATH))['id'] + else: + self._clientIdentifier = str(uuid.uuid4()) + JsonFileSaver.write(self.get(Setting.ID_FILE_PATH), {'id': self._clientIdentifier}) + except Exception: + self._clientIdentifier = str(uuid.uuid4()) + return self._clientIdentifier + + def getGenerationalConfig(self) -> Optional[Dict[str, Any]]: + days = self.get(Setting.GENERATIONAL_DAYS) + weeks = self.get(Setting.GENERATIONAL_WEEKS) + months = self.get(Setting.GENERATIONAL_MONTHS) + years = self.get(Setting.GENERATIONAL_YEARS) + if days + weeks + months + years == 0: + return None + base = GenConfig( + days=days, + weeks=weeks, + months=months, + years=years, + day_of_week=self.get(Setting.GENERATIONAL_DAY_OF_WEEK), + day_of_month=self.get(Setting.GENERATIONAL_DAY_OF_MONTH), + day_of_year=self.get(Setting.GENERATIONAL_DAY_OF_YEAR), + aggressive=self.get(Setting.GENERATIONAL_DELETE_EARLY) + ) + if base.days <= 1: + # must always be >= 1, otherwise we'll just create and delete backups constantly. + base.days = 1 + return base + + def _loadRetained(self) -> List[str]: + if JsonFileSaver.exists(self.get(Setting.RETAINED_FILE_PATH)): + try: + return JsonFileSaver.read(self.get(Setting.RETAINED_FILE_PATH))['retained'] + except json.decoder.JSONDecodeError: + logger.error("Unable to parse retained backup settings") + return [] + return [] + + def isRetained(self, slug): + return slug in self.retained + + def setRetained(self, slug, retain): + if retain and slug not in self.retained: + self.retained.append(slug) + JsonFileSaver.write(self.get(Setting.RETAINED_FILE_PATH), {'retained': self.retained}) + elif not retain and slug in self.retained: + self.retained.remove(slug) + JsonFileSaver.write(self.get(Setting.RETAINED_FILE_PATH), {'retained': self.retained}) + + def isExplicit(self, setting): + return setting in self.config or setting.value in self.config + + def override(self, setting: Setting, value): + self.overrides[setting] = value + return self + + def get(self, setting: Setting) -> Any: + if setting in self.overrides: + return self.overrides[setting] + if setting in self.config: + return self.config[setting] + if setting.key() in self.config: + return self.config[setting.key()] + else: + if setting == Setting.IGNORE_UPGRADE_BACKUPS and self._legacy_ignored_behavior: + # Use the old behavior, rather than the new one + return False + return setting.default() + + def getForUi(self, setting: Setting): + return _VALIDATORS[setting].formatForUi(self.get(setting)) + + def getTokenServers(self, path: str = "") -> List[URL]: + return list(map(lambda s: URL(s).with_path(path), self.get(Setting.TOKEN_SERVER_HOSTS).split(","))) + + def mustSaveUpgradeChanges(self): + return self._config_was_upgraded + + def getAllConfig(self) -> Dict[Setting, Any]: + return self.config.copy() + + def persistedChanges(self): + self._config_was_upgraded = False + + def useLegacyIgnoredBehavior(self, value: bool): + """If the user upgrades from an old version and hasn't explicitely said they want to include upgrade backups, then this reverts them to the old behavior where they aren't ignored""" + self._legacy_ignored_behavior = value diff --git a/hassio-google-drive-backup/backup/config/createoptions.py b/hassio-google-drive-backup/backup/config/createoptions.py new file mode 100644 index 0000000..12a08f9 --- /dev/null +++ b/hassio-google-drive-backup/backup/config/createoptions.py @@ -0,0 +1,13 @@ +from datetime import datetime +from typing import Dict +from ..logger import getLogger + +logger = getLogger(__name__) + + +class CreateOptions(object): + def __init__(self, when: datetime, name_template: str, retain_sources: Dict[str, bool] = {}, note: str = None): + self.when: datetime = when + self.name_template: str = name_template + self.retain_sources: Dict[str, bool] = retain_sources + self.note = note diff --git a/hassio-google-drive-backup/backup/config/durationasstringvalidator.py b/hassio-google-drive-backup/backup/config/durationasstringvalidator.py new file mode 100644 index 0000000..8df5a3a --- /dev/null +++ b/hassio-google-drive-backup/backup/config/durationasstringvalidator.py @@ -0,0 +1,37 @@ +from datetime import timedelta +from .durationparser import DurationParser +from .validator import Validator + + +class DurationAsStringValidator(Validator): + def __init__(self, name, minimum=None, maximum=None, base_seconds=1, default_as_empty=None): + super().__init__(name) + self.min = minimum + self.max = maximum + self.base_seconds = base_seconds + self.default_as_empty = default_as_empty + + def validate(self, value): + if value is None or (type(value) == str and len(value) == 0): + return None + try: + if type(value) == str: + if self.default_as_empty is not None and value == "": + value = self.default_as_empty + else: + value = DurationParser().parse(value).total_seconds() / self.base_seconds + value = float(value) + except ValueError: + self.raiseForValue(value) + + if self.max is not None and value > self.max: + self.raiseForValue(value) + if self.min is not None and value < self.min: + self.raiseForValue(value) + return value + + def formatForUi(self, value): + if self.default_as_empty is not None and value == self.default_as_empty: + return "" + else: + return DurationParser().format(timedelta(seconds=value * self.base_seconds)) diff --git a/hassio-google-drive-backup/backup/config/durationparser.py b/hassio-google-drive-backup/backup/config/durationparser.py new file mode 100644 index 0000000..ed353f1 --- /dev/null +++ b/hassio-google-drive-backup/backup/config/durationparser.py @@ -0,0 +1,80 @@ +import re +from datetime import timedelta +from injector import inject, singleton + +SECOND_IDENTIFIERS = ["s", "sec", "secs", "second", "seconds"] +MINUTE_IDENTIFIERS = ["m", "min", "mins", "minute", "minutes"] +HOUR_IDENTIFIERS = ["h", "hr", "hour", "hours"] +DAY_IDENTIFIERS = ["d", "day", "days"] +NUMBER_REGEX = "^([0-9]*[.])?[0-9]+" +VALID_REGEX = "^([ ]*([0-9]*[.])?[0-9]+[ ]*(seconds|second|secs|sec|s|minutes|minute|mins|min|m|hours|hour|hr|h|days|day|d)?[ ,]*)*" + + +@singleton +class DurationParser(): + @inject + def __init__(self): + pass + + def parse(self, source: str): + source = source.lower() + total_match = re.match(VALID_REGEX, source) + if not total_match or total_match.group(0) != source: + raise ValueError() + parts = source.split() + i = 0 + total = timedelta(seconds=0) + while (i < len(parts)): + part = parts[i].strip().strip(',') + match = re.match(NUMBER_REGEX, part) + i += 1 + if not match: + raise ValueError() + length = float(match.group(0)) + if match.group(0) == part: + + if i < len(parts): + next_part = parts[i].strip().strip(',') + if next_part in SECOND_IDENTIFIERS or next_part in MINUTE_IDENTIFIERS or next_part in HOUR_IDENTIFIERS or next_part in DAY_IDENTIFIERS: + identifier = next_part + i += 1 + else: + identifier = SECOND_IDENTIFIERS[0] + else: + identifier = "s" + else: + identifier = part[len(match.group(0)):] + if identifier in SECOND_IDENTIFIERS: + total += timedelta(seconds=length) + elif identifier in MINUTE_IDENTIFIERS: + total += timedelta(minutes=length) + elif identifier in HOUR_IDENTIFIERS: + total += timedelta(hours=length) + elif identifier in DAY_IDENTIFIERS: + total += timedelta(days=length) + else: + raise ValueError() + return total + + def format(self, duration: timedelta): + parts = [] + if duration >= timedelta(days=1): + days = int(duration.days) + parts.append("{} days".format(days)) + duration = duration - timedelta(days=days) + if duration >= timedelta(hours=1): + hours = int(duration.seconds / (60 * 60)) + parts.append("{} hours".format(hours)) + duration = duration - timedelta(hours=hours) + if duration >= timedelta(minutes=1): + minutes = int(duration.seconds / 60) + parts.append("{} minutes".format(minutes)) + duration = duration - timedelta(minutes=minutes) + if duration >= timedelta(seconds=1): + seconds = int(duration.seconds) + parts.append("{} seconds".format(seconds)) + duration = duration - timedelta(seconds=seconds) + if len(parts) > 0: + return ", ".join(parts) + else: + return "0 seconds" diff --git a/hassio-google-drive-backup/backup/config/floatvalidator.py b/hassio-google-drive-backup/backup/config/floatvalidator.py new file mode 100644 index 0000000..fcf47b3 --- /dev/null +++ b/hassio-google-drive-backup/backup/config/floatvalidator.py @@ -0,0 +1,25 @@ +from .validator import Validator +from ..logger import getLogger + +logger = getLogger(__name__) + + +class FloatValidator(Validator): + def __init__(self, name, minimum=None, maximum=None): + super().__init__(name) + self.min = minimum + self.max = maximum + + def validate(self, value): + if value is None or (type(value) == str and len(value) == 0): + return None + try: + value = float(value) + except ValueError: + self.raiseForValue(value) + + if self.max is not None and value > self.max: + self.raiseForValue(value) + if self.min is not None and value < self.min: + self.raiseForValue(value) + return value diff --git a/hassio-google-drive-backup/backup/config/intvalidator.py b/hassio-google-drive-backup/backup/config/intvalidator.py new file mode 100644 index 0000000..3e46534 --- /dev/null +++ b/hassio-google-drive-backup/backup/config/intvalidator.py @@ -0,0 +1,25 @@ +from .validator import Validator +from ..logger import getLogger + +logger = getLogger(__name__) + + +class IntValidator(Validator): + def __init__(self, name, minimum=None, maximum=None): + super().__init__(name) + self.min = minimum + self.max = maximum + + def validate(self, value): + if value is None or (type(value) == str and len(value) == 0): + return None + try: + value = int(value) + except ValueError: + self.raiseForValue(value) + + if self.max is not None and value > self.max: + self.raiseForValue(value) + if self.min is not None and value < self.min: + self.raiseForValue(value) + return value diff --git a/hassio-google-drive-backup/backup/config/listvalidator.py b/hassio-google-drive-backup/backup/config/listvalidator.py new file mode 100644 index 0000000..7f5050e --- /dev/null +++ b/hassio-google-drive-backup/backup/config/listvalidator.py @@ -0,0 +1,15 @@ +from .validator import Validator +from ..logger import getLogger + +logger = getLogger(__name__) + + +class ListValidator(Validator): + def __init__(self, name, values): + super().__init__(name) + self.values = values + + def validate(self, value): + if value not in self.values: + self.raiseForValue(value) + return value diff --git a/hassio-google-drive-backup/backup/config/regexvalidator.py b/hassio-google-drive-backup/backup/config/regexvalidator.py new file mode 100644 index 0000000..23c5639 --- /dev/null +++ b/hassio-google-drive-backup/backup/config/regexvalidator.py @@ -0,0 +1,19 @@ +from .validator import Validator +import re +from ..logger import getLogger + +logger = getLogger(__name__) + + +class RegexValidator(Validator): + def __init__(self, name, regex): + super().__init__(name) + self.re = re.compile(regex) + + def validate(self, value): + if value is None or (type(value) == str and len(value) == 0): + return "" + value = str(value) + if not self.re.match(value): + self.raiseForValue(value) + return value diff --git a/hassio-google-drive-backup/backup/config/settings.py b/hassio-google-drive-backup/backup/config/settings.py new file mode 100644 index 0000000..dfd605b --- /dev/null +++ b/hassio-google-drive-backup/backup/config/settings.py @@ -0,0 +1,514 @@ +import json +from enum import Enum, unique +from os.path import abspath, join + +from .boolvalidator import BoolValidator +from .floatvalidator import FloatValidator +from .intvalidator import IntValidator +from .regexvalidator import RegexValidator +from .stringvalidator import StringValidator +from .listvalidator import ListValidator +from .durationasstringvalidator import DurationAsStringValidator +from .bytesizeasstringvalidator import BytesizeAsStringValidator +from ..logger import getLogger + +logger = getLogger(__name__) + + +@unique +class Setting(Enum): + MAX_BACKUPS_IN_HA = "max_backups_in_ha" + MAX_BACKUPS_IN_GOOGLE_DRIVE = "max_backups_in_google_drive" + DAYS_BETWEEN_BACKUPS = "days_between_backups" + IGNORE_OTHER_BACKUPS = "ignore_other_backups" + IGNORE_UPGRADE_BACKUPS = "ignore_upgrade_backups" + DELETE_IGNORED_AFTER_DAYS = "delete_ignored_after_days" + DELETE_BEFORE_NEW_BACKUP = "delete_before_new_backup" + BACKUP_NAME = "backup_name" + BACKUP_TIME_OF_DAY = "backup_time_of_day" + SPECIFY_BACKUP_FOLDER = "specify_backup_folder" + NOTIFY_FOR_STALE_BACKUPS = "notify_for_stale_backups" + ENABLE_BACKUP_STALE_SENSOR = "enable_backup_stale_sensor" + ENABLE_BACKUP_STATE_SENSOR = "enable_backup_state_sensor" + BACKUP_PASSWORD = "backup_password" + CALL_BACKUP_SNAPSHOT = "call_backup_snapshot" + + # Basic backup settings + WARN_FOR_LOW_SPACE = "warn_for_low_space" + LOW_SPACE_THRESHOLD = "low_space_threshold" + DELETE_AFTER_UPLOAD = "delete_after_upload" + + # generational settings + GENERATIONAL_DAYS = "generational_days" + GENERATIONAL_WEEKS = "generational_weeks" + GENERATIONAL_MONTHS = "generational_months" + GENERATIONAL_YEARS = "generational_years" + GENERATIONAL_DAY_OF_WEEK = "generational_day_of_week" + GENERATIONAL_DAY_OF_MONTH = "generational_day_of_month" + GENERATIONAL_DAY_OF_YEAR = "generational_day_of_year" + GENERATIONAL_DELETE_EARLY = "generational_delete_early" + + # Partial backups + EXCLUDE_FOLDERS = "exclude_folders" + EXCLUDE_ADDONS = "exclude_addons" + + STOP_ADDONS = "stop_addons" + DISABLE_WATCHDOG_WHEN_STOPPING = "disable_watchdog_when_stopping" + + # UI Server Options + USE_SSL = "use_ssl" + CERTFILE = "certfile" + KEYFILE = "keyfile" + INGRESS_PORT = "ingress_port" + PORT = "port" + REQUIRE_LOGIN = "require_login" + EXPOSE_EXTRA_SERVER = "expose_extra_server" + + # Add-on options + VERBOSE = "verbose" + SEND_ERROR_REPORTS = "send_error_reports" + CONFIRM_MULTIPLE_DELETES = "confirm_multiple_deletes" + ENABLE_DRIVE_UPLOAD = "enable_drive_upload" + WATCH_BACKUP_DIRECTORY = "watch_backup_directory" + TRACE_REQUESTS = "trace_requests" + + # Theme Settings + BACKGROUND_COLOR = "background_color" + ACCENT_COLOR = "accent_color" + + # Network and dns stuff + DRIVE_EXPERIMENTAL = "drive_experimental" + DRIVE_IPV4 = "drive_ipv4" + IGNORE_IPV6_ADDRESSES = "ignore_ipv6_addresses" + GOOGLE_DRIVE_TIMEOUT_SECONDS = "google_drive_timeout_seconds" + GOOGLE_DRIVE_PAGE_SIZE = "google_drive_page_size" + ALTERNATE_DNS_SERVERS = "alternate_dns_servers" + DEFAULT_DRIVE_CLIENT_ID = "default_drive_client_id" + DEFAULT_DRIVE_CLIENT_SECRET = "default_drive_client_secret" + DRIVE_PICKER_API_KEY = "drive_picker_api_key" + MAXIMUM_UPLOAD_CHUNK_BYTES = "maximum_upload_chunk_bytes" + + # Files and folders + FOLDER_FILE_PATH = "folder_file_path" + CREDENTIALS_FILE_PATH = "credentials_file_path" + RETAINED_FILE_PATH = "retained_file_path" + SECRETS_FILE_PATH = "secrets_file_path" + BACKUP_DIRECTORY_PATH = "backup_directory_path" + INGRESS_TOKEN_FILE_PATH = "ingress_token_file_path" + CONFIG_FILE_PATH = "config_file_path" + ID_FILE_PATH = "id_file_path" + DATA_CACHE_FILE_PATH = "data_cache_file_path" + + # endpoints + AUTHORIZATION_HOST = "authorization_host" + TOKEN_SERVER_HOSTS = "token_server_hosts" + SUPERVISOR_URL = "supervisor_url" + DRIVE_URL = "drive_url" + SUPERVISOR_TOKEN = "hassio_header" + DRIVE_HOST_NAME = "drive_host_name" + DRIVE_REFRESH_URL = "drive_refresh_url" + DRIVE_AUTHORIZE_URL = "drive_authorize_url" + DRIVE_DEVICE_CODE_URL = "drive_device_code_url" + DRIVE_TOKEN_URL = "drive_token_url" + SAVE_DRIVE_CREDS_PATH = "save_drive_creds_path" + STOP_ADDON_STATE_PATH = "stop_addon_state_path" + + # Timing and timeouts + MAX_SYNC_INTERVAL_SECONDS = "max_sync_interval_seconds" + DEFAULT_SYNC_INTERVAL_VARIATION = "default_sync_interval_variation" + BACKUP_STALE_SECONDS = "backup_stale_seconds" + PENDING_BACKUP_TIMEOUT_SECONDS = "pending_backup_timeout_seconds" + FAILED_BACKUP_TIMEOUT_SECONDS = "failed_backup_timeout_seconds" + NEW_BACKUP_TIMEOUT_SECONDS = "new_backup_timeout_seconds" + DOWNLOAD_TIMEOUT_SECONDS = "download_timeout_seconds" + DEFAULT_CHUNK_SIZE = "default_chunk_size" + DEBUGGER_PORT = "debugger_port" + SERVER_PROJECT_ID = "server_project_id" + LOG_LEVEL = "log_level" + CONSOLE_LOG_LEVEL = "console_log_level" + BACKUP_STARTUP_DELAY_MINUTES = "backup_startup_delay_minutes" + EXCHANGER_TIMEOUT_SECONDS = "exchanger_timeout_seconds" + HA_REPORTING_INTERVAL_SECONDS = "ha_reporting_interval_seconds" + LONG_TERM_STALE_BACKUP_SECONDS = "long_term_stale_backup_seconds" + PING_TIMEOUT = "ping_timeout" + CACHE_WARMUP_MAX_SECONDS = "cache_warmup_max_seconds" + CACHE_WARMUP_ERROR_TIMEOUT_SECONDS = "cache_warmup_error_timeout" + MAX_BACKOFF_SECONDS = "max_backoff_seconds" + + # Old, deprecated settings + DEPRECTAED_MAX_BACKUPS_IN_HA = "max_snapshots_in_hassio" + DEPRECTAED_MAX_BACKUPS_IN_GOOGLE_DRIVE = "max_snapshots_in_google_drive" + DEPRECATED_DAYS_BETWEEN_BACKUPS = "days_between_snapshots" + DEPRECTAED_IGNORE_OTHER_BACKUPS = "ignore_other_snapshots" + DEPRECTAED_IGNORE_UPGRADE_BACKUPS = "ignore_upgrade_snapshots" + DEPRECTAED_BACKUP_NAME = "snapshot_name" + DEPRECTAED_BACKUP_TIME_OF_DAY = "snapshot_time_of_day" + DEPRECATED_BACKUP_PASSWORD = "snapshot_password" + DEPRECTAED_SPECIFY_BACKUP_FOLDER = "specify_snapshot_folder" + DEPRECTAED_DELETE_BEFORE_NEW_BACKUP = "delete_before_new_snapshot" + DEPRECTAED_NOTIFY_FOR_STALE_BACKUPS = "notify_for_stale_snapshots" + DEPRECTAED_ENABLE_BACKUP_STALE_SENSOR = "enable_snapshot_stale_sensor" + DEPRECTAED_ENABLE_BACKUP_STATE_SENSOR = "enable_snapshot_state_sensor" + + def default(self): + if "staging" in VERSION and self in _STAGING_DEFAULTS: + return _STAGING_DEFAULTS[self] + return _DEFAULTS[self] + + def validator(self): + return _VALIDATORS[self] + + def key(self): + return self.value + + +_DEFAULTS = { + Setting.MAX_BACKUPS_IN_HA: 4, + Setting.MAX_BACKUPS_IN_GOOGLE_DRIVE: 4, + Setting.DAYS_BETWEEN_BACKUPS: 3, + Setting.IGNORE_OTHER_BACKUPS: False, + Setting.IGNORE_UPGRADE_BACKUPS: True, + Setting.DELETE_IGNORED_AFTER_DAYS: 0, + Setting.DELETE_BEFORE_NEW_BACKUP: False, + Setting.BACKUP_NAME: "{type} Backup {year}-{month}-{day} {hr24}:{min}:{sec}", + Setting.BACKUP_TIME_OF_DAY: "", + Setting.SPECIFY_BACKUP_FOLDER: False, + Setting.NOTIFY_FOR_STALE_BACKUPS: True, + Setting.ENABLE_BACKUP_STALE_SENSOR: True, + Setting.ENABLE_BACKUP_STATE_SENSOR: True, + Setting.BACKUP_PASSWORD: "", + Setting.WATCH_BACKUP_DIRECTORY: True, + Setting.TRACE_REQUESTS: False, + + # Basic backup settings + Setting.DEPRECTAED_MAX_BACKUPS_IN_HA: 4, + Setting.DEPRECTAED_MAX_BACKUPS_IN_GOOGLE_DRIVE: 4, + Setting.DEPRECATED_DAYS_BETWEEN_BACKUPS: 3, + Setting.DEPRECTAED_IGNORE_OTHER_BACKUPS: False, + Setting.DEPRECTAED_IGNORE_UPGRADE_BACKUPS: False, + Setting.DEPRECTAED_BACKUP_TIME_OF_DAY: "", + Setting.DEPRECTAED_BACKUP_NAME: "{type} Snapshot {year}-{month}-{day} {hr24}:{min}:{sec}", + Setting.DEPRECATED_BACKUP_PASSWORD: "", + Setting.DEPRECTAED_SPECIFY_BACKUP_FOLDER: False, + Setting.WARN_FOR_LOW_SPACE: True, + Setting.LOW_SPACE_THRESHOLD: 1024 * 1024 * 1024, + Setting.DELETE_AFTER_UPLOAD: False, + Setting.DEPRECTAED_DELETE_BEFORE_NEW_BACKUP: False, + Setting.CALL_BACKUP_SNAPSHOT: False, + + # Generational backup settings + Setting.GENERATIONAL_DAYS: 0, + Setting.GENERATIONAL_WEEKS: 0, + Setting.GENERATIONAL_MONTHS: 0, + Setting.GENERATIONAL_YEARS: 0, + Setting.GENERATIONAL_DAY_OF_WEEK: "mon", + Setting.GENERATIONAL_DAY_OF_MONTH: 1, + Setting.GENERATIONAL_DAY_OF_YEAR: 1, + Setting.GENERATIONAL_DELETE_EARLY: False, + + # Partial backup settings + Setting.EXCLUDE_FOLDERS: "", + Setting.EXCLUDE_ADDONS: "", + + Setting.STOP_ADDONS: "", + Setting.DISABLE_WATCHDOG_WHEN_STOPPING: False, + + # UI Server settings + Setting.USE_SSL: False, + Setting.REQUIRE_LOGIN: False, + Setting.EXPOSE_EXTRA_SERVER: False, + Setting.CERTFILE: "/ssl/fullchain.pem", + Setting.KEYFILE: "/ssl/privkey.pem", + Setting.INGRESS_PORT: 8099, + Setting.PORT: 1627, + + # Add-on options + Setting.DEPRECTAED_NOTIFY_FOR_STALE_BACKUPS: True, + Setting.DEPRECTAED_ENABLE_BACKUP_STALE_SENSOR: True, + Setting.DEPRECTAED_ENABLE_BACKUP_STATE_SENSOR: True, + Setting.SEND_ERROR_REPORTS: False, + Setting.VERBOSE: False, + Setting.CONFIRM_MULTIPLE_DELETES: True, + Setting.ENABLE_DRIVE_UPLOAD: True, + + # Theme Settings + Setting.BACKGROUND_COLOR: "", + Setting.ACCENT_COLOR: "", + + # Network and DNS settings + Setting.ALTERNATE_DNS_SERVERS: "8.8.8.8,8.8.4.4", + Setting.DRIVE_EXPERIMENTAL: False, + Setting.DRIVE_IPV4: "", + Setting.IGNORE_IPV6_ADDRESSES: False, + Setting.GOOGLE_DRIVE_TIMEOUT_SECONDS: 180, + Setting.GOOGLE_DRIVE_PAGE_SIZE: 100, + Setting.MAXIMUM_UPLOAD_CHUNK_BYTES: 10 * 1024 * 1024, + + # Remote endpoints + Setting.AUTHORIZATION_HOST: "https://habackup.io", + Setting.TOKEN_SERVER_HOSTS: "https://token1.habackup.io,https://habackup.io", + Setting.SUPERVISOR_URL: "", + Setting.SUPERVISOR_TOKEN: "", + Setting.DRIVE_URL: "https://www.googleapis.com", + Setting.DRIVE_REFRESH_URL: "https://www.googleapis.com/oauth2/v4/token", + Setting.DRIVE_AUTHORIZE_URL: "https://accounts.google.com/o/oauth2/v2/auth", + Setting.DRIVE_DEVICE_CODE_URL: "https://oauth2.googleapis.com/device/code", + Setting.DRIVE_TOKEN_URL: "https://oauth2.googleapis.com/token", + Setting.DRIVE_HOST_NAME: "www.googleapis.com", + Setting.SAVE_DRIVE_CREDS_PATH: "token", + + # File locations used to store things + Setting.FOLDER_FILE_PATH: "/data/folder.dat", + Setting.CREDENTIALS_FILE_PATH: "/data/credentials.dat", + Setting.BACKUP_DIRECTORY_PATH: "/backup", + Setting.RETAINED_FILE_PATH: "/data/retained.json", + Setting.SECRETS_FILE_PATH: "/config/secrets.yaml", + Setting.INGRESS_TOKEN_FILE_PATH: "/data/ingress.dat", + Setting.CONFIG_FILE_PATH: "/data/options.json", + Setting.ID_FILE_PATH: "/data/id.json", + Setting.STOP_ADDON_STATE_PATH: '/data/stop_addon_state.json', + Setting.DATA_CACHE_FILE_PATH: '/data/data_cache.json', + + # Various timeouts and intervals + Setting.BACKUP_STALE_SECONDS: 60 * 60 * 3, + Setting.PENDING_BACKUP_TIMEOUT_SECONDS: 60 * 60 * 5, + Setting.FAILED_BACKUP_TIMEOUT_SECONDS: 60 * 15, + Setting.NEW_BACKUP_TIMEOUT_SECONDS: 5, + Setting.MAX_SYNC_INTERVAL_SECONDS: 60 * 60 * 3, # 3 hours + Setting.DEFAULT_SYNC_INTERVAL_VARIATION: 0.5, # intermittent checkup syncs happen between 1.5 and 3 hours since the last one, randomly + Setting.DEFAULT_DRIVE_CLIENT_ID: "933944288016-n35gnn2juc76ub7u5326ls0iaq9dgjgu.apps.googleusercontent.com", + Setting.DEFAULT_DRIVE_CLIENT_SECRET: "", + Setting.DRIVE_PICKER_API_KEY: "", + Setting.DEFAULT_CHUNK_SIZE: 1024 * 1024 * 5, + Setting.DOWNLOAD_TIMEOUT_SECONDS: 60, + Setting.DEBUGGER_PORT: None, + Setting.SERVER_PROJECT_ID: "", + Setting.LOG_LEVEL: 'DEBUG', + Setting.CONSOLE_LOG_LEVEL: 'INFO', + Setting.BACKUP_STARTUP_DELAY_MINUTES: 10, + Setting.EXCHANGER_TIMEOUT_SECONDS: 10, + Setting.HA_REPORTING_INTERVAL_SECONDS: 10, + Setting.LONG_TERM_STALE_BACKUP_SECONDS: 60 * 60 * 24, + Setting.PING_TIMEOUT: 5, + Setting.CACHE_WARMUP_MAX_SECONDS: 15 * 60, # 30 minutes + Setting.CACHE_WARMUP_ERROR_TIMEOUT_SECONDS: 24 * 60 * 60, # 1 day + Setting.MAX_BACKOFF_SECONDS: 60 * 60 * 2, # 2 hours +} + +_STAGING_DEFAULTS = { + Setting.AUTHORIZATION_HOST: "https://dev.habackup.io", + Setting.TOKEN_SERVER_HOSTS: "https://token1.dev.habackup.io,https://dev.habackup.io", + Setting.DEFAULT_DRIVE_CLIENT_ID: "795575624694-jcdhoh1jr1ngccfsbi2f44arr4jupl79.apps.googleusercontent.com", +} + +_CONFIG = { + Setting.MAX_BACKUPS_IN_HA: "int(0,)?", + Setting.MAX_BACKUPS_IN_GOOGLE_DRIVE: "int(0,)?", + Setting.DAYS_BETWEEN_BACKUPS: "float(0,)?", + Setting.IGNORE_OTHER_BACKUPS: "bool?", + Setting.IGNORE_UPGRADE_BACKUPS: "bool?", + Setting.DELETE_IGNORED_AFTER_DAYS: "float(0,)?", + Setting.DELETE_BEFORE_NEW_BACKUP: "bool?", + Setting.BACKUP_NAME: "str?", + Setting.BACKUP_TIME_OF_DAY: "match(^[0-2]\\d:[0-5]\\d$)?", + Setting.SPECIFY_BACKUP_FOLDER: "bool?", + Setting.NOTIFY_FOR_STALE_BACKUPS: "bool?", + Setting.ENABLE_BACKUP_STALE_SENSOR: "bool?", + Setting.ENABLE_BACKUP_STATE_SENSOR: "bool?", + Setting.BACKUP_PASSWORD: "str?", + Setting.WATCH_BACKUP_DIRECTORY: "bool?", + Setting.TRACE_REQUESTS: "bool?", + + # Basic backup settings + Setting.DEPRECTAED_MAX_BACKUPS_IN_HA: "int(0,)?", + Setting.DEPRECTAED_MAX_BACKUPS_IN_GOOGLE_DRIVE: "int(0,)?", + Setting.DEPRECATED_DAYS_BETWEEN_BACKUPS: "float(0,)?", + Setting.DEPRECTAED_IGNORE_OTHER_BACKUPS: "bool?", + Setting.DEPRECTAED_IGNORE_UPGRADE_BACKUPS: "bool?", + Setting.DEPRECTAED_BACKUP_TIME_OF_DAY: "match(^[0-2]\\d:[0-5]\\d$)?", + Setting.DEPRECTAED_BACKUP_NAME: "str?", + Setting.DEPRECATED_BACKUP_PASSWORD: "str?", + Setting.DEPRECTAED_SPECIFY_BACKUP_FOLDER: "bool?", + Setting.WARN_FOR_LOW_SPACE: "bool?", + Setting.LOW_SPACE_THRESHOLD: "int(0,)?", + Setting.DELETE_AFTER_UPLOAD: "bool?", + Setting.DEPRECTAED_DELETE_BEFORE_NEW_BACKUP: "bool?", + Setting.CALL_BACKUP_SNAPSHOT: "bool?", + + # Generational backup settings + Setting.GENERATIONAL_DAYS: "int(0,)?", + Setting.GENERATIONAL_WEEKS: "int(0,)?", + Setting.GENERATIONAL_MONTHS: "int(0,)?", + Setting.GENERATIONAL_YEARS: "int(0,)?", + Setting.GENERATIONAL_DAY_OF_WEEK: "match(^(mon|tue|wed|thu|fri|sat|sun)$)?", + Setting.GENERATIONAL_DAY_OF_MONTH: "int(1,31)?", + Setting.GENERATIONAL_DAY_OF_YEAR: "int(1,365)?", + Setting.GENERATIONAL_DELETE_EARLY: "bool?", + + # Partial backup settings + Setting.EXCLUDE_FOLDERS: "str?", + Setting.EXCLUDE_ADDONS: "str?", + + Setting.STOP_ADDONS: "str?", + Setting.DISABLE_WATCHDOG_WHEN_STOPPING: "bool?", + + # UI Server settings + Setting.USE_SSL: "bool?", + Setting.REQUIRE_LOGIN: "bool?", + Setting.EXPOSE_EXTRA_SERVER: "bool?", + Setting.CERTFILE: "str?", + Setting.KEYFILE: "str?", + Setting.INGRESS_PORT: "int(0,)?", + Setting.PORT: "int(0,)?", + + # Add-on options + Setting.DEPRECTAED_NOTIFY_FOR_STALE_BACKUPS: "bool?", + Setting.DEPRECTAED_ENABLE_BACKUP_STALE_SENSOR: "bool?", + Setting.DEPRECTAED_ENABLE_BACKUP_STATE_SENSOR: "bool?", + Setting.SEND_ERROR_REPORTS: "bool?", + Setting.VERBOSE: "bool?", + Setting.CONFIRM_MULTIPLE_DELETES: "bool?", + Setting.ENABLE_DRIVE_UPLOAD: "bool?", + + # Theme Settings + Setting.BACKGROUND_COLOR: "match(^(#[0-9ABCDEFabcdef]{6}|)$)?", + Setting.ACCENT_COLOR: "match(^(#[0-9ABCDEFabcdef]{6}|)$)?", + + # Network and DNS settings + Setting.ALTERNATE_DNS_SERVERS: "match(^([0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3})(,[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3})*$)?", + Setting.DRIVE_EXPERIMENTAL: "bool?", + Setting.DRIVE_IPV4: "match(^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$)?", + Setting.IGNORE_IPV6_ADDRESSES: "bool?", + Setting.GOOGLE_DRIVE_TIMEOUT_SECONDS: "float(1,)?", + Setting.GOOGLE_DRIVE_PAGE_SIZE: "int(1,)?", + Setting.MAXIMUM_UPLOAD_CHUNK_BYTES: f"float({1024 * 256},)?", + + # Remote endpoints + Setting.AUTHORIZATION_HOST: "url?", + Setting.TOKEN_SERVER_HOSTS: "str?", + Setting.SUPERVISOR_URL: "url?", + Setting.SUPERVISOR_TOKEN: "str?", + Setting.DRIVE_URL: "url?", + Setting.DRIVE_REFRESH_URL: "url?", + Setting.DRIVE_AUTHORIZE_URL: "url?", + Setting.DRIVE_DEVICE_CODE_URL: "url?", + Setting.DRIVE_TOKEN_URL: "url?", + Setting.DRIVE_HOST_NAME: "str?", + Setting.SAVE_DRIVE_CREDS_PATH: "str?", + + # File locations used to store things + Setting.FOLDER_FILE_PATH: "str?", + Setting.CREDENTIALS_FILE_PATH: "str?", + Setting.BACKUP_DIRECTORY_PATH: "str?", + Setting.RETAINED_FILE_PATH: "str?", + Setting.SECRETS_FILE_PATH: "str?", + Setting.INGRESS_TOKEN_FILE_PATH: "str?", + Setting.CONFIG_FILE_PATH: "str?", + Setting.ID_FILE_PATH: "str?", + Setting.STOP_ADDON_STATE_PATH: "str?", + Setting.DATA_CACHE_FILE_PATH: "str?", + + # Various timeouts and intervals + Setting.BACKUP_STALE_SECONDS: "float(0,)?", + Setting.PENDING_BACKUP_TIMEOUT_SECONDS: "float(0,)?", + Setting.FAILED_BACKUP_TIMEOUT_SECONDS: "float(0,)?", + Setting.NEW_BACKUP_TIMEOUT_SECONDS: "float(0,)?", + Setting.MAX_SYNC_INTERVAL_SECONDS: "float(300,)?", + Setting.DEFAULT_SYNC_INTERVAL_VARIATION: "float(0,1)?", + Setting.DEFAULT_DRIVE_CLIENT_ID: "str?", + Setting.DEFAULT_DRIVE_CLIENT_SECRET: "str?", + Setting.DRIVE_PICKER_API_KEY: "str?", + Setting.DEFAULT_CHUNK_SIZE: "int(1,)?", + Setting.DOWNLOAD_TIMEOUT_SECONDS: "float(0,)?", + Setting.DEBUGGER_PORT: "int(100,)?", + Setting.SERVER_PROJECT_ID: "str?", + Setting.LOG_LEVEL: "list(DEBUG|TRACE|INFO|WARN|CRITICAL|WARNING)?", + Setting.CONSOLE_LOG_LEVEL: "list(DEBUG|TRACE|INFO|WARN|CRITICAL|WARNING)?", + Setting.BACKUP_STARTUP_DELAY_MINUTES: "float(0,)?", + Setting.EXCHANGER_TIMEOUT_SECONDS: "float(0,)?", + Setting.HA_REPORTING_INTERVAL_SECONDS: "int(1,)?", + Setting.LONG_TERM_STALE_BACKUP_SECONDS: "int(1,)?", + Setting.PING_TIMEOUT: "float(0,)?", + Setting.CACHE_WARMUP_MAX_SECONDS: "float(0,)", + Setting.CACHE_WARMUP_ERROR_TIMEOUT_SECONDS: "float(0,)", + Setting.MAX_BACKOFF_SECONDS: "int(3600,)?", +} + +PRIVATE = [ + Setting.DEPRECATED_BACKUP_PASSWORD, + Setting.DEPRECTAED_BACKUP_NAME, + Setting.BACKUP_PASSWORD, + Setting.BACKUP_NAME +] + +_LOOKUP = {} +_VALIDATORS = {} + + +def getValidator(name, schema): + if schema.endswith("?"): + schema = schema[:-1] + if schema.startswith("int("): + # its a int + parts = schema[4:-1] + minimum = None + maximum = None + if parts.endswith(","): + minimum = int(parts[0:-1]) + elif parts.startswith(","): + maximum = int(parts[1:]) + else: + digits = parts.split(",") + minimum = int(digits[0]) + maximum = int(digits[1]) + return IntValidator(name, minimum, maximum) + elif schema.startswith("float("): + # its a float + parts = schema[6:-1] + minimum = None + maximum = None + if parts.endswith(","): + minimum = float(parts[0:-1]) + elif parts.startswith(","): + maximum = float(parts[1:]) + else: + digits = parts.split(",") + minimum = float(digits[0]) + maximum = float(digits[1]) + return FloatValidator(name, minimum, maximum) + elif schema.startswith("bool"): + # its a bool + return BoolValidator(name) + elif schema.startswith("str") or schema.startswith("url"): + # its a url (treat it just like any string) + return StringValidator(name) + elif schema.startswith("match("): + return RegexValidator(name, schema[6:-1]) + elif schema.startswith("list("): + return ListValidator(name, schema[5:-1].split("|")) + else: + raise Exception("Invalid schema: " + schema) + + +# initalize validators +for setting in Setting: + _LOOKUP[setting.value] = setting + +with open(abspath(join(__file__, "..", "..", "..", "config.json"))) as f: + # Thsi is a static file included in the container, so don't worry about using JsonFileLoader + addon_config = json.load(f) + +for setting in Setting: + _VALIDATORS[setting] = getValidator(setting.value, _CONFIG[setting]) +for key in addon_config["schema"]: + _VALIDATORS[_LOOKUP[key]] = getValidator(key, addon_config["schema"][key]) + +_VALIDATORS[Setting.MAX_SYNC_INTERVAL_SECONDS] = DurationAsStringValidator("max_sync_interval_seconds", minimum=1, maximum=None) +_VALIDATORS[Setting.HA_REPORTING_INTERVAL_SECONDS] = DurationAsStringValidator("ha_reporting_interval_seconds", minimum=1, maximum=None) +_VALIDATORS[Setting.DELETE_IGNORED_AFTER_DAYS] = DurationAsStringValidator("delete_ignored_after_days", minimum=0, maximum=None, base_seconds=60 * 60 * 24, default_as_empty=0) +_VALIDATORS[Setting.MAXIMUM_UPLOAD_CHUNK_BYTES] = BytesizeAsStringValidator("maximum_upload_chunk_bytes", minimum=256 * 1024) +VERSION = addon_config["version"] + + +def isStaging(): + return "staging" in VERSION diff --git a/hassio-google-drive-backup/backup/config/startable.py b/hassio-google-drive-backup/backup/config/startable.py new file mode 100644 index 0000000..cdcf6fb --- /dev/null +++ b/hassio-google-drive-backup/backup/config/startable.py @@ -0,0 +1,11 @@ +from ..logger import getLogger + +logger = getLogger(__name__) + + +class Startable(): + async def start(self): + pass + + async def stop(self): + pass diff --git a/hassio-google-drive-backup/backup/config/stringvalidator.py b/hassio-google-drive-backup/backup/config/stringvalidator.py new file mode 100644 index 0000000..5692c2a --- /dev/null +++ b/hassio-google-drive-backup/backup/config/stringvalidator.py @@ -0,0 +1,14 @@ +from .validator import Validator +from ..logger import getLogger + +logger = getLogger(__name__) + + +class StringValidator(Validator): + def __init__(self, name): + super().__init__(name) + + def validate(self, value): + if value is None or (type(value) == str and len(value) == 0): + return "" + return str(value) diff --git a/hassio-google-drive-backup/backup/config/validator.py b/hassio-google-drive-backup/backup/config/validator.py new file mode 100644 index 0000000..6606a5b --- /dev/null +++ b/hassio-google-drive-backup/backup/config/validator.py @@ -0,0 +1,21 @@ +from abc import ABC, abstractmethod + +from ..exceptions import InvalidConfigurationValue +from ..logger import getLogger + +logger = getLogger(__name__) + + +class Validator(ABC): + def __init__(self, name): + self.name = name + + @abstractmethod + def validate(self, value): + return True + + def raiseForValue(self, value): + raise InvalidConfigurationValue(self.name, str(value)) + + def formatForUi(self, value): + return value diff --git a/hassio-google-drive-backup/backup/config/version.py b/hassio-google-drive-backup/backup/config/version.py new file mode 100644 index 0000000..d138ec6 --- /dev/null +++ b/hassio-google-drive-backup/backup/config/version.py @@ -0,0 +1,84 @@ +STAGING_KEY = ".staging." +EXPECTED_VERISON_CHARS = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.'] + + +class Version: + def __init__(self, *args): + self._identifiers = args + self.staging = False + + @classmethod + def default(cls): + return Version(0) + + @classmethod + def parse(cls, version: str): + staging_version = None + if STAGING_KEY in version: + index = version.find(STAGING_KEY) + staging_version = int(version[index + len(STAGING_KEY):]) + version = version[0:index] + version = Version._removeUnexpected(version) + parts = [] + for part in version.split("."): + if len(part) > 0: + parts.append(int(part)) + if staging_version is not None: + parts.append(staging_version) + if len(parts) == 0: + parts.append(0) + ret = Version(*parts) + if staging_version is not None: + ret.staging = True + return ret + + @classmethod + def _removeUnexpected(cls, version: str): + ret = "" + for c in version: + if c in EXPECTED_VERISON_CHARS: + ret += c + while ".." in ret: + ret = ret.replace("..", ".") + return ret + + def __getitem__(self, key): + return self._identifiers[key] + + def length(self): + return len(self._identifiers) + + def _compare(self, other): + i = 0 + while(i < min(self.length(), other.length())): + if self[i] < other[i]: + return -1 + if self[i] > other[i]: + return 1 + i += 1 + if self.length() < other.length(): + return -1 + if self.length() > other.length(): + return 1 + return 0 + + def __lt__(self, other): + return self._compare(other) < 0 + + def __le__(self, other): + return self._compare(other) <= 0 + + def __eq__(self, other): + return self._compare(other) == 0 + + def __ne__(self, other): + return self._compare(other) != 0 + + def __gt__(self, other): + return self._compare(other) > 0 + + def __ge__(self, other): + return self._compare(other) >= 0 + + def __str__(self): + return ".".join(str(i) for i in self._identifiers) diff --git a/hassio-google-drive-backup/backup/const.py b/hassio-google-drive-backup/backup/const.py new file mode 100644 index 0000000..d7fdbe5 --- /dev/null +++ b/hassio-google-drive-backup/backup/const.py @@ -0,0 +1,128 @@ + +SOURCE_GOOGLE_DRIVE = "GoogleDrive" +SOURCE_HA = "HomeAssistant" + +ERROR_PLEASE_WAIT = "please_wait" +ERROR_NOT_UPLOADABLE = "not_uploadable" +ERROR_NO_BACKUP = "invalid_slug" +ERROR_CREDS_EXPIRED = "creds_bad" +ERROR_UPLOAD_FAILED = "upload_failed" +ERROR_BAD_PASSWORD_KEY = "password_key_invalid" +ERROR_BACKUP_IN_PROGRESS = "backup_in_progress" +ERROR_PROTOCOL = "protocol_error" +ERROR_LOGIC = "logic_error" +ERROR_INVALID_CONFIG = "illegal_config" +ERROR_DRIVE_FULL = "drive_full" +ERROR_GOOGLE_DNS = "google_dns" +ERROR_GOOGLE_CONNECT = "google_cant_connect" +ERROR_GOOGLE_INTERNAL = "google_server_error" +ERROR_GOOGLE_SESSION = "google_session_expired" +ERROR_GOOGLE_TIMEOUT = "google_timeout" +ERROR_GOOGLE_UNEXPECTED = "google_unexpected" +ERROR_HA_DELETE_ERROR = "delete_error" +ERROR_MULTIPLE_DELETES = "multiple_deletes" +ERROR_SUPERVISOR_UNEXPECTED = "supervisor_unexpected" +ERROR_SUPERVISOR_TIMEOUT = "supervisor_timeout" +ERROR_SUPERVISOR_FILE_SYSTEM = "supervisor_fs_error" +ERROR_GOOGLE_CRED_PROCESS = "unable_to_make_creds" + +ERROR_EXISTING_FOLDER = "existing_backup_folder" +ERROR_BACKUP_FOLDER_MISSING = "backup_folder_missing" +CHOOSE_BACKUP_FOLDER = "choose_backup_folder" +ERROR_BACKUP_FOLDER_INACCESSIBLE = "backup_folder_inaccessible" +ERROR_LOW_SPACE = "low_space" +LOG_IN_TO_DRIVE = "log_in_to_drive" +SUPERVISOR_PERMISSION = "supervisor_permission" + +# these keys are necessary because they use the name "snapshot" in non-user-visible +# places persisted outside the codebase. They can't be changed without an upgrade path. +NECESSARY_OLD_BACKUP_NAME = "snapshot" +NECESSARY_OLD_BACKUP_PLURAL_NAME = "snapshots" +NECESSARY_OLD_SUPERVISOR_URL = "http://hassio" +NECESSARY_PROP_KEY_SLUG = "snapshot_slug" +NECESSARY_PROP_KEY_DATE = "snapshot_date" +NECESSARY_PROP_KEY_NAME = "snapshot_name" +PROP_NOTE = "note" + +DRIVE_FOLDER_URL_FORMAT = "https://drive.google.com/drive/u/0/folders/{0}" +GITHUB_ISSUE_URL = "https://github.com/sabeechen/hassio-google-drive-backup/issues/new?labels[]=People%20Management&labels[]=[Type]%20Bug&title={title}&assignee=sabeechen&body={body}" +GITHUB_BUG_TEMPLATE = """ +###### Description: +``` +If you have anything else that could help explain what happened, click "Markdown" above and write it here. +``` + + Addon version: `{version}` + Home Assistant Version: `{ha_version}` + Supervisor Version: `{super_version}` + Supervisor Channel: `{supervisor_channel}` + Hassos Version: `{hassos_version}` + Docker Version: `{docker_version}` + Architecture: `{arch}` + Machine: `{machine}` + Date: `{time}` + Timezone: `{timezone}` + Failure Time: `{failure_time}` + Last Good Sync: `{sync_last_start}` + Next Sync: `{next_sync}` + Next Backup: `{next_backup}` + Next Cache Warm: `{next_cache_warm}` + Time Offset: `{time_offset}` + ###### Exception: + ``` + {error} + ``` + Backups: + ``` + {backups} + ``` + ###### Config: + ``` + {config} + ``` + ###### Addon Logs: + ``` + {addon_logs} + ``` + ###### Supervisor Logs: + ``` + {super_logs} + ``` + ###### Home Assistant Core Logs: + ``` + {core_logs} + ``` + """ + +FOLDERS = [ + { + 'slug': "homeassistant", + 'id': "folder_homeassistant", + 'name': "Home Assistant Configuration", + 'description': 'Backup the files and folders from your Home Assistant config directory, eg configuration.yaml' + }, + { + 'slug': "media", + 'id': "folder_media", + 'name': "Media", + 'description': 'Backup your "/media" directory.' + }, + { + 'slug': "ssl", + 'id': "folder_ssl", + 'name': "SSL", + 'description': 'Backup your "/ssl" directory, where your certfile and keyfile are typically stored.' + }, + { + 'slug': "share", + 'id': "folder_share", + 'name': "Share", + 'description': 'Backup your "/share" directory.' + }, + { + 'slug': "addons/local", + 'id': "folder_addons", + 'name': "Local Addons", + 'description': 'Backup your local addons directory. This directory will be empty unless you use it for add-on development.' + } +] diff --git a/hassio-google-drive-backup/backup/creds/__init__.py b/hassio-google-drive-backup/backup/creds/__init__.py new file mode 100644 index 0000000..4136c0f --- /dev/null +++ b/hassio-google-drive-backup/backup/creds/__init__.py @@ -0,0 +1,5 @@ +# flake8: noqa +from .exchanger import Exchanger +from .creds import Creds, KEY_TOKEN_EXPIRY, KEY_ACCESS_TOKEN, KEY_CLIENT_ID, KEY_CLIENT_SECRET +from .driverequester import DriveRequester +MANUAL_CODE_REDIRECT_URI: str = "urn:ietf:wg:oauth:2.0:oob" diff --git a/hassio-google-drive-backup/backup/creds/creds.py b/hassio-google-drive-backup/backup/creds/creds.py new file mode 100644 index 0000000..d5450ef --- /dev/null +++ b/hassio-google-drive-backup/backup/creds/creds.py @@ -0,0 +1,90 @@ +from ..exceptions import ensureKey +from ..time import Time +from typing import Optional +from datetime import datetime, timedelta + +KEY_REFRESH_TOKEN = 'refresh_token' +KEY_CLIENT_ID = 'client_id' +KEY_CLIENT_SECRET = 'client_secret' +KEY_EXPIRES_IN = 'expires_in' +KEY_TOKEN_EXPIRY = 'token_expiry' +KEY_ACCESS_TOKEN = 'access_token' + + +class Creds(): + def __init__(self, time: Time, id: str, expiration: datetime, + access_token: str, refresh_token: str, + secret: Optional[str] = None, original_expiration: datetime = None): + self._id = id + self.time: Time = time + self._secret = secret + self._access_token = access_token + self._refresh_token = refresh_token + self._expiration = expiration + self._original_expiration = original_expiration + + @property + def id(self): + return self._id + + @property + def secret(self): + return self._secret + + @property + def refresh_token(self): + return self._refresh_token + + @property + def access_token(self): + return self._access_token + + @property + def expiration(self): + if self._expiration is None: + return self.time.now() + return self._expiration + + @property + def original_expiration(self) -> datetime: + return self._original_expiration + + @property + def is_expired(self): + return self.time.now() >= self.expiration + + def serialize(self, include_secret=True): + ret = { + "client_id": self.id + } + if self.secret is not None and include_secret: + ret[KEY_CLIENT_SECRET] = self.secret + if self.refresh_token is not None: + ret[KEY_REFRESH_TOKEN] = self.refresh_token + if self.access_token is not None: + ret[KEY_ACCESS_TOKEN] = self.access_token + if self.expiration is not None: + ret[KEY_TOKEN_EXPIRY] = self.time.asRfc3339String(self.expiration) + return ret + + @classmethod + def load(cls, time: Time, data, id=None, secret=None, original_expiration=None): + if id is None: + id = ensureKey(KEY_CLIENT_ID, data, "credentials") + if secret is None and KEY_CLIENT_SECRET in data: + secret = data[KEY_CLIENT_SECRET] + refresh = ensureKey(KEY_REFRESH_TOKEN, data, "credentials") + access = ensureKey(KEY_ACCESS_TOKEN, data, "credentials") + expires = None + try: + if KEY_TOKEN_EXPIRY in data: + expires = time.parse(data[KEY_TOKEN_EXPIRY]) + if original_expiration is None: + original_expiration = expires + elif KEY_EXPIRES_IN in data: + expires = time.now() + timedelta(seconds=int(data[KEY_EXPIRES_IN])) + else: + expires = time.now() + except BaseException: + expires = time.now() + return Creds(time=time, id=id, access_token=access, refresh_token=refresh, secret=secret, expiration=expires, original_expiration=original_expiration) diff --git a/hassio-google-drive-backup/backup/creds/driverequester.py b/hassio-google-drive-backup/backup/creds/driverequester.py new file mode 100644 index 0000000..ed15ed3 --- /dev/null +++ b/hassio-google-drive-backup/backup/creds/driverequester.py @@ -0,0 +1,113 @@ +from aiohttp import ClientSession, ContentTypeError, ClientConnectorError, ClientTimeout, ClientResponse +from aiohttp.client_exceptions import ServerTimeoutError, ServerDisconnectedError, ClientOSError +from backup.exceptions import GoogleUnexpectedError, GoogleInternalError, GoogleRateLimitError, GoogleCredentialsExpired, CredRefreshGoogleError, DriveQuotaExceeded, GoogleDrivePermissionDenied, GoogleDnsFailure, GoogleCantConnect, GoogleTimeoutError +from backup.util import Resolver +from backup.logger import getLogger +from backup.config import Config, Setting +from injector import singleton, inject +from dns.exception import DNSException + +RATE_LIMIT_EXCEEDED = [403] +TOO_MANY_REQUESTS = [429] +INTERNAL_ERROR = [500, 503] +PERMISSION_DENIED = [401] +REQUEST_TIMEOUT = [408] + +logger = getLogger(__name__) + + +@singleton +class DriveRequester(): + @inject + def __init__(self, config: Config, session: ClientSession, resolver: Resolver): + self.session = session + self.resolver = resolver + self.config = config + + async def request(self, method, url, headers={}, json=None, data=None) -> ClientResponse: + try: + response = await self.session.request(method, url, headers=headers, json=json, timeout=self.buildTimeout(), data=data) + if response.status < 400: + return response + await self.raiseForKnownErrors(response) + if response.status in PERMISSION_DENIED: + response.release() + raise GoogleCredentialsExpired() + elif response.status in INTERNAL_ERROR: + response.release() + raise GoogleInternalError() + elif response.status in RATE_LIMIT_EXCEEDED or response.status in TOO_MANY_REQUESTS: + response.release() + raise GoogleRateLimitError() + elif response.status in REQUEST_TIMEOUT: + response.release() + raise GoogleTimeoutError() + response.raise_for_status() + return response + except ClientConnectorError as e: + logger.debug( + "Ran into trouble reaching Google Drive's servers. We'll use alternate DNS servers on the next attempt.") + self.resolver.toggle() + if "Cannot connect to host" in str(e) or "Connection reset by peer" in str(e): + raise GoogleCantConnect() + if e.os_error.errno == -2: + # -2 means dns lookup failed. + raise GoogleDnsFailure() + elif str(e.os_error) == "Domain name not found": + raise GoogleDnsFailure() + elif e.os_error.errno in [99, 111, 10061, 104]: + # 111 means connection refused + # Can't connect + raise GoogleCantConnect() + elif "Could not contact DNS serve" in str(e.os_error): + # Wish there was a better way to identify this exception + raise GoogleDnsFailure() + raise + except ClientOSError as e: + if e.errno == 1: + raise GoogleUnexpectedError() + raise + except ServerTimeoutError: + raise GoogleTimeoutError() + except ServerDisconnectedError: + raise GoogleUnexpectedError() + except DNSException: + logger.debug( + "Ran into trouble resolving Google Drive's servers. We'll use normal DNS servers on the next attempt.") + self.resolver.toggle() + raise GoogleDnsFailure() + + def buildTimeout(self): + return ClientTimeout( + sock_connect=self.config.get( + Setting.GOOGLE_DRIVE_TIMEOUT_SECONDS), + sock_read=self.config.get(Setting.GOOGLE_DRIVE_TIMEOUT_SECONDS)) + + async def raiseForKnownErrors(self, response): + try: + message = await response.json() + except ContentTypeError: + return + except ValueError: + # parsing json failed, just give up + return + except TypeError: + # Same + return + if "error" not in message: + return + error_obj = message["error"] + if isinstance(error_obj, str): + if error_obj == "expired": + raise GoogleCredentialsExpired() + else: + raise CredRefreshGoogleError(error_obj) + if "errors" not in error_obj: + return + for error in error_obj["errors"]: + if "reason" not in error: + continue + if error["reason"] == "storageQuotaExceeded": + raise DriveQuotaExceeded() + elif error["reason"] in ["forbidden", "insufficientFilePermissions"]: + raise GoogleDrivePermissionDenied() diff --git a/hassio-google-drive-backup/backup/creds/exchanger.py b/hassio-google-drive-backup/backup/creds/exchanger.py new file mode 100644 index 0000000..d769692 --- /dev/null +++ b/hassio-google-drive-backup/backup/creds/exchanger.py @@ -0,0 +1,160 @@ +import asyncio +from aiohttp import ClientSession, ClientConnectorError, ClientTimeout +from .creds import Creds, KEY_CLIENT_ID, KEY_CLIENT_SECRET, KEY_ACCESS_TOKEN, KEY_REFRESH_TOKEN, KEY_EXPIRES_IN +from ..exceptions import ensureKey, GoogleCredentialsExpired, CredRefreshGoogleError, CredRefreshMyError +from ..config import Config, Setting, VERSION +from yarl import URL +from ..time import Time +from ..logger import getLogger +from .driverequester import DriveRequester +from datetime import timedelta +from injector import singleton, inject + + +SCOPE = 'https://www.googleapis.com/auth/drive.file' + +KEY_REDIRECT_URI = 'redirect_uri' +KEY_SCOPE = 'scope' +KEY_RESPONSE_TYPE = 'response_type' +KEY_INCLUDE_GRANTED_SCOPES = 'include_granted_scopes' +KEY_ACCESS_TYPE = 'access_type' +KEY_STATE = 'state' +KEY_PROMPT = 'prompt' +KEY_CODE = 'code' +KEY_GRANT_TYPE = 'grant_type' +KEY_VERSION = 'version' +KEY_CLIENT = 'client' + +CRED_OBJECT_NAME = "credential token response" + +logger = getLogger(__name__) + + +@singleton +class Exchanger(): + @inject + def __init__(self, + time: Time, + session: ClientSession, + config: Config, + drive: DriveRequester, + client_id: str, + client_secret: str, + redirect: URL): + self.time = time + self.config = config + self.session = session + self.drive = drive + self._client_id = client_id + self._client_secret = client_secret + self._redirect = redirect + + async def getAuthorizationUrl(self, state="") -> str: + url = URL(self.config.get(Setting.DRIVE_AUTHORIZE_URL)).with_query({ + KEY_CLIENT_ID: self._client_id, + KEY_SCOPE: SCOPE, + KEY_RESPONSE_TYPE: 'code', + KEY_INCLUDE_GRANTED_SCOPES: 'true', + KEY_ACCESS_TYPE: "offline", + KEY_STATE: state, + KEY_REDIRECT_URI: str(self._redirect), + KEY_PROMPT: "consent" + }) + return str(url) + + async def exchange(self, code): + data = { + KEY_CLIENT_ID: self._client_id, + KEY_CLIENT_SECRET: self._client_secret, + KEY_CODE: code, + KEY_REDIRECT_URI: str(self._redirect), + KEY_GRANT_TYPE: 'authorization_code' + } + resp = None + async with await self.drive.request("post", self.config.get(Setting.DRIVE_TOKEN_URL), data=data) as resp: + return Creds.load(self.time, await resp.json(), id=self._client_id, secret=self._client_secret) + + async def refresh(self, creds: Creds): + if creds.secret is not None: + return await self._refresh_google(creds) + else: + return await self._refresh_default(creds) + + async def _refresh_google(self, creds: Creds): + data = { + KEY_CLIENT_ID: creds.id, + KEY_CLIENT_SECRET: creds.secret, + KEY_REFRESH_TOKEN: creds.refresh_token, + KEY_GRANT_TYPE: 'refresh_token' + } + async with await self.drive.request("post", self.config.get(Setting.DRIVE_REFRESH_URL), data=data) as resp: + data = await resp.json() + return Creds( + self.time, + id=creds.id, + secret=creds.secret, + access_token=ensureKey(KEY_ACCESS_TOKEN, data, CRED_OBJECT_NAME), + refresh_token=creds.refresh_token, + expiration=self._get_expiration(data), + original_expiration=creds.original_expiration) + + async def _refresh_default(self, creds: Creds): + data = { + KEY_CLIENT_ID: creds.id, + KEY_REFRESH_TOKEN: creds.refresh_token, + } + token_paths = self.config.getTokenServers("/drive/refresh") + last_error = None + for url in token_paths: + try: + headers = { + 'addon_version': VERSION, + 'client': self.config.clientIdentifier() + } + async with self.session.post(str(url), headers=headers, json=data, timeout=ClientTimeout(total=self.config.get(Setting.EXCHANGER_TIMEOUT_SECONDS))) as resp: + if resp.status < 400: + return Creds.load(self.time, await resp.json(), original_expiration=creds.original_expiration) + elif resp.status == 503: + json = {} + try: + json = await resp.json() + except BaseException: + pass + if "error" in json: + if "invalid_grant" in json["error"]: + raise GoogleCredentialsExpired() + else: + # Record the error, but still try other hosts + last_error = CredRefreshGoogleError(json["error"]) + else: + last_error = CredRefreshMyError("HTTP 503 from " + url.host) + elif resp.status == 401: + raise GoogleCredentialsExpired() + else: + try: + extra = (await resp.json())["error"] + except BaseException: + extra = "" + + # this is likely due to misconfiguration + logger.warning("Got {0}:{1} from {2}, trying alternate server(s)...".format(resp.status, extra, url.host)) + last_error = CredRefreshMyError("HTTP {} {}".format(resp.status, extra)) + except ClientConnectorError: + logger.warning("Unable to reach " + str(url.host) + ", trying alternate server(s)...") + last_error = "Couldn't communicate with " + url.host + except asyncio.exceptions.TimeoutError: + logger.warning("Timed out communicating with " + str(url.host) + ", trying alternate server(s)...") + last_error = "Timed out communicating with " + url.host + logger.error("Unable to refresh credentials with Google Drive") + if isinstance(last_error, str): + raise CredRefreshMyError(last_error) + elif isinstance(last_error, Exception): + raise last_error + else: + raise Exception("Unexpected error type: " + str(last_error)) + + def refreshCredentials(self, refresh_token): + return Creds(self.time, id=self._client_id, expiration=None, access_token=None, refresh_token=refresh_token, secret=self._client_secret) + + def _get_expiration(self, data): + return self.time.now() + timedelta(seconds=int(ensureKey(KEY_EXPIRES_IN, data, CRED_OBJECT_NAME))) diff --git a/hassio-google-drive-backup/backup/debug/__init__.py b/hassio-google-drive-backup/backup/debug/__init__.py new file mode 100644 index 0000000..8535767 --- /dev/null +++ b/hassio-google-drive-backup/backup/debug/__init__.py @@ -0,0 +1,2 @@ +# flake8: noqa +from .debug_server import DebugServer \ No newline at end of file diff --git a/hassio-google-drive-backup/backup/debug/debug_server.py b/hassio-google-drive-backup/backup/debug/debug_server.py new file mode 100644 index 0000000..7e7b292 --- /dev/null +++ b/hassio-google-drive-backup/backup/debug/debug_server.py @@ -0,0 +1,18 @@ +from backup.config import Config, Setting, Startable +from backup.logger import getLogger +from injector import inject, singleton +logger = getLogger(__name__) + + +@singleton +class DebugServer(Startable): + @inject + def __init__(self, config: Config): + self._config = config + + async def start(self): + if self._config.get(Setting.DEBUGGER_PORT) is not None: + import debugpy + port = self._config.get(Setting.DEBUGGER_PORT) + logger.info("Starting debugger on port {}".format(port)) + debugpy.listen(("0.0.0.0", port)) diff --git a/hassio-google-drive-backup/backup/debugworker.py b/hassio-google-drive-backup/backup/debugworker.py new file mode 100644 index 0000000..d5c04d7 --- /dev/null +++ b/hassio-google-drive-backup/backup/debugworker.py @@ -0,0 +1,216 @@ +import asyncio +import socket +import aioping +from datetime import datetime, timedelta + +from aiohttp import ClientSession, ClientTimeout +from injector import inject, singleton + +from backup.config import Config, Setting, VERSION, _DEFAULTS, PRIVATE +from backup.exceptions import KnownError +from backup.util import GlobalInfo, Resolver +from backup.time import Time +from backup.worker import Worker +from backup.logger import getLogger, getHistory +from backup.ha import HaRequests, HaSource +from backup.model import Coordinator, DestinationPrecache +from yarl import URL + +logger = getLogger(__name__) +ERROR_LOG_LENGTH = 30 + + +@singleton +class DebugWorker(Worker): + @inject + def __init__(self, time: Time, info: GlobalInfo, config: Config, resolver: Resolver, session: ClientSession, ha: HaRequests, coord: Coordinator, ha_source: HaSource, precache: DestinationPrecache): + super().__init__("Debug Worker", self.doWork, time, interval=10) + self.time = time + self._info = info + self.config = config + self.ha = ha + self.ha_source = ha_source + self.coord = coord + + self.last_dns_update = None + self.dns_info = None + + self.last_sent_error = None + self.last_sent_error_time = None + self._health = None + self.resolver = resolver + self.session = session + self._last_server_check = None + self._last_server_refresh = timedelta(days=1) + self._precache = precache + + async def doWork(self): + if not self.last_dns_update or self.time.now() > self.last_dns_update + timedelta(hours=12): + await self.updateDns() + if not self._last_server_check or self.time.now() > self._last_server_check + self._last_server_refresh: + await self.updateHealthCheck() + if self.config.get(Setting.SEND_ERROR_REPORTS): + try: + await self.maybeSendErrorReport() + except Exception: + pass + + # Once per day, query the health endpoint of the token server to see who is up. + # This checks for broadcast messages for all users and also finds which token + # servers are available. + async def updateHealthCheck(self): + headers = { + 'client': self.config.clientIdentifier(), + 'addon_version': VERSION + } + self._last_server_check = self.time.now() + for host in self.config.getTokenServers(): + url = host.with_path("/health") + try: + async with self.session.get(url, headers=headers, timeout=ClientTimeout(total=10)) as resp: + resp.raise_for_status() + self._health = await resp.json() + self._last_server_refresh = timedelta(days=1) + return + except: # noqa: E722 + # ignore any error and just try a different endpoint + pass + + # no good token host could be found, so reset it to the default and check again sooner. + self._last_server_refresh = timedelta(minutes=1) + + async def maybeSendErrorReport(self): + error = self._info._last_error + if error is not None: + if isinstance(error, KnownError): + error = error.code() + else: + error = logger.formatException(error) + if error != self.last_sent_error: + self.last_sent_error = error + if error is not None: + self.last_sent_error_time = self.time.now() + package = await self.buildErrorReport(error) + else: + package = self.buildClearReport() + logger.info("Sending error report (see settings to disable)") + headers = { + 'client': self.config.clientIdentifier(), + 'addon_version': VERSION + } + url = URL(self.config.get(Setting.AUTHORIZATION_HOST)).with_path("/logerror") + async with self.session.post(url, headers=headers, json=package): + pass + + async def updateDns(self): + self.last_dns_update = self.time.now() + try: + # Resolve google's addresses + self.dns_info = await self.getPingInfo() + self._info.setDnsInfo(self.dns_info) + except Exception as e: + self.dns_info = logger.formatException(e) + + async def buildErrorReport(self, error): + config_special = {} + for setting in Setting: + if self.config.get(setting) != _DEFAULTS[setting]: + if setting in PRIVATE: + config_special[str(setting)] = "REDACTED" + else: + config_special[str(setting)] = self.config.get(setting) + report = {} + report['config'] = config_special + report['time'] = self.formatDate(self.time.now()) + report['start_time'] = self.formatDate(self._info._start_time) + report['addon_version'] = VERSION + report['failure_time'] = self.formatDate(self._info._last_failure_time) + report['failure_count'] = self._info._failures + report['sync_last_start'] = self.formatDate(self._info._last_sync_start) + report['sync_count'] = self._info._syncs + report['sync_success_count'] = self._info._successes + report['sync_last_success'] = self.formatDate(self._info._last_sync_success) + report['upload_count'] = self._info._uploads + report['upload_last_size'] = self._info._last_upload_size + report['upload_last_attempt'] = self.formatDate(self._info._last_upload) + report['next_sync'] = self.formatDate(self.coord.nextSyncAttempt()) + report['next_backup'] = self.formatDate(self.coord.nextBackupTime()) + report['next_cache_warm'] = self.formatDate(self._precache.getNextWarmDate()) + report['time_offset'] = self._time.offset.total_seconds() + + report['debug'] = self._info.debug + report['version'] = VERSION + report['error'] = error + report['client'] = self.config.clientIdentifier() + + if self.ha_source.isInitialized() and self.ha_source.host_info and self.ha_source.super_info and self.ha_source.ha_info: + report["super_version"] = self.ha_source.host_info.get('supervisor', "None") + report["hassos_version"] = self.ha_source.host_info.get('hassos', "None") + report["docker_version"] = self.ha_source.host_info.get('docker', "None") + report["machine"] = self.ha_source.host_info.get('machine', "None") + report["supervisor_channel"] = self.ha_source.host_info.get('channel', "None") + report["arch"] = self.ha_source.super_info.get('arch', "None") + report["timezone"] = self.ha_source.super_info.get('timezone', "None") + report["ha_version"] = self.ha_source.ha_info.get('version', "None") + else: + report["super_version"] = "Uninitialized" + report["arch"] = "Uninitialized" + report["timezone"] = "Uninitialized" + report["ha_version"] = "Uninitialized" + report["backups"] = self.coord.buildBackupMetrics() + return report + + async def buildBugReportData(self, error): + report = await self.buildErrorReport(error) + report['addon_logs'] = "\n".join(b for a, b in list(getHistory(0, False))[-ERROR_LOG_LENGTH:]) + try: + report['super_logs'] = "\n".join((await self.ha.getSuperLogs()).split("\n")[-ERROR_LOG_LENGTH:]) + except Exception as e: + report['super_logs'] = logger.formatException(e) + try: + report['core_logs'] = "\n".join((await self.ha.getCoreLogs()).split("\n")[-ERROR_LOG_LENGTH:]) + except Exception as e: + report['core_logs'] = logger.formatException(e) + return report + + def buildClearReport(self): + duration = self.time.now() - self.last_sent_error_time + report = { + 'duration': str(duration) + } + return report + + def formatDate(self, date: datetime): + if date is None: + return "Never" + else: + return date.isoformat() + + async def getPingInfo(self): + who = self.config.get(Setting.DRIVE_HOST_NAME) + ips = await self.resolve(who) + results = {who: {}} + tasks = {who: {}} + for ip in ips: + results[who][ip] = "Unknown" + tasks[who][ip] = asyncio.create_task(aioping.ping(ip, timeout=self.config.get(Setting.PING_TIMEOUT))) + + # ping each server + for server in tasks.keys(): + for ip in tasks[server].keys(): + try: + time = await tasks[server][ip] + results[server][ip] = f"{round(time * 1000, 0)} ms" + except Exception as e: + results[server][ip] = str(e) + return results + + async def resolve(self, who: str): + try: + ret = [who] + addresses = await self.resolver.resolve(who, 443, socket.AF_INET) + for address in addresses: + ret.append(address['host']) + return ret + except Exception: + return [who] diff --git a/hassio-google-drive-backup/backup/drive/__init__.py b/hassio-google-drive-backup/backup/drive/__init__.py new file mode 100644 index 0000000..2524c3a --- /dev/null +++ b/hassio-google-drive-backup/backup/drive/__init__.py @@ -0,0 +1,5 @@ +# flake8: noqa +from .driverequests import DriveRequests, RETRY_SESSION_ATTEMPTS, UPLOAD_SESSION_EXPIRATION_DURATION, URL_START_UPLOAD, OOB_CRED_CUTOFF +from .drivesource import DriveSource, SOURCE_GOOGLE_DRIVE +from .folderfinder import FolderFinder +from .authcodequery import AuthCodeQuery \ No newline at end of file diff --git a/hassio-google-drive-backup/backup/drive/authcodequery.py b/hassio-google-drive-backup/backup/drive/authcodequery.py new file mode 100644 index 0000000..715371a --- /dev/null +++ b/hassio-google-drive-backup/backup/drive/authcodequery.py @@ -0,0 +1,107 @@ +from datetime import datetime, timedelta + +from backup.config import Config, Setting +from backup.time import Time +from backup.exceptions import GoogleCredGenerateError, KnownError, LogicError, ensureKey +from aiohttp import ClientSession +from injector import inject +from .driverequests import DriveRequester +from backup.logger import getLogger +from backup.creds import Creds +import asyncio + +logger = getLogger(__name__) +SCOPE = 'https://www.googleapis.com/auth/drive.file' + + +class AuthCodeQuery: + @inject + def __init__(self, config: Config, session: ClientSession, time: Time, drive: DriveRequester): + self.session = session + self.config = config + self.drive = drive + self.time = time + self.client_id: str = None + self.client_secret: str = None + self.device_code: str = None + self.verification_url: str = None + self.user_code: str = None + self.check_interval: timedelta = timedelta(seconds=5) + self.expiration: datetime = time.now() + self.last_check = time.now() + + async def requestCredentials(self, client_id: str, client_secret: str): + self.client_id = client_id + self.client_secret = client_secret + request_data = { + 'client_id': self.client_id, + 'scope': SCOPE + } + resp = await self.session.post(self.config.get(Setting.DRIVE_DEVICE_CODE_URL), data=request_data, timeout=30) + if resp.status != 200: + raise GoogleCredGenerateError(f"Google responded with error status HTTP {resp.status}. Please verify your credentials are set up correctly.") + data = await resp.json() + self.device_code = str(ensureKey("device_code", data, "Google's authorization request")) + self.verification_url = str(ensureKey("verification_url", data, "Google's authorization request")) + self.user_code = str(ensureKey("user_code", data, "Google's authorization request")) + self.expiration = self.time.now() + timedelta(seconds=int(ensureKey("expires_in", data, "Google's authorization request"))) + self.check_interval = timedelta(seconds=int(ensureKey("interval", data, "Google's authorization request"))) + + async def waitForPermission(self) -> Creds: + if not self.device_code: + raise LogicError("Please call requestCredentials() first") + error_count = 0 + data = { + 'client_id': self.client_id, + 'client_secret': self.client_secret, + 'device_code': self.device_code, + 'grant_type': 'urn:ietf:params:oauth:grant-type:device_code' + } + while self.expiration > self.time.now(): + start = self.time.now() + resp = None + try: + resp = await self.session.post(self.config.get(Setting.DRIVE_TOKEN_URL), data=data, timeout=self.check_interval.total_seconds()) + try: + reply = await resp.json() + except Exception: + reply = {} + if resp.status == 403: + if reply.get("error", "") == "slow_down": + # google wants us to chill out, so do that + await asyncio.sleep(self.check_interval.total_seconds()) + else: + # Google says no + logger.error(f"Getting credentials from Google failed with HTTP 403 and error: {reply.get('error', 'unspecified')}") + raise GoogleCredGenerateError("Google refused the request to connect your account, either because you rejected it or they were set up incorrectly.") + elif resp.status == 428: + # Google says PEBKAC + logger.info(f"Waiting for you to authenticate with Google at {self.verification_url}") + elif resp.status / 100 != 2: + # Mysterious error + logger.error(f"Getting credentials from Google failed with HTTP {resp.status} and error: {reply.get('error', 'unspecified')}") + raise GoogleCredGenerateError("Failed unexpectedly while trying to reach Google. See the add-on logs for details.") + else: + # got the token, return it + return Creds.load(self.time, reply, id=self.client_id, secret=self.client_secret) + except KnownError: + raise + except Exception as e: + logger.error("Error while trying to retrieve credentials from Google") + logger.printException(e) + + # Allowing 10 errors is arbitrary, but prevents us from just erroring out forever in the background + error_count += 1 + if error_count > 10: + raise GoogleCredGenerateError("Failed unexpectedly too many times while attempting to reach Google. See the logs for details.") + finally: + if resp is not None: + resp.release() + + # Make sure we never query more than google says we should + remainder = self.check_interval - (self.time.now() - start) + if remainder > timedelta(seconds=0): + await asyncio.sleep(remainder.total_seconds()) + + logger.error("Getting credentials from Google expired, please try again") + raise GoogleCredGenerateError("Credentials expired while waiting for you to authorize with Google") diff --git a/hassio-google-drive-backup/backup/drive/driverequests.py b/hassio-google-drive-backup/backup/drive/driverequests.py new file mode 100644 index 0000000..8e7fcc7 --- /dev/null +++ b/hassio-google-drive-backup/backup/drive/driverequests.py @@ -0,0 +1,373 @@ +import io +import math +import re +from typing import Any, Dict, Optional +from urllib.parse import urlencode +from datetime import datetime, timedelta + +from aiohttp import ClientSession, ClientTimeout, ClientResponse +from aiohttp.client_exceptions import ClientResponseError, ServerTimeoutError +from injector import inject, singleton + +from ..util import AsyncHttpGetter +from ..config import Config, Setting +from ..exceptions import (GoogleCredentialsExpired, + GoogleSessionError, LogicError, + ProtocolError, ensureKey, KnownTransient, GoogleTimeoutError, GoogleUnexpectedError) +from backup.util import Backoff +from backup.file import JsonFileSaver +from ..time import Time +from ..logger import getLogger +from backup.creds import Creds, Exchanger, DriveRequester +from datetime import timezone + +logger = getLogger(__name__) + +MIME_TYPE = "application/tar" +FOLDER_MIME_TYPE = 'application/vnd.google-apps.folder' +FOLDER_NAME = 'Home Assistant Backups' +DRIVE_VERSION = "v3" +DRIVE_SERVICE = "drive" + +SELECT_FIELDS = "id,name,appProperties,size,trashed,mimeType,modifiedTime,capabilities,parents,driveId" +THUMBNAIL_MIME_TYPE = "image/png" +QUERY_FIELDS = "nextPageToken,files(" + SELECT_FIELDS + ")" +CREATE_FIELDS = SELECT_FIELDS +URL_FILES = "/drive/v3/files/" +URL_ABOUT = "/drive/v3/about" +URL_START_UPLOAD = "/upload/drive/v3/files/?uploadType=resumable&supportsAllDrives=true" +PAGE_SIZE = 100 +CHUNK_SIZE = 5 * 262144 +RANGE_RE = re.compile("^bytes=0-\\d+$") + +BASE_CHUNK_SIZE = 256 * 1024 # Google's api requires uploading chunks in multiples of 256kb + +# During upload, chunks get sized to complete upload after 10s so we can give status updates on progress. +CHUNK_UPLOAD_TARGET_SECONDS = 10 + +# don't attempt to resume a session with than this many times consistant failures, just in case something is broken on Google's +# end so we don't retry the same broken session forever. Because the addon eventually backs off to doing 1 attempt/hour, this will +# cause uploads to fail and start over after about 4 days. This gets reset every time a chunk successfully uploads. +# God be with you if your upload takes that long. +RETRY_SESSION_ATTEMPTS = 100 + +# Google claims that an upload session becomes invalid after 7 days. I have not verified this, but probably better to call it +# after 6 and restart the session. +UPLOAD_SESSION_EXPIRATION_DURATION = timedelta(days=6) + + +RATE_LIMIT_EXCEEDED = 403 +TOO_MANY_REQUESTS = 429 + + +# Defines the retry strategy for calls made to Drive +# max # of time to retry and call to Drive +DRIVE_MAX_RETRIES: int = 5 +# The initial backoff for drive retries. +DRIVE_RETRY_INITIAL_SECONDS: int = 2 +# How uch longer to wait for each Drive service call (Exponential backoff) +DRIVE_EXPONENTIAL_BACKOFF: int = 2 + +OOB_CRED_CUTOFF = datetime(2022, 3, 16, tzinfo=timezone.utc) + + +@singleton +class DriveRequests(): + @inject + def __init__(self, config: Config, time: Time, drive: DriveRequester, session: ClientSession, exchanger: Exchanger): + self.session = session + self.config = config + self.time = time + self.drive = drive + self.creds: Optional[Creds] = None + self.exchanger: Exchanger = exchanger + + # Between attempts to upload, we keep track of the info needed to resume a resumable upload. + self.last_attempt_metadata = None + self.last_attempt_location = None + self.last_attempt_count = 0 + self.last_attempt_start_time = None + self.tryLoadCredentials() + + async def _getHeaders(self): + return { + "Authorization": "Bearer " + await self.getToken(), + "Client-Identifier": self.config.clientIdentifier() + } + + @property + def might_be_oob_creds(self): + """Attempts to determine if the user might be using custom creds affected by google's OOB cred deprecation""" + if not self.isCustomCreds(): + return False + if self.creds.original_expiration is None: + # These creds must be old, so assume they're affected + return True + try: + return self.creds.original_expiration < OOB_CRED_CUTOFF + except: # noqa: E722 + # Regardless of why this happens, assume they need to check + return True + + def isCustomCreds(self): + return self.creds is not None and self.creds.id != self.config.get(Setting.DEFAULT_DRIVE_CLIENT_ID) + + def _getAuthHeaders(self): + return { + "Client-Identifier": self.config.clientIdentifier() + } + + def enabled(self): + return self.creds is not None and self.config.get(Setting.ENABLE_DRIVE_UPLOAD) + + def _enabledCheck(self): + if not self.enabled(): + raise LogicError( + "Attempt to use Google Drive before credentials are configured") + + def tryLoadCredentials(self): + path = self.config.get(Setting.CREDENTIALS_FILE_PATH) + if JsonFileSaver.exists(path): + try: + self.creds = Creds.load(self.time, JsonFileSaver.read(path)) + except Exception: + pass + + def saveCredentials(self, creds: Creds): + path = self.config.get(Setting.CREDENTIALS_FILE_PATH) + if not creds: + if JsonFileSaver.exists(path): + JsonFileSaver.delete(path) + self.creds = None + return + JsonFileSaver.write(path, creds.serialize()) + self.tryLoadCredentials() + + async def getToken(self, refresh=False): + if self.creds and not self.creds.is_expired and not refresh: + return self.creds.access_token + + # refresh the credentials + logger.debug("Requesting refreshed Google Drive credentials") + self.creds = await self.exchanger.refresh(self.creds) + return self.creds.access_token + + async def refreshToken(self): + await self.getToken(refresh=True) + + async def get(self, id): + q = { + "fields": SELECT_FIELDS, + "supportsAllDrives": "true" + } + async with await self.retryRequest("GET", URL_FILES + id + "/?" + urlencode(q)) as response: + return await response.json() + + async def download(self, id, size): + ret = AsyncHttpGetter(self.config.get(Setting.DRIVE_URL) + URL_FILES + id + "/?alt=media&supportsAllDrives=true", + await self._getHeaders(), + self.session, + size=size, + timeoutFactory=GoogleTimeoutError.factory, + otherErrorFactory=GoogleUnexpectedError.factory, + timeout=ClientTimeout( + sock_connect=self.config.get(Setting.DOWNLOAD_TIMEOUT_SECONDS), + sock_read=self.config.get(Setting.DOWNLOAD_TIMEOUT_SECONDS)), + time=self.time) + return ret + + async def query(self, query): + # SOMEDAY: Add a test for page size, test server support is needed too for continuation tokens + continuation = None + while True: + q = { + "q": query, + "fields": QUERY_FIELDS, + "pageSize": self.config.get(Setting.GOOGLE_DRIVE_PAGE_SIZE), + "supportsAllDrives": "true", + "includeItemsFromAllDrives": "true", + "corpora": "allDrives" + } + if continuation: + q["pageToken"] = continuation + async with await self.retryRequest("GET", URL_FILES + "?" + urlencode(q)) as response: + data = await response.json() + for item in data['files']: + yield item + if "nextPageToken" not in data or len(data['nextPageToken']) <= 0: + break + else: + continuation = data['nextPageToken'] + + async def update(self, id, update_metadata): + async with await self.retryRequest("PATCH", URL_FILES + id + "/?supportsAllDrives=true", json=update_metadata): + pass + + async def delete(self, id): + async with await self.retryRequest("DELETE", URL_FILES + id + "/?supportsAllDrives=true"): + pass + + async def getAboutInfo(self): + q = {"fields": 'storageQuota,user'} + async with await self.retryRequest("GET", URL_ABOUT + "?" + urlencode(q)) as resp: + return await resp.json() + + async def create(self, stream, metadata, mime_type): + # Upload logic is complicated. See https://developers.google.com/drive/api/v3/manage-uploads#resumable + total_size = stream.size() + location = None + if metadata == self.last_attempt_metadata and self.last_attempt_location is not None and self.last_attempt_count < RETRY_SESSION_ATTEMPTS and self.time.now() < self.last_attempt_start_time + UPLOAD_SESSION_EXPIRATION_DURATION: + logger.debug( + "Attempting to resume a previously failed upload where we left off") + self.last_attempt_count += 1 + # Attempt to resume from a partially completed upload. + headers = { + "Content-Length": "0", + "Content-Range": "bytes */{0}".format(total_size) + } + try: + async with await self.retryRequest("PUT", self.last_attempt_location, headers=headers, patch_url=False) as initial: + if initial.status == 308: + # We can resume the upload, check where it left off + if 'Range' in initial.headers: + position = int(initial.headers["Range"][len("bytes=0-"):]) + stream.position(position + 1) + else: + # No range header in the response means no bytes have been uploaded yet. + stream.position(0) + logger.debug("Resuming upload at byte {0} of {1}".format( + stream.position(), total_size)) + location = self.last_attempt_location + else: + logger.debug("Drive returned status code {0}, so we'll have to start the upload over again.".format( + initial.status)) + except ClientResponseError as e: + if e.status == 410: + # Drive doesn't recognize the resume token, so we'll just have to start over. + logger.debug("Drive upload session wasn't recognized, restarting upload from the beginning.") + location = None + else: + raise + + if location is None: + # There is no session resume, so start a new one. + logger.debug("Starting a new upload session with Google Drive") + headers = { + "X-Upload-Content-Type": mime_type, + "X-Upload-Content-Length": str(total_size), + } + async with await self.retryRequest("POST", URL_START_UPLOAD, headers=headers, json=metadata) as initial: + # Google returns a url in the header "Location", which is where subsequent requests to upload + # the backup's bytes should be sent. Logic below handles uploading the file bytes in chunks. + location = ensureKey( + 'Location', initial.headers, "Google Drive's Upload headers") + self.last_attempt_count = 0 + stream.position(0) + + # Keep track of the location in case the upload fails and we want to resume where we left off. + # "metadata" is a durable fingerprint that uniquely identifies a backup, so we can use it to identify a + # resumable partial upload in future retrys. + self.last_attempt_location = location + self.last_attempt_metadata = metadata + self.last_attempt_start_time = self.time.now() + + # Always start with the minimum chunk size and work up from there in case the last attempt + # failed due to connectivity errors or ... whatever. + current_chunk_size = BASE_CHUNK_SIZE + while True: + start = stream.position() + data = await stream.read(current_chunk_size) + chunk_size = len(data.getbuffer()) + if chunk_size == 0: + raise LogicError( + "Backup file stream ended prematurely while uploading to Google Drive") + headers = { + "Content-Length": str(chunk_size), + "Content-Range": "bytes {0}-{1}/{2}".format(start, start + chunk_size - 1, total_size) + } + startTime = self.time.now() + logger.debug("Sending {0} bytes to Google Drive".format(current_chunk_size)) + try: + async with await self.retryRequest("PUT", location, headers=headers, data=data, patch_url=False) as partial: + # Base the next chunk size on how long it took to send the last chunk. + current_chunk_size = self._getNextChunkSize( + current_chunk_size, (self.time.now() - startTime).total_seconds()) + + # any time a chunk gets uploaded, reset the retry counter. This lets very flaky connections + # complete eventually after enough retrying. + self.last_attempt_count = 1 + yield float(start + chunk_size) / float(total_size) + if partial.status == 200 or partial.status == 201: + # Upload completed, return the object json + self.last_attempt_location = None + self.last_attempt_metadata = None + yield await self.get((await partial.json())['id']) + break + elif partial.status == 308: + # Upload partially complete, seek to the new requested position + range_bytes = ensureKey( + "Range", partial.headers, "Google Drive's upload response headers") + if not RANGE_RE.match(range_bytes): + raise ProtocolError( + "Range", partial.headers, "Google Drive's upload response headers") + position = int(partial.headers["Range"][len("bytes=0-"):]) + stream.position(position + 1) + else: + partial.raise_for_status() + except ClientResponseError as e: + if math.floor(e.status / 100) == 4: + # clear the cached session location URI, since a 4XX error + # always means the upload session is no good anymore (AFAIK) + self.last_attempt_location = None + self.last_attempt_metadata = None + + if e.status == 404: + raise GoogleSessionError() + else: + raise e + + def _getNextChunkSize(self, last_chunk_size, last_chunk_seconds): + max = BASE_CHUNK_SIZE * math.floor(self.config.get(Setting.MAXIMUM_UPLOAD_CHUNK_BYTES) / BASE_CHUNK_SIZE) + if max < BASE_CHUNK_SIZE: + max = BASE_CHUNK_SIZE + if last_chunk_seconds <= 0: + return max + next_chunk = CHUNK_UPLOAD_TARGET_SECONDS * last_chunk_size / last_chunk_seconds + if next_chunk >= max: + return max + if next_chunk < BASE_CHUNK_SIZE: + return BASE_CHUNK_SIZE + return math.floor(next_chunk / BASE_CHUNK_SIZE) * BASE_CHUNK_SIZE + + async def createFolder(self, metadata): + async with await self.retryRequest("POST", URL_FILES + "?supportsAllDrives=true", json=metadata) as resp: + return await resp.json() + + async def retryRequest(self, method, url, auth_headers: Optional[Dict[str, str]] = None, headers: Optional[Dict[str, str]] = None, json: Optional[Dict[str, Any]] = None, data: Any = None, cred_retry: bool = True, patch_url: bool = True) -> ClientResponse: + backoff = Backoff(base=DRIVE_RETRY_INITIAL_SECONDS, attempts=DRIVE_MAX_RETRIES) + if patch_url: + url = self.config.get(Setting.DRIVE_URL) + url + while True: + headers_to_use = await self._getHeaders() + if headers: + headers_to_use.update(headers) + if self.config.get(Setting.TRACE_REQUESTS): + logger.trace("Making Google Drive request: " + url) + try: + data_to_use = data + if isinstance(data_to_use, io.BytesIO): + # This is a pretty low-down dirty hack, but it works and lets us reuse the byte stream. + # aiohttp complains if you pass it a large byte object + data_to_use = io.BytesIO(data_to_use.getbuffer()) + data_to_use.seek(0) + return await self.drive.request(method, url, headers=headers_to_use, json=json, data=data_to_use) + except GoogleCredentialsExpired: + # Get fresh credentials, then retry right away. + logger.debug("Google Drive credentials have expired. We'll retry with new ones.") + await self.refreshToken() + except KnownTransient as e: + backoff.backoff(e) + logger.error("{0}: we'll retry in {1} seconds".format(e.message(), backoff.peek())) + await self.time.sleepAsync(backoff.peek()) + except ServerTimeoutError: + raise GoogleTimeoutError() diff --git a/hassio-google-drive-backup/backup/drive/drivesource.py b/hassio-google-drive-backup/backup/drive/drivesource.py new file mode 100644 index 0000000..680e151 --- /dev/null +++ b/hassio-google-drive-backup/backup/drive/drivesource.py @@ -0,0 +1,282 @@ +from datetime import datetime +from io import IOBase +from asyncio import Event +from typing import Dict + +from aiohttp import ClientSession +from aiohttp.client_exceptions import ClientResponseError +from injector import inject, singleton + +from ..util import AsyncHttpGetter, GlobalInfo +from ..config import Config, Setting, CreateOptions +from ..const import SOURCE_GOOGLE_DRIVE +from ..exceptions import (BackupFolderInaccessible, + ExistingBackupFolderError, + GoogleDrivePermissionDenied, LogicError) +from ..model.backups import (PROP_NOTE, PROP_PROTECTED, PROP_RETAINED, PROP_TYPE, PROP_VERSION) +from ..time import Time +from .driverequests import DriveRequests +from .folderfinder import FolderFinder +from .thumbnail import THUMBNAIL_IMAGE +from ..model import BackupDestination, DriveBackup, Backup +from ..logger import getLogger +from ..creds.creds import Creds +from backup.const import NECESSARY_OLD_BACKUP_NAME, NECESSARY_OLD_BACKUP_PLURAL_NAME, NECESSARY_PROP_KEY_SLUG, NECESSARY_PROP_KEY_DATE, NECESSARY_PROP_KEY_NAME + +logger = getLogger(__name__) + +MIME_TYPE = "application/tar" +THUMBNAIL_MIME_TYPE = "image/png" +FOLDER_MIME_TYPE = 'application/vnd.google-apps.folder' +FOLDER_NAME = 'Home Assistant Backups' +FOLDER_CACHE_SECONDS = 30 +DRIVE_MAX_PROPERTY_LENGTH = 120 + + +@singleton +class DriveSource(BackupDestination): + # SOMEDAY: read backups all in one big batch request, then sort the folder and child addons from that. Would need to add test verifying the "current" backup directory is used instead of the "latest" + @inject + def __init__(self, config: Config, time: Time, drive_requests: DriveRequests, info: GlobalInfo, session: ClientSession, folderfinder: FolderFinder): + super().__init__() + self.session = session + self.config = config + self.drivebackend: DriveRequests = drive_requests + self.time = time + self.folder_finder = folderfinder + self._info = info + self._uploadedAtLeastOneChunk = False + self._drive_info = None + self._cred_trigger = Event() + + def saveCreds(self, creds: Creds) -> None: + logger.info("Saving new Google Drive credentials") + self.drivebackend.saveCredentials(creds) + self.trigger() + self._cred_trigger.set() + + async def debug_wait_for_credentials(self): + await self._cred_trigger.wait() + self._cred_trigger.clear() + + def isCustomCreds(self): + return self.drivebackend.isCustomCreds() + + @property + def might_be_oob_creds(self) -> bool: + return self.drivebackend.might_be_oob_creds + + def name(self) -> str: + return SOURCE_GOOGLE_DRIVE + + def title(self) -> str: + return "Google Drive" + + def maxCount(self) -> None: + return self.config.get(Setting.MAX_BACKUPS_IN_GOOGLE_DRIVE) + + def upload(self) -> bool: + return self.config.get(Setting.ENABLE_DRIVE_UPLOAD) + + def enabled(self) -> bool: + return self.drivebackend.enabled() + + def needsConfiguration(self) -> bool: + if not self.config.get(Setting.ENABLE_DRIVE_UPLOAD): + return False + return super().needsConfiguration() + + def freeSpace(self): + if self._drive_info and self._drive_info.get("storageQuota") is not None and not self.folder_finder.currentIsSharedDrive(): + info = self._drive_info.get("storageQuota") + if 'limit' in info and 'usage' in info: + return int(info.get("limit")) - int((info.get("usage"))) + return super().freeSpace() + + async def create(self, options: CreateOptions) -> DriveBackup: + raise LogicError("Backups can't be created in Drive") + + def checkBeforeChanges(self): + existing = self.folder_finder.getExisting() + if existing: + raise ExistingBackupFolderError( + existing.get('id'), existing.get('name')) + + def icon(self) -> str: + return "google-drive" + + def isWorking(self): + return self._uploadedAtLeastOneChunk + + def detail(self): + if self._drive_info and 'user' in self._drive_info and 'emailAddress' in self._drive_info['user']: + return f'{self._drive_info["user"]["emailAddress"]}' + else: + return super().detail() + + async def get(self, allow_retry=True) -> Dict[str, DriveBackup]: + parent = await self.getFolderId() + try: + self._drive_info = await self.drivebackend.getAboutInfo() + except Exception as e: + # This is just used to get the remaining space in Drive, which is a + # nice to have. Just log the error to debug if we can't get it + logger.debug("Unable to retrieve Google Drive storage info: " + str(e)) + backups: Dict[str, DriveBackup] = {} + try: + async for child in self.drivebackend.query("'{}' in parents".format(parent)): + properties = child.get('appProperties') + if properties and NECESSARY_PROP_KEY_DATE in properties and NECESSARY_PROP_KEY_SLUG in properties and not child['trashed']: + backup = DriveBackup(child) + backups[backup.slug()] = backup + except ClientResponseError as e: + if e.status == 404: + # IIUC, 404 on create can only mean that the parent id isn't valid anymore. + if not self.config.get(Setting.SPECIFY_BACKUP_FOLDER) and allow_retry: + self.folder_finder.deCache() + await self.folder_finder.create() + return await self.get(False) + raise BackupFolderInaccessible(parent) + raise e + except GoogleDrivePermissionDenied: + # This should always mean we lost permission on the backup folder, but at least it still exists. + if not self.config.get(Setting.SPECIFY_BACKUP_FOLDER) and allow_retry: + self.folder_finder.deCache() + await self.folder_finder.create() + return await self.get(False) + raise BackupFolderInaccessible(parent) + return backups + + async def delete(self, backup: Backup): + item = self._validateBackup(backup) + if item.canDeleteDirectly(): + logger.info("Deleting '{}' From Google Drive".format(item.name())) + await self.drivebackend.delete(item.id()) + else: + logger.info("Trashing '{}' in Google Drive".format(item.name())) + await self.drivebackend.update(item.id(), {"trashed": True}) + backup.removeSource(self.name()) + + async def save(self, backup: Backup, source: AsyncHttpGetter) -> DriveBackup: + retain = backup.getOptions() and backup.getOptions().retain_sources.get(self.name(), False) + parent_id = await self.getFolderId() + if backup.note() is not None: + desc = backup.note() + else: + desc = 'A Home Assistant backup file uploaded by Home Assistant Google Drive Backup' + file_metadata = { + 'name': str(backup.name()) + ".tar", + 'parents': [parent_id], + 'description': desc, + 'appProperties': { + NECESSARY_PROP_KEY_SLUG: backup.slug(), + NECESSARY_PROP_KEY_DATE: str(backup.date()), + PROP_TYPE: str(backup.backupType()), + PROP_VERSION: str(backup.version()), + PROP_PROTECTED: str(backup.protected()), + PROP_RETAINED: str(retain), + }, + 'contentHints': { + 'indexableText': 'Home Assistant hassio ' + NECESSARY_OLD_BACKUP_NAME + ' ' + NECESSARY_OLD_BACKUP_PLURAL_NAME + ' backup backups home assistant ' + desc, + 'thumbnail': { + 'image': THUMBNAIL_IMAGE, + 'mimeType': THUMBNAIL_MIME_TYPE + } + }, + 'createdTime': self._timeToRfc3339String(backup.date()), + 'modifiedTime': self._timeToRfc3339String(backup.date()) + } + + if backup.note() is not None: + file_metadata['appProperties'][PROP_NOTE] = self.truncateAppProperty(PROP_NOTE, backup.note()) + file_metadata['appProperties'][NECESSARY_PROP_KEY_NAME] = self.truncateAppProperty(NECESSARY_PROP_KEY_NAME, str(backup.name())) + + async with source: + try: + logger.info("Uploading '{}' to Google Drive".format( + backup.name())) + size = source.size() + self._info.upload(size) + backup.overrideStatus("Uploading {0}%", source) + backup.setUploadSource(self.title(), source) + async for progress in self.drivebackend.create(source, file_metadata, MIME_TYPE): + self._uploadedAtLeastOneChunk = True + if isinstance(progress, float): + logger.debug("Uploading {1} {0:.2f}%".format( + progress * 100, backup.name())) + else: + return DriveBackup(progress) + raise LogicError( + "Google Drive backup upload didn't return a completed item before exiting") + except ClientResponseError as e: + if e.status == 404: + # IIUC, 404 on create can only mean that the parent id isn't valid anymore. + raise BackupFolderInaccessible(parent_id) + raise e + except GoogleDrivePermissionDenied: + # This should always mean we lost permission on the backup folder, since we could have only just + # created the backup item on this request. + raise BackupFolderInaccessible(parent_id) + finally: + backup.clearUploadSource() + self._uploadedAtLeastOneChunk = False + backup.clearStatus() + + def truncateAppProperty(self, key: str, value: str): + # Annoylingly, Drive properties can be a maximum of 124 bytes, in len(key + value) UTF8 encoded. + # https://developers.google.com/drive/api/guides/properties + # Is the extra indexing REALLY that expensive? Thats like some 1990's mainframe limitation. + # Make sure we stay well under that limit + if value is None: + return value + permitted = "" + current = 0 + while current < len(value) and len(str(key + permitted + value[current]).encode('utf-8')) < DRIVE_MAX_PROPERTY_LENGTH: + permitted += value[current] + current += 1 + return permitted + + async def read(self, backup: Backup) -> IOBase: + item = self._validateBackup(backup) + return await self.drivebackend.download(item.id(), item.size()) + + async def retain(self, backup: Backup, retain: bool) -> None: + item = self._validateBackup(backup) + if item.retained() == retain: + return + file_metadata: Dict[str, str] = { + 'appProperties': { + PROP_RETAINED: str(retain), + }, + } + await self.drivebackend.update(item.id(), file_metadata) + item.setRetained(retain) + + async def note(self, backup, note: str) -> None: + item = self._validateBackup(backup) + truncated = self.truncateAppProperty(PROP_NOTE, note) + file_metadata: Dict[str, str] = { + 'appProperties': { + PROP_NOTE: truncated, + }, + 'description': note, + } + logger.debug(f"Adding a note to drive backup '{item.name()}'") + await self.drivebackend.update(item.id(), file_metadata) + item.setNote(truncated) + + async def getFolderId(self): + return await self.folder_finder.get() + + def _validateBackup(self, backup: Backup) -> DriveBackup: + drive_item: DriveBackup = backup.getSource(self.name()) + if not drive_item: + raise LogicError( + "Requested to do something with a backup from Google Drive, but the backup has no Google Drive source") + return drive_item + + def _timeToRfc3339String(self, time: datetime) -> str: + return time.strftime("%Y-%m-%dT%H:%M:%SZ") + + async def _get(self, id): + return await self.drivebackend.get(id) diff --git a/hassio-google-drive-backup/backup/drive/folderfinder.py b/hassio-google-drive-backup/backup/drive/folderfinder.py new file mode 100644 index 0000000..cde5fe0 --- /dev/null +++ b/hassio-google-drive-backup/backup/drive/folderfinder.py @@ -0,0 +1,204 @@ +from datetime import timedelta +from typing import Any, Dict +from backup.file import File +from aiohttp.client_exceptions import ClientResponseError +from injector import inject, singleton + +from ..config import Config, Setting +from ..exceptions import (BackupFolderInaccessible, BackupFolderMissingError, + GoogleDrivePermissionDenied, LogInToGoogleDriveError) +from ..time import Time +from .driverequests import DriveRequests +from ..logger import getLogger + +logger = getLogger(__name__) + +FOLDER_MIME_TYPE = 'application/vnd.google-apps.folder' +FOLDER_NAME = 'Home Assistant Backups' +FOLDER_CACHE_SECONDS = 60 * 31 # 31 minutes + + +@singleton +class FolderFinder(): + @inject + def __init__(self, config: Config, time: Time, drive_requests: DriveRequests): + self.config = config + self.drivebackend: DriveRequests = drive_requests + self.time = time + + # The cached folder id + self._folderId = None + + # When the fodler id was last cached + self._folder_queryied_last = None + + # These get set when an existing folder is found and should cause the UI to + # prompt for what to do about it. + self._existing_folder = None + self._use_existing = None + self._folder_details = None + + def resolveExisting(self, val): + if self._existing_folder: + self._use_existing = val + else: + self._use_existing = None + + def _isSharedDrive(self, folder): + driveId = folder.get("driveId", None) + return driveId and len(driveId) > 0 + + def currentIsSharedDrive(self): + return self._folder_details and self._isSharedDrive(self._folder_details) + + async def get(self): + if self._existing_folder and self._use_existing is not None: + if self._use_existing: + await self.save(self._existing_folder) + else: + await self.create() + self._use_existing = None + if not self._folder_queryied_last or self._folder_queryied_last + timedelta(seconds=FOLDER_CACHE_SECONDS) < self.time.now(): + try: + self._folderId = await self._readFolderId() + except (BackupFolderMissingError, BackupFolderInaccessible): + if not self.config.get(Setting.SPECIFY_BACKUP_FOLDER): + # Search for a folder, they may have created one before + self._existing_folder = await self._search() + if self._existing_folder: + self._folderId = self._existing_folder.get('id') + else: + # Create folder, since no other folder is available + await self.create() + else: + raise + self._folder_queryied_last = self.time.now() + return self._folderId + + def getExisting(self): + return self._existing_folder + + async def save(self, folder: Any) -> str: + if not isinstance(folder, str): + self._folder_details = folder + folder = folder.get('id') + else: + self._folder_details = None + logger.info("Saving backup folder: " + folder) + File.write(self.config.get(Setting.FOLDER_FILE_PATH), folder) + self._folderId = folder + self._folder_queryied_last = self.time.now() + self._existing_folder = None + + def reset(self): + if File.exists(self.config.get(Setting.FOLDER_FILE_PATH)): + File.delete(self.config.get(Setting.FOLDER_FILE_PATH)) + self._folderId = None + self._folder_queryied_last = None + self._existing_folder = None + + def getCachedFolder(self): + return self._folderId + + def deCache(self): + self._folderId = None + self._folder_queryied_last = None + + async def _readFolderId(self) -> str: + # First, check if we cached the drive folder + if not File.exists(self.config.get(Setting.FOLDER_FILE_PATH)): + raise BackupFolderMissingError() + else: + folder_id: str = File.read(self.config.get(Setting.FOLDER_FILE_PATH)).strip() + if await self._verify(folder_id): + return folder_id + else: + raise BackupFolderInaccessible(folder_id) + + async def _search(self) -> str: + folders = [] + + try: + async for child in self.drivebackend.query("mimeType='" + FOLDER_MIME_TYPE + "'"): + if self._isValidFolder(child): + folders.append(child) + except ClientResponseError as e: + # 404 means the folder doesn't exist (maybe it got moved?) + if e.status == 404: + "Make Error" + raise LogInToGoogleDriveError() + else: + raise e + + if len(folders) == 0: + return None + + folders.sort(key=lambda c: Time.parse(c.get("modifiedTime"))) + # Found a folder, which means we're probably using the add-on from a + # previous (or duplicate) installation. Record and return the id but don't + # persist it until the user chooses to do so. + folder = folders[len(folders) - 1] + logger.info("Found " + folder.get('name')) + return folder + + async def _verify(self, id): + if self.drivebackend.isCustomCreds(): + # If the user is using custom creds and specifying the backup folder, then chances are the + # app doesn't have permission to access the parent folder directly. Ironically, we can still + # query for children and add/remove backups. Not a huge deal, just + # means we can't verify the folder still exists, isn't trashed, etc. Just let it be valid + # and handle potential errors elsewhere. + return True + # Query drive for the folder to make sure it still exists and we have the right permission on it. + try: + folder = await self.drivebackend.get(id) + if not self._isValidFolder(folder): + logger.info("Provided backup folder {0} is invalid".format(id)) + return False + self._folder_details = folder + return True + except ClientResponseError as e: + if e.status == 404: + # 404 means the folder doesn't exist (maybe it got moved?) but can also mean that we + # just don't have permission to see the folder. Often we can still upload into it, so just + # let it pass without further verification and let other error handling (on upload) identify problems. + return True + else: + raise e + except GoogleDrivePermissionDenied: + # Lost permission on the backup folder + return False + + def _isValidFolder(self, folder) -> bool: + try: + caps = folder.get('capabilities') + if folder.get('trashed'): + return False + elif not caps['canAddChildren']: + return False + elif not caps['canListChildren']: + return False + elif not caps.get('canDeleteChildren', False) and not caps.get('canRemoveChildren', False): + if self._isSharedDrive(folder) and caps.get("canTrashChildren", False): + # Allow folders in shared drives if you can still trash items inside it. + return True + return False + elif folder.get("mimeType") != FOLDER_MIME_TYPE: + return False + except Exception: + return False + return True + + async def create(self) -> str: + logger.info('Creating folder "{}" in "My Drive"'.format(FOLDER_NAME)) + file_metadata: Dict[str, str] = { + 'name': FOLDER_NAME, + 'mimeType': FOLDER_MIME_TYPE, + 'appProperties': { + "backup_folder": "true", + }, + } + folder = await self.drivebackend.createFolder(file_metadata) + self._folder_details = folder + await self.save(folder) + return folder.get('id') diff --git a/hassio-google-drive-backup/backup/drive/thumbnail.py b/hassio-google-drive-backup/backup/drive/thumbnail.py new file mode 100644 index 0000000..e815ab5 --- /dev/null +++ b/hassio-google-drive-backup/backup/drive/thumbnail.py @@ -0,0 +1,2 @@ +# flake8: noqa +THUMBNAIL_IMAGE = "iVBORw0KGgoAAAANSUhEUgAAAgcAAAIHCAYAAAALof87AAAABmJLR0QA_wD_AP-gvaeTAAB1s0lEQVR42uy9d5Bc15Wnqd3Z7d7ZmNk_diJmY3p3Yzc2JmZ2Y2J2oyV1t-hQqII3hCNAT0qUREl0kuhJiS1RotQylCipRallSVG0AEkQJAigCt5774ECUIXyLm1lOYDi3XNu5stK88x9LvOZ3434Am2IQuXL9-793jnnnvuJT2BgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYARkLFsh_tUTJ8T_89gJsfiJk-JJ-p9fevKUWEn_88YnTomD9Od54iIAAIBYcoTWhW3052riDVoXvvfkCXHnUyfF3z17SvwbrKIRGvRFf5r4Jn3R64gsIQAAAACbXH38pNj_5EnxAnHjQ63ir7HChmx847T4D_TlPUGcwg0NAADAB4aIX_ILKFbdgI_HT4tPFsJBfzH7Uh89_hfxtaNXxYNHroj7D06IL-8fAwAAACT3HRwXDxyaEF89clU8QuvFYyc-thKFIxRVuPtZIf5brMRBSh2cFX9DuaE_Ex9Vfmn8pX7t6EfiKwfGxef25MSdO7Pi9h0Zcdv2NAAAAGDK7QytGZ_dPSzu3T8qHqKXSn7B1JMETjs8fkrcgFW5zuPZNvE_UEjnWZKC0fIv6GPx8LGPxJdJCO7elaUvGDIAAADAG-4kWfjC3hHx0OGr4nH9qMLqR0-L_wOrdB3GU8fF_0VicKjyS_k6RQnu2Z0j2yMh2JYGAAAAfOH2bRlx985h8eChK3qphxTthluI1bqGgwoNb6ULP1wSyiGDu0JhnxxuWAAAADXnzh1ZilaP6UUSfkvbIP8KK7f_WxN5W-LH2oV_-NhfxOcpvINIAQAAgHrDkQR-WaW1alIQTon1VBv3b7GC-1FfsEX8dxQxeLnUyPgLYFvDDQkAACA46Ya03PVQkWo4-cQx8b9hNfdSDGh7CKUOXindingPRQtupS8AAAAACCK8O463Q5YIwjnuw4NV3YshxH9TKgaPUBrhLgrb4MYDAAAQdHh3w4OcZpgUhPOPHRP_Hou728ZGp8S3ijsRaHviXWRiuOEAAACEBa6J44Z7JYKw_eHd4l9jhXdafHhK3FFaX8D9CnCjAQAACCNf3DdaKghvYpV3MJ4-Lv5vbbsiRwxuhxgAAAAIM5Ri4G692k4GKrK_F6u9zZ0J3IZS26rIOxJwYwEAAIhCDQKf21CIHuRoB8N_xqqvnk54VNuVgBoDAAAA0StSLO5i2MGF91j5LcbDR8X_Shcryxft83RQEm4kAAAAUeMOOp_h0eP5PgjUZvl2rP7WUYM_8MXiXtWcn8FNBAAAIIp8YW-xQPEyHyQIAzASA8q98LHL3Mvgju2cTsgAAAAA0aR8i-PXYQHGByrJ9sifJ5vCjQMAACDq3LljWNbX0drXi94HelGDs-Jv6OJMcCepW-iCAQAAAHHg81p64YT4AmygMmpwSjzFF-euXTncLAAAAGIDpxce4ejBKbEbNlCdUjjFB1TgRgEAABA37qVTHPkF-ekz4j_BCDQxOC7-K3eM-tyeEdwkAAAAYsdtVIQvtzaeEN-EFUxuX3z64aMfydAKbhIAAABx5Mv7x5FaqJCD9V-ii4KbAwAAQFy5a-eweOz4xx891Cr-p9iLAV2Ev37s5Mdjd9BFwc0BAAAgtlD0nGvvHj8h5sdeDh4_LT75NVmImMWNAQAAIOaFieN8WuN3UIxIPaW_fAApBQAAAOCuXZRaOPrxytjLwWMnxPc-uxu7FAAAAADumvjwkY_aYy8HXzv6UTNv4cBNAQAAAMi6g6uxl4P7D0-cvJkuBgAAAAAy4gE6lfjZU-KvYi0HtK-zDzcDAAAAkOeL1C2ROiX-u1jLAV2EHG4GAAAAIM89dBDTU2fE_xlvOdg3No6bAQAAAMhzNx1A-NDhK_9frOXgnr0jV3AzAAAAAHm4KeAXDoxeF3M5GL2KmwEAAACYlIM7tiVmxloO6CTGj3AzAAAAAHluJzm4bWv2xnjLwW7IAQAAAAA5gBwAEGy2pp2D6wcA5AByAEAMBQDiAADkAHIAAEQAwgAA5KCmcrCMLgYAwGNo4Q0d-N4AgBxADgCAEEAUACjnNsgB5AAACAFEAQDIAeQAgECLwdItKddAEACAHEAOAAipFHghAjUXBnz_AHIAOQAAeCcFtZQB32UB9wOAHEAOAIAY1EsIkgrUSRRwXwDIAeQAAIiBn1KQ9IEaSALuDwA5gBwAADHwSgqSdcAnScB9AiAHkAMAIAZOpcDmYr45oY4PogBBAJADyAEAwIEYeCIFdiTALh6IAgQBQA5iNj4LOQDAJzHwTghu2jRkiLeiAEEAAHJQkIOldDEAAAQv-ArcRIulMUljeKHX4Y5tSfG9o1nxu3Mj4qXzOfGrMznx9CE6BIb-f0tIAJxg9G8VMfs9TT6f6jXC_QTCyq2QA8gBADURA4MFetmWhPjR8WFxIXtV_OVjUTYmPvpYHBi6Ih7dnxZLNg4a41YWIAgAQA4gBwD4IQbqUqAt1ks3D4lXL4yIib8I05Ec_wtFFTLmgmBTGryQBAgCgBxADgCAGNgRAxMpYJaRGLxOYlAZLTAa4xRF-CcShJs28cI_YIB9UVCXBAgCgBxADgCAGPgiBQzXEbAYXFUUA21kKMTw_aNpEzlQlIV6SQLuNwA5gBwAEO2IgbUY6C7C9ObvRAy0kb3ysfj-kbRYvKFfF1uioCIJEAQAOYAcAAAxcBAxUJACXoyX0Z-vX1RPJZilGFgQlrAQrO8zFAVjYbAWBWtJgCAAyAHkAACIgTMxKCy-mhhcdSkGZSmGwymxZH1BECpRjipAEACAHEAOAMSgNmmEkkVXphI8FINKQVjc0jfJ-j4lWTCNJHiQZoAgAMgB5ACAWIuBkRQwS0kMXruQc51KME0xsCCs7yuXBCNZ8EISIAgAcgA5AABiYD9awIutJgZXfRKD0gjC9w4lSQZ6xaIS7IiCSqrBSZoBggAgByEdd5Mc3EQXA4DIwgu_BUtoIasmWQ7LQAmLacEsQgvpJPlFthZiMLmLIS8IC5t78pAclLKI5KDI-gpIDhj-vScp-Twln7PyGlRdI53rqHL9cZ-CIAE5gBwAiIFtMTCTAia_XdG_VIJZiuG5QwmxSBMEO6JQEARHkgBBAJADyAEAEAMTMSBer2HEQC_FwIKwcF33JAaioBtJ0JUECAKAHEAOAIAYOBKDJRvqKwZlgnBwSCxY2-VMEiAIAHIAOZBysJUnUwAiwpa0JUs2p3RIlrMpUWTxxqFJNgwWGChyE_Faa-1TCaYphgNDYiEJwoI1eRau7c6zrmeS5l7Joua-SYqy0C8p_ZzFz15yPUqvU9U11LnOKt8P7mNQT27dATmAHACIgQsx4MVzKb1Nv95a_4iBXpHic_sHSQw6i4KgLAkVglAuCRAEADmAHAAAMTAUgyXrKWJwfjhwYlAqCI_v6i8IgoUkGAmCbhQBggAgB5ADAOIsBkUpKBcDTiW8HqBUgpkgfHf_gFhYFASHkmAkCCWSAEEAkAPIAQCxjxhcITP4-OPgk5n4SArCgg878lRIAgQBAMgB5ABADFxGDF4lMfgoJGKgMXq1QhAUJQGCACAHkAMAIAYKYnAlZGJQFUFYfVncSJRLAgQBAMhBiRws2ZoVAISKLRlLFm9O65AqZ1OyyKKNiTwbhkoYlCykNMLi9YPizyEWg1JB-M6-ATH_AxaEDnHjh515SAzydFOPhJ4863rzkBgwC5v7qTdCAbomjHaNyq5b4VqWXt-qa6_z_ah8r7j_gd_csiMHObh79yjkAEAMLMRgyYa8GHwUcjHQGLv6FykIUg40KgShKAl6gtAMQQCQA8gBABCD0EcMjCIIN66-XC4IRUnoro4iQBAA5AByAABSCdEUgzJB2NsvUwz6aQZFQShIAgQBQA4gBwBEO2JARCmVYLaLgQWBIwhFQdBJMyCCACAHkAMAIAYRjhgYRRAgCABADiAHAGIQw1SCdYqhvTrNoCII2MUAIAeQAwCQSohmiuG7HEHQE4SSOgS9rY56NQiTkgBBAJADyAEAIRWDV2MYMdCNIOzp80wQEEEAkAPIAQBIJURIEObbEQTsYgCQA8gBAEglxGQXgxSEduutjihSBJADyAEA2JUQowjC-20QBAA5iNO4i-RgMV0MAOoKL_wWLKIFoZpUOSwEBRbSgiOhBWiSQckCXqgolYAaAzVBeGR7j5hHgjCPBGEepxlIEOaTHEg0QSA5kJQIwgKSgwUtBeiaM9p3UPa9FL6r0u-v6rvV-f5V7hs8X8AuN0MOIAcgnmKweH04j12upyA8W6hBMBUETRJIECYlob9KEiAIAHIAOQAAEYMICUJVBEGTBAgCgBxADgAIa8QAxYfuihR1IwgmglCWYmhGigFADiAHAAQwlYCIgTvS41dtpxggCAByADkAIJCphD-fy0IM_E4xqAoCihQB5AByAACKD6PbB8FOisGsBmFSEiAIAHIAOQAQA6QSoreLAUWKAHIAOQAAuxJi3iiJIgh2djHo1iBAEADkAHIAIAa-70qgGgOkEoKbYkCRIoAcQA4AxKDm2xURMcAuBggCgBy4kINFdDEA8Bxe-C1YSBN3NalyWAgKLKCFQUILxSSDkhtl__5B8QrEoK4phm-TIMylFMNcEoS5JAjzSBDmkRxISBDmkxzMJzmQcA0CyUEeOuSppQB9l4z23ZZ934V7oPS-qLpndO4rlfsRzy1glkEOIAcgOmKwCA2OgtMoiWsQpByUCIImCZogaJJAgjApCf0mkgBBAJADyAGAGNgUg1eoxmDio79ggQ5KisFDQUAEAUAOIAcAYmA_lVAQg7_8BQSF1NgV_RSDiSCUpRjqLQiQBMgB5AA3Awh3xOAqxCCQjFy5KgWhKoJQWYNgJQj1qkGAJEAOIAcAhFAMqMYAEYNwRBCCKAiQBAA5gBwApBJA0FIMqrsYgiQI9QZzDuQAcgAgBjoRgw2DSCWENMVQlyJFP9IMEAjIAeQAgGDuSsCCGz60XQx2ihTVBGHQE0GInCRAGCAHkAOAVAIIS4rhke097ncxVKUYqgXBaZoh0pIAWYAcQA4AdiWAIEcQvC9SdCII5pIAUYAcQA4AQMQA1FAQlIoUFQVBtw7BQ0EIGhAFyIGv406Sg4V0MQCwhBd-CxbQpFVNqhwWggI30qQtoYl8ksE8UgzyLZERMYhukeK3WBBIDuZILou5JAgSEoS5JAjzSA4kJAjzSBCY-SQI80kOJC15ipKg3T_a_aTdYywJJfeepPLeLJIOLSrPqRIxnuuWQg4gByDYYoCIQTwiCL4LgpkkGApCuCXBM1GAHEAOAAiKGCygVMKf0eAodimGOZRiqIsgWEqCXWFI-UAdRQFyADkAICgRA6QSkGLwThD00wy6kmBLFIICJAFyADkASCWAKEcQuA9CpSCwHHgpCFZRhFCLQspfSYAcQA4AxKDWqQSIAdBqEKpSDEqC0KcvCAppBktJCBL1lgTIAeQAQAywKwHU6zRHZykGd1GEUMqCB4WVEATIAeQAIJUAwtMHQVUQCpLgWBBMJMFMGFT-jlf4LQkQBMgB5AAglQCit4tBVRCsJEFRFIKAM1HwUBIgB5ADADHArgQQxBRDURJKBMGZJBiIQlBwKgoOJCHOggA5gBwApBJARFIMdgXBWhL8EoVB7_4tO5LgpyBADiAHAGKAVAIIaopBWRCMogiGkhAU7ImCuiRAECAHOnKwgC4GiCG88FtwI00O1aTKYSEoMJ8mJAlNVpMM5pFiQBED7EoALhslsRzMllyWzCFBmEOCIJGS0J2HBEFCgsDMI0HI0z9JyyR8j1ah3b920Ps5TlD694aq0Z7DAqXPqKTyGS6S1kVlrojS3HgT5AByADGooRjQn39CxAC4JDV2RfwjCcJsiiC4EwRjSagUhWWbBsV9uxLiif1J8eT-VG05kBJf25MUt23JRw-UJcFKEAwlAYIAOYAcQAxqJAY3UirhpbMZMX71I_HRRwC4Izk6Ib62rbtEENp1BcGtJNyyaUC81jos2jITMmoxQfdvPRi78pHoy10RaztGxAO7kxbRBS-iCA4FAXIAOQAQAzuphJcpYnAFYgA8RIsgzNETBE0SLATBTBIe3jMkLmcnAve5cxNXxQsnMlK4zdMQEATIAeQABDhiwGKAiAHwTRB29-YjCCVphjJBcCAJX9g2IDqHJwL7uVkQnj2U0q-VMIskeCAIkAPIAYAYIGIAAk9uIi8IcyoEwSiKoCsJJaLAuxp2944G_nN3kbzcuXXIuKASggA5gByAQIoBagxAjSMI5YKgGEWokIQ7N_WJ4fErgf_MV4kXjqetd124FgTIAeQAQAw8SiX8CakEUAdB4G2Os1ddqhIEZUkgvrFvUC68YfjMzR258lqJoAgC5AByACAGSCWAoKUYioJgQxI0UXj-yFBoPu_-_lH9gko9SdBLM0AQIAeQA4hBLcQAEQMQjAhCQRCqJOGysSQUROGFo4nQfNaDA2NSCnQbPHkuCJADyAGAGCCVACIgCLPeu2gaRdCThJ-GKHIg5cBqW6ahICikGCyiB5CDCI87IAcQA48iBkglgKClGMoiCLqSUJ1uCJ0c6Oy4gCBADtzLwa7Rj26kiwEiAC_8Fsynh7maVDksBAXm0UQhocljksE8hYNq0McABLaTotzF0CNmrbooZlIUYRYJgoQEQUJyoKFJwk9CJAcH-kbEnLXdlA4hSA4kJAfMXE43SPrztOTh51aiPcelz7b2vLMklMwD5XNE-fxhOueEdC5dAjkoyMEWXlhAqNmcsURXDDalytmYLDJvQ6JAyeSxfjBPC7-BFFIJV7AIgWC3Wv7HXT1i5soLUhCkJLx3Kc-qtkneb5f85NBguORgTXcJPXnW9krmruvLowlCAX5-JdrzXCb_-ee-dC6omicqBcFo3gnpfLpkO-SgIAfDAoSYzVlL5ks5qGBTupyNqSLzNiQLJCZZP5SnhQ6AaWExGEYqAYQjxUDHPbMgzFpJcsCQGMyUgtA2yap2Sejk4EM6Y6JAUQ7W9OZZy3LQn6coBwMSfo4l2nNdfNaTRUrnhLK5omIuMZx7QjqnQg4gBxADF2KAiAEIYwShKAglklAqCj85NBAuOVjdJWZr2BCEohyYCIKhHERcECAHkAOIgU0xyKcSIAYgzILQLWa-eyGPjig8f7A_PH0OenMkB51UK9HpThCq5CAR6-gB5AByADFAKgHEMsVQIggVohA2OZj9QUeBckGYlIS8HFQLgoP0gkn0AHIAOQAxFYOXz6L4EEQngvDMzi6SglYx4508miQ8f6AvVGmFSTkoCIImCZURhFJBsFl_4Cp6ADmAHIDophIgBiCqgjDjnfMF8pIQOjl4_zLtssijIgj20gsW0QPIAeQAIJWABQVEMcXwzA6KIBQF4bx4fn9vuOSgsMtiUg46DGoQjOoPXEQPIAeQA4BdCQBENoJAgjBjxVkx4-1zJAc94ak56BnO92jQtmLqCYJZ_YGN6IFKagFyADkAMUkljE1cFVevAhBtEiPj4qGN7VIQfrS3KzS_d5kcWAiCp9GDiKcWIAeQA4iBScRg4goWDRAvQXhme2f45KCyoVOZICikF_SKE2OeWoAcFORgPl0MEEDkwm_OPHoYq6AHtgwWAo3CQz6XHvgiNAlIWvKtVF8mMUDEAMRVEDZcSoZKDnj75QxGSkKbmElyIOEiRRKEWSQIs7T0AslBHjqQitMLJAeSZmZAwvOAhOeEwhxRfKEonUtK55iS-Ud3rgrZ3As5gBxADErEYH5LXgziHDHIjV8RhwbGxesXRsSPjw-Lbx7KimePZMU_nxoW6zpGRUf2SiSuT5YOJDoyOC5eac2JHx7PimcO5z_ni6eHxYeXR0RXdoKKUCELoZKDSkEolYMqQcjLQVEQmicFoUwOzAQBcgA5APERg7hGDHqGJ8QLJ7JiMZ0617AuIaYY0NScEPfsSElRYJEI2-fsGL4iRWfBJvPPOa0lIb60KyVWXx4Vo4giBVsOqDfDDKYgB4aCYDd6ADmAHGBBRiohrmIwNnFFrKaFfumWlOFCqcdUkoSH9qZFO71hh-FzcrRj-aURsZCO37XzOVkgnjiQEZfSE1iMA8i-7iyJQWteDkoEwUgOHEUPVFILkAPIAYhgxIB2JcQxlZAjGfoGpQ14obezYJbCx9k2d4wEOgSfoRQCpwwa1jn_nPPoc-7qHcWCHFQ5eKd1Ug4sBKFMDkqjBxapBVU50BUEyAHkAIRPDOIYMRiliMEPj2UdL5alzFyfpDx9MBfOYUp9fOuwOzHQuJEE4UD_GBbloMkBNW6arnV4VIgeaH0PappagBxADkCIUgkxFQN-y19xadRVxKCSOXStzybHA_dZf3Mm54kYaNyyNSmLFbEwB1gOVAXBMLUAOYAcQA6QSojhhHoyMS5mrfduwdS4Z2dapMeCU6R4NjkhCwu9_pxPHcxgJ0OQ5IC6Ok5n7EYP7KQWIAeQA4BdCZGOGpAQ_ePhrGUR3l3bU-JXtK2PixVXtY-KX1CF_5LNKcu_t4KK_oISHfnuEfPPOa0lKWsu3rw4Ij6kz_kG_cn_-wwLoeC_dz6F6EGY5UAvelCZWlCqO4jojgXIAeQgdqmEl2Le4Ghw5IqYtSFpusD_7mxOFvFV_t1OCqffvydtHj3YkRYjAbi-7ZkJmeow-j2XbU2LXX1jVREAjibt6h2zFATuh4DFORhyMJ1aPkvcRA_s1h1EfMcC5AByEKuIwUtoiSzW0s4CMzHgHgBmIfMBkgvuc2D0MxqpjuHQYP1rD167MGLaq-H4kPnvyAWWjSY1Gcu2JHUFCtRaDjKeyYHrugPIAeQAhFAMcIiS5MfHs6YLXmLUesHb0j1q-lbNYfp6f06uCzD6_R7YY10zwI2PHt6fMU0tnEqMY4EOhBycEdOWn9EXBCcNkSrrDiAH8Ry3kxzMo4sBaoBc-M2ZSw9WFfTglUEPZBEpBUkKIScmoQdZIh9siEFpyPxBk7QARw1Utwfyvn-jn_M8CUi9P-cd24yjGxxVUPk5XIdgGGWhqMK6TvQ9CIIcTCM5aCI5mEZyMI3kYJrcvdAqppMcTC92TWwTM1gQSA5mkhwws0gOJCQHs0gOZnH0gORgdrPGgJxDJDyfFOYXbc4pzkHavFQyZ-nObyGaqxdDDiAHURaDeUglVL0Nf3Z7yvUbP1_P20wW3-8fra8ccNfHRQbFk3JR71D7nPv6x00jJMsvjuC-CoIcLD-tLAczKHpQFAQuTNSTg3WQA8gB5CDyYoCIQemieVV8fqdx5OCl8znlBkpmOxe4uVK95WDhJuPfb2W72hv_1p4xUzl4pw2Rg-DIgYEg2JSD8uhBhRwUBEFFDnQFAXIAOQBIJQS1-dGjJnn0-3enxZhClOV8apwaKBmnFf50fiTQaYXvKkQ2-Fo9b1KfwcWbXHuB-yrgclCMHjiTg6IgqMiBVfQAcgA5AHWOGMS4wZEVvAXPrMjOasEbp-v6_aMZ0wOZtvfUf9Hkw5IMz4OgqMKQReFlKx20NN-kroK3Ol7MoNdB3eWgKy2mvaUiB4XogZkcVNUdQA4gB1jIIxUxGKWCuStXgB7besx3GizdnJTHN-v93bGJCfHmhZzcCmj09xfQqYf9uYm6f87X6fc0-5w_OZGV6Qe9vzs0MiG-vNu8nwPXboyMT-CeqjN7O1N5OXjrVJUgWNYdkBwY1h1UFiVCDiAHILxi8NPjGYiBBdmxK5bHFt9CWxrfbRsRF-ntuX94XHSTLGzvGRFf35exPKfgx8epAHSi_otmF_3O803qDjjC8RilWPb2jUoZ4s_J6ZLXWnO0pdP6-Gr-73A_BVsOZPTAaVGiiRwUBaF0foIcQA5AMFMJfzwDMVCFtyxaLX4sAdNbOBKQEnMovK56eBF3UOyjhbben3FD54g8LdLq92VJ4E6K_Dmn03_foHAY1Wz673tziBoERQ6a3jxlLAcOihJ927EAOYAcQAyQSggyHPa_fVvK8wOJiiF36qBYL0GYIDZ2jfpy4JImTX88O4z7KGhyUBQE_boDyAHkAHIQw-LD8YlgTVjp0Qlxmrrn8SLF4fn3CM71c7__oOSpuWhw5vqEb4LwUB0iCJoYmJ2p4Ppz7eWTJxE1CK4cFKIHLnYsQA4gB5CDCIhBUCIGLChnkuMyZM9tiPnNtTREzW-cM2gx_sLOlCzq6x2eqPvv-zadoDi12V9BSI3W7nP6LQYcEWnP-CM8fJ12946KV6mW4YWTw7KR1M_pT75Xjg6OyVoRyIANOeDogd6OBbe9DiAHkAOAVIIqHBHg4ramFvWFiRexX9DkX8-3UH7Tfr99xLQVshdv2n5HEGqRSvjq3gylY8Y9_71PUoTpG3QOBN8PRvUO_O_fSEWkL9AOi4vp8UAUfAZLDk5CDiAHkANEDKj4MECpBA7PL9nsbHHlxeAhWnQuZ-orCCeHxsTDtBPBryiCn4KgicFsnyIG_HP_QDUGXkdAeLvkz0gOZ9n8vbn3Atc8oMZGQQ5MtjP61usAchCdcRvJwVy6GMAmcuE3Zw49IFXQA1TGxtQkUgqSNCEnJqGHUcIRA-IPAYoYHOj3ZlHibn6X0vV9Gxyha9pCBwnxSYTTW4wr-xdT2-TnKOR99w57BY28i2EoN-5LKsHOd8Cf4TtHsuKLu9Ky6ZPRmzr3e-Dw_tnkmBQQL39n3unwJfr3G5qd3TvcnZK_g1qmbILKHpKDRpKDRpKDRpKDxoIcNJEcNJEcNFH0YBrJwTSSg-kkB9NJDqaTHEyn6MEMEoQZJAgzuN8ByUGeQvSA5GBWMzMg4fmnOBdtSBTnquLcpc1nJXNd1ZwYovl9EeQAchAWMZizPlhicJzywPM9DMezICQDMNnzQjhIb7UH-8fEB5RyePNiTrxFeW8WB-57kCsUVHIkgEPtdj4jL4hd2Ym6pRL4v11On2dC1lxMUN3HuNhFef6VbfnPuYIOUuKf2Ur9DkZ9ikxdppqFOz3aKcInbA7mxiEHJAdT3ygIgp4cUPTAUg4oegA5gBxADkImBvPozyClEjJUJ3CfRQc9J_zmTM7zt1Q_YUH4rM0IgleC0Nxpr_iwkd62NTGoXwOqCRmZ8awWohBBmIAcTMqBFj2AHEAOIAfRTyX8MWB9DLii3KxZjhaW5v7-zxzKiM_tSCsVK3LTHs7_XwmZINiNINy2NelYEHgh_ODyiGkLZ7OIQT2v1e_O5pSaLNlLMSTETqp7gRzopBYgB5ADyEF0UwlBEwMON5s1EOKq8tUUjk9QaJ4jHbwgDdMb4x4KX9-hEE7-IZ0EGLYJulYpBk0MZrSETwySdD9Yta1mFtN_wwdbvUGpnJfpyHEWTKvUCW8ZHZ2AHJjKARclQg4gB5CDiBQfUkvkoDU4OpMYM6zq58_DvfqN_m5ndlz2OTCb6HnnQxgP9HGaYrBTpGhXDPh7CoIYMOs6Riy3S_JhUNz_YqKiH4UUy-3G15ajKAf6x-IrB2-cUJKDfPTArhz025ODEkHQnSshB5ADiEG0UgnF0_5ajU_7-8M568N42qggzaznPy8SZ5PhLDLzK4LAxYNcHBnGVIIW8TA7Qpr57pGM6e_aR9LAzbWM_v6PQhhxCrwcrDOWg6IgmMhBmKMHkAPIAVIJdiZ5WqS4AExvcp5BCz6f6mf9M66IpywWCt4ZENbJ2mtB0MTATqvnIBQflsIppsUmKQXeinkhbX3vrLls3NGSj5CegBz4KweaIEAOIAcQg_pEDMYDmj_lyZebBRmlA1IjaukAziebLW78_w_zhM2C8DmPUgxOxOBfzgwHaqG8QNJoVpDKjbBU7nnua7DUoOEWP1e5GJ75YCoHvGOhVnIQsboDyAHkAKkEm3LwdQM54GIz1fz5pu5R06r1V86H_9Q_7gD4-AHnEYQw70qobpY1JhsXGf3e3z-WdZ2e4MjV4AjkwFoOLqnJQUVRoqEcRLQoEXIAOQhUKmGE3nw4dB9UOMRtNDk30iK2n4oRVX7Ohs4RUzlYeSkX6OugyiBFEHjBtysIXLj5fnvOUfHheACvA3fSNJODp-lsBdX775F9aUM5GBgej8R9Y4fdHUlzOZCnM7ZWd0mEHEAOIAfh2JUwNh6OyeinJ7KGk_y3qKfB6Lj1BG9Ut6Cdt7BXUTLCAC_0dgXhTsqf2xEDji7weQPjAb0G56kFc6OJDHKqgKNOVj8nOTIulm4xTisMj8VZDgqCoCoHfACTrhwobGeEHEAOIAY1aIlMYhD0iEEpzSZb0qYWFikj0eH_-3sUFTBbKPhY5_7hsUhN4BxBeGx_2peDkfh6fdCWC6wYMAla-M0O5-IdKtzTwOrnrKAjthsMfsbnd6YDfQ1qJgdvOjtfQcpBLXodhEQQIAcFOZhDFyPWyIXfnNl0o1dBD0IZ9IAUkVKQpFPnEpPQQyUpHIHKqYSwRAw0uuhNeJpJcRkv_L86PSyL8sZLPluSFogX6f8-zaJTIh9QFMVJ3kmKQSViEHQx0KSQj2W2OsL7YL9xxOhiakws2Gj8939GEa2JmMoBi0FDQQ6mkhxMldGDM6KR5KCxIAdNJAdNJAfTSA6mkRxMK8jBdJKD6SQH0yl6MIPkYAbJwQySg5kkBzNJDmY2awzIeas4h21IFOe44pxXOheWzJNV82kI1oSFkAPIQT3FIEwRg7K6A4W34FmUA76XFsOnD6bFg1SNPn9TSilvzvUIUZ3IvYwgaBGDsHz2rRZFqPIzteT7FRwdGJXRhjTtTuikvhivUoHqApOtkFyIeWxwFHJQIgdTKXpQJgeUWvBaDoqCADmAHEAM3IvB7PXhFYNigRnVBMxYn_Q8RP4ARQ2Gx6I9mXshCGGJGJSSpYX-5i1J5c-3aFN-eyw_Sw0W__2jVKQ4Oj4BObCSA04t2JWDdRVyoAlCqRzoCYKZHIRAECAHcZaDOkUMwlR8aBYm_unxjKdiwOmGIwPxePtzk2LghfOV87lQpl7eI6Ex27Xg6HpQ1OBQfzyjBkU5eP24vhxwaqFCDvLRA0U5KIseQA4gBxADpBIUyIyOi28eyniWO19xMRerSd1JBCEMxYdmjI6PixeoNqDBqyObiV-czJbVtkAOKuSg0OugXA7y0QNdOTBMLQx4W3cQcEGAHMRRDuqUSgjbrgSlBY7ywg_tdScIfM4Cn9fAC0fcJnY7EYQwphL0SNF2xGc8kMqGQn8ErkuIqxgwexTkIB89aA1WUSLkAHKAiEE0UgnGgjAmfkxFZFOb7U_w3G9_W_dIZK-NClxIN02hp8HD-6Kzi4P7FfDC7uSe0QpXf0hdFRO5sViLQb6WY1w8selied2Brhy42LHgV2ohwIIAOYiTHCCV4OsOhkNUL_AUTfgqhYosBZw3H8yNx35yb6UtetNbrK_ZNxS7CIYnxTAhWqhnxu3b1GsQeLfDfbvTYicd4Tw2jntnMgI1mheEN0-ab2d0smPBrO7Ai-hBQAUBchAXOcCuhJrllL992DpkvLZjBJN6zOWgNDXFxzWryAGfuZEdwz1jJAhPbmlT2rFQFARTOXBRd2A3ehBAQYAcxEEOkEqoKf9k0hpZY2Mn5AByMMmbF3NKchClttq-FAlTuoYFodFGUWItUwthEgTIQdTloI5iMBLTNxzIAeQAclDfCMJTW9vpjIXT3tUd1CK1EDBBgBxEWQ6wKwFyADmAHMRVELZdNq47UO534KBbotvoQUAEAXIQVTnAWQmQA8gB5CDmKQYWhMYVZ8zrDmqRWgihIEAOoigH2JUAOYAcQA5AIYLQIU9ndFN3oFqY6Gl6oc6CADmgcSvJwWy6GJFALvzmzKIbswq6ccugm7qIlIKkmEk3fhF6GCTy4UAqAXIAOYAcBHgXw1ZKMZAcTKXoQSMJQiMJQiMLAslBE8lBE8nBNJKDaSQH00gOppEcMNNJEKaTIExnQSA5mEFyIGlmBuT8J-G5sDA3avNlcf4snVcr5l2V-bpea8kCyEGE5KAOYsDnm_-exGB0jI8nBsz3FbalbejI4VoVOJ8cVZaDqF6DNy4MK8nBnt4R3DMOSFOzqCcpgtBYKQcUPTAThFI5KBOEghwUBUGbEyMkCJCDqMhBHcUgN4rJB3IAOYAcBJuB7Ih4cntHIXpgQw70ogfNNqMHpYKgMw8HURAgB1GQgzqlEiAGwZeDYfp-DtKJfS9RPQg3Z7pvd4rOMkiJJw6k5YE9_HsMDo9BDiAHsRGE-zZedhw9kHLgQ_QgiIIAOQi7HCCVADnQC6NSpfYH7TkpAk0t5j36l25Oin85nRWdmTHIgQ5jBUbHxiSQg7ALwqiMIDS96zJ6YFR7EBFBWLB9BHJw664xkoOcCB2bhy2ZJeWggk2ZcjamJ9mQksyUclBgfSJPS74qFxGDYMtBa2JU3EdSYPdY4CUkCevp9xqDHMhtcNvpQKwXT2XpWOmM-OyOlLhjW0rcT5EX_n7fo9Mhu0mmxkIuByMk-JdSo_J7f_NCjp7trDw6fGvXiOjKjEX2BUCmGHZ0UfTgAgkCycHKghy8106CQHLwfkEOPuDCRJYDre6gTzJjrSYHAwUG5fwo4bmyMHdq82nZHFs69-rMzyrzei3WF8hBWOWgDmIwu2UIEYOAy8GHFC2YuyHp-BhgjiR8j37_4dF4ygFHTzjdsogOxmqwODGRD9h6hE6K3N074loSai0HHekx8Uv6nLdsTYnGZuMjsj9HUvTKuawYqHPqybciRRKEJgU5qBSEohwUBWFwUhC0OTPkggA5CKMc1FEMEDEIrhzsoZP6-PtyKgbFk_-IF09zz4qx2MgBL-47KFJw02b7YjWdJOHXp4dFMjcWeDnIjY6JN1qH5amgdk6CvHN7SkYXRkbHIhlBkHKgHD3oVYsemAlC5VwcQEGAHIRNDpBKgBzocJneBPkt0K0YlL41rrk8Egs5YDFYcXGY7vekK6F6lKIICYeCUAs54N_tB8cyttNNxXuCald-diIrcmPRSzE8taNbXw5M0gtSDuymF0IkCJCDMMkBUgmQA4MdCY_tT3smBhrzNyZEKy3cUZaDMbqvubNnY3PSk2v20J606HcQgvdbDvqGx-Xv5jqqxGmnoxmRHY1eioEFYZqUA5vphbUG6YWQCwLkICxygFQC5MAALpxrtMiPz6Hv9ou70uJre9MyRKy6GP74eDbScnBycFTMaPFWqp49zDUbY4GRAxb7Zw5lPPt8HHn4w9ls5J5b3sXw9M4eS0EwTC94UX8QIEGAHLAc7CQ52MwLcEDZNGzJrE3ZajZmytmQnmR9SsKh1CItiTzNfF4CicFpiEHQ5YBD4g_vS5umB_jNmAvKxkoWi7NDo3LBsAoxz6XtV373QaiXHKRHxuRWT9VCTVWh4v_2LVrsgyIHa9qHlVMJ_Lur_LfTaJ44NTQaWUFoWklysJLk4D2Sg1WcXugsQILwAQnCahKED0kOPuyTzFijRRAGCgzKeVSizauFeVabe8vm48q5Wmc-V1kHvFx3FmyDHARbDuogBrOb82KAVELw5WCIwqGzDXYn8CT_mzPDhpX0nI7455NZizByUkYmoigHzZdzcjE0-zdv2ZIUvz87LPb1jYijA6Pye_unoxm5d93s7y2jv5cZGau7HHCdwV3bU5ZC8DRdW64xOUKfkZtmraStmhxpMtux8c2D0ZwjOMXw9M5eMU1XEPJyUCkIUg4iJgiQg5DLgV9igIhBOOSAFyyjf-OenWmRtFiguFkS7983-125QVLU5ICF6ZF95jl4Xvx6s2M6YfoxsbkrR2_PSdPQ-76-0brLwVopQMa_Jz__XIyptzNliCJGTx0wvkYsSGcTo5F8hvNFipxiaDcUBCkHlYKwtlQQBkMtCJCDghzMoosROOTib8xMulGqoBuqDLrhikgpSMn92UXoZpU05wtoIAbhkoP324xP83u9VS20_V6b-YmAT9EiORYxOeBUidnuhC_sTImMxXPw5_Pm4foXTmTrKgf8Vm8mQBwxWHmJIksmW1Z5e-aDe4zlkSNTke6kSILQSHLQSHLANJEgNJEgNGkRBJKDaSQHebhBUr-YvrYACcJ0kgOG51dJS6Js_tXm5LJ5unIO15nnrdYGL9afGyEHAZQDqy_eBzGYhVRCKOWA96wbvbnu6lFbSM4lzBdnLmKMmhwcGxwzDJnztdvQaf398Jv17SZRFz7HQvV58kMOBnLjcseJ0c_6IgmQyq4D7p9hJEEP0A6IKM8Z-UZJPbI4sVwOXApCiST4JgguJQFyEEI5cCoGZXJQKQbUNhURg2jJgWqtAG-9m7cxXnKw7rJxtGQBLagDw2o_56cnjGs2bqa6g7Ri3YEfcnCMUk5mu1i4lkLl56ToM8w1uD8WU9OoRG4s0s8zRxCeohqEJp3oQbUg9FkLQoUcBFUQIAdBk4Mai4FMJUAMQisHa0wWuV8p1gr0ZscNJ39ta17Uag7eMlmMb9um_sb_Wuuw6VkVqTrKweZO83QRp6RUz19YuiVpuEWWWzHH4rCmkhRDVXrBTBCkHJgLQuk8HZQUA-QgSHJQj4gBpRK4JSrnHYE3qMqBF__WsYERw3_jps0JOjxn1PJnvNmaNc2dv0xbIf28XucSI0py8DQVx3n1b3Ku3ejf4dbCQ8OjSj_nFyczpjsdUjm130dVDnb3qN83Gy3k4E_nhpV-Dkc_5m3Q_xm8U-ZyejQWz3Vq2DiCUJSDEkGQcqAjCJbphYAIAuQgKHLgRZ2BAzEYHsFiXg854G10XvxbCZqw5m4w69iXkkVlRn__EG3Rm7XevOqeBSRqcrCjO2dSqJekhdX6M7NA3GmyTZAL-UbH6icHu-i_NZO-e7nocsR6YedohdHPWUgiJXtoxOTZ7s-MSEHgCIKUg4AIgh_pBchBEOSgxmKg7UqAGNRPDu6h0-4uJN0vurz4mG03y9cMpGjL4wiljiYXAt6Dv5ZOcFy0yXq_fnZkNHJywG-7Zj0OuFgvnTP_3K9a7Fb42YmM8u_jhxy00nU16_7In39Vm3n0IEkC9NW9adNdHXGLPLIg3L-l2_8CRTNBqEH9AeQgBHLgpRjMpj9_h1RC3eWA-axHgrCV9txbtU_mbXtfpm6A36cGPt-hGoK7t6cs_44sWjuT9f161UMOuL3xrSYHVfGi_8uTGXmKoZ6Q7aTIw6z15i2Gt3fn6ioHLIB37zAXR04XGP1Mjozw8d2mAnQyG8tnXIsgNNkRBK8jCD6nFyAH9ZYDL-sMFCIGv0PEIDBywCylojW3gsARAV7wvT546XMkL0O5sUjKAfPbM1nLzoG8HfFdahLE6Zcj_SNiNUVbHqdDrposxIp3PKRyo3WVA-als9Y_l8WRT1tkyeSmWvt6c7KHw7ItSYsWyklxuH8kts95McVguYPBwwhCDesPIAf1lAMn6QQ7vQwqIgYoPqwNHE62swjzW_zpQXc1CB0UJr99m3dHNnMVOi8Stbhe9ZIDTi0s2Kh2PRrp9-NIS0Oz-pHX3CuhnnLAEY6Xz2WVf2eOEPBnnNqsegJlSkZg4vysF4sUK-oPHG1xrGUPBMhBuOXASTpBr8mRJgaIGNSGTZ0jthfj27YmSRDcvYUdore4ORvcHz3cRAvhKtrmNlqj61UvORgdzS-eXkdcVEL2fsvBaGEXyrQWfz4bC8Sunhye99IUg0KBouUWRyddFCEHEZODGtUZoPiw9mTpWj-w2_5bPFd-uxUEDn3fsd15BGEB9TvgBkGjNXwjrJcc5L-rUdP2wF5EYLZ1DddUDjQxUD1F0glcuzKKZ90yxRDm-gPIQVjkwEE6AamE-sFhfs5NT7U5Qd9KEYTjLrcOdtG_zTsYmlqStt4E79uVci0nYZMDpj01Kusr_FpIWfr4M9ZCDvyOGGhbNLkpEJ5zkxSDX_UHXm5vhByETw68SCcglVB_WMp4olbN4RbD0ZQHP-pSEPjfPkkL_bcPpS3_PT7Sl0PEuTpJZL3lgOG9-t87al6Z7wYu7jMTLy_kgMXgV6cytu83VfjaPLo_o9wgKvYpBtUOil4d0mQ3egA5sB63kBzMpItRM6QAGDODvtQy6Isvg24KiZSCPNPp5pEUjgXFroRgwBP2r0_bFwTexXDUg-ZDvJhY_Vu8Xa2e1ygIciC3N1KK4W3amcBnIvghCVx4ykdA-yEHWsTALzGYsz6_rTWZgxioCMKTu_pEIwlCIwlCI0cQSA6aSA4kJAhNLAgkBxKSg2kkB9M0QSAxkNA8LinM7aXzfXENqFwbKtYOq7XGbJ2CHARMDqrEoFIOtJuiRA5KxYD3XqOPQbDgim6ngnCwLwc5qJEcFCd3iiK8Q5LwFUqzzKbnqjI11ETP2c3UI4GPZObW0jPWq3-nnGrSe_N2IwdOUgn833IDo2kW133u-vzZGmeHRlBjYDPFwILQJOWgQhCkHJQIwtpJQdCiB5aCULoOmMiBpSCYrFPzIQc1lgMfowZSDMjuETEIYophVB72Y1cQ5JZCF1XhkAN33xm3pz5Pvx9v6-RreWJwVHTTeRXaFj4u3NxG_QHsCMLXqeNgZcthp3LgpPiQ76n1dLYHf75Oqk9ZT1suf30qK56j--AbB1LiB5ReYenZT70dkEJwkarKlgpCQQ7KBMEkeqAJQh2jB5CDAMmBm6gBNzKBGARfEJxEELigzakgQA5qkDoiQeATDlUFgfsOfPNguqxHgBM5cBoxYDEYNUmD4Vn1uAZhd38ooweQgyDLgWLUYBa9CSCVEK4UQ4ODivc9DgQBclC74lMWhKYW9eI-7tCoPbN25YAXcT5VcZqNHSksBiwTEIA67GIgQWj6oEtHEPqsBcHv6AHkIORyYBA1YDH4PSIGsShS5OgQh7AhB2ORiCBwLcOLhfMb7MgBt8t-mcRgqo17Z3YhlQAxqG8EgQXBsDjRx-gB5CDocuBxSoEXi99T73SIQXgjCHYb1czfaE8QIAf1iSCoCgK3Wf7z-ax4vVVNDvjAJxaDpmbvUgmgdoLw9J4B3eiB450LPqYWIAdBlQOLlMLMDSnx4ql8WHKU3iRA-ODufC-dtZ9iYEE43JdT-jd2dQ8ryUE9r8PZoZyyHIThe83R97rqkvoCzpP9w3vVmjA9ui9lWww4lTCC5y0Q8BHYX9_RV5faA8hBFOVAJ2rAFc-85xgPXLjJFQShyWaKYTa9mbZcHhYjIyOQgwAyUhAEO7sY_GjdLO8RPGeB4vzQsFjS3FPz6AHkIIRyoJxSKNwMszemZGgZD1o0GKYF3okgaJM_5CDYEYR6CAJHDCAGARVH3rV0POF_YaKd1ALkoI5y4GHU4PM707T_egQPWsQmjPcu2RcEXlTNFgHIQf0jCHb7ILhlroI0gvpyuj8rZq_pCXRhIuQghHLAXdbwRhBNQXCUYqDFYNUl_bwy5CAoKQbqc78-6bsYaAWrmB-CzVA2Jxas7Q50agFyEEI5eCqkkyRQTzHYLVLknSurLmYhBwEWP36b9_O0RI5OIN0YDlIU-V24rtu71ALkIHpyYLfegPnK7pTIoBgRRYo62-IqIwiQg2DBgsC1In5FDPD8hIO2RE7M-dBFx0TtPB09QXDaEAlyEHA5UGh8tHBTKn8QCh4yFClWvj3SxLGct66NQA6CHEHwUhAQMQgfH1ygQ68-8Pi0RrfRA8iBvhzMoIvhK1IEqplOX1YZ9GUWoS-5iBQDuqHoZmB-cDQt3zDxoEVfEKY6LVIcgRxEPcXAYqCXTgLBpT-TE_ds7hVTqRlSI8mBhOSgkeSgkVMLJAYSkoMmEoMmKQiDYhqJgaQlvxtFWwu0taFsvShdRyrWGKO1qHLNghzUQg6MvgwXcsBvHmvbUZFc7_B_e2pEhvJ_fiIjD9PhBezbh9Oyb_7GjmHRlR6x7EXgR5Eiv1G8RRGEnV2Qg6imGJBKCGdx6s8PD8iDmIzkoCgItZaDCkGYBzkIgRxoX36JHPDNwYfxbKXJARGE2kvBnp5hKQNz1udP2TM6XGf-xoR4_lhaLoAjNU4xsCA8tT8FOYhgigGphHAWIf75FKUF3u8gMcjLQVEQjOSgIAhSDjRBMJKDUkGAHIRbDqrEwKYc8I0yk_58bF9SNLdnZZEL9-8eyDqDw12uSA8bMhwRgblA15h3i0y1eSYCL37_dMR5fwq7LXntADmoL3vp_As7goCIQTVpeq7M5h_XOJwT-9I50To4LJafGRL3bOwWU9-7LKau0uSgU0cOeq3lQBMEh3KgmlqAHARJDkq_UB05KBODghxIZLHKoLhx_aC4edOguEVj4wDRL7l5Q5-4eX2vuLmlp0C3WNbclWddp1i2tkOydO1lsXRNu1j6YVuBS-Km1RfFTR9cyPN-K3FesmQV8d65PCvPisUrz-R593Sed05Jbnr3lDjSnQz9BLSPJvFlW5yHgTnCwD30u9POBcFJBAFyEJ0UA8RAn-XHOsXCNw9X89YRyaK3jk6y_JhYtOJ4nrdPSBa_fTJPYc4qzmGFOY3nt-Jct-p8cQ6U86E2N9I8yfNlce6keXTJh-1i1qo2koJ20bCy3Vs5MEstVK4lkIMIyYFJvYGeHDAyP6XdUHRzaSEqecPxzUc3oRbKkjcm36R0s_JNm6dNNLx7STS8c1FMeftCgVYxZcV5ccPyc-KGt84WOCNuePO0uP6NU3leP0mcENe_lue6V4-L6_58rMBRMeXVo-JARyLUk8_ZwZxM43ixGH-VzsVIOYwgOC1ShBwEPVU1Ip6g5mZW1-A9FB_q8qdDl8U1f9hbYN8kf9yf56UDkmtfPljgUJ4_Hc7zyhE5V-UpzF00j2lzmpzfeJ4rzHk8_8l5UJsTaX7keVLOl4W5k-dROZ_yvLpSTw66_JMDD4oSIQeQA105mBSEEjkoCkJeDoqCoMnB6yVyUBQEloNjoZaDnsyouGubt3vTuQ4h57BQ0WmRIuQg2IVq_PmsrgHvPoEMKMpBhRhMykG1GFTJQWEOmxSDSTnQ5r6iHBTmRsgB5CDUcjApCJNyMCkIxnJQHj04Ux49kHJgHD0Iuxz85Fja8zw_L4Y7XUz0TosUIQfm0pWkiM4QHavLcHRnBHIQMjnwMWpQKgfaHFgaNSiRA20OLZUDba4tlYPq3QqQA8hBUOSgLHpQkIOy1EKrvdSCTvQgzHLQSfUBsy1ywYso3fAcFRu-ci4rfn86Iyf52Qr54wf3cKfLEVeC8Np59ymGOMsBR2_OUEMx_u4e25cSt25N0cFFCTGPuH1bSv7fXjmbkYWouZERyEFY5MCGGEzKwTHLqIF1SqG1PGpQKQfanKu6lRFyADmoiRyUpRY6ndUd2I0eEFNeOy4OdIazIPH3ZzLGBYbEs9TTgN8yK_9eB_U_-NahtOkZCbyof4f-_s-oR4JTXjieFjdvSUIOHEQJdlOB6cP70qKxWa01Ncvc4T5_DjWCHLiUg8Md1WJgJAeq6QTFqIGzlEJFMSLkAHLgqRxoglAqB5ogrCkVhF7LugM30QMrQWA52E-RA24GFCYyFFb-wg7jReuh3Ul5uprR30_Q_497IdTqqF6nPEeCUs_rbEcOvPj3kvS9sPTNcHBK4izqL_Bbig5x-sHLa5CjCJKqHITtOaoFL8vIgY2ogZ4YqKQT3EQN7NQb6DVBghxADozkwDB6YFMOLFMLTmsPdNILTFjl4PzQsDztUL9mICGO9OUsf0ZHMmf4M4IC920YzsVDDrpSOfFVigA0uEjF8N99kMRwMAs5CJwceFpnYJROsFNrYCeloCgHKk2QIAfx6nNglVookwOl1IKd6IFiekFHEKbQn2GUg-3UethoEbl7e1LWC1hO-gSHo4MsBxwyf5mKG-slCLWSg_RwTjy6z7tIzncp4pIehhwERg5qUmegmE6wEzWwSCkot09GnwPIgZ26A2fRA8X0gqIgNLxxMpRysLrN-FyCR6lQLaf4c7gzYtBTCywIf6RQez0EoVZy8M6FrGkNiF24ZiR_AibkILhyUAsxME4nuIoa2K03gBzEsH2y3aJE3dSCQvRANb1gUxDCKgdr2o3l4PM7UiKruJB-51Dw5UBb7H51MiNrLaImB-fo3-AdCF5fs3m0K-XMYA5yEBQ5UK4z8EEMjNIJdqMGPtcbQA4ieiqjb9EDo_SCB4LQQH-GUQ54IjZ60-Q6gpMD1hM1v4nfvT0VCjlguHL_V6dqG0HwWw44wvPT4xnLn8_fKX9XX9iZkt0wVaMMPzqWhhwEQQ68EgOjnQlmBYhKYuB91ACnMvo8bo6iHJjVHvghCKU7GCoEoYH-5zDKQRsVE3J1utFE_W2KCGSGzd8auWhxanMiNHKgCcKb52tXg-C3HPTQ4TeLNxv__Gn0b7M8nKMDcrgugSNCfXSA2bvUqljlLA0WiUQ2Bzmoqxx02CtAtBsxMBCD6m2LNsTAy6gB5MA_OZhOF8M3pAToM42-rDLoyyyDvugiUgxSooluiiJ0o0joxpFIORgUjXRjNdJNJllToHATTqUbcirdmBK6URvops3TmYdu6AaWA2IK3ehT6IaX0AMwheWAuIEeihvoAZGURBCup4foenqgJPSAXUcP2pSQygEvEg_uSZuG4Z-nRcWoMLGVdjt43Xa5doKQzKcYcuGXg3VtxrUGLELrL2cN60dYLL5uUVDKP3t_zzDkIABycA2JwTUkBRKSgmu0qAFJQZ5j4lqSAuY63mr9usZJOVcxPG8V57CSVIKc57Q5j8WgMBfKeVGbI_mFqjB3ynlUm1MLcyzPt8W5t0QMinO0NmdLOcjP5cW5XZvrmcL8r60JZetE5RpSscaYrUel69ZcyEHA5KBSEHTkoEwQzOTAhiBMSoJdQbhgIAhni4LQQH-GUQ6YN89nLBeGx6k4kVsh89Y23kPfS2-d71_Kilss3jr5jfXXFMJ_7bz_vEqRgLu2Jx0Jgt8RBL_l4BcnjL_Dr9HCb_X5OII026Je4dVzGchBveVAisFBm2Jw0kAMzhqIwQVHYqDNsapiYCoHlWJgJgc66wvkIEhyYCIIlnLgNnrgqSC0TwpCQRKqBKEgCaWC0LCc5IA6JIZx0uHw8vyNCnvfCw1y-L9VWeiYZw5xf4FczT4L7_G_26YgTC3UIPhZpOi3HDyxP2XYr2Bjh9puA6uahR8dTUMOAiEHk1EDrc7AVcRAm9MqIwbaHFiQglqLgdOogakcVKxZkIM6y4HT1IJu9MAvQShIglNBCLMcMNxNr8HjuoH5G5Pi9GCu5p_l_JB9QWj0WRD8loOvGaQFuACRUz8qP2PDZfNtkG67TEIOvJADEzEoyIGeGEgp0IsYlInBBWsx0OZKv8TAbtTARUoBchBUOTBLLdiMHgRBEBqWnw21HAxRuuD-3SlPtwy-fSFbt8_DgrB0SzIwuxj8lgOjxkczKNJzflBtsTXbucL8AJGDYMiBSTpBisFrZhED_8WgKAdWYqAaNfAppQA5CIscqEQPfBOELnNBKKlDMBKEhhXnQi0HTCeF5G_bmvSo0K--7YqdRhCKfRBy4ZKDHx5NG6aCuNGVys9YQTJn9rv96VwWclBPOTjcaSOd4EIMSuoL9MWgy33EwAsxgBxEQw7cRA9U0wvKguDDToYoyIFcxCgNcO_OlKsuhJyiyOSC8XmcpRi838Xgtxy81Wq8sHNEyKqZFYvc10x2LLBk7OjCboW6y0FV1EAvnWBPDBzvSNDEoDRa4FQMVNIJbqMGkIMQy4Hd6EFQBIEetKlvn4-EHMgCRdra9nOqfueDl-wsqrduTYk1bdm6Rwy8STEkxYseRj_8loMTAzm5M8RoYed-BmatsPfQgtxk8vtxISrfF5CD-suBajrBuvjQm62KXoqBr1EDyEEd5cBJaiEigjD1nVaSg1SkJqMLtKBxeuBO6mOg1-Qov3shSaf3pURze9Z1k5ygRRD48_3kmDcHD_ktBxwZeHhvyjSi88OjGbllMZcrP9r5PRIHq7bL3zqUcX0NIAdey4Fx1KCyADFyYuBBSgFyECA5cBQ9sCsIdncxeNQLIYpyoMGL_qG-YXmoz29PZ-QbNZ9wyNXtFxM5yw6KYRaEqRRBYEFwG0GoxdkKWzrN3_7zO0gS4h8PpsU_n0jLlsifo_MzrLpb8o6Hg73DkIMAyIFh1OB1o6iBRR8DFz0MaiYGPkUNIAdhkINaCkKFJHghCHk5uBBZOYgS3ekR8UWbNRUcQeAeAG4iCLWQAxYYjg54uR2VP_tvTqWVT-e0ksz7FHbEQA4M5OBIl3rUQK_OwA8xqJQCs-2KPooB5CACcqAcPTBJLzgWBB-7KWpykMvlQMC5lBi2XXSppRhSWWf_5hnaTqgiB0_td3cPdaeGLbtW2oGLFAcz7q85n-fwHB3rrXLQ086uLO5THarkQDVqYNTgyKeuh67EwI90AuQgIHLgZfTAaQTBizoEo62OlYJQ2BPc-O5FyEHIBOHWLc5qELLDwZUD5lBv1nYBph63U61JO10nL8SAU1GqJ0By_Qru0Wpe4rSCjaiBXjpBr49BtRj4uCPBTcTA46gB5CCAcuCbIHhdh6AaQWA5WHkJchCTCMLzR1O2Iwi1lAP52agr4iNUoOik6yU3g_oOdUPkA5nc_h6JjHrEQGv3vKUjg_vTSg4sdyiopBMUIwZ2xUAhWmApBl6mEyAHMZKDgAhCaaMkyEF8BIGLFO0KQq3lgBnKDIvfnU6LBRvVPxsf37yWtqOms8M1TSVo3Ez_PnfrxL2pKAdWYqAXNdBrcBR0MYAcREQO6iAIypJQmWZwIghVnRRJDt5rE_u70pjEYpJi0ARBNcVQDzlg-NCrNvp8X91j_W9ztIAPrvLi37WbSmAWbkqKvT3DuCdN5eCYPTmwqjOo6HxoVwzs7EawrC-osRhADgIsB7YFwSLF4Fk_BIMiRbP6A8gBihSDKAcaLxyz3kLIx1578W_ZTSVoYnCyH2KgJAem3RDVogamdQZGxYcqhYeqRYeqNQZuxQByoCYH0-hi1Ay58FvTRF-yIXRj6EI3UhVSCiZppBuyDLpZJXQDF5FyMCim0k0uoRtfQg_CVCkHfaKBHpAGelAkJYIwhR6oKfRgTaEH7AZ62Ka-1w45iLEgWEUQ4iIHTlIJLAZ7EDFQkoNrtHMUSA6ufZ0pyAGJwXUUNbiOty_yYXAkBteTGFzPtQa83ZrE4AYSA56rGJ635PzF85gmBzy_aXOdjBrk50A5H2pzY2Gu1ObOsvlUm2OZkrm3cm7Wnb-N5nqT9UF1jTFbpyAH9ZADG4LgSBL0bjArSSi9eV0KgvZQaYIAOYgGPSlngvBjSjFwjj_OcjBAEYNnD9m7drduSSBioCoHtJXRCzmwLQZrLMTAjRQ4EAM764rVGgU5qJcc-C0ITqIIqoKwZlIQpBxogsAPUakgFOSgcdVlyEGsixQT4rnDKRlSj6MccGrlGwdSSCX4LAfa6Yt5MaiWAyUxKJGDohhoclCY62oiBg6iBZCDmMqBpSB4JQleCEJFegFyAEHQBCE9HC85YDHg1ApSCTWSA4-iBsUaKisxKMiBihi4kgKv0giKYgA5qLcc1FIQFCTBtA6hVBDspBdYDt7vgBwgxSD36X-Pqv4rIwhRlQNOJdiNGCCVUAs5sK410IsaGNUZGIqBUbTAQynwSwwgB0GQAz8Ewa4k2BAEo_oDs-gB5CC6EYQv7bJfg1ApCFGUgyT1QkAqIWBy4LTWoDJqoFeAqBox8FAK_BQDyEFQ5MCBIHguCU4FwSK9UJSDbnR2iyIdyWFHxz2zIGgphqjJAYvB8zZTCTdtpj4G3WiN7LkcOE0p6BUhGtUZOIkY1FoKbIoB5CBIcuBQEJQlQXVng1ENgt30QqkcfNAJOYgwnSQID-62n2LQihSjJAdOUglLSQz4yGzcSy7lQLHewG7UwGk6wVa0QHEOd7pGOFmPIAdBkgMXguBKEjwUBL3aA8gBBMGsSPFEfzTkwMmuBBYDPgwK91Ct5MCbqIFnYuC3FDgUA8hBEOXApSB4JgkKglCVXjCIHkAO4gG_NTuJIDywmyZThUOQgiwHWsTAzmfndAwiBjWSAycpBb2ogUI6QSmNUAspcCEGkIMyORgRgWNTzjVNUhZM2JgtZ0Mmz_p0kUYpCQS93fGpdJJ1Q5KpLAlrB0pqD_rpoSJBWE2C8AE9YO93kxx0Qw4QQXBNUOXASfEhUgl-ycFJglsmkxi8yWJAEYPlJAYr8mJw_dskBu-QGLyriQFJwSpOJ3TJuUrC8xbPX1IMSuqqeJ6TEYP83FecC3leLMyRpfNmcS6tnGMt5mQv5n0v1p-520YhB4GVAw8lwVQU9OSgRBDK5KBCEKQclAqCJgclggA5gCBEVQ6SlEqwW3yIVEKd5eBdbfvi5Wo5KBGDMjng-a1UDkrFQE8OSudRBTHwao73ct2BHIRBDjyWBF1RcCIIitGDOMoB558vDg2LI7QI7OmmP_uy9L9n6f8-jBRDROSgl3o9PLbPXsQAqYQaycGb-XRCpRyURg2s5ECKwYcmUQOXYuDlfO7HegM5CJMc-J1yMEkvKMlBZfSgIAdNlL_bR30OhoeHI00qkxX7SIJ-ejwtbt-WFLPW598uphbeMmZSXvIu-r_zWQOHSRoy2Wykr0dHIuupILAc-PF7qsjBq-fK79_B9LB4dJ-9z8YRg3ODw5F_DurBpByURg0q5eCifTnQixqoyoHfYuDzGgM5CKsceCQKjgVBJbUQIzk4PTAsHtlLR622qC0UM-i_4zz18b7oC4LdPghBlwMWA95lMcVBKgELeYDlgMXASA4Kc5xlrYHfYlDDdQVyEAU5cCkLnkYPCnJQKghRloMMpQn-cDotF3snC95Mqm5-nRaedBYRhDDIQU8yG6mIwSCdlrmPmi_9-WxaPE8Rre_SyZHfI_H5l5Npsb49I9oTWXmPh0IOquoNSuRApRjRy5SCgRwEWQYgB3GQAwfSYEsQbNYdRFUOeNJ86Uxapg3cLHp8HX91EoIQdDngY6ediEEQIwadJDl8z926Nf8cG3WynLcxIZ45wGmwTKAlwVQOKncq2EkpKMiBJ2IQwPUCchAnOTCRBMP0gp3UgkHdQRTlIEsT5bsXMpRG8CZkztfx9XNppBgCKgccHXruUPjFgBf49y9mxM1b7H0PHBn70dG06ExkQyUHutsY3ciBnZSCSjohwOsE5IDGsrjJgYogeFh3EEU54FAsFxx6WY0_g1IMxyJeg8DbHB_ZlwqdHMy2-V0HMZUwRLUSP6LUgZtIF58BcSSAkZCXj3T7Iwdu6g1CLAbMHMhBXg6a6GLEDikEkzTSTVyEbu4idMNLpBxQGJ0eiKn0cEi4Gp8emgZ6eCT0MDWsYTHoF1PoIZtCD1sjdRqLkhxkia_u8a_RT73Dt7zd8gRJygcX0-Llsxnxm1Mp8Sf6c11bRrQOUg7a5c_vSdoPz9dbDqbYOnY5KQ73ZAJ1zyZpJ833D9vbdmkmPkd6MwGLHHSLa0gOriE5uJbk4FqSg2s5rUBycB3JwXUFObie5OD6ghzcQHJwA8nBDfIMmG4xheSA56spssdBf34e4_msMLfxPCfnO23u43lQCkJ6co4snTdL5tPKuTYM6wPkAHKgLwg25aAoCDGQA35zalA8gvcJWtB46-ITtBgu3GT9d3i3w66u-ky83IfhBdqGuWyL8SLSRN_5bZSn5oI1zls7fot1kL8Pgxx8cUdSXsegpcB-cTztiRhoLCMB6kmFUA6oGNGJHGhzXKUcaHMi5AByEM_oAeSgjBdPpCzrB35OkzFXemdLog2cc_8h5W2bLOoUWCZqGm6mhfoVqlZfsNHeAsF561UX0sXP6HcEIehycOuWRODEgDnVn1XeTcPPs2odzS9OOP_uIy8HIRcDyAHkwLkcaIIQMzngRkdf2mk8efLbGb9VGzU4She2Ppq9xXGjpFo1SOqnPPTj-5znoVmEfk2fN5lxLgj8th12OWAxOBHAehFOUakUUt5FhaK8nXF_T1YcpZTBh5cy4qE9XFdk_N3MoTqMMwPZ6MgBi4GqHJSmFCAHkAPIAeSgm0Lpc02K07ixkdWWRJ6wH95rPGFzoeNA2v_P0uXRGQgN8nMnZQTCye_BjaBUmkcFVQ6m0gLKTZG4NiNo8Nu9WdSAvzvehTCUzurW1my4TPf7BuO__6MaR7kgB5ADyEFU5ICIkhzwmxLvKjB6i97dpfYmtfFyxjB6wD-n3ectY1x0-I8HvSuq5M_yi-POForTdE2nK4Syw1KQGCaeOZgWiXTWtF5hRWvGtDixPw05gBxADiAHMZcDs4WMCxBVi7TaKDdttD2O5eCSz7nr96hHg9vmTXrdHk_2ZyEHIWG2YlpgkA7RmrfBuED1YAC2NkIOIAeQA8hBXTk3aFzctdjGWxQ3kzGbcDt8jBxwy1z-Xf1YcPiUQrtbMSEH9YFrTbKKux3u22X8_XAzMMgB5AByADmItRz0prJU1Z80fHM-rVigdYj2wRu1reU3Oqf5exWa2zKWC8dtW_OFlWva8sVpnDK4RaGrHqdcDtnc419POeDaDtWCyKjBp4eqXqeHdhtfoz-eTkMOIAeQA8hBvOWA34rNGiD99Jj19i7-_3_3kPFk-3larPzaIsY_97F9SdOtbEYFaoP0f_vZceuzJF44ng6FHHB05oFdydjWG_ABS6r1Kcu2QA4gB3GVg028UMaQjbkyGjcOT7Ihm2d9Jk9LWjK1JZWHqrSn8oKydkjSsJbkYM1A_sFaTXKwuk_SuDpafQ5ePpM23WmwvTNj2XrZbDF88YR_16qP3pQXbTL-t7-6x3zXAS8UPz5qHob_ws6UrS6K9ZADTv_EWQzyEpqS36d1lMu86Vdg0gqvFeTgdZKDN0gO3uSDl0gOlpMcrCA5WMEdEkkO3iU5eJfkYCXJwXskB6tIDlZxn4PePKv75PyVF4QBOa9pc9zUwu4UbQ7U5sTiHKnNmSXzaOUcG5a1Yc5WyAHkAHJgs-5g2LRRDDcT2tlVfdQtv7Vvol0KN202f3Pf2uHfZJvfNpg03HGwReHf7qLtnDdvMV4sbqS0C2-TDKocsBjwltM4i4GsbaF7rbk9Y5l2-ZrJtltOje3ryUIOIAeQA8gB5IAXeattgDxpcrOk355Ki5X0ZvU7Cr3ev1ut2VD-0B5_Jtydncb1BjOp0JIr01V-Dh_3a9YC2s7vX0s5iHsqQe87f5WaH-lFEM7S92J1hghL4kBQtjJCDiAHkAPIQRD6HZiF593CRxv7IQg7TOSAz35QrXVYfSljIgdJubAETQ4GHKQSuFvlv5DgBbHBkVITJCokndli3TKZ00nLqZ_BXkp5bbmclvJ3o0I77Z8cC8ZzDTmAHEAOQioHeztTkZIDZi1V8hs1RPICXpic9A0wg_ekG7XE5S2aqlsx37tgHDngjnp2mjjVQg4GHKQS8oIW_gLaHx5J2ej2mFA-oGkm1dcE5XRGyAHkAHIQYjnI0nkBUYLPP1h7Ka18qI0TuLHScZqAvfqdLw9lZE983ZoDWhjWtqWVPvcPTBYcXlSTafXf6VR_RlkOnH5muxED_gz896Jwn7bT55i3wft785cnUvJeCMJnhBxADiAHkIPACUILLahLN_sXQeBDfbwShKTFwVH3UAU7nx9h9jNO02JuFnLmN1U7v5OfctCXyjoSA06LROk-feO8tx0x-UROq_sEcgA5gBxADmIrBxo9NFHyFkSuQ2hoTiq1rm1othdB2NXpjSC8csZ8K-JXaDFtN3hrPkkLOac7zOoNdtv8Pf2SAz4--b5d9lIJLBJRiRiUks5QUSzVTjR6IAg3UW3K0Z50oD4f5AByADmAHAQ3ipDJh7BXXczI_DYfg8tv2BzGn09h3TtoUX2GdjksP58WF6jYkP8bO4Iwn7YIHuxxv3BdHKRaiZYh03-LuyHyEb68CPB_z5GLN85lLIswH6BOeol0tu5y0JXI2O58GKVUgm7UKJ0RfyIxbHIhCBwhO9IbvGsEOYAcQA4gB6FJNwylMjKs3ZvMiH76k7sOZipC3naL5Pitza0gsMTwEcMq_x5HArjwjLcnWkVEtB4Ndn8fr-WAxeCRvUgl6EcQsrIdtlmfCqOeCE_TvXpuIJjyBDmAHPgmB410MWKJFIJJptLNXIRucgnd9BIpBmnRQA-FhB4SrmxukGJAb6L0EE2hh2kKt06mh-sGesiYqTGUA1WG0hnbEYR5G90LQictoJ_b4W0jIBYOJwVqXsoBpxLsRgwepP4TUY4Y6MGtsN86lxL3Uv3JtBbjY7g58vVtinYdoQgSi0VQPw_LwWcKcnANycE1JAfXkBxcS3JwLcnBtSQH15EcXEdycB3JwfUkB9eTHFxPcnA9ycENJAc3kBhI5EtNf34e4_mMX3oKc5yc73jeK8yB2pxYnCO1ObNkHq2cY8OyNsyGHHziE0shB5CDOsIRhGcPpZS3kGkRhL1d7vK-R3q4W6M3YsDphO6EswXWKzlAKsE-CZLTE30Zmer6BZ2JwYLHO1F-ezIlNlM3zw66NlyvEPTPATmAHEAOIAeRZIDe5L57KGV7m-OuDneCwBGIhS6bOX19j7sF1gs5QCoh3kAOIAeQA8hBdAvGMnlBsBNB4F0PbncxcHHkNym1YbdQjZsd_Z7aQtstQPRaDpykEuIeMYAcQA4gB5ADyEEIIwi13ubIleybqG3ul3clLeWEt8M9TQs1b2v0ogmOGzlwmkpAxAByADmAHEAOIAehIkURhB8fTdlqWsMRhJUX3O8957fwmevN_60ltJ2Nd1949XmdyoGTiMHDexExgBxADiAHkAPIQYh3Mfz4aNKWIPABO1xE5rYHgoocDKTrKwdOIgb833clIAaQA8gB5AByADmIgCCodFwsbZTkRhDCIAdOig9ZDDjSgPsKcgA5gBxADiAHEUgxZMQLx1K2Iwgb29Oy2VHU5MBJKoFFAhEDyAHkAHIAOYAcRG4Xw4sn07ZrEPjESLuCEGQ5eHBPCqkEADmAHEAOIAegUhDsbnNkQYiKHDTY7L-AVALkAHIAOYAcQA5i0Bc_YzuCwItuPoKQCb0cIGIAIAeQA8gB5AAYRBD46F27KYb3L6aVehJEQQ4QMYAcQA4gB5ADyEHs4EX-D6ftHb3L_-0fTlkfjBR2OUDxIeQAcgA5gBxADmK9i8GJIKxsNY8ghFkOkEqAHEAOIAeQA8gBBEEKgr1tjjNa8oIQNTlAKgFyADmAHEAOIAegJMXw-rm0mNZiL4LAfyediYYcIJUAIAeQA8gB5AB4IAi8GL-hIwhhk4Mv7YQYgKz44-EuyAHkAHIQRjnY05GU2-mAP_A2x9fOpmxvc3yd_k46PflzVORg8aYEHbzk3e9-si_tSA4eoBMk2wfT-P4B5AByADmAHAAzQVh-nhfaIeUFtpG-w1dJEFLpcMkBi0FvEt85gBxADiAHkIMIkkznF3Wvfh4v8u-22tvFMI0W5j_Szgf-u2GQA0QMAOQAcgA5gBxEhkQqLXZ3psWvT6TEV3cn6cFLiiW00N6yJSm-QgvePx1OiuZLKdGdcBlBKAiCnQgCf4_PH02J1oFgy8HT-5OiL2YRA_4-B-h6tw6kxfGetDjRmxYX6HsaSkEKIAeQA8gB5CC8UkCTe0tbStxPAmD1Rs_Xc-nmhHj1TMpV2DzlUBB-dCRl-XfqIQd8r_2A5Kk_RmLQlUiLVRdS4pskREupCJRP2-T7h-Gul3duS4rvHUqKTXQCZ9xFAXIAOYAcQA5CRcdQWjy1L2krzM9MpWvLe_cPdKVdCEJafHjJ3i4GbfILnBwQ6y7FI5XACz1Lwc1b8veBSt3IAxSJ2nw55WmKCnIAOYAcQA4gBz5wsDsjJ3g3zX24aREfnFS6o8Au_Pdnr_fuUCPIgX-c6svIFFODg--Fd6p8lyIJXUNpyAHkAHLglRxMpYsRS6QQTNJAN3MRuskldNNLpBikxRR6KCTaW40UgyFxAz1EN9DDdMOHLAb94np6yJiGGMoB54U5PeDFYswL-7YO5xN-2mNBgBz4w9n-tFjm8p7hhev-XYnYFWz-geTgH0gOPkNy8BmSg8-QHHyG5OAakoNrSA6uITm4luTgWpKDa0kOriM5uI7k4DqSg-tIDq4nObiexEAiX2r68_MYz2f80lOY4-R15nmvMAdqc2JxjtTmzJJ5tHKODcvaMAtyADmAHHhfY8Bvf14eQcwRCC5IcysIM1ogB8GsL8iIL3t4z3x9T5LSE2nIAeQAcgA5gBwEhRXUa8BLMdB4cr_7nDIXr83bmIQcBAguHuWiQy_vlQY-hfN0ylU6CnIAOYAcQA4gBx7BOwzu2JZUyg9zmH_eBvqflfsRJMTpvvzWNsewIFx2F0GAHHjL4Z6MUsHqjRsT4vF9KfHswXwB4gyLa8b31qm-NOQAcgA5gBxADurN2ra0aTEZSwGHfHdSv4NL1HDoMuWGD9He9V9R74OFm6yl4vO0g-HJfS6ht9QFGyEHpXAfAd5ZwjsF-E2-VnATLF7srQ7K-uXxFN0v6WInS_57R3sz8vs0a5nNvSsgB5ADyAHkAHJQ5_Dw0ybhYZ7EX6H-Bcm0_iLHsnDntqQvKQkviZIcDCTT4nuHU2LOBj5rIh_NmbMhWTP43zNb3KevT4r3L6RMO2yyWJpFG-JQnAg5gBxADiAHgYUb9NxsUm1-L50gmLDIAe-nvgYzWyAHtZCDQSrY4zfrhubgXmvummlVZ8IdI_k7MbpuzW2QA8gB5AByADmoG9zS1iiXz5P06ovWkzQvBN8_nAq0HPAb9hoPF-p6yAGnDzjK0xDg6zxD1piofebnTO6Znx9PQQ4gB5ADyAHkoF6coYncKEQ8g8LDqsVhLBFBTy1wOHxXRzqUcsBiICMGAb_Gd1GKaTCp9pn_5aSxHHxjfxJyADmAHEAOIAf1bGLTaCAHfJgRH2qk8nO44VHQ5YDhAkovBKGWchAWMWDu2Z6wTENp_PSYsRw8shdyADmAHEAOIAd1PEchQwVgScPFbWObWnj3ndZUKORAiyDsdCkItZIDrjEIeiqhciviZYViQi5w_ew245_zzQOQA8gB5AByADmo65HMX9ppvMhxo5u0wo6HJ_clQiMHWgTBjSDUQg5YDH4ckohB6eFbK85bCyVfe7MdD79AzYG6HBTmLMgB5AByADnwlF-cSJs0MaIwfKf5399OEz3XJ4RJDtwKgt9yoInB1JBdU-Ym2oXA53QYfTYuWDRrusXXbc1F7FaAHEAOIAeQg7pyrCdFC92Q4WTN--nfovbKlblkjihwY6T5G80n-tfOUtOk7trA2yof3pP0vUjRTzlwsiuBPwcvyjVhc8KyOyLfE2_TPdNJTZrShaO4uxNp8WfqmbHAonHWQvo3ON0FOYAcQA4gB5CDOjdCeuZAyrJ1Mp-c9xL1vv-QFrw3zqVlRflMi1MT-cS-wVS2tgcC0cJy786kr0WKfsmBk-JDXrD3kRRx74Bawccsq7TbXkTX9oFd-dbJizepCY9KnwTIAeQAcgA5gBzUgPO0K8Gr45EnewskRUtbfXLHfF6EfPtuVn_zXmtjIfdDDpwUH7IYnOmv_fW9OOj9_cIsoIjDuf74nq1wzVvnyuXg7UuQA8gB5AByUF9epqjA1GavCtPyRWWpdP0mek0QbKUYOtN1kQMnxYcsBge663d9Xzzu7S4K_lncqjvORzZDDiAHkIOAyoEmCHGUgyGtNa8HEz13vxtI1f8zsSBwSNvOm-um9lRN5WAwnaUOk0lbxYf1FgOtBfLX93hXiPoVSj8MpjLxlQOZUnAmB9rcBTmAHEAO3MpBQRAgB9X1B7wVzenxyNym-Jd0qI5qI5wgphj4nIhN7emayMEA1WNww58pIUgl6NGdyIj7drkTBP5euIiUT5iM07NWJgfFegMFOdDE4P0eaznQrjPkAHIAOXAhBx9CDrRdCBxev39XUjnN0FA4pGnL5XQgi8nsphjmywhC2lc5YDF4zuaZFEGIGFTCOxK4QLGpxb4kNNLzyNGqnkT8nrNJOTitKweyGLFSDkqjBpVy8CHkAHIAOYAc1AA-GngDFRQ-Rm-23PmuQWfx47fsr-5OiHdbg5FGsBKE-3d7Iwhu5UATAzspHP59giYGpc20mi-lZP-CBkWZvHsbH-yVktGqOD5fLAfVOxUKcqDtVCiVg8qUAuQAcgA5qJ8cpKmgLu6kaOLvJ1E43pMWmykfv44WgU0kDUfpf--lveupEH2WnkTaVgSB5YdbSFf-nBO9KSU5WHux-u-yRDlJJezvSgX--vKhS7s7UuJnx5Lic9u5nXJSTG8ekmkq_p-_sCMpfnI0KXbQfzOUTMX6ufr9oU5lOdCtN4AcQA4gBw7kQBMEVTmo2LEAOYguLAjcs8HOG3ulIDiVAxaD7x6MphjoiQKnHLgjIp_-2TGYkkWvuAct5KC0x4FdOdDmMsgB5ABy4IEc6GxnhBxEXxDsFily6sSNHPQnnUUMeHHFdxYXOTgPOYAcQA5qIgcFQYAcAC9TDHbloF-xq2AUIgbAphwY7VSw6o7oRg60uRFyADmAHJTIQUmvA0M5-LAPchCXFIPNIkUWBDtyoImBneLDW7ckxUGIQQzk4LS9bYwqclCY08p6HEAOIAeQg6x5UWKFHBh1SYQcIMVgFkH40xk1OXivNWk7lXDrloRoHcD3EnXyuxVsbGO0IQdVDZAKcmCYUoAcQA4gB5AD4D7FMK0lYRkJYNm4bat9MTjWg-8j1nJgcxsj5AByADmohRwUBAFygF0MtYZPh4QYxEwOzOoN7PQ4gBxADiAHFnJg1eugVA5Meh1ADhBBqCWIGEAOdOXATkrBrMeBlRxUzKGQA8hBJOTAs0ZIJakFKQedKAiLI9zYyW6dAMQAeCUHruoN_NjGCDmAHMRaDirqDiAH8aaPGvh856C3RxKj-BBUyYFKSsHNTgXIAeQAcmBTDiy2M06lP7kNLCYxCAIiBsCXrYyHuy3lwFQMIAeQA8iBv3Kgt2NhCv3ZfBE1BxAEf1IMEAPw_N7OKjFw1N9AsRgRcgA5gBxUyoHDHQs_PzqESQx4nmJAKgHwYWX3tlzyLqXgZKcC5ABygO2MNnYslEQPbtnYL08jxGQGvEoxIGIAmNM9CTH17fN5MVCRAxIDx8WIbrcxQg4gB7GRA8WiRP7zN8cH5ZHFmNCA2xQD-hgAec4G7Yb5-qZ211EDz-oNItLjAHIAOfBFDoxSC9PX9Ik3zwyJBAQBuIggIGIAZB-NoZR4bleHuLZUDPxKKUAOIAeQA5uNkKzqDipaKTfSCY3f2T8gzvalRBKSAEGwGUG4a1tCnOzFzpc4M0T3zP72AfHllosFMVCIGjClYuCi3iAODZAgB5AD7-XA4oRGCT2Q01b3iNs39IgHtvWKr27rEQ9t7Sa6xENbOvNs7pA8uIm5nGdje54NbZIH1l8qcDFPy4VJmlsl9687r8O5ctaeNWbNGeAzX1l7Tty2tl0sXXtZ3LS2QyxZ0ykWru0SC9Z1ixvX9oh5a3vFnHV9Yi79-aU153DNgo7Z81T57EnKn0_t2S17ngvP-L3NF8XiVefFdW9p2xZ1dii4TSlY1BtADiAH2LGgumNBpe6gVBD4oeSHkx5SWRzEDy1TMHx-oOWDzQ84PejyoddChrLoKD8p8L5m2RWN4UNX-Dx34h9eO5nn1RN5_ny8hGNF_v6Vo-X8CQSRf5DfzbECuB6hpOJZK30Oy55P7ZktPMPaM108VKnQBbGqp4FV1MDmFkZbxYgR3KkAOYAc2JMDj1ILxaYjlYLAD3ClHJgIgjZJlAuCfUkwlAUAgGdUP3PGUuBKDFSjBla7FOymFCAHkAPIgfPUgmn0gB9iPUHgh95WBOF0ycSiIwlVomAsDAAArziuLwS6kYJTk89yiRhUpRJKxcAoneBh1AByADmAHCjKgVm_A6NdC2VyYBY9sCsIqpJQKQqGsgAA8IzKZ043SqAvBXbFwDRqYFGIqJRSgBxADiAHNusOFFILlrUHldEDS0EwiCKoSIKRLAAAvEXnuTOVAr1ogZUY6KUTnBQiqqYU7BQjQg4gB5GWA7epBdXaAxVB0CRBTxAMIwmnKyYkE2EAAHiG3nNX9lxWRAoMxaB0V4KZGKjWGviQUoAcQA4iKweOUgtWDZEsogfKglBapGgVRaiUhCpRMBYGAICX6Dx3FVECUynQ266oKgZWtQYKjY9c1xtADiAHsak78CJ6oJdeMBIEnV0MKpJgKgqmwgAA8Ay9505XCCykwEwMKuoMfI0aRLjeAHIAOfA-taAaPbBKL1gJgi1JOF8x8VTIgpk0AADco_O8VT6TZc-rDSkwihgoFyGaRQ1imlKAHEAO3MuBYvRAOb1gIQhmaQZDSagShfM6E5OBMAAAXKP3vFU9k6XPa6UUqEQLVMVAJ53gZdQAcgA5QN2BlRzo7VxQSS-oCoKeJOhFEipFQVcWjKUBAOAGg2et8pkseV6rhMAoWmBDDCzTCX6mFCAHkINIy4FX0QMbgqC7zdFMEgxEoUoW9IRBSR4AAEqYPV8Vz6JuhKBUCKykICBiADmAHEAODOTAUXpBVRBUJMFMFCpkQVcYzDCb7ACIMzaeo8pnsOoZ1RECZSlQFQPFOgPIAeQAcuBH9MCtINiRBCNRqJQFPWEwEAcAgEcYPXOVz2bFs1slBKpSoCAGhnUGiBpADiAHdYge6G1vrBAE3V0MJYKgKwkGoqAkCyriAABwhtnzZiQDRkKgJwVeiIFZOgFyADmAHCjKgZfpBStBcCIJJqJgKAx2BAIAYI3FM6b3XBoKgRMpsBIDlToDG2IAOYAcxE4OLKMHLgTBrEixTBD0JMFIFKxkwUQaAAA1QO95NJABQyHQkQIrMSidd1zVGdiJGkAOIAeIHpjIgYogaA-xS0moEgU9WTASBgCA_-g9jxXPbOUz7VgKnIpBjKMGkAPIQW2jB3YiCDqCYCgJlaKgIgtm0gAA8A-DZ9FUBkqFwEgKTNIItncmxDxqADmAHLiTA68EwawOQUUSKkVBQRaUxAEA4Clmz6GpDFQIgaUUmNUXeCwGkAPIQezlwHb0wK0gGEmCXVHQkwVFcQAAeIjZc6j33KoIgYEUeCIGbqMGkAPIAaIHLgTBKs2gKAm6omAkC6riAADwDqtnUef5rXzGbUmBRR8Dt2IQ1agB5ABy4E30wGtBUJWESlHQkQVDYVAVBwCAt5g8j3rPb9UzXjkHWElBvcQAcgA5QPTAf0GokgQVUTCQBSVpAAD4htlzqfscVz7rZlJQIzGIctQAcgA58DZ64EYQ3EiCnigYyYKCNAAAaoDZ86n3PFcIgVMpqIkYQA4gB3GVA-X0goUgmEYRKiVBVRSMZEFVHAAA_mP1jBo826ZCUCkFZtECFTFwmk6AHEAOED1wKAh2ogh6kqAjCqayoCoNAIDaYvHM6j3nuvOBiRRYRgtsiEEcogaQA8iBP9EDr6IIqpJgIArKwgCZAKBmi71tETATArtS4CJaEKeoAeQAchAMQVCRBD1RMJMFC2HwVCAAAO4WflUR0JMBHSFQkgKIAeQAcuCvHPglCLYkQUUWHIoDAMAHVJ9Vs-ddRQpqKQaQA8gBBMEDQbAhCYaioCIMdsUBAFBbVJ5fAxkwFAI7UgAxgBxADuooCB5JgqUo2BEGAEDwUHi-zeYHr6QgrmIAOYAceC4HrqIIRpJgIQrKsgB5ACA0i78tGTASAjMp8CpaADmAHEAQPBQEp5KgKAuupAEAUDdUn23LOcKhFEAMIAeQgxoIgmtJUBEFG7IAmQAgoIu9WxmwEgI_pCCiYgA5gBwERxBUJEFVFHwSBwCAzzh5vlXmDYX5B2IAOYAc1EkQlCVBVRTcSgMAIDzYnRMU5xqnc1nU53jIAY2bdkAOAisJTkQB4gBAPATAoRBACtTk4JZNmTnxloOdY1ex0NdeEByJglfCAAAINw7mDbdzVZzm9TnbxsS8jckbYp5WGJ3AIl9_SXAsChAHACAAfglBzKRAY_72MXHDuz2fjLUc3LxjbBSLe7AkwTNRgEwAEKnFvmZCEFMp0Fi0c1x8csWF_xjvyMHW7BAW9eBKQl2EAQAQCvyYZzB_j4glO8fE_7vywr-PtRws2pQ4P3UzboawiQIEAgAs_BACf7hpx4i4ZkXnv461HMxtGdzeBDmInCQAAACkwBmLt2Q__kTcx3UrO16cS5WZuCEgCwAAyAAYEQvWDw3EXg4-ufzSvYt2juKGgDQAACACsWcm9Ti4bmXnptjLwd8uvzRl0dYsbgqIAwAAi3_sWbhjTHxq-aWfxF4OqOjif56xrv_jGVsQPYBcAACwuMcYqr9bvH2Y5KDts5_A-MQnPv1W26HFO1B3AAAAIL7wS3LT6l7xt292_A3MQMrBpR8v3pIRjdi1AAAAIK67FPIphbOwAq0ocUXb7IYPusS87YgeAAAAiB_8crx4S0p86q22X8MKCmPZCvGvyJZ6lmHXAgAAgJgWIn7mnXbxd29euBZWUDK4OnP6hz1iGgoTAQAAxC2lsDWDlIJu3cGb7X9LF0YgegAAACBupzBe916H-OTytm_DBvSjB-sbP-iRZ1njhgEAABB1-PiAhZtl1GD471d2_TuYgG70oG0qRw8Wb8uKRtw0AAAAop5OoBMY_25FGxUiXvopLMAserDi0l4uyli2axw3DgAAgMjCUfJZawc4ajBOhYj_OwzAdFvjhU_Rhfpo-po-MQvpBQAAAFHcukgsoaMDOFr-6eUXn8XqrxI9oH2efMGWbElj9wIAAIDI9TTg4vtPczqBdij8x7Wtf42VX2FwUQZdsH7Ow9y2awydEwEAAESGm3aOixve65RRg0-tuDgPq7794sSP_v5trj9AegEAAED4WbBjVDSu7tHSCT_Dau9EEJZf-j5fwGvfvSxu3j1OORqkGAAAAIS3n8GMtX35iMHyS3s-9duD_z1Wegfjv6w49Ve0e2E7X8hrSBCW0ZaPps0QBAAAAOGLGEz7sFcTg8SnV1z6z1jlXYzr3j_7b-kiHuAL-g-cYqAiDtQgAAAACE8vg3HR8H63JgbZv33z4qexunuxe-Hd9v9ADSIu8oX9eypSvHn7MLooAgAACDTc_fAW6tlzw6pOTQyuUJ3BXKzqHo5_eOPS_0IphsOFCyxmNfdLG0MUAQAAQPAaHI3KtsifXt6miUGGC-2xmvtTg_Bv-PwFTRB4K8hNlMeZgToEAAAAAWlutHjHuJi5pl-TAqabtiz-V6zifqYYVvf8j5966-LLJRed2k_2y90M0xBFAAAAUCcpWEBSsGhzOn9WwuQadeTv37r4n7B610oSlrctooue1L4A2vYoZrcMiSW0owFdFQH4_9u7f9amojCAwyou4uaok-Ks2D8WtYNQtaWBxlDd3P0ODuImOPoRHLQNtSIODlIaUnByLujQXBNBB0FiNbro8T1RMNUWFaSk5HngRz7ACZxz7z33XEnbtiiod9LMUrv7Vl3PouBLPhb57PLyXjP29n-H4WgsCh733kXIK7bSk7dxAlXHpkVJ0v8v7lKfj4vQysqndLHWTqcXm72LgtTdQO_kw35YJBSVGIxiw-BEZ2LAykvvuicslmMQz8VgOkhJkvSvTcTetlJccF6Ktw9ma-tp4tHr9OucE3Xy3YJT1dY-M3O_uJH2xALhcvRikwFLYwuN7slU5XgeNFv_ECu-2EkajyBKcWLVVC0-m1nrpAtRXg1KkgavyVqeDzppOhYBM_XP3TsDlfrHuDvwPk3HY-vf7hD8rJ0XBfm7QCbjPpW_bjUy17gajxuexoB93WIgNzyGGFso0viDZvQqakmSBq3F7795ATBaLdKf5o4fjw-K-IrwzePV1iGz7w5yslocHppfuxaDuJIPn_irwZYkaetWo9uxp2B8V0q7zbQ73LE7b_YPzTcmh6uN6_Gmw93oWQzwuj-6JGmT8gXl87jAfDgyt3Yrfq-cuNc8aDYdEPk1k9g8cmD0_ssjkqTBLp_Ga0MhAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA_eEbwHgaUWDtHnYAAAAASUVORK5CYII" diff --git a/hassio-google-drive-backup/backup/exceptions/__init__.py b/hassio-google-drive-backup/backup/exceptions/__init__.py new file mode 100644 index 0000000..731c24c --- /dev/null +++ b/hassio-google-drive-backup/backup/exceptions/__init__.py @@ -0,0 +1,2 @@ +# flake8: noqa +from .exceptions import GoogleCredGenerateError, SupervisorUnexpectedError, SupervisorTimeoutError, GoogleUnexpectedError, SupervisorFileSystemError, SupervisorPermissionError, LogInToGoogleDriveError, KnownTransient, GoogleInternalError, GoogleRateLimitError, CredRefreshGoogleError, CredRefreshMyError, BackupFolderInaccessible, BackupFolderMissingError, DeleteMutlipleBackupsError, DriveQuotaExceeded, ensureKey, ExistingBackupFolderError, UserCancelledError, UploadFailed, SupervisorConnectionError, BackupPasswordKeyInvalid, BackupInProgress, SimulatedError, ProtocolError, PleaseWait, NotUploadable, NoBackup, LowSpaceError, LogicError, KnownError, InvalidConfigurationValue, HomeAssistantDeleteError, GoogleTimeoutError, GoogleSessionError, GoogleInternalError, GoogleDrivePermissionDenied, GoogleDnsFailure, GoogleCredentialsExpired, GoogleCantConnect, ExistingBackupFolderError diff --git a/hassio-google-drive-backup/backup/exceptions/exceptions.py b/hassio-google-drive-backup/backup/exceptions/exceptions.py new file mode 100644 index 0000000..4f5e036 --- /dev/null +++ b/hassio-google-drive-backup/backup/exceptions/exceptions.py @@ -0,0 +1,446 @@ +from abc import ABC, abstractmethod + +from ..const import (DRIVE_FOLDER_URL_FORMAT, ERROR_BACKUP_FOLDER_INACCESSIBLE, + ERROR_BACKUP_FOLDER_MISSING, ERROR_BAD_PASSWORD_KEY, + ERROR_CREDS_EXPIRED, ERROR_DRIVE_FULL, + ERROR_EXISTING_FOLDER, ERROR_GOOGLE_CONNECT, ERROR_GOOGLE_CRED_PROCESS, + ERROR_GOOGLE_DNS, ERROR_GOOGLE_INTERNAL, + ERROR_GOOGLE_SESSION, ERROR_GOOGLE_TIMEOUT, + ERROR_HA_DELETE_ERROR, ERROR_INVALID_CONFIG, ERROR_LOGIC, + ERROR_LOW_SPACE, ERROR_MULTIPLE_DELETES, ERROR_NO_BACKUP, + ERROR_NOT_UPLOADABLE, ERROR_PLEASE_WAIT, ERROR_PROTOCOL, + ERROR_BACKUP_IN_PROGRESS, ERROR_UPLOAD_FAILED, LOG_IN_TO_DRIVE, + SUPERVISOR_PERMISSION, ERROR_GOOGLE_UNEXPECTED, ERROR_SUPERVISOR_TIMEOUT, ERROR_SUPERVISOR_UNEXPECTED, ERROR_SUPERVISOR_FILE_SYSTEM) + + +def ensureKey(key, target, name): + if key not in target: + raise ProtocolError(key, name, target) + return target[key] + + +class KnownError(Exception, ABC): + @abstractmethod + def message(self) -> str: + pass + + @abstractmethod + def code(self) -> str: + pass + + def httpStatus(self) -> int: + return 500 + + def data(self): + return {} + + def retrySoon(self): + return True + + +class KnownTransient(KnownError): + pass + + +class SimulatedError(KnownError): + def __init__(self, code=None): + self._code = code + + def code(self): + return self._code + + def message(self): + return "Gave code " + str(self._code) + + +class LogicError(KnownError): + def __init__(self, message=None): + self._message = message + + def message(self): + return self._message + + def code(self): + return ERROR_LOGIC + + +class ProtocolError(KnownError): + def __init__(self, parameter=None, object_name=None, debug_object=None): + self._parameter = parameter + self._object_name = object_name + self._debug_object = debug_object + + def message(self): + if self._object_name: + return "Required key '{0}' was missing from {1}".format(self._parameter, self._object_name) + else: + return self._parameter + + def code(self): + return ERROR_PROTOCOL + + +class BackupInProgress(KnownError): + def message(self): + return "A backup is already in progress" + + def code(self): + return ERROR_BACKUP_IN_PROGRESS + + +class BackupPasswordKeyInvalid(KnownError): + def message(self): + return "Couldn't find your backup password in your secrets file. Please check your settings." + + def code(self): + return ERROR_BAD_PASSWORD_KEY + + def retrySoon(self): + return False + + +class UploadFailed(KnownError): + def message(self): + return "Backup upload failed. Please check the supervisor logs for details." + + def code(self): + return ERROR_UPLOAD_FAILED + + +class GoogleCredentialsExpired(KnownError): + def message(self): + return "Your Google Drive credentials have expired. Please reauthorize with Google Drive through the Web UI." + + def code(self): + return ERROR_CREDS_EXPIRED + + def retrySoon(self): + return False + + +class NoBackup(KnownError): + def message(self): + return "The backup doesn't exist anymore" + + def code(self): + return ERROR_NO_BACKUP + + +class NotUploadable(KnownError): + def message(self): + return "This backup can't be uploaded to Home Assistant yet" + + def code(self): + return ERROR_NOT_UPLOADABLE + + +class PleaseWait(KnownError): + def message(self): + return "Please wait until the sync is finished." + + def code(self): + return ERROR_PLEASE_WAIT + + +class InvalidConfigurationValue(KnownError): + def __init__(self, key=None, current=None): + self.key = key + self.current = current + + def message(self): + return "'{0}' isn't a valid value for {1}".format(str(self.current), str(self.key)) + + def code(self): + return ERROR_INVALID_CONFIG + + +# UI Handler Done and updated + +class DeleteMutlipleBackupsError(KnownError): + def __init__(self, delete_sources=None): + self.delete_sources = delete_sources + + def message(self): + return "The add-on has been configured to delete more than one older backups. Please confirm this by visiting the add-on's web UI or by setting the config option 'confirm_multiple_deletes'=false in your add-on configuration." + + def code(self): + return ERROR_MULTIPLE_DELETES + + def data(self): + return self.delete_sources + + def retrySoon(self): + return False + + +class DriveQuotaExceeded(KnownError): + def message(self): + return "Google Drive is out of space" + + def code(self): + return ERROR_DRIVE_FULL + + def retrySoon(self): + return False + + +class GoogleDnsFailure(KnownError): + def message(self): + return "Unable to resolve host www.googleapis.com" + + def code(self): + return ERROR_GOOGLE_DNS + + +class GoogleCantConnect(KnownError): + def message(self): + return "Unable to connect to www.googleapis.com" + + def code(self): + return ERROR_GOOGLE_CONNECT + + +class GoogleInternalError(KnownTransient): + def message(self): + return "Google Drive returned an internal error (HTTP: 5XX)" + + def code(self): + return ERROR_GOOGLE_INTERNAL + + +class GoogleTimeoutError(KnownError): + def message(self): + return "Timed out while trying to reach Google Drive" + + def code(self): + return ERROR_GOOGLE_TIMEOUT + + @classmethod + def factory(cls): + return GoogleTimeoutError() + + +class GoogleRateLimitError(KnownTransient): + def message(self): + return "The addon has made too many requests to Google Drive, and will back off" + + def code(self): + return "google_rate_limit" + + +class GoogleSessionError(KnownError): + def message(self): + return "Upload session with Google Drive expired. The upload could not complete." + + def code(self): + return ERROR_GOOGLE_SESSION + + +class HomeAssistantDeleteError(KnownError): + def message(self): + return "Home Assistant refused to delete the backup." + + def code(self): + return ERROR_HA_DELETE_ERROR + + +class ExistingBackupFolderError(KnownError): + def __init__(self, existing_id: str = None, existing_name: str = None): + self.existing_id = existing_id + self.existing_name = existing_name + + def message(self): + return "A backup folder already exists. Please visit the add-on Web UI to select where to backup." + + def code(self): + return ERROR_EXISTING_FOLDER + + def data(self): + return { + "existing_url#href": DRIVE_FOLDER_URL_FORMAT.format(self.existing_id), + "existing_name": self.existing_name + } + + def retrySoon(self): + return False + + +class BackupFolderMissingError(KnownError): + def message(self): + return "Please visit the add-on Web UI to select where to backup." + + def code(self): + return ERROR_BACKUP_FOLDER_MISSING + + def retrySoon(self): + return False + + +class BackupFolderInaccessible(KnownError): + def __init__(self, existing_id: str = None): + self.existing_id = existing_id + + def message(self): + return "The choosen backup folder has become inaccessible. Please visit the addon web UI to select a backup folder." + + def data(self): + return { + "existing_url#href": DRIVE_FOLDER_URL_FORMAT.format(self.existing_id) + } + + def code(self): + return ERROR_BACKUP_FOLDER_INACCESSIBLE + + +class GoogleDrivePermissionDenied(KnownError): + def message(self): + return "Google Drive denied the request due to permissions." + + def code(self): + return "google_drive_permissions" + + +class LowSpaceError(KnownError): + def __init__(self, pct_used=None, space_remaining=None): + self.pct_used = pct_used + self.space_remaining = space_remaining + + def message(self): + return "Your backup folder is low on disk space. Backups can't be created until space is available." + + def code(self): + return ERROR_LOW_SPACE + + def data(self): + return { + "pct_used": self.pct_used, + "space_remaining": self.space_remaining + } + + +class SupervisorConnectionError(KnownError): + def message(self): + return "The addon couldn't connect to the supervisor. Backups can't continue until the supervisor is responding." + + def code(self): + return "supervisor_connection" + + +class UserCancelledError(KnownError): + def message(self): + return "Sync was cancelled by you" + + def code(self): + return "cancelled" + + def retrySoon(self): + return False + + +class CredRefreshGoogleError(KnownError): + def __init__(self, from_google=None): + self.from_google = from_google + + def message(self): + return "Couldn't refresh your credentials with Google because: '{}'".format(self.from_google) + + def code(self): + return "token_refresh_google_error" + + def data(self): + return { + "from_google": self.from_google + } + + +class CredRefreshMyError(KnownError): + def __init__(self, reason: str = None): + self.reason = reason + + def message(self): + return "Couldn't refresh Google Drive credentials because: {}".format(self.reason) + + def code(self): + return "token_refresh_my_error" + + def data(self): + return { + "reason": self.reason + } + + +class LogInToGoogleDriveError(KnownError): + def message(self): + return "Please visit drive.google.com to activate your Google Drive account." + + def code(self): + return LOG_IN_TO_DRIVE + + def retrySoon(self): + return False + + +class SupervisorPermissionError(KnownError): + def message(self): + return "The supervisor is rejecting requests from the addon. Please visit the web-UI for guidance" + + def code(self): + return SUPERVISOR_PERMISSION + + def retrySoon(self): + return True + + +class GoogleUnexpectedError(KnownError): + def message(self): + return "Google gave an unexpected response" + + def code(self): + return ERROR_GOOGLE_UNEXPECTED + + @classmethod + def factory(cls): + return GoogleUnexpectedError() + + +class SupervisorTimeoutError(KnownError): + def message(self): + return "A request to the supervisor timed out" + + def code(self): + return ERROR_SUPERVISOR_TIMEOUT + + @classmethod + def factory(cls): + return SupervisorTimeoutError() + + +class SupervisorUnexpectedError(KnownError): + def message(self): + return "The supervisor gave an unexpected response" + + def code(self): + return ERROR_SUPERVISOR_UNEXPECTED + + @classmethod + def factory(cls): + return SupervisorUnexpectedError() + + +class SupervisorFileSystemError(KnownError): + def message(self): + return "The host file system is read-only. Please restart Home Assistant and verify you have enough free space." + + def code(self): + return ERROR_SUPERVISOR_FILE_SYSTEM + + +class GoogleCredGenerateError(KnownError): + def __init__(self, message): + self._msg = message + + def message(self): + return self._msg + + def code(self): + return ERROR_GOOGLE_CRED_PROCESS diff --git a/hassio-google-drive-backup/backup/file/__init__.py b/hassio-google-drive-backup/backup/file/__init__.py new file mode 100644 index 0000000..1f3f3f6 --- /dev/null +++ b/hassio-google-drive-backup/backup/file/__init__.py @@ -0,0 +1,2 @@ +from .jsonfilesaver import JsonFileSaver +from .file import File \ No newline at end of file diff --git a/hassio-google-drive-backup/backup/file/file.py b/hassio-google-drive-backup/backup/file/file.py new file mode 100644 index 0000000..fcda778 --- /dev/null +++ b/hassio-google-drive-backup/backup/file/file.py @@ -0,0 +1,76 @@ +import os +from backup.logger import getLogger +from os.path import exists + +logger = getLogger(__name__) + + +class File: + """ + The envrionment Home Assistant runs in is notorious for disk-related failures, often from running completely out of space and SD card corruption. + Both of these can leave the addon in a state where the files it need to run are either corrupted or empty. This class attempts to mitigate that + by writing all config files twice, first to a backup file and then to the "real" file path. Then when reading it will check both locations to try + and find a copy of the file that isn't corrupted or deleted. + This avoids a number of common failures, namely: + - A power failure while writing a file can leave it empty or malformed. + - Overwriting a file while the disk is full can truncateit without writing the new data + - HD corruption cna make a file malformed, but its less likely to affect both files. + """ + @classmethod + def _read(cls, path): + with open(path, "r") as f: + return f.read() + + @classmethod + def read(cls, path): + try: + data = File._read(path) + if len(data) == 0: + logger.error(f"The configuration file {path} had an invalid format. This could be caused by hard drive corruption or an unstable power event. We'll attempt to load from a backup file instead.") + backup = File._backup_path(path) + if not exists(backup): + logger.error("Unable to locate a backup path") + raise + return File._read(backup) + else: + return data + except FileNotFoundError: + logger.error(f"The configuration file {path} was not found. This could be caused by hard drive corruption or an unstable power event. We'll attempt to load from a backup file instead.") + backup = File._backup_path(path) + if not exists(backup): + logger.error("Unable to locate a backup path") + raise + return File._read(backup) + + @classmethod + def _write(cls, path, data): + with open(path, "w") as f: + f.write(data) + + @classmethod + def write(cls, path, data): + # Crete the backup (recovery) file first. This ensures its present if the subsequent write is corrupted. + File._write(File._backup_path(path), data) + File._write(path, data) + + @classmethod + def exists(cls, path): + if exists(path): + return True + return exists(File._backup_path(path)) + + @classmethod + def delete(sels, path): + if exists(File._backup_path(path)): + os.remove(File._backup_path(path)) + if exists(path): + os.remove(path) + + @classmethod + def _backup_path(cls, path): + return path + ".backup" + + @classmethod + def touch(cls, file): + with open(file, "w"): + pass diff --git a/hassio-google-drive-backup/backup/file/jsonfilesaver.py b/hassio-google-drive-backup/backup/file/jsonfilesaver.py new file mode 100644 index 0000000..e985717 --- /dev/null +++ b/hassio-google-drive-backup/backup/file/jsonfilesaver.py @@ -0,0 +1,71 @@ +import json +import os +from backup.logger import getLogger +from os.path import exists + +logger = getLogger(__name__) + + +class JsonFileSaver: + """ + The envrionment Home Assistant runs in is notorious for disk-related failures, often from running completely out of space and SD card corruption. + Both of these can leave the addon in a state where the files it need to run are either corrupted or empty. This class attempts to mitigate that + by writing all config files twice, first to a backup file and then to the "real" file path. Then when reading it will check both locations to try + and find a copy of the file that isn't corrupted or deleted. + This avoids a number of common failures, namely: + - A power failure while writing a file can leave it empty or malformed. + - Overwriting a file while the disk is full can truncateit without writing the new data + - HD corruption cna make a file malformed, but its less likely to affect both files. + """ + @classmethod + def _read(cls, path): + with open(path, "r") as f: + return json.load(f) + + @classmethod + def read(cls, path): + try: + return JsonFileSaver._read(path) + except json.decoder.JSONDecodeError: + logger.error(f"The configuration file {path} had an invalid format. This could be caused by hard drive corruption or an unstable power event. We'll attempt to load from a backup file instead.") + backup = JsonFileSaver._backup_path(path) + if not exists(backup): + logger.error("Unable to locate a backup path") + raise + return JsonFileSaver._read(backup) + except FileNotFoundError: + logger.error(f"The configuration file {path} was not found. This could be caused by hard drive corruption or an unstable power event. We'll attempt to load from a backup file instead.") + backup = JsonFileSaver._backup_path(path) + if not exists(backup): + logger.error("Unable to locate a backup path") + raise + return JsonFileSaver._read(backup) + + @classmethod + def _write(cls, path, data): + with open(path, "w") as f: + json.dump(data, f, indent=4) + + @classmethod + def write(cls, path, data): + # Crete the backup (rcovery) file first. This ensures its present if the subsequent write is corrupted. + JsonFileSaver._write(JsonFileSaver._backup_path(path), data) + JsonFileSaver._write(path, data) + + @classmethod + def exists(cls, path): + if exists(path): + return True + return exists(JsonFileSaver._backup_path(path)) + + @classmethod + def delete(sels, path): + if exists(JsonFileSaver._backup_path(path)): + os.remove(JsonFileSaver._backup_path(path)) + if exists(path): + os.remove(path) + + @classmethod + def _backup_path(cls, path): + return path + ".backup" + diff --git a/hassio-google-drive-backup/backup/ha/__init__.py b/hassio-google-drive-backup/backup/ha/__init__.py new file mode 100644 index 0000000..f9b5b16 --- /dev/null +++ b/hassio-google-drive-backup/backup/ha/__init__.py @@ -0,0 +1,8 @@ +# flake8: noqa +from .hasource import HaSource, HABackup, PendingBackup, SOURCE_HA +from .haupdater import HaUpdater +from .harequests import HaRequests, EVENT_BACKUP_END, EVENT_BACKUP_START, VERSION_BACKUP_PATH +from .backupname import BackupName, BACKUP_NAME_KEYS +from .password import Password +from .addon_stopper import AddonStopper + diff --git a/hassio-google-drive-backup/backup/ha/addon_stopper.py b/hassio-google-drive-backup/backup/ha/addon_stopper.py new file mode 100644 index 0000000..efad301 --- /dev/null +++ b/hassio-google-drive-backup/backup/ha/addon_stopper.py @@ -0,0 +1,146 @@ +from backup.config import Config, Setting +from backup.file import JsonFileSaver +from backup.worker import Worker +from backup.exceptions import SupervisorFileSystemError +from .harequests import HaRequests +from injector import inject, singleton +from backup.time import Time +from backup.logger import getLogger +from datetime import timedelta +from asyncio import Lock + +LOGGER = getLogger(__name__) +CHECK_DURATION = timedelta(seconds=60) +ATTR_STATE = "state" +ATTR_WATCHDOG = "watchdog" +ATTR_NAME = "name" +STATE_STOPPED = "stopped" +STATE_STARTED = "started" + +STATES_STOPPED = ["stopped", "unknown", "error"] + + +@singleton +class AddonStopper(Worker): + @inject + def __init__(self, config: Config, requests: HaRequests, time: Time): + super().__init__("StartandStopTimer", self.check, time, 10) + self.requests = requests + self.config = config + self.time = time + self.must_start = set() + self.must_enable_watchdog = set() + self.stop_start_check_time = time.now() + self._backing_up = False + self.allow_run = False + self.lock = Lock() + + async def start(self, schedule=True): + if schedule: + await super().start() + path = self.config.get(Setting.STOP_ADDON_STATE_PATH) + if JsonFileSaver.exists(path): + data = JsonFileSaver.read(path) + self.must_enable_watchdog = set(data.get("watchdog", [])) + self.must_start = set(data.get("start", [])) + + def allowRun(self): + if not self.allow_run: + for slug in self.config.get(Setting.STOP_ADDONS).split(','): + if len(slug) == 0: + continue + self.must_start.add(slug) + self.allow_run = True + + def isBackingUp(self, backingUp): + self._backing_up = backingUp + + async def stopAddons(self, self_slug): + async with self.lock: + self._backing_up = True + for slug in self.config.get(Setting.STOP_ADDONS).split(','): + if slug == self_slug or len(slug) == 0: + # Don't ask the supervisor to stop yourself. That would be BAD. + continue + try: + info = await self.requests.getAddonInfo(slug) + if info.get(ATTR_STATE, None) == STATE_STARTED: + if info.get(ATTR_WATCHDOG, False): + try: + LOGGER.info("Temporarily disabling watchdog for addon '%s'", info.get(ATTR_NAME, slug)) + await self.requests.updateAddonOptions(slug, {ATTR_WATCHDOG: False}) + self.must_enable_watchdog.add(slug) + except Exception as e: + LOGGER.error("Unable to disable watchdog for addon {0}".format(info.get(ATTR_NAME, slug))) + LOGGER.printException(e) + try: + LOGGER.info("Stopping addon '%s'", info.get(ATTR_NAME, slug)) + await self.requests.stopAddon(slug) + self.must_start.add(slug) + except Exception as e: + LOGGER.error("Unable to stop addon '{0}'".format(info.get(ATTR_NAME, slug))) + LOGGER.printException(e) + except Exception as e: + LOGGER.error("Unable to lookup info for addon '{0}', please check your configuration".format(slug)) + LOGGER.printException(e) + self._save() + + async def startAddons(self): + self._backing_up = False + self.stop_start_check_time = self.time.now() + CHECK_DURATION + await self.check() + + async def check(self): + async with self.lock: + if self._backing_up: + return + if not self.allow_run: + return + changes = False + if len(self.must_start) > 0: + for slug in list(self.must_start): + try: + info = await self.requests.getAddonInfo(slug) + state = info.get(ATTR_STATE, None) + if info.get(ATTR_STATE, None) in STATES_STOPPED: + LOGGER.info("Starting addon '%s'", info.get(ATTR_NAME, slug)) + await self.requests.startAddon(slug) + self.must_start.remove(slug) + changes = True + elif info.get(ATTR_STATE, None) == STATE_STARTED and self.time.now() > self.stop_start_check_time: + # Give up on restarting it, looks like it was never stopped + self.must_start.remove(slug) + changes = True + else: + LOGGER.error(f"Addon '{info.get(ATTR_NAME, slug)} had unrecognized state {state}'. The addon will most likely be unable to automatically restart this addon.", ) + except Exception as e: + LOGGER.error("Unable to start addon '%s'", slug) + LOGGER.printException(e) + self.must_start.remove(slug) + changes = True + + if len(self.must_enable_watchdog) > 0: + for slug in list(self.must_enable_watchdog): + if slug in self.must_start: + # Wait until we're done trying to start the addon before re-enabling the watchdog, otherwise the supervisor complains + continue + try: + info = await self.requests.getAddonInfo(slug) + if not info.get(ATTR_WATCHDOG, True): + LOGGER.info("Re-enabling watchdog for addon '%s'", info.get(ATTR_NAME, slug)) + await self.requests.updateAddonOptions(slug, {ATTR_WATCHDOG: True}) + except Exception as e: + LOGGER.error("Unable to re-enable watchdog for addon '%s'", slug) + LOGGER.printException(e) + self.must_enable_watchdog.remove(slug) + changes = True + if changes: + self._save() + + def _save(self): + try: + path = self.config.get(Setting.STOP_ADDON_STATE_PATH) + data = {"start": list(self.must_start), "watchdog": list(self.must_enable_watchdog)} + JsonFileSaver.write(path, data) + except OSError: + raise SupervisorFileSystemError() diff --git a/hassio-google-drive-backup/backup/ha/backupname.py b/hassio-google-drive-backup/backup/ha/backupname.py new file mode 100644 index 0000000..b34f5bb --- /dev/null +++ b/hassio-google-drive-backup/backup/ha/backupname.py @@ -0,0 +1,39 @@ +from datetime import datetime + +from ..logger import getLogger + +logger = getLogger(__name__) + +BACKUP_NAME_KEYS = { + "{type}": lambda backup_type, now_local, host_info: backup_type, + "{year}": lambda backup_type, now_local, host_info: now_local.strftime("%Y"), + "{year_short}": lambda backup_type, now_local, host_info: now_local.strftime("%y"), + "{weekday}": lambda backup_type, now_local, host_info: now_local.strftime("%A"), + "{weekday_short}": lambda backup_type, now_local, host_info: now_local.strftime("%a"), + "{month}": lambda backup_type, now_local, host_info: now_local.strftime("%m"), + "{month_long}": lambda backup_type, now_local, host_info: now_local.strftime("%B"), + "{month_short}": lambda backup_type, now_local, host_info: now_local.strftime("%b"), + "{ms}": lambda backup_type, now_local, host_info: now_local.strftime("%f"), + "{day}": lambda backup_type, now_local, host_info: now_local.strftime("%d"), + "{hr24}": lambda backup_type, now_local, host_info: now_local.strftime("%H"), + "{hr12}": lambda backup_type, now_local, host_info: now_local.strftime("%I"), + "{min}": lambda backup_type, now_local, host_info: now_local.strftime("%M"), + "{sec}": lambda backup_type, now_local, host_info: now_local.strftime("%S"), + "{ampm}": lambda backup_type, now_local, host_info: now_local.strftime("%p"), + "{version_ha}": lambda backup_type, now_local, host_info: str(host_info.get('homeassistant', 'Unknown')), + "{version_hassos}": lambda backup_type, now_local, host_info: str(host_info.get('hassos', 'Unknown')), + "{version_super}": lambda backup_type, now_local, host_info: str(host_info.get('supervisor', 'Unknown')), + "{date}": lambda backup_type, now_local, host_info: now_local.strftime("%x"), + "{time}": lambda backup_type, now_local, host_info: now_local.strftime("%X"), + "{datetime}": lambda backup_type, now_local, host_info: now_local.strftime("%c"), + "{isotime}": lambda backup_type, now_local, host_info: now_local.isoformat(), + "{hostname}": lambda backup_type, now_local, host_info: str(host_info.get('hostname', 'Unknown')), +} + + +class BackupName(): + def resolve(self, backup_type: str, template: str, now_local: datetime, host_info) -> str: + for key in BACKUP_NAME_KEYS: + template = template.replace(key, BACKUP_NAME_KEYS[key]( + backup_type, now_local, host_info)) + return template diff --git a/hassio-google-drive-backup/backup/ha/harequests.py b/hassio-google-drive-backup/backup/ha/harequests.py new file mode 100644 index 0000000..ac4b4e2 --- /dev/null +++ b/hassio-google-drive-backup/backup/ha/harequests.py @@ -0,0 +1,329 @@ +import os +from typing import Any, Dict + +from aiohttp import ClientSession, ClientTimeout +from aiohttp.client_exceptions import ClientResponseError, ClientConnectorError +from injector import inject +from asyncio.exceptions import TimeoutError + +from ..util import AsyncHttpGetter +from ..config import Config, Setting, Version +from ..exceptions import HomeAssistantDeleteError, SupervisorConnectionError, SupervisorPermissionError, SupervisorTimeoutError, SupervisorUnexpectedError +from ..model import HABackup +from ..logger import getLogger +from ..util import DataCache +from backup.time import Time +from backup.const import NECESSARY_OLD_BACKUP_PLURAL_NAME, NECESSARY_OLD_SUPERVISOR_URL +from yarl import URL + +logger = getLogger(__name__) + +HEADER_TOKEN = "X-Supervisor-Token" + +NOTIFICATION_ID = "backup_broken" +EVENT_BACKUP_START = "backup_started" +EVENT_BACKUP_END = "backup_ended" + +VERSION_BACKUP_PATH = Version.parse("2021.8") + + +def supervisor_call(func): + async def wrap_and_call(*args, **kwargs): + try: + return await func(*args, **kwargs) + except ClientConnectorError: + raise SupervisorConnectionError() + except TimeoutError: + raise SupervisorConnectionError() + except ClientResponseError as e: + if e.status == 403: + raise SupervisorPermissionError() + raise + return wrap_and_call + + +class HaRequests(): + """ + Stores logic for interacting with the supervisor add-on API + """ + @inject + def __init__(self, config: Config, session: ClientSession, time: Time, data_cache: DataCache): + self.config: Config = config + self.cache = {} + self.session = session + self._time = time + self._data_cache = data_cache + + # default the supervisor versio to using the "most featured" when it can't be parsed. + self._super_version = VERSION_BACKUP_PATH + + def getSupervisorURL(self) -> URL: + if len(self.config.get(Setting.SUPERVISOR_URL)) > 0: + return URL(self.config.get(Setting.SUPERVISOR_URL)) + if 'SUPERVISOR_TOKEN' in os.environ: + return URL("http://supervisor") + else: + return URL(NECESSARY_OLD_SUPERVISOR_URL) + + def _getBackupPath(self): + if self.supportsBackupPaths(): + return "backups" + return NECESSARY_OLD_BACKUP_PLURAL_NAME + + def supportsBackupPaths(self): + return not self._super_version or self._super_version >= VERSION_BACKUP_PATH + + @supervisor_call + async def createBackup(self, info): + if 'folders' in info or 'addons' in info: + url = self.getSupervisorURL().with_path("{0}/new/partial".format(self._getBackupPath())) + else: + url = self.getSupervisorURL().with_path("{0}/new/full".format(self._getBackupPath())) + return await self._postHassioData(url, info, timeout=ClientTimeout(total=self.config.get(Setting.PENDING_BACKUP_TIMEOUT_SECONDS))) + + @supervisor_call + async def auth(self, user: str, password: str) -> None: + await self._postHassioData(self.getSupervisorURL().with_path("auth"), {"username": user, "password": password}, headers=self._altAuthHeaders()) + + @supervisor_call + async def upload(self, stream): + url = self.getSupervisorURL().with_path("{0}/new/upload".format(self._getBackupPath())) + return await self._postHassioData(url, data=stream) + + @supervisor_call + async def delete(self, slug) -> None: + if slug in self.cache: + del self.cache[slug] + try: + if self.supportsBackupPaths(): + delete_url = self.getSupervisorURL().with_path("{1}/{0}".format(slug, self._getBackupPath())) + await self._sendHassioData("delete", delete_url, {}) + else: + delete_url = self.getSupervisorURL().with_path("{1}/{0}/remove".format(slug, self._getBackupPath())) + await self._sendHassioData("post", delete_url, {}) + except ClientResponseError as e: + if e.status == 400: + raise HomeAssistantDeleteError() + raise e + + @supervisor_call + async def startAddon(self, slug) -> None: + url = self.getSupervisorURL().with_path("addons/{0}/start".format(slug)) + await self._postHassioData(url, {}) + + @supervisor_call + async def stopAddon(self, slug) -> None: + url = self.getSupervisorURL().with_path("addons/{0}/stop".format(slug)) + await self._postHassioData(url, {}) + + @supervisor_call + async def backup(self, slug): + if slug in self.cache: + info = self.cache[slug] + else: + info = await self._getHassioData(self.getSupervisorURL().with_path("{1}/{0}/info".format(slug, self._getBackupPath()))) + self.cache[slug] = info + return HABackup(info, self._data_cache, self.config, self.config.isRetained(slug)) + + @supervisor_call + async def backups(self): + return await self._getHassioData(self.getSupervisorURL().with_path(self._getBackupPath())) + + @supervisor_call + async def haInfo(self): + return await self._getHassioData(self.getSupervisorURL().with_path("core/info")) + + @supervisor_call + async def selfInfo(self) -> Dict[str, Any]: + return await self.getAddonInfo("self") + + @supervisor_call + async def getAddonInfo(self, addon_slug) -> Dict[str, Any]: + return await self._getHassioData(self.getSupervisorURL().with_path("addons/{0}/info".format(addon_slug))) + + @supervisor_call + async def getAddons(self) -> Dict[str, Any]: + return await self._getHassioData(self.getSupervisorURL().with_path("addons")) + + @supervisor_call + async def hassosInfo(self) -> Dict[str, Any]: + return await self._getHassioData(self.getSupervisorURL().with_path("hassos/info")) + + @supervisor_call + async def info(self) -> Dict[str, Any]: + return await self._getHassioData(self.getSupervisorURL().with_path("info")) + + @supervisor_call + async def refreshBackups(self): + url = self.getSupervisorURL().with_path("{0}/reload".format(self._getBackupPath())) + return await self._postHassioData(url) + + @supervisor_call + async def supervisorInfo(self): + url = self.getSupervisorURL().with_path("supervisor/info") + info = await self._getHassioData(url) + + # parse the supervisor version + if 'version' in info: + self._super_version = Version.parse(info['version']) + return info + + @supervisor_call + async def restore(self, slug: str, password: str = None) -> None: + url = self.getSupervisorURL().with_path("{1}/{0}/restore/full".format(slug, self._getBackupPath())) + if password: + await self._postHassioData(url, {'password': password}) + else: + await self._postHassioData(url, {}) + + @supervisor_call + async def download(self, slug) -> AsyncHttpGetter: + url = self.getSupervisorURL().with_path("{1}/{0}/download".format(slug, self._getBackupPath())) + ret = AsyncHttpGetter(url, + self._getAuthHeaders(), + self.session, + timeoutFactory=SupervisorTimeoutError.factory, + otherErrorFactory=SupervisorUnexpectedError.factory, + timeout=ClientTimeout(sock_connect=self.config.get(Setting.DOWNLOAD_TIMEOUT_SECONDS), + sock_read=self.config.get(Setting.DOWNLOAD_TIMEOUT_SECONDS)), + time=self._time) + return ret + + @supervisor_call + async def getSuperLogs(self): + url = self.getSupervisorURL().with_path("supervisor/logs") + async with self.session.get(url, headers=self._getAuthHeaders()) as resp: + resp.raise_for_status() + return await resp.text() + + @supervisor_call + async def getCoreLogs(self): + url = self.getSupervisorURL().with_path("core/logs") + async with self.session.get(url, headers=self._getAuthHeaders()) as resp: + resp.raise_for_status() + return await resp.text() + + async def _validateHassioReply(self, resp) -> Dict[str, Any]: + async with resp: + resp.raise_for_status() + details: Dict[str, Any] = await resp.json() + if "result" not in details or details["result"] != "ok": + if "result" in details: + raise Exception("Hassio said: " + details["result"]) + else: + raise Exception( + "Malformed response from Hassio: " + str(details)) + + if "data" not in details: + return {} + if self.config.get(Setting.TRACE_REQUESTS): + logger.trace("Hassio replied: %s", details) + return details["data"] + + async def getAddonLogo(self, slug: str): + url = self.getSupervisorURL().with_path("addons/{0}/icon".format(slug)) + async with self.session.get(url, headers=self._getAuthHeaders()) as resp: + resp.raise_for_status() + return (resp.headers['Content-Type'], await resp.read()) + + def _getToken(self): + configured = self.config.get(Setting.SUPERVISOR_TOKEN) + if configured and len(configured) > 0: + return configured + if "SUPERVISOR_TOKEN" in os.environ: + return os.environ.get("SUPERVISOR_TOKEN") + # Older versions of the supervisor use a different name for the token. + return os.environ.get("HASSIO_TOKEN") + + def _getAuthHeaders(self): + return { + 'Authorization': 'Bearer ' + self._getToken() + } + + def _altAuthHeaders(self): + return { + HEADER_TOKEN: self._getToken() + } + + @supervisor_call + async def _getHassioData(self, url: URL) -> Dict[str, Any]: + if self.config.get(Setting.TRACE_REQUESTS): + logger.trace("Making Hassio request: " + str(url)) + return await self._validateHassioReply(await self.session.get(url, headers=self._getAuthHeaders())) + + async def _postHassioData(self, url: URL, json=None, file=None, data=None, timeout=None, headers=None) -> Dict[str, Any]: + return await self._sendHassioData("post", url, json, file, data, timeout, headers) + + @supervisor_call + async def _sendHassioData(self, method: str, url: URL, json=None, file=None, data=None, timeout=None, headers=None) -> Dict[str, Any]: + if headers is None: + headers = self._getAuthHeaders() + if self.config.get(Setting.TRACE_REQUESTS): + logger.trace("Making Hassio request: " + str(url)) + return await self._validateHassioReply(await self.session.request(method, url, headers=headers, json=json, data=data, timeout=timeout)) + + async def _postHaData(self, path: str, data: Dict[str, Any]) -> None: + url = self.getSupervisorURL().with_path("/core/api/" + path) + async with self.session.post(url, headers=self._getAuthHeaders(), json=data) as resp: + resp.raise_for_status() + + async def sendNotification(self, title: str, message: str) -> None: + data: Dict[str, str] = { + "title": title, + "message": message, + "notification_id": NOTIFICATION_ID + } + await self._postHaData("services/persistent_notification/create", data) + + async def eventBackupStart(self, name, time): + await self._sendEvent(EVENT_BACKUP_START, { + 'backup_name': name, + 'backup_time': str(time) + }) + + async def eventBackupEnd(self, name, time, completed): + await self._sendEvent(EVENT_BACKUP_END, { + 'completed': completed, + 'backup_name': name, + 'backup_time': str(time) + }) + + async def _sendEvent(self, event_name: str, data: Dict[str, str]) -> None: + await self._postHaData("events/" + event_name, data) + + async def dismissNotification(self) -> None: + data: Dict[str, str] = { + "notification_id": NOTIFICATION_ID + } + await self._postHaData("services/persistent_notification/dismiss", data) + + async def updateBackupStaleSensor(self, state: bool) -> None: + if self.config.get(Setting.CALL_BACKUP_SNAPSHOT): + data: Dict[str, Any] = { + "state": state, + "attributes": { + "friendly_name": "Snapshots Stale", + "device_class": "problem" + } + } + await self._postHaData("states/binary_sensor." + NECESSARY_OLD_BACKUP_PLURAL_NAME + "_stale", data) + else: + data: Dict[str, Any] = { + "state": state, + "attributes": { + "friendly_name": "Backups Stale", + "device_class": "problem" + } + } + await self._postHaData("states/binary_sensor.backups_stale", data) + + @supervisor_call + async def updateConfig(self, config) -> None: + return await self._postHassioData(self.getSupervisorURL().with_path("addons/self/options"), {'options': config}) + + @supervisor_call + async def updateAddonOptions(self, slug, options): + return await self._postHassioData(self.getSupervisorURL().with_path("addons/{0}/options".format(slug)), options) + + async def updateEntity(self, entity, data): + await self._postHaData("states/" + entity, data) diff --git a/hassio-google-drive-backup/backup/ha/hasource.py b/hassio-google-drive-backup/backup/ha/hasource.py new file mode 100644 index 0000000..1234b04 --- /dev/null +++ b/hassio-google-drive-backup/backup/ha/hasource.py @@ -0,0 +1,537 @@ +import asyncio +import aiohttp +from datetime import timedelta +from io import IOBase +from threading import Lock, Thread +from typing import Dict, List, Optional + +from aiohttp.client_exceptions import ClientResponseError +from injector import inject, singleton + +from backup.util import AsyncHttpGetter, GlobalInfo, Estimator, DataCache, KEY_NOTE, KEY_LAST_SEEN, KEY_PENDING, KEY_NAME, KEY_CREATED, KEY_I_MADE_THIS, KEY_IGNORE +from ..config import Config, Setting, CreateOptions, Startable +from ..const import SOURCE_HA +from ..model import BackupSource, AbstractBackup, HABackup, Backup +from ..exceptions import (LogicError, BackupInProgress, + UploadFailed, ensureKey) +from .harequests import HaRequests +from .password import Password +from .backupname import BackupName +from ..time import Time +from ..logger import getLogger, StandardLogger +from backup.const import FOLDERS, NECESSARY_OLD_BACKUP_PLURAL_NAME +from .addon_stopper import LOGGER, AddonStopper + +logger: StandardLogger = getLogger(__name__) + + +class PendingBackup(AbstractBackup): + def __init__(self, backupType, protected, options: CreateOptions, request_info, config, time): + super().__init__( + name=request_info['name'], + slug="pending", + date=options.when, + size="pending", + source=SOURCE_HA, + backupType=backupType, + version="", + protected=protected, + retained=False, + uploadable=False, + details=None, + note=options.note, + pending=True) + self._config = config + self._failed = False + self._complete = False + self._exception = None + self._failed_at = None + self.setOptions(options) + self._request_info = request_info + self._completed_slug = None + self._time = time + self._pending_subverted = False + self._start_time = time.now() + + def considerForPurge(self) -> bool: + return False + + def startTime(self): + return self._start_time + + def failed(self, exception, time): + self._failed = True + self._exception = exception + self._failed_at = time + + def getFailureTime(self): + return self._failed_at + + def complete(self, slug): + self._complete = True + self._completed_slug = slug + + def setPendingUnknown(self): + self._name = "Pending Backup" + self._backupType = "unknown" + self._protected = False + self._pending_subverted = True + self._note = None + + def createdSlug(self): + return self._completed_slug + + def isComplete(self): + return self._complete + + def isFailed(self): + return self._failed + + def status(self): + if self._complete: + return "Created" + if self._failed: + return "Failed!" + return "Pending" + + def raiseIfNeeded(self): + if self.isFailed(): + raise self._exception + if self._pending_subverted: + raise BackupInProgress() + + def isStale(self): + if self._pending_subverted: + delta = timedelta(seconds=self._config.get( + Setting.BACKUP_STALE_SECONDS)) + if self._time.now() > self.startTime() + delta: + return True + if not self.isFailed(): + return False + delta = timedelta(seconds=self._config.get( + Setting.FAILED_BACKUP_TIMEOUT_SECONDS)) + staleTime = self.getFailureTime() + delta + return self._time.now() >= staleTime + + def madeByTheAddon(self): + return True + + +@singleton +class HaSource(BackupSource[HABackup], Startable): + """ + Stores logic for interacting with the supervisor add-on API + """ + @inject + def __init__(self, config: Config, time: Time, ha: HaRequests, info: GlobalInfo, stopper: AddonStopper, estimator: Estimator, data_cache: DataCache): + super().__init__() + self.config: Config = config + self._data_cache = data_cache + self.backup_thread: Thread = None + self.pending_backup_error: Optional[Exception] = None + self.pending_backup_slug: Optional[str] = None + self.self_info = None + self.host_info = None + self.ha_info = None + self.super_info = None + self.lock: Lock = Lock() + self.time = time + self.harequests = ha + self.last_slugs = set() + self.retained = [] + self.cached_retention = {} + self._info = info + self.pending_options = {} + self.stopper = stopper + self.estimator = estimator + self._addons = {} + self._changes_from_last_query = False + + # This lock should be used for _ANYTHING_ that interacts with self._pending_backup + self._pending_backup_lock = asyncio.Lock() + self.pending_backup: Optional[PendingBackup] = None + self._pending_backup_task = None + self._initialized = False + + def isInitialized(self): + return self._initialized + + async def check(self) -> bool: + pending = self.pending_backup + if pending and pending.isStale(): + self.trigger() + return await super().check() + + def icon(self) -> str: + return "home-assistant" + + def name(self) -> str: + return SOURCE_HA + + def title(self) -> str: + return "Home Assistant" + + def maxCount(self) -> None: + return self.config.get(Setting.MAX_BACKUPS_IN_HA) + + def enabled(self) -> bool: + return True + + def freeSpace(self): + return self.estimator.getBytesFree() + + async def create(self, options: CreateOptions) -> HABackup: + # Make sure instance info is up-to-date, for the backup name + await self._refreshInfo() + + # Set a default name if it was unspecified + if options.name_template is None or len(options.name_template) == 0: + options.name_template = self.config.get(Setting.BACKUP_NAME) + + # Build the backup request json, get type, etc + request, type_name, protected = self._buildBackupInfo( + options) + + async with self._pending_backup_lock: + # Check if a backup is already in progress + if self.pending_backup: + if not self.pending_backup.isFailed() and not self.pending_backup.isComplete(): + logger.info("A backup was already in progress") + raise BackupInProgress() + + # try to stop addons + await self.stopper.stopAddons(self.self_info['slug']) + + # Create the backup palceholder object + self.pending_backup = PendingBackup( + type_name, protected, options, request, self.config, self.time) + logger.info("Requesting a new backup") + self._pending_backup_task = asyncio.create_task(self._requestAsync( + self.pending_backup), name="Pending Backup Requester") + await asyncio.wait({self._pending_backup_task}, timeout=self.config.get(Setting.NEW_BACKUP_TIMEOUT_SECONDS)) + self.pending_backup.raiseIfNeeded() + + # There is not other backup in progress, so assume its been requested. + pending = self._data_cache.backup(KEY_PENDING) + pending[KEY_NAME] = request['name'] + pending[KEY_CREATED] = options.when.isoformat() + pending[KEY_LAST_SEEN] = self.time.now().isoformat() + self._data_cache.makeDirty() + if self.pending_backup.isComplete(): + # It completed while we waited, so just query the new backup + ret = await self.harequests.backup(self.pending_backup.createdSlug()) + if options.note is not None: + ret.setNote(options.note) + self.setDataCacheInfo(ret) + self._data_cache.backup(ret.slug())[KEY_I_MADE_THIS] = True + return ret + else: + return self.pending_backup + + def _isHttp400(self, e): + if isinstance(e, ClientResponseError): + return e.status == 400 + return False + + async def start(self): + try: + await self.init() + except Exception: + pass + + async def stop(self): + if self._pending_backup_task: + self._pending_backup_task.cancel() + await asyncio.wait([self._pending_backup_task]) + + @property + def query_had_changes(self): + return self._changes_from_last_query + + async def get(self) -> Dict[str, HABackup]: + if not self._initialized: + await self.init() + else: + # Always ensure the supervisor version is fresh before makign any other requests + self.super_info = await self.harequests.supervisorInfo() + slugs = set() + retained = [] + backups: Dict[str, HABackup] = {} + query = await self.harequests.backups() + + # Different supervisor version use different names for the list of backups + backup_list = [] + if NECESSARY_OLD_BACKUP_PLURAL_NAME in query: + backup_list = query[NECESSARY_OLD_BACKUP_PLURAL_NAME] + if 'backups' in query: + backup_list = query['backups'] + + for backup in backup_list: + slug = backup['slug'] + slugs.add(slug) + item = await self.harequests.backup(slug) + if slug in self.pending_options: + item.setOptions(self.pending_options[slug]) + backups[slug] = item + if item.retained(): + retained.append(item.slug()) + self.setDataCacheInfo(item) + if self.pending_backup: + async with self._pending_backup_lock: + if self.pending_backup: + if self.pending_backup.isStale(): + # The backup is stale, so just let it die. + self._killPending() + elif self.pending_backup.isComplete() and self.pending_backup.createdSlug() in backups: + # Copy over options if we got the requested backup. + backups[self.pending_backup.createdSlug()].setOptions( + self.pending_backup.getOptions()) + if self.pending_backup.note() is not None: + # Save the note with the now known slug + await self.note(backups[self.pending_backup.createdSlug()], self.pending_backup.note()) + self._killPending() + elif self.last_slugs.symmetric_difference(slugs).intersection(slugs): + # New backup added, ignore pending backup. + sorted = list(backups.values()) + sorted.sort(key=HABackup.date) + if self.pending_backup.note() is not None and len(sorted) > 0: + # Save the note with the newest backup + await self.note(sorted[-1], self.pending_backup.note()) + self._killPending() + if self.pending_backup: + backups[self.pending_backup.slug()] = self.pending_backup + for slug in retained: + if not self.config.isRetained(slug): + self.config.setRetained(slug, False) + self._changes_from_last_query = self.last_slugs != slugs + self.last_slugs = slugs + return backups + + def setDataCacheInfo(self, backup: HABackup): + if backup.slug() not in self._data_cache.backups: + # its a new backup, so we need to create a record for it + pending = self._data_cache.backups.get(KEY_PENDING, {}) + pending_created = self.time.parse(pending.get(KEY_CREATED, self.time.now().isoformat())) + + # If the backup has the same name as the one we created and it was created within a day + # of the requested time, then assume the addon created it. + self_created = backup.name() == pending.get(KEY_NAME, None) and abs((pending_created - backup.date()).total_seconds()) < timedelta(days=1).total_seconds() + + stored_backup = self._data_cache.backup(backup.slug()) + stored_backup[KEY_I_MADE_THIS] = self_created + stored_backup[KEY_CREATED] = backup.date().isoformat() + stored_backup[KEY_NAME] = backup.name() + stored_backup[KEY_NOTE] = backup.note() + if self_created: + # Remove the pending backup info from the cache so it doesn't get reused. + del self._data_cache.backups[KEY_PENDING] + # bump the last seen time + self._data_cache.backup(backup.slug())[KEY_LAST_SEEN] = self.time.now().isoformat() + self._data_cache.makeDirty() + + async def delete(self, backup: Backup): + slug = self._validateBackup(backup).slug() + logger.info("Deleting '{0}' from Home Assistant".format(backup.name())) + await self.harequests.delete(slug) + backup.removeSource(self.name()) + + async def ignore(self, backup: Backup, ignore: bool): + slug = self._validateBackup(backup).slug() + logger.info("Updating ignore settings for '{0}'".format(backup.name())) + self._data_cache.backup(slug)[KEY_IGNORE] = ignore + self._data_cache.makeDirty() + + async def note(self, backup, note: str) -> None: + if isinstance(backup, HABackup): + validated = backup + else: + validated = self._validateBackup(backup) + logger.debug(f"Adding a note to ha backup '{validated.name()}'") + if isinstance(validated, PendingBackup): + # The ntoe will get set once the backup is created and we know the slug + validated.setNote(note) + else: + self._data_cache.backup(validated.slug())[KEY_NOTE] = note + self._data_cache.makeDirty() + validated.setNote(note) + return await super().note(backup, note) + + async def save(self, backup: Backup, source: AsyncHttpGetter) -> HABackup: + logger.info("Downloading '{0}'".format(backup.name())) + self._info.upload(0) + resp = None + try: + backup.overrideStatus("Loading {0}%", source) + backup.setUploadSource(self.title(), source) + async with source: + with aiohttp.MultipartWriter('mixed') as mpwriter: + mpwriter.append(source, {'CONTENT-TYPE': 'application/tar'}) + resp = await self.harequests.upload(mpwriter) + backup.clearStatus() + backup.clearUploadSource() + except Exception as e: + logger.printException(e) + backup.overrideStatus("Failed!") + backup.uploadFailure(logger.formatException(e)) + if resp and 'slug' in resp and resp['slug'] == backup.slug(): + self.config.setRetained(backup.slug(), True) + return await self.harequests.backup(backup.slug()) + else: + raise UploadFailed() + + async def read(self, backup: Backup) -> IOBase: + item = self._validateBackup(backup) + return await self.harequests.download(item.slug()) + + async def retain(self, backup: Backup, retain: bool) -> None: + item: HABackup = self._validateBackup(backup) + item._retained = retain + self.config.setRetained(backup.slug(), retain) + + async def init(self): + await self._refreshInfo() + self._initialized = True + + async def refresh(self): + await self._refreshInfo() + + async def _refreshInfo(self) -> None: + try: + self.self_info = await self.harequests.selfInfo() + self.host_info = await self.harequests.info() + self.ha_info = await self.harequests.haInfo() + self.super_info = await self.harequests.supervisorInfo() + addon_info = ensureKey("addons", await self.harequests.getAddons(), "Supervisor Metadata") + self.config.update( + ensureKey("options", self.self_info, "addon metdata")) + if self.config.mustSaveUpgradeChanges(): + LOGGER.info("The configuration format has changed in this version of the addon and your configuration will be automatically updated") + options = {} + for option in self.config.getAllConfig().keys(): + options[option.value] = self.config.get(option) + await self.harequests.updateConfig(options) + self.config.persistedChanges() + + self._info.ha_port = ensureKey( + "port", self.ha_info, "Home Assistant metadata") + self._info.ha_ssl = ensureKey( + "ssl", self.ha_info, "Home Assistant metadata") + self._info.addons = addon_info + self._info.slug = ensureKey( + "slug", self.self_info, "addon metdata") + self._info.url = self.getAddonUrl() + + self._addons = {} + for addon in addon_info: + self._addons[addon.get('slug', "default")] = addon + + self._info.addDebugInfo("self_info", self.self_info) + self._info.addDebugInfo("host_info", self.host_info) + self._info.addDebugInfo("ha_info", self.ha_info) + self._info.addDebugInfo("super_info", self.super_info) + except Exception as e: + logger.debug("Failed to connect to supervisor") + logger.debug(logger.formatException(e)) + raise e + + def addonHasLogo(self, slug): + return self._addons.get(slug, {}).get('logo', False) + + def getAddonUrl(self): + """ + Returns the relative path to the add-on, for the purpose of linking to the add-on page from within Home Assistant. + """ + if self._info.slug is None: + return "" + return "/hassio/ingress/" + str(self._info.slug) + + def getHostInfo(self): + if not self.isInitialized(): + return {} + return self.host_info + + def getFullAddonUrl(self): + if not self.isInitialized(): + return "" + return self._haUrl() + "hassio/ingress/" + str(self._info.slug) + + def getHomeAssistantUrl(self): + if not self.isInitialized(): + return "" + return self._haUrl() + + def _haUrl(self): + if self._info.ha_ssl: + protocol = "https://" + else: + protocol = "http://" + return "".join([protocol, "{host}:", str(self._info.ha_port), "/"]) + + def _validateBackup(self, backup) -> HABackup: + item: HABackup = backup.getSource(self.name()) + if not item: + raise LogicError( + "Requested to do something with a backup from Home Assistant, but the backup has no Home Assistant source") + return item + + def _killPending(self): + self.pending_backup = None + if self._pending_backup_task and not self._pending_backup_task.done(): + self._pending_backup_task.cancel() + + def postSync(self): + self.stopper.allowRun() + self.stopper.isBackingUp(self.pending_backup is not None) + + async def _requestAsync(self, pending: PendingBackup, start=[]) -> None: + try: + result = await asyncio.wait_for(self.harequests.createBackup(pending._request_info), timeout=self.config.get(Setting.PENDING_BACKUP_TIMEOUT_SECONDS)) + slug = ensureKey( + "slug", result, "supervisor's create backup response") + pending.complete(slug) + self.config.setRetained( + slug, pending.getOptions().retain_sources.get(self.name(), False)) + logger.info("Backup finished") + except Exception as e: + if self._isHttp400(e): + logger.warning("A backup was already in progress") + pending.setPendingUnknown() + else: + logger.error("Backup failed:") + logger.printException(e) + pending.failed(e, self.time.now()) + finally: + await self.stopper.startAddons() + self.trigger() + + def _buildBackupInfo(self, options: CreateOptions): + addons: List[str] = [] + for addon in self.super_info.get('addons', {}): + addons.append(addon['slug']) + request_info = { + 'addons': [], + 'folders': [] + } + folders = list(map(lambda f: f['slug'], FOLDERS)) + type_name = "Full" + for folder in folders: + if folder not in self.config.get(Setting.EXCLUDE_FOLDERS): + request_info['folders'].append(folder) + else: + type_name = "Partial" + for addon in addons: + if addon not in self.config.get(Setting.EXCLUDE_ADDONS): + request_info['addons'].append(addon) + else: + type_name = "Partial" + if type_name == "Full": + del request_info['addons'] + del request_info['folders'] + protected = False + password = Password(self.config).resolve() + if password: + request_info['password'] = password + name = BackupName().resolve(type_name, options.name_template, + self.time.toLocal(options.when), self.host_info) + request_info['name'] = name + return request_info, type_name, protected diff --git a/hassio-google-drive-backup/backup/ha/haupdater.py b/hassio-google-drive-backup/backup/ha/haupdater.py new file mode 100644 index 0000000..e1ff1b7 --- /dev/null +++ b/hassio-google-drive-backup/backup/ha/haupdater.py @@ -0,0 +1,189 @@ +from datetime import timedelta + +from aiohttp.client_exceptions import ClientResponseError +from injector import inject, singleton + +from ..model import Coordinator, Backup +from ..config import Config, Setting +from ..util import GlobalInfo, Backoff, Estimator +from .harequests import HaRequests +from ..time import Time +from ..worker import Worker +from ..const import SOURCE_HA, SOURCE_GOOGLE_DRIVE +from ..logger import getLogger + +logger = getLogger(__name__) + +NOTIFICATION_TITLE = "Home Assistant Google Drive Backup is Having Trouble" +NOTIFICATION_DESC_LINK = "The add-on is having trouble making backups and needs attention. Please visit the add-on [status page]({0}) for details." +NOTIFICATION_DESC_STATIC = "The add-on is having trouble making backups and needs attention. Please visit the add-on status page for details." + +MAX_BACKOFF = 60 * 5 # 5 minutes +FIRST_BACKOFF = 60 # 1 minute + +# Wait 5 minutes before logging +NOTIFY_DELAY = 60 * 5 # 5 minute + +OLD_BACKUP_ENTITY_NAME = "sensor.snapshot_backup" +BACKUP_ENTITY_NAME = "sensor.backup_state" + +REASSURING_MESSAGE = "Unable to reach Home Assistant (HTTP {0}). This is normal if Home Assistant is restarting. You will probably see some errors in the supervisor logs until it comes back online." + + +@singleton +class HaUpdater(Worker): + @inject + def __init__(self, requests: HaRequests, coordinator: Coordinator, config: Config, time: Time, global_info: GlobalInfo): + self._config = config + super().__init__("Sensor Updater", self.update, time, self.getInterval) + self._time = time + self._coordinator = coordinator + self._requests: HaRequests = requests + self._info = global_info + self._notified = False + self._backoff = Backoff(max=MAX_BACKOFF, base=FIRST_BACKOFF) + self._first_error = None + self._trigger_once = False + + self._last_backup_update = None + self.last_backup_update_time = time.now() - timedelta(days=1) + self._config.subscribe(self.config_updated) + self._last_interval = self.getInterval() + + def config_updated(self): + if self._last_interval != self.getInterval(): + self._wait_event.set() + self._last_interval = self.getInterval() + + def getInterval(self): + return self._config.get(Setting.HA_REPORTING_INTERVAL_SECONDS) + + async def update(self): + try: + if self._config.get(Setting.ENABLE_BACKUP_STALE_SENSOR): + await self._requests.updateBackupStaleSensor('on' if self._stale() else 'off') + if self._config.get(Setting.ENABLE_BACKUP_STATE_SENSOR): + await self._maybeSendBackupUpdate() + if self._config.get(Setting.NOTIFY_FOR_STALE_BACKUPS): + if self._stale() and not self._notified: + if self._info.url is None or len(self._info.url) == 0: + message = NOTIFICATION_DESC_STATIC + else: + message = NOTIFICATION_DESC_LINK.format(self._info.url) + await self._requests.sendNotification(NOTIFICATION_TITLE, message) + self._notified = True + elif not self._stale() and self._notified: + await self._requests.dismissNotification() + self._notified = False + self._backoff.reset() + self._first_error = None + self._trigger_once = False + except ClientResponseError as e: + if self._first_error is None: + self._first_error = self._time.now() + if int(e.status / 100) == 5: + if self._time.now() > self._first_error + timedelta(seconds=NOTIFY_DELAY): + logger.error( + "Unable to reach Home Assistant (HTTP {0}). This is normal if Home Assistant is restarting. You will probably see some errors in the supervisor logs until it comes back online.".format(e.status)) + else: + logger.error("Trouble updating Home Assistant sensors.") + self._last_backup_update = None + await self._time.sleepAsync(self._backoff.backoff(e)) + except Exception as e: + self._last_backup_update = None + logger.error("Trouble updating Home Assistant sensors.") + logger.printException(e) + await self._time.sleepAsync(self._backoff.backoff(e)) + + async def _maybeSendBackupUpdate(self): + update = self._buildBackupUpdate() + if self._trigger_once or update != self._last_backup_update or self._time.now() > self.last_backup_update_time + timedelta(hours=1): + if self._config.get(Setting.CALL_BACKUP_SNAPSHOT): + await self._requests.updateEntity(OLD_BACKUP_ENTITY_NAME, update) + else: + await self._requests.updateEntity(BACKUP_ENTITY_NAME, update) + self._last_backup_update = update + self.last_backup_update_time = self._time.now() + + def _stale(self): + if self._info._first_sync: + return False + if self._info._last_error: + return self._time.now() > self._info._last_success + timedelta(seconds=self._config.get(Setting.BACKUP_STALE_SECONDS)) + else: + next_backup = self._coordinator.nextBackupTime(include_pending=False) + if not next_backup: + # no backups are configured + return False + + # Determine if a lot of time has passed since the last backup "should" have been made. + warn_after = next_backup + timedelta(seconds=self._config.get(Setting.LONG_TERM_STALE_BACKUP_SECONDS)) + return self._time.now() >= warn_after + + def _state(self): + if self._stale(): + return "error" + else: + return "waiting" if self._info._first_sync else "backed_up" + + def triggerRefresh(self): + self._trigger_once = True + + def _buildBackupUpdate(self): + backups = list(filter(lambda s: not s.ignore(), self._coordinator.backups())) + last = "Never" + if len(backups) > 0: + last = max(backups, key=lambda s: s.date()).date().isoformat() + + def makeBackupData(backup: Backup): + return { + "name": backup.name(), + "date": str(backup.date().isoformat()), + "state": backup.status(), + "size": backup.sizeString(), + "slug": backup.slug() + } + ha_backups = list(filter(lambda s: s.getSource(SOURCE_HA) is not None, backups)) + drive_backups = list(filter(lambda s: s.getSource(SOURCE_GOOGLE_DRIVE) is not None, backups)) + + last_uploaded = "Never" + if len(drive_backups) > 0: + last_uploaded = max(drive_backups, key=lambda s: s.date()).date().isoformat() + if self._config.get(Setting.CALL_BACKUP_SNAPSHOT): + return { + "state": self._state(), + "attributes": { + "friendly_name": "Snapshot State", + "last_snapshot": last, # type: ignore + "snapshots_in_google_drive": len(drive_backups), + "snapshots_in_hassio": len(ha_backups), + "snapshots_in_home_assistant": len(ha_backups), + "size_in_google_drive": Estimator.asSizeString(sum(map(lambda v: v.sizeInt(), drive_backups))), + "size_in_home_assistant": Estimator.asSizeString(sum(map(lambda v: v.sizeInt(), ha_backups))), + "snapshots": list(map(makeBackupData, backups)) + } + } + else: + source_metrics = self._coordinator.buildBackupMetrics() + next = self._coordinator.nextBackupTime() + if next is not None: + next = next.isoformat() + attr = { + "friendly_name": "Backup State", + "last_backup": last, # type: ignore + "next_backup": next, + "last_uploaded": last_uploaded, + "backups_in_google_drive": len(drive_backups), + "backups_in_home_assistant": len(ha_backups), + "size_in_google_drive": Estimator.asSizeString(sum(map(lambda v: v.sizeInt(), drive_backups))), + "size_in_home_assistant": Estimator.asSizeString(sum(map(lambda v: v.sizeInt(), ha_backups))), + "backups": list(map(makeBackupData, backups)) + } + if SOURCE_GOOGLE_DRIVE in source_metrics and 'free_space' in source_metrics[SOURCE_GOOGLE_DRIVE]: + attr["free_space_in_google_drive"] = source_metrics[SOURCE_GOOGLE_DRIVE]['free_space'] + else: + attr["free_space_in_google_drive"] = "" + return { + "state": self._state(), + "attributes": attr + } diff --git a/hassio-google-drive-backup/backup/ha/password.py b/hassio-google-drive-backup/backup/ha/password.py new file mode 100644 index 0000000..67ffe00 --- /dev/null +++ b/hassio-google-drive-backup/backup/ha/password.py @@ -0,0 +1,31 @@ +import os + +import yaml + +from ..config import Config, Setting +from ..exceptions import BackupPasswordKeyInvalid +from ..logger import getLogger + +logger = getLogger(__name__) + + +class Password(): + def __init__(self, config: Config): + self.config = config + + def resolve(self, password=None): + if password is None: + password = self.config.get(Setting.BACKUP_PASSWORD) + if len(password) == 0: + return None + if password.startswith("!secret "): + if not os.path.isfile(self.config.get(Setting.SECRETS_FILE_PATH)): + raise BackupPasswordKeyInvalid() + with open(self.config.get(Setting.SECRETS_FILE_PATH)) as f: + secrets_yaml = yaml.load(f, Loader=yaml.SafeLoader) + key = password[len("!secret "):] + if key not in secrets_yaml: + raise BackupPasswordKeyInvalid() + return str(secrets_yaml[key]) + else: + return password diff --git a/hassio-google-drive-backup/backup/logger.py b/hassio-google-drive-backup/backup/logger.py new file mode 100644 index 0000000..a52191d --- /dev/null +++ b/hassio-google-drive-backup/backup/logger.py @@ -0,0 +1,217 @@ +import logging +from logging import LogRecord, Formatter, ERROR +from traceback import TracebackException +from colorlog import ColoredFormatter +from os.path import join, abspath + +HISTORY_SIZE = 1000 +PATH_BASE = abspath(join(__file__, "..", "..")) + +logging.addLevelName(5, "TRACE") +logging.TRACE = 5 + + +class HistoryHandler(logging.Handler): + def __init__(self): + super(HistoryHandler, self).__init__() + self.history = [None] * HISTORY_SIZE + self.history_index = 0 + + def reset(self): + self.history = [None] * HISTORY_SIZE + self.history_index = 0 + + def emit(self, record: LogRecord): + self.history[self.history_index % HISTORY_SIZE] = record + self.history_index += 1 + + def getHistory(self, start=0, html=False): + end = self.history_index + if end - start >= HISTORY_SIZE: + start = end - HISTORY_SIZE + for x in range(start, end): + item = self.history[x % HISTORY_SIZE] + if html: + if item.levelno == logging.WARN: + style = "console-warning" + elif item.levelno == logging.ERROR: + style = "console-error" + elif item.levelno == logging.DEBUG: + style = "console-debug" + elif item.levelno == logging.CRITICAL: + style = "console-critical" + elif item.levelno == logging.FATAL: + style = "console-fatal" + elif item.levelno == logging.WARNING: + style = "console-warning" + elif item.levelno == logging.TRACE: + style = "console-trace" + else: + style = "console-default" + line = "" + self.format(item) + "" + yield (x + 1, line) + else: + yield (x + 1, self.format(item)) + + def getLast(self) -> LogRecord: + return self.history[(self.history_index - 1) % HISTORY_SIZE] + + +CONSOLE = logging.StreamHandler() +CONSOLE.setLevel(logging.INFO) +formatter_color = ColoredFormatter( + '%(log_color)s%(asctime)s %(levelname)s %(message)s%(reset)s', + datefmt='%m-%d %H:%M:%S', + reset=True, + log_colors={ + "DEBUG": "cyan", + "INFO": "green", + "WARNING": "yellow", + "ERROR": "red", + "CRITICAL": "red", + "TRACE": "white", + }, +) +CONSOLE.setFormatter(formatter_color) + +HISTORY = HistoryHandler() +HISTORY.setLevel(logging.DEBUG) +HISTORY.setFormatter(Formatter('%(asctime)s %(levelname)s [%(name)s] %(message)s', '%m-%d %H:%M:%S')) + + +class StandardLogger(logging.Logger): + def __init__(self, name): + super().__init__(name) + self.setLevel(logging.TRACE) + self.addHandler(CONSOLE) + self.addHandler(HISTORY) + + def trace(self, msg, *args, **kwargs): + self.log(logging.TRACE, msg, *args, **kwargs) + + def printException(self, ex: Exception, level=ERROR): + self.log(level, self.formatException(ex)) + + def formatException(self, e: Exception) -> str: + trace = None + if (hasattr(e, "__traceback__")): + trace = e.__traceback__ + tbe = TracebackException(type(e), e, trace, limit=None) + lines = list(self._format(tbe)) + return '\n%s' % ''.join(lines) + + def _format(self, tbe): + if (tbe.__context__ is not None and not tbe.__suppress_context__): + yield from self._format(tbe.__context__) + yield "Whose handling caused:\n" + is_addon, stack = self._formatStack(tbe) + yield from stack + yield from tbe.format_exception_only() + + def _formatStack(self, tbe): + _RECURSIVE_CUTOFF = 3 + result = [] + last_file = None + last_line = None + last_name = None + count = 0 + is_addon = False + buffer = [] + for frame in tbe.stack: + line_internal = True + if (last_file is None or last_file != frame.filename or last_line is None or last_line != frame.lineno or last_name is None or last_name != frame.name): + if count > _RECURSIVE_CUTOFF: + count -= _RECURSIVE_CUTOFF + result.append( + f' [Previous line repeated {count} more ' + f'time{"s" if count > 1 else ""}]\n' + ) + last_file = frame.filename + last_line = frame.lineno + last_name = frame.name + count = 0 + count += 1 + if count > _RECURSIVE_CUTOFF: + continue + fileName = frame.filename + pos = fileName.rfind(PATH_BASE) + if pos >= 0: + is_addon = True + line_internal = False + fileName = "addon" + \ + fileName[pos + len(PATH_BASE):] + + pos = fileName.rfind("site-packages") + if pos > 0: + fileName = fileName[pos - 1:] + + pos = fileName.rfind("python3.7") + if pos > 0: + fileName = fileName[pos - 1:] + pass + line = ' {}:{} ({})\n'.format(fileName, frame.lineno, frame.name) + if line_internal: + buffer.append(line) + else: + result.extend(self._compressFrames(buffer)) + buffer = [] + result.append(line) + if count > _RECURSIVE_CUTOFF: + count -= _RECURSIVE_CUTOFF + result.append( + f' [Previous line repeated {count} more ' + f'time{"s" if count > 1 else ""}]\n' + ) + result.extend(self._compressFrames(buffer)) + return is_addon, result + + def overrideLevel(self, console, history): + CONSOLE.setLevel(console) + HISTORY.setLevel(history) + + def _compressFrames(self, buffer): + if len(buffer) > 1: + yield buffer[0] + if len(buffer) == 3: + yield buffer[1] + elif len(buffer) > 2: + yield " [{} hidden frames]\n".format(len(buffer) - 2) + yield buffer[len(buffer) - 1] + elif len(buffer) > 0: + yield buffer[len(buffer) - 1] + pass + + +def getLogger(name): + return StandardLogger(name) + + +def getHistory(index, html): + return HISTORY.getHistory(index, html) + + +def getLast() -> LogRecord: + return HISTORY.getLast() + + +def reset() -> None: + return HISTORY.reset() + + +class TraceLogger(StandardLogger): + def __init__(self, name): + super().__init__(name) + self.setLevel(logging.TRACE) + + def log(self, lvl, msg, *args, **kwargs): + super().log(logging.TRACE, msg, *args, **kwargs) + + def info(self, *args, **kwargs): + super().log(logging.TRACE, *args, **kwargs) + + def error(self, *args, **kwargs): + super().log(logging.TRACE, *args, **kwargs) + + def warn(self, *args, **kwargs): + super().log(logging.TRACE, *args, **kwargs) diff --git a/hassio-google-drive-backup/backup/model/__init__.py b/hassio-google-drive-backup/backup/model/__init__.py new file mode 100644 index 0000000..ee366f2 --- /dev/null +++ b/hassio-google-drive-backup/backup/model/__init__.py @@ -0,0 +1,13 @@ +# flake8: noqa +from .backupscheme import GenerationalScheme, OldestScheme, GenConfig, BackupScheme +from .coordinator import Coordinator +from .model import BackupSource, BackupDestination, Model +from .syncer import Scyncer +from .backups import AbstractBackup, Backup +from .drivebackup import DriveBackup +from .dummybackup import DummyBackup +from .dummybackupsource import DummyBackupSource +from .habackup import HABackup +from .simulatedsource import SimulatedSource +from .precache import Precache +from .destinationprecache import DestinationPrecache diff --git a/hassio-google-drive-backup/backup/model/backups.py b/hassio-google-drive-backup/backup/model/backups.py new file mode 100644 index 0000000..a2563a8 --- /dev/null +++ b/hassio-google-drive-backup/backup/model/backups.py @@ -0,0 +1,318 @@ + +from datetime import datetime, timedelta +from typing import Dict, Optional +from dateutil.tz import tzutc +from ..util import Estimator + +from ..const import SOURCE_GOOGLE_DRIVE, SOURCE_HA +from ..logger import getLogger +from ..config import CreateOptions + +logger = getLogger(__name__) + +PROP_TYPE = "type" +PROP_VERSION = "version" +PROP_PROTECTED = "protected" +PROP_RETAINED = "retained" +PROP_NOTE = "note" + +DRIVE_KEY_TEXT = "Google Drive's backup metadata" +HA_KEY_TEXT = "Home Assistant's backup metadata" + + +class AbstractBackup(): + def __init__(self, name: str, slug: str, source: str, date: str, size: int, version: str, backupType: str, protected: bool, note=None, retained: bool = False, uploadable: bool = False, details={}, pending=False): + self._options = None + self._name = name + self._slug = slug + self._source = source + self._date = date + self._size = size + self._retained = retained + self._uploadable = uploadable + self._details = details + self._version = version + self._backupType = backupType + self._protected = protected + self._ignore = False + self._note = note + self._pending = pending + + def isPending(self): + return self._pending + + def setOptions(self, options: CreateOptions): + self._options = options + + def getOptions(self) -> CreateOptions: + return self._options + + def name(self) -> str: + return self._name + + def slug(self) -> str: + return self._slug + + def size(self) -> int: + return self._size + + def note(self) -> str: + return self._note + + def sizeInt(self) -> int: + try: + return int(self.size()) + except ValueError: + return 0 + + def date(self) -> datetime: + return self._date + + def source(self) -> str: + return self._source + + def retained(self) -> str: + return self._retained + + def version(self): + return self._version + + def backupType(self): + return self._backupType + + def protected(self): + return self._protected + + def setRetained(self, retained): + self._retained = retained + + def uploadable(self) -> bool: + return self._uploadable + + def considerForPurge(self) -> bool: + return not self.retained() + + def setUploadable(self, uploadable): + self._uploadable = uploadable + + def details(self): + return self._details + + def setNote(self, note: str): + self._note = note + + def status(self): + return None + + def madeByTheAddon(self): + return True + + def ignore(self): + return self._ignore + + def setIgnore(self, ignore): + self._ignore = ignore + + +class Backup(object): + """ + Represents a Home Assistant backup stored on Google Drive, locally in + Home Assistant, or a pending backup we expect to see show up later + """ + + def __init__(self, backup: Optional[AbstractBackup] = None): + self.sources: Dict[str, AbstractBackup] = {} + self._purgeNext: Dict[str, bool] = {} + self._options = None + self._status_override = None + self._status_override_args = None + self._state_detail = None + self._upload_source = None + self._upload_source_name = None + self._upload_fail_info = None + if backup is not None: + self.addSource(backup) + + def setOptions(self, options): + self._options = options + + def getOptions(self): + return self._options + + def updatePurge(self, source: str, purge: bool): + self._purgeNext[source] = purge + + def addSource(self, backup: AbstractBackup): + self.sources[backup.source()] = backup + if backup.getOptions() and not self.getOptions(): + self.setOptions(backup.getOptions()) + + def getStatusDetail(self): + return self._state_detail + + def setStatusDetail(self, info): + self._state_detail = info + + def removeSource(self, source): + if source in self.sources: + del self.sources[source] + if source in self._purgeNext: + del self._purgeNext[source] + + def getPurges(self): + return self._purgeNext + + def uploadInfo(self): + if not self._upload_source: + return {} + elif self._upload_source.progress() == 100: + return {} + else: + return { + 'progress': self._upload_source.progress() + } + + def getSource(self, source: str): + return self.sources.get(source, None) + + def name(self): + for backup in self.sources.values(): + return backup.name() + return "error" + + def note(self): + longest = None + for backup in self.sources.values(): + if backup.note() is not None and (longest is None or len(backup.note()) > len(longest)): + longest = backup.note() + return longest + + def slug(self) -> str: + for backup in self.sources.values(): + return backup.slug() + return "error" + + def size(self) -> int: + for backup in self.sources.values(): + return backup.size() + return 0 + + def sizeInt(self) -> int: + for backup in self.sources.values(): + return backup.sizeInt() + return 0 + + def backupType(self) -> str: + for backup in self.sources.values(): + return backup.backupType() + return "error" + + def version(self) -> str: + for backup in self.sources.values(): + if backup.version() is not None: + return backup.version() + return None + + def details(self): + for backup in self.sources.values(): + if backup.details() is not None: + return backup.details() + return {} + + def getUploadInfo(self, time): + if self._upload_source_name is None: + return None + ret = { + 'name': self._upload_source_name + } + if self._upload_fail_info: + ret['failure'] = self._upload_fail_info + elif self._upload_source is not None: + ret['progress'] = self._upload_source.progress() + ret['speed'] = self._upload_source.speed(timedelta(seconds=20)) + ret['total'] = self._upload_source.position() + ret['started'] = time.formatDelta(self._upload_source.startTime()) + return ret + + def protected(self) -> bool: + for backup in self.sources.values(): + return backup.protected() + return False + + def ignore(self) -> bool: + for backup in self.sources.values(): + if not backup.ignore(): + return False + return True + + def date(self) -> datetime: + for backup in self.sources.values(): + return backup.date() + return datetime.now(tzutc()) + + def sizeString(self) -> str: + size_string = self.size() + if type(size_string) == str: + return size_string + return Estimator.asSizeString(size_string) + + def status(self) -> str: + # TODO: Drive Specific + if self._status_override is not None: + return self._status_override.format(*self._status_override_args) + + for backup in self.sources.values(): + status = backup.status() + if status: + return status + + inDrive = self.getSource(SOURCE_GOOGLE_DRIVE) is not None + inHa = self.getSource(SOURCE_HA) is not None + + if inDrive and inHa: + return "Backed Up" + if inDrive: + return "Drive Only" + if inHa: + return "HA Only" + return "Deleted" + + def isDeleted(self) -> bool: + return len(self.sources) == 0 + + def overrideStatus(self, format, *args) -> None: + self._status_override = format + self._status_override_args = args + + def setUploadSource(self, source_name: str, source): + self._upload_source = source + self._upload_source_name = source_name + self._upload_fail_info = None + + def clearUploadSource(self): + self._upload_source = None + self._upload_source_name = None + self._upload_fail_info = None + + def uploadFailure(self, info): + self._upload_source = None + self._upload_fail_info = info + + def clearStatus(self): + self._status_override = None + self._status_override_args = None + + def isPending(self): + for backup in self.sources.values(): + if backup.isPending(): + return True + return False + + def __str__(self) -> str: + return "".format(self.slug(), " ".join(self.sources), self.date().isoformat()) + + def __format__(self, format_spec: str) -> str: + return self.__str__() + + def __repr__(self) -> str: + return self.__str__() diff --git a/hassio-google-drive-backup/backup/model/backupscheme.py b/hassio-google-drive-backup/backup/model/backupscheme.py new file mode 100644 index 0000000..0d0511f --- /dev/null +++ b/hassio-google-drive-backup/backup/model/backupscheme.py @@ -0,0 +1,236 @@ +from abc import ABC, abstractmethod +from calendar import monthrange +from datetime import datetime, timedelta, date +from typing import List, Optional, Sequence, Set, Tuple, Any, Union + +from .backups import Backup +from backup.util import RangeLookup +from ..time import Time +from ..config import GenConfig +from ..logger import getLogger +logger = getLogger(__name__) + + +class BackupScheme(ABC): + def __init__(self): + pass + + @abstractmethod + def getOldest(self, backups: Sequence[Backup]) -> Tuple[str, Optional[Backup]]: + pass + + def handleNaming(self, backups: Sequence[Backup]) -> None: + for backup in backups: + backup.setStatusDetail(None) + + +class DeleteAfterUploadScheme(BackupScheme): + def __init__(self, source: str, destinations: List[str]): + self.source = source + self.destinations = destinations + + def getOldest(self, backups: List[Backup]): + consider = [] + for backup in backups: + uploaded = True + if backup.getSource(self.source) is None: + # No source, so ignore it + uploaded = False + for destination in self.destinations: + if backup.getSource(destination) is None: + # its not in destination, so ignore it + uploaded = False + if uploaded: + consider.append(backup) + + # Delete the oldest first + return OldestScheme().getOldest(consider) + + +class OldestScheme(BackupScheme): + def __init__(self, count=0): + self.count = count + + def getOldest(self, backups: Sequence[Backup]) -> Tuple[Any, Union[Backup, None]]: + if len(backups) <= self.count: + return None, None + return "default", min(backups, default=None, key=lambda s: s.date()) + + def handleNaming(self, backups: Sequence[Backup]) -> None: + for backup in backups: + backup.setStatusDetail(None) + + +class Partition(object): + def __init__(self, start: datetime, end: datetime, prefer: datetime, time: Time, details=None, delete_only: bool = False): + self.start: datetime = start + self.end: datetime = end + self.prefer: datetime = prefer + self.time = time + self.details = details + self.selected = None + self._delete_only_partitions = delete_only + + def select(self, backups: List[Backup]) -> Optional[Backup]: + options = list(RangeLookup(backups, lambda s: s.date()).matches(self.start, self.end - timedelta(milliseconds=1))) + + searcher = lambda s: self.day(s.date()) == self.day(self.prefer) + + preferred = list(filter(searcher, options)) + if len(preferred) > 0: + # If there is a backup on the "preferred" day, then use the latest backup on that day + self.selected = max(preferred, default=None, key=Backup.date) + else: + # Otherwise, use the earliest backup over the valid period. + self.selected = min(options, default=None, key=Backup.date) + return self.selected + + def delta(self) -> timedelta: + return self.end - self.start + + def day(self, date: datetime): + # TODO: this conversion isn't time-zone safe, but is ok because we only use it to compare local day to local day. + local = self.time.toLocal(date) + return datetime(day=local.day, month=local.month, year=local.year) + + # True if the partition exists only to determine why a snapshot is getting deleted. + @property + def is_delete_only(self): + return self._delete_only_partitions + + def __hash__(self): + """Overrides the default implementation""" + return hash(tuple(sorted(self.__dict__.items()))) + + +class GenerationalScheme(BackupScheme): + def __init__(self, time: Time, config: GenConfig, count=0): + self.count = count + self.time: Time = time + self.config = config + + def _buildPartitions(self, backups_input): + backups: List[Backup] = list(backups_input) + + # build the list of dates we should partition by + day_of_week = 3 + weekday_lookup = { + 'mon': 0, + 'tue': 1, + 'wed': 2, + 'thu': 3, + 'fri': 4, + 'sat': 5, + 'sun': 6, + } + if self.config.day_of_week in weekday_lookup: + day_of_week = weekday_lookup[self.config.day_of_week] + + last = self.time.toLocal(backups[len(backups) - 1].date()) + lookups: List[Partition] = [] + currentDay = self.day(last) + if self.config.days > 0: + for x in range(0, self.config.days + 1): + nextDay = self.day(currentDay, add_days=1) + lookups.append( + Partition(currentDay, nextDay, currentDay, self.time, "Day {0} of {1}".format(x + 1, self.config.days), delete_only=(x >= self.config.days))) + currentDay = self.day(currentDay, add_days=-1) + + if self.config.weeks > 0: + for x in range(0, self.config.weeks + 1): + # Start at the first monday preceeding the last backup + start = self.time.local(last.year, last.month, last.day) + start = self.day(start, add_days=-1 * start.weekday()) + + # Move back x weeks + start = self.day(start, add_days=-7 * x) + end = self.day(start, add_days=7) + + # Only consider backups from that week after the start day + # TODO: should this actually "prefer" the day of week but start on monday? + start = self.day(start, add_days=day_of_week) + lookups.append(Partition(start, end, start, self.time, "Week {0} of {1}".format(x + 1, self.config.weeks), delete_only=(x >= self.config.weeks))) + + if self.config.months > 0: + for x in range(0, self.config.months + 1): + year_offset = int(x / 12) + month_offset = int(x % 12) + if last.month - month_offset < 1: + year_offset = year_offset + 1 + month_offset = month_offset - 12 + start = self.time.local( + last.year - year_offset, last.month - month_offset, 1) + weekday, days = monthrange(start.year, start.month) + end = start + timedelta(days=days) + lookups.append(Partition( + start, end, start + timedelta(days=self.config.day_of_month - 1), self.time, + "{0} ({1} of {2} months)".format(start.strftime("%B"), x + 1, self.config.months), delete_only=(x >= self.config.months))) + + if self.config.years > 0: + for x in range(0, self.config.years + 1): + start = self.time.local(last.year - x, 1, 1) + end = self.time.local(last.year - x + 1, 1, 1) + lookups.append(Partition( + start, end, start + timedelta(days=self.config.day_of_year - 1), self.time, + "{0} ({1} of {2} years)".format(start.strftime("%Y"), x + 1, self.config.years), delete_only=(x >= self.config.years))) + + # Keep track of which backups are being saved for which time period. + for lookup in lookups: + lookup.select(backups) + return lookups + + def getOldest(self, backups: Sequence[Backup]): + if len(backups) == 0: + return None, None + + sorted = list(backups) + sorted.sort(key=lambda s: s.date()) + + partitions = self._buildPartitions(sorted) + keepers: Set[Backup] = set() + for part in partitions: + if part.selected is not None and not part.is_delete_only: + keepers.add(part.selected) + + extras = [] + for backup in sorted: + if backup not in keepers: + extras.append(backup) + + if self.config.aggressive and len(extras) > 0: + match = min(filter(lambda p: p.selected == extras[0], partitions), key=Partition.delta, default=None) + if match is not None: + return match, extras[0] + return "default", extras[0] + + if len(sorted) <= self.count and not self.config.aggressive: + return "default", None + elif (self.config.aggressive or len(sorted) > self.count) and len(extras) > 0: + return "default", min(extras, default=None, key=lambda s: s.date()) + elif len(sorted) > self.count: + # no non-keep is invalid, so delete the oldest keeper + return "default", min(keepers, default=None, key=lambda s: s.date()) + return None, None + + def handleNaming(self, backups: Sequence[Backup]) -> None: + sorted = list(backups) + sorted.sort(key=lambda s: s.date()) + for backup in sorted: + backup.setStatusDetail(None) + # Ignored snapshots should have their label cleared in case + # it was added previosuly, but should not get new labels + unignored = list(filter(lambda s: not s.ignore(), sorted)) + + if len(unignored) == 0: + return + for part in self._buildPartitions(unignored): + if part.selected is not None: + if part.selected.getStatusDetail() is None: + part.selected.setStatusDetail([]) + part.selected.getStatusDetail().append(part.details) + + def day(self, utc_datetime: datetime, add_days=0): + local = self.time.toLocal(utc_datetime) + + local_date = date.fromordinal(date(local.year, local.month, local.day).toordinal() + add_days) + return self.time.localize(datetime(local_date.year, local_date.month, local_date.day, 0, 0)) diff --git a/hassio-google-drive-backup/backup/model/coordinator.py b/hassio-google-drive-backup/backup/model/coordinator.py new file mode 100644 index 0000000..6efb324 --- /dev/null +++ b/hassio-google-drive-backup/backup/model/coordinator.py @@ -0,0 +1,354 @@ +from asyncio import CancelledError, Task, create_task, wait, Event +from datetime import timedelta +from threading import Lock +from typing import Dict, List + +from injector import inject, singleton + +from backup.config import Config, Setting, CreateOptions, DurationParser +from backup.exceptions import (KnownError, LogicError, NoBackup, PleaseWait, + UserCancelledError) +from backup.util import GlobalInfo, Backoff, Estimator +from backup.time import Time +from backup.worker import Trigger +from backup.logger import getLogger +from backup.creds.creds import Creds +from .precache import Precache +from .model import BackupSource, Model +from .backups import AbstractBackup, Backup, SOURCE_HA +from random import Random + +logger = getLogger(__name__) + + +@singleton +class Coordinator(Trigger): + @inject + def __init__(self, model: Model, time: Time, config: Config, global_info: GlobalInfo, estimator: Estimator): + super().__init__() + self._model = model + self._precache: Precache = None + self._time = time + self._config = config + self._lock: Lock = Lock() + self._global_info: GlobalInfo = global_info + self._sources: Dict[str, BackupSource] = { + self._model.source.name(): self._model.source, + self._model.dest.name(): self._model.dest + } + self._backoff = Backoff(initial=0, base=10, max=config.get(Setting.MAX_BACKOFF_SECONDS)) + self._estimator = estimator + self._busy = False + self._sync_task: Task = None + self._sync_start = Event() + self._sync_wait = Event() + self._sync_wait.set() + self._random = Random() + self._random.seed() + self._next_sync_offset = self._random.random() + self._global_info.triggerBackupCooldown(timedelta(minutes=self._config.get(Setting.BACKUP_STARTUP_DELAY_MINUTES))) + self.trigger() + + def saveCreds(self, creds: Creds): + if not self._model.dest.enabled(): + # Since this is the first time saving credentials (eg the addon was just enabled). Hold off on + # automatic backups for a few minutes to give the user a little while to figure out whats going on. + self._global_info.triggerBackupCooldown(timedelta(minutes=self._config.get(Setting.BACKUP_STARTUP_DELAY_MINUTES))) + + self._model.dest.saveCreds(creds) + self._global_info.credsSaved() + + def setPrecache(self, precache: Precache): + self._precache = precache + + def name(self): + return "Coordinator" + + def enabled(self) -> bool: + return self._model.enabled() + + def isWaitingForStartup(self): + return self._model.waiting_for_startup + + def ignoreStartupDelay(self): + self._model.ignore_startup_delay = True + + async def check(self) -> bool: + if self._time.now() >= self.nextSyncAttempt(): + self.reset() + return True + else: + return await super().check() + + async def sync(self): + await self._withSoftLock(lambda: self._sync_wrapper()) + + def isSyncing(self): + task = self._sync_task + return task is not None and not task.done() + + def isWorkingThroughUpload(self): + return self.isSyncing() and self._model.isWorkingThroughUpload() + + async def waitForSyncToFinish(self): + task = self._sync_task + if task is not None: + await task + + async def cancel(self): + task = self._sync_task + if task is not None and not task.done(): + task.cancel() + self.clearCaches() + await wait([task]) + + def nextSyncAttempt(self): + if self._global_info._last_error is not None: + # we had an error last + failure = self._global_info._last_failure_time + if failure is None: + return self._time.now() - timedelta(minutes=1) + return failure + timedelta(seconds=self._backoff.peek()) + else: + scheduled = self._global_info._last_success + if scheduled is None: + scheduled = self._time.now() - timedelta(minutes=1) + else: + scheduled += timedelta(seconds=self.nextSyncCheckOffset()) + next_backup = self.nextBackupTime() + if next_backup is None: + return scheduled + else: + return min(self.nextBackupTime(), scheduled) + + def nextSyncCheckOffset(self): + """Determines how long we shoudl wait from the last check the refresh the cache of backups from Google Drive and Home Assistant""" + # If we always sync MAX_SYNC_INTERVAL_SECONDS secodns after the last + # check, then the addon in aggregate puts a really high strain on google + # on every hour and the addon's auth servers need to be provisioned for + # a big peak, which is epxensive. Instead we add some randomness to the time interval. + randomness_max = self._config.get(Setting.MAX_SYNC_INTERVAL_SECONDS) * self._config.get(Setting.DEFAULT_SYNC_INTERVAL_VARIATION) + non_randomness = self._config.get(Setting.MAX_SYNC_INTERVAL_SECONDS) - randomness_max + + # The offset should be stable between syncs, which gets controlled by updating _next_sync_offset on each good sync + return self._next_sync_offset * randomness_max + non_randomness + + def nextBackupTime(self, include_pending=True): + return self._buildModel().nextBackup(self._time.now(), include_pending) + + def buildBackupMetrics(self): + info = {} + for source in self._sources: + source_class = self._sources[source] + source_info = { + 'backups': 0, + 'retained': 0, + 'deletable': 0, + 'name': source, + 'title': source_class.title(), + 'latest': None, + 'max': source_class.maxCount(), + 'enabled': source_class.enabled(), + 'icon': source_class.icon(), + 'ignored': 0, + 'detail': source_class.detail() + } + size = 0 + ignored_size = 0 + latest = None + for backup in self.backups(): + data: AbstractBackup = backup.getSource(source) + if data is None: + continue + if data.ignore() and backup.ignore(): + source_info['ignored'] += 1 + if backup.ignore(): + ignored_size += backup.size() + continue + source_info['backups'] += 1 + if data.retained(): + source_info['retained'] += 1 + else: + source_info['deletable'] += 1 + if latest is None or data.date() > latest: + latest = data.date() + size += int(data.sizeInt()) + if latest is not None: + source_info['latest'] = self._time.asRfc3339String(latest) + source_info['size'] = Estimator.asSizeString(size) + source_info['ignored_size'] = Estimator.asSizeString(ignored_size) + free_space = source_class.freeSpace() + if free_space is not None: + source_info['free_space'] = Estimator.asSizeString(free_space) + info[source] = source_info + return info + + async def _sync_wrapper(self): + self._sync_task = create_task( + self._sync(), name="Internal sync worker") + await wait([self._sync_task]) + + async def _sync(self): + try: + self._sync_start.set() + await self._sync_wait.wait() + logger.info("Syncing Backups") + self._global_info.sync() + self._estimator.refresh() + await self._buildModel().sync(self._time.now()) + self._next_sync_offset = self._random.random() + self._global_info.success() + self._backoff.reset() + self._global_info.setSkipSpaceCheckOnce(False) + except BaseException as e: + self.handleError(e) + finally: + if self._precache: + # Any sync should invalidate the precache regardless of the outcome + # so the next sync uses fresh data + self.clearCaches() + self._updateFreshness() + + def handleError(self, e): + if isinstance(e, CancelledError): + e = UserCancelledError() + if isinstance(e, KnownError): + known: KnownError = e + logger.error(known.message()) + if known.retrySoon(): + self._backoff.backoff(e) + else: + self._backoff.maxOut() + else: + logger.printException(e) + self._backoff.backoff(e) + self._global_info.failed(e) + + text = DurationParser().format(timedelta(seconds=self._backoff.peek())) + logger.info("I'll try again in {0}".format(text)) + + def backups(self) -> List[Backup]: + ret = list(self._model.backups.values()) + ret.sort(key=lambda s: s.date()) + return ret + + async def uploadBackups(self, slug): + await self._withSoftLock(lambda: self._uploadBackup(slug)) + + async def _uploadBackup(self, slug): + self.clearCaches() + backup = self._ensureBackup(self._model.dest.name(), slug) + backup_dest = backup.getSource(self._model.dest.name()) + backup_source = backup.getSource(self._model.source.name()) + if backup_source: + raise LogicError("This backup already exists in Home Assistant") + if not backup_dest: + # Unreachable? + raise LogicError("This backup isn't in Google Drive") + created = await self._model.source.save(backup, await self._model.dest.read(backup)) + backup.addSource(created) + self._updateFreshness() + + async def startBackup(self, options: CreateOptions): + return await self._withSoftLock(lambda: self._startBackup(options)) + + async def _startBackup(self, options: CreateOptions): + self.clearCaches() + self._estimator.refresh() + self._estimator.checkSpace(self.backups()) + created = await self._buildModel().source.create(options) + backup = Backup(created) + self._model.backups[backup.slug()] = backup + self._updateFreshness() + self._estimator.refresh() + return backup + + def getBackup(self, slug): + return self._ensureBackup(None, slug) + + async def download(self, slug): + self.clearCaches() + backup = self._ensureBackup(None, slug) + for source in self._sources.values(): + if not source.enabled(): + continue + if backup.getSource(source.name()): + return await source.read(backup) + raise NoBackup() + + async def retain(self, sources: Dict[str, bool], slug: str): + self.clearCaches() + for source in sources: + backup = self._ensureBackup(source, slug) + await self._ensureSource(source).retain(backup, sources[source]) + self._updateFreshness() + + async def note(self, note: str, slug: str): + self.clearCaches() + backup = self._ensureBackup(None, slug) + for source in backup.sources.keys(): + await self._ensureSource(source).note(backup, note) + + async def delete(self, sources, slug): + await self._withSoftLock(lambda: self._delete(sources, slug)) + + async def ignore(self, slug: str, ignore: bool): + await self._withSoftLock(lambda: self._ignore(slug, ignore)) + + async def _delete(self, sources, slug): + self.clearCaches() + for source in sources: + backup = self._ensureBackup(source, slug) + await self._ensureSource(source).delete(backup) + if backup.isDeleted(): + del self._model.backups[slug] + self._updateFreshness() + + async def _ignore(self, slug: str, ignore: bool): + self.clearCaches() + backup = self._ensureBackup(SOURCE_HA, slug) + await self._ensureSource(SOURCE_HA).ignore(backup, ignore) + + def _ensureBackup(self, source: str = None, slug=None) -> Backup: + backup = self._buildModel().backups.get(slug) + if not backup: + raise NoBackup() + if not source: + return backup + if not source: + return backup + if not backup.getSource(source): + raise NoBackup() + return backup + + def _ensureSource(self, source): + ret = self._sources.get(source) + if ret and ret.enabled(): + return ret + raise LogicError() + + def _buildModel(self) -> Model: + self._model.reinitialize(self._precache) + return self._model + + def _updateFreshness(self): + purges = self._buildModel().getNextPurges() + for backup in self._model.backups.values(): + for source in purges: + if backup.getSource(source): + backup.updatePurge(source, backup == purges[source]) + + def clearCaches(self): + if self._precache: + self._precache.clear() + + async def _withSoftLock(self, callable): + with self._lock: + if self._busy: + raise PleaseWait() + self._busy = True + try: + return await callable() + finally: + with self._lock: + self._busy = False diff --git a/hassio-google-drive-backup/backup/model/destinationprecache.py b/hassio-google-drive-backup/backup/model/destinationprecache.py new file mode 100644 index 0000000..d45ad85 --- /dev/null +++ b/hassio-google-drive-backup/backup/model/destinationprecache.py @@ -0,0 +1,81 @@ +from .coordinator import Coordinator +from backup.worker import Worker +from injector import inject, singleton +from backup.time import Time +from backup.logger import getLogger +from backup.config import Config, Setting +from .model import BackupDestination +from .precache import Precache +from random import Random +from datetime import datetime, timedelta +from dataclasses import dataclass +from typing import Any, Dict +from logging import DEBUG + +logger = getLogger(__name__) + + +@dataclass +class CacheItem: + """Class for keeping track of an item in inventory.""" + valid_until: datetime + data: Any + + +@singleton +class DestinationPrecache(Worker, Precache): + @inject + def __init__(self, coord: Coordinator, time: Time, dest: BackupDestination, config: Config): + super().__init__("Traffic Smoothing Cache", self.checkForSmoothing, time, 60) + self._config = config + self._coord = coord + self._dest = dest + self._offset = Random().random() + self._cache: Dict[str, CacheItem] = {} + self._last_error: datetime = None + + async def checkForSmoothing(self): + if self._config.get(Setting.CACHE_WARMUP_MAX_SECONDS) == 0: + # disable cache warmup + return + try: + self._coord.setPrecache(self) + nextSync = self._coord.nextSyncAttempt() + now = self._time.now() + if nextSync <= now: + # No reason to warm the cache if we should sync right now anyway + return + if self.cached(self._dest.name(), now): + # A value is already cached, so don't do anything + return + if now >= self.getNextWarmDate(): + # Warm the cache + logger.debug("Preemptively retrieving and caching info from the backup destination to avoid peak demand") + data = await self._dest.get() + validity = nextSync + timedelta(minutes=1) + self._cache[self._dest.name()] = CacheItem(validity, data) + self._offset = Random().random() + except Exception as e: + # Any error should make us avoid precaching for a solid day. + logger.debug("Unable to precache data from backup destination") + logger.printException(e, level=DEBUG) + self._offset = Random().random() + if self._config.get(Setting.CACHE_WARMUP_ERROR_TIMEOUT_SECONDS) != 0: + self._last_error = self._time.now() + + def getNextWarmDate(self): + warm_date = self._coord.nextSyncAttempt() - timedelta(seconds=self._config.get(Setting.CACHE_WARMUP_MAX_SECONDS) * self._offset) + if self._last_error: + return max(warm_date, self._last_error + timedelta(self._config.get(Setting.CACHE_WARMUP_ERROR_TIMEOUT_SECONDS))) + return warm_date + + def cached(self, source: str, date: datetime) -> Any: + cached = self._cache.get(source) + if cached and cached.valid_until >= date: + return cached.data + return None + + def clear(self): + """Clears any precached data""" + self._cache = {} + self._offset = Random().random() diff --git a/hassio-google-drive-backup/backup/model/drivebackup.py b/hassio-google-drive-backup/backup/model/drivebackup.py new file mode 100644 index 0000000..1e22704 --- /dev/null +++ b/hassio-google-drive-backup/backup/model/drivebackup.py @@ -0,0 +1,74 @@ +from .backups import AbstractBackup +from typing import Any, Dict + +from ..const import SOURCE_GOOGLE_DRIVE, NECESSARY_PROP_KEY_SLUG, NECESSARY_PROP_KEY_DATE, NECESSARY_PROP_KEY_NAME, PROP_NOTE +from ..exceptions import ensureKey +from ..config import BoolValidator +from ..time import Time +from ..logger import getLogger + +logger = getLogger(__name__) + +PROP_TYPE = "type" +PROP_VERSION = "version" +PROP_PROTECTED = "protected" +PROP_RETAINED = "retained" +DRIVE_KEY_TEXT = "Google Drive's backup metadata" + + +class DriveBackup(AbstractBackup): + + """ + Represents a Home Assistant backup stored on Google Drive + """ + + def __init__(self, data: Dict[Any, Any]): + props = ensureKey('appProperties', data, DRIVE_KEY_TEXT) + retained = BoolValidator.strToBool(props.get(PROP_RETAINED, "False")) + if NECESSARY_PROP_KEY_NAME in props: + backup_name = ensureKey(NECESSARY_PROP_KEY_NAME, props, DRIVE_KEY_TEXT) + else: + backup_name = data['name'].replace(".tar", "") + super().__init__( + name=backup_name, + slug=ensureKey(NECESSARY_PROP_KEY_SLUG, props, DRIVE_KEY_TEXT), + date=Time.parse( + ensureKey(NECESSARY_PROP_KEY_DATE, props, DRIVE_KEY_TEXT)), + size=int(ensureKey("size", data, DRIVE_KEY_TEXT)), + source=SOURCE_GOOGLE_DRIVE, + backupType=props.get(PROP_TYPE, "?"), + version=props.get(PROP_VERSION, None), + protected=BoolValidator.strToBool(props.get(PROP_PROTECTED, "?")), + retained=retained, + uploadable=False, + details=None, + note=props.get(PROP_NOTE, None), + pending=False) + self._drive_data = data + self._id = ensureKey('id', data, DRIVE_KEY_TEXT) + + def id(self) -> str: + return self._id + + def canDeleteDirectly(self) -> str: + caps = self._drive_data.get("capabilities", {}) + if caps.get('canDelete', False): + return True + + # check if the item is in a shared drive + sharedId = self._drive_data.get("driveId") + if sharedId and len(sharedId) > 0 and caps.get("canTrash", False): + # Its in a shared drive and trashable, so trash won't exhaust quota + return False + + # We aren't certain we can trash or delete, so just make a try at deleting. + return True + + def __str__(self) -> str: + return "".format(self.slug(), self.name(), self.id()) + + def __format__(self, format_spec: str) -> str: + return self.__str__() + + def __repr__(self) -> str: + return self.__str__() diff --git a/hassio-google-drive-backup/backup/model/dummybackup.py b/hassio-google-drive-backup/backup/model/dummybackup.py new file mode 100644 index 0000000..7adc180 --- /dev/null +++ b/hassio-google-drive-backup/backup/model/dummybackup.py @@ -0,0 +1,26 @@ +from .backups import Backup +from .dummybackupsource import DummyBackupSource +from ..logger import getLogger + +logger = getLogger(__name__) + + +class DummyBackup(Backup): + def __init__(self, name, date, source, slug, size=0, ignore=False, note=None): + super().__init__(None) + self._size = size + self._ignore = ignore + self._note = note + self.addSource(DummyBackupSource(name, date, source, slug)) + + def size(self): + return self._size + + def ignore(self): + return self._ignore + + def note(self): + if self._note is not None: + return self._note + else: + return super().note() diff --git a/hassio-google-drive-backup/backup/model/dummybackupsource.py b/hassio-google-drive-backup/backup/model/dummybackupsource.py new file mode 100644 index 0000000..fd51628 --- /dev/null +++ b/hassio-google-drive-backup/backup/model/dummybackupsource.py @@ -0,0 +1,20 @@ +from .backups import AbstractBackup +from ..logger import getLogger + +logger = getLogger(__name__) + + +class DummyBackupSource(AbstractBackup): + def __init__(self, name, date, source, slug, retain=False): + super().__init__( + name=name, + slug=slug, + date=date, + size=0, + source=source, + backupType="dummy", + version="dummy_version", + protected=True, + retained=retain, + uploadable=True, + details={}) diff --git a/hassio-google-drive-backup/backup/model/habackup.py b/hassio-google-drive-backup/backup/model/habackup.py new file mode 100644 index 0000000..51108af --- /dev/null +++ b/hassio-google-drive-backup/backup/model/habackup.py @@ -0,0 +1,72 @@ +from typing import Any, Dict + +from backup.const import SOURCE_HA +from backup.exceptions import ensureKey +from backup.time import Time +from .backups import AbstractBackup +from backup.logger import getLogger +from backup.util import DataCache, KEY_I_MADE_THIS, KEY_IGNORE, KEY_NOTE +from backup.config import Config, Setting + +logger = getLogger(__name__) + +HA_KEY_TEXT = "Home Assistant's backup metadata" + + +class HABackup(AbstractBackup): + """ + Represents a Home Assistant backup stored locally in Home Assistant + """ + + def __init__(self, data: Dict[str, Any], data_cache: DataCache, config: Config, retained=False): + super().__init__( + name=ensureKey('name', data, HA_KEY_TEXT), + slug=ensureKey('slug', data, HA_KEY_TEXT), + date=Time.parse(ensureKey('date', data, HA_KEY_TEXT)), + size=float(ensureKey("size", data, HA_KEY_TEXT)) * 1024 * 1024, + source=SOURCE_HA, + backupType=ensureKey('type', data, HA_KEY_TEXT), + version=ensureKey('homeassistant', data, HA_KEY_TEXT), + protected=ensureKey('protected', data, HA_KEY_TEXT), + retained=retained, + uploadable=True, + details=data, + pending=False) + self._data_cache = data_cache + self._config = config + + def madeByTheAddon(self): + return self._data_cache.backup(self.slug()).get(KEY_I_MADE_THIS, False) + + def note(self): + parent = super().note() + if parent is None: + return self._data_cache.backup(self.slug()).get(KEY_NOTE, None) + else: + return parent + + def ignore(self): + override = self._data_cache.backup(self.slug()).get(KEY_IGNORE, None) + if override is not None: + return override + if self.madeByTheAddon(): + return False + if self._config.get(Setting.IGNORE_OTHER_BACKUPS): + return True + archive_count = len(self.details().get("addons", [])) + len(self.details().get("folders", [])) + if self.details().get("homeassistant", None) is not None: + # Supervisor backup query API doesn't quite match the create API, if the HA config folder + # is present in a backup then the Home Assistant version is present in its details + archive_count += 1 + if archive_count == 1 and self._config.get(Setting.IGNORE_UPGRADE_BACKUPS): + return True + return super().ignore() + + def __str__(self) -> str: + return "".format(self.slug(), self.name(), self.date().isoformat()) + + def __format__(self, format_spec: str) -> str: + return self.__str__() + + def __repr__(self) -> str: + return self.__str__() diff --git a/hassio-google-drive-backup/backup/model/model.py b/hassio-google-drive-backup/backup/model/model.py new file mode 100644 index 0000000..e678738 --- /dev/null +++ b/hassio-google-drive-backup/backup/model/model.py @@ -0,0 +1,397 @@ +from datetime import datetime, timedelta, date +from io import IOBase +from typing import Dict, Generic, List, Optional, Tuple, TypeVar + +from injector import inject, singleton + +from .backupscheme import GenerationalScheme, OldestScheme, DeleteAfterUploadScheme +from backup.config import Config, Setting, CreateOptions +from backup.exceptions import DeleteMutlipleBackupsError, SimulatedError +from backup.util import GlobalInfo, Estimator, DataCache +from .backups import AbstractBackup, Backup +from .dummybackup import DummyBackup +from .precache import Precache +from backup.time import Time +from backup.worker import Trigger +from backup.logger import getLogger + +logger = getLogger(__name__) + +T = TypeVar('T') + + +class BackupSource(Trigger, Generic[T]): + def __init__(self): + super().__init__() + pass + + def name(self) -> str: + return "Unnamed" + + def title(self) -> str: + return "Default" + + def enabled(self) -> bool: + return True + + def needsConfiguration(self) -> bool: + return not self.enabled() + + def upload(self) -> bool: + return True + + def icon(self) -> str: + return "sd_card" + + def freeSpace(self): + return None + + async def create(self, options: CreateOptions) -> T: + pass + + async def get(self) -> Dict[str, T]: + pass + + async def delete(self, backup: T): + pass + + async def ignore(self, backup: T, ignore: bool): + pass + + async def save(self, backup: AbstractBackup, bytes: IOBase) -> T: + pass + + async def read(self, backup: T) -> IOBase: + pass + + async def retain(self, backup: T, retain: bool) -> None: + pass + + async def note(self, backup, note: str) -> None: + pass + + def maxCount(self) -> None: + return 0 + + def postSync(self) -> None: + return + + def detail(self) -> str: + return "" + + def isDestination(self) -> bool: + return False + + # Gets called after reading state but before any changes are made + # to check for additional errors. + def checkBeforeChanges(self) -> None: + pass + + +class BackupDestination(BackupSource): + def isWorking(self): + return False + + @property + def might_be_oob_creds(self) -> bool: + return False + + def isDestination(self) -> bool: + return True + + +@singleton +class Model(): + @inject + def __init__(self, config: Config, time: Time, source: BackupSource, dest: BackupDestination, info: GlobalInfo, estimator: Estimator, data_cache: DataCache): + self.config: Config = config + self.time = time + self.precache: Precache = None + self.source: BackupSource = source + self.dest: BackupDestination = dest + self.reinitialize() + self.backups: Dict[str, Backup] = {} + self.firstSync = True + self.info = info + self.simulate_error = None + self.estimator = estimator + self.waiting_for_startup = False + self.ignore_startup_delay = False + self._data_cache = data_cache + + def enabled(self): + if self.source.needsConfiguration(): + return False + if self.dest.needsConfiguration(): + return False + return True + + def allSources(self): + return [self.source, self.dest] + + def reinitialize(self, precache: Precache = None): + self.precache = precache + self._time_of_day: Optional[Tuple[int, int]] = self._parseTimeOfDay() + + # SOMEDAY: this should be cached in config and regenerated on config updates, not here + self.generational_config = self.config.getGenerationalConfig() + + def getTimeOfDay(self): + return self._time_of_day + + def _nextBackup(self, now: datetime, last_backup: Optional[datetime]) -> Optional[datetime]: + timeofDay = self.getTimeOfDay() + if self.config.get(Setting.DAYS_BETWEEN_BACKUPS) <= 0: + next = None + elif self.dest.needsConfiguration(): + next = None + elif not last_backup: + # this isn't the cleanest logic, but the idea here is that if there are no backups, + # then the backups shoudl be made right when the addon starts up. + next = self.info.start_time + elif not timeofDay: + next = last_backup + timedelta(days=self.config.get(Setting.DAYS_BETWEEN_BACKUPS)) + else: + newest_local: datetime = self.time.toLocal(last_backup) + time_that_day_local = self.time.localize(datetime(newest_local.year, newest_local.month, newest_local.day, timeofDay[0], timeofDay[1])) + if newest_local < time_that_day_local: + # Latest backup is before the backup time for that day + next = self.time.toUtc(time_that_day_local) + else: + # return the next backup after the delta + next_date = date.fromordinal(int(date(newest_local.year, newest_local.month, newest_local.day).toordinal() + self.config.get(Setting.DAYS_BETWEEN_BACKUPS))) + next_datetime_local = self.time.localize(datetime(next_date.year, next_date.month, next_date.day, timeofDay[0], timeofDay[1])) + next = self.time.toUtc(next_datetime_local) + + if next is None: + self.waiting_for_startup = False + return None + + # Don't backup X minutes after startup, since that can put an unreasonable amount of strain on + # the system while booting up. + cooldown_minimum = self.info.backupCooldownTime() + if next <= now and now < cooldown_minimum and not self.ignore_startup_delay: + self.waiting_for_startup = True + return cooldown_minimum + elif self.ignore_startup_delay: + self.waiting_for_startup = False + return next + elif cooldown_minimum > next: + self.waiting_for_startup = cooldown_minimum > now + return cooldown_minimum + else: + self.waiting_for_startup = False + return next + + def nextBackup(self, now: datetime, include_pending=True): + latest = max(filter(lambda s: not s.ignore() and (not s.isPending() or include_pending), self.backups.values()), + default=None, key=lambda s: s.date()) + if latest: + latest = latest.date() + return self._nextBackup(now, latest) + + async def sync(self, now: datetime): + if self.simulate_error is not None: + if self.simulate_error.startswith("test"): + raise Exception(self.simulate_error) + else: + raise SimulatedError(self.simulate_error) + await self._syncBackups([self.source, self.dest], now) + + self.source.checkBeforeChanges() + self.dest.checkBeforeChanges() + + if not self.dest.needsConfiguration(): + if self.source.enabled(): + await self._purge(self.source) + if self.dest.enabled(): + await self._purge(self.dest) + + # Delete any "ignored" backups that have expired + if (self.config.get(Setting.IGNORE_OTHER_BACKUPS) or self.config.get(Setting.IGNORE_UPGRADE_BACKUPS)) and self.config.get(Setting.DELETE_IGNORED_AFTER_DAYS) > 0: + cutoff = now - timedelta(days=self.config.get(Setting.DELETE_IGNORED_AFTER_DAYS)) + delete = [] + for backup in self.backups.values(): + if backup.ignore() and backup.date() < cutoff: + delete.append(backup) + for backup in delete: + await self.deleteBackup(backup, self.source) + + self._handleBackupDetails() + next_backup = self.nextBackup(now) + if next_backup and now >= next_backup and self.source.enabled() and not self.dest.needsConfiguration(): + if self.config.get(Setting.DELETE_BEFORE_NEW_BACKUP): + await self._purge(self.source, pre_purge=True) + await self.createBackup(CreateOptions(now, self.config.get(Setting.BACKUP_NAME))) + await self._purge(self.source) + self._handleBackupDetails() + + if self.dest.enabled() and self.dest.upload(): + # get the backups we should upload + uploads = [] + for backup in self.backups.values(): + if backup.getSource(self.source.name()) is not None and backup.getSource(self.source.name()).uploadable() and backup.getSource(self.dest.name()) is None and not backup.ignore(): + uploads.append(backup) + uploads.sort(key=lambda s: s.date()) + uploads.reverse() + for upload in uploads: + # only upload if doing so won't result in it being deleted next + dummy = DummyBackup( + "", upload.date(), self.dest.name(), "dummy_slug_name") + proposed = list(self.backups.values()) + proposed.append(dummy) + if self._nextPurge(self.dest, proposed)[1] != dummy: + if self.config.get(Setting.DELETE_BEFORE_NEW_BACKUP): + await self._purge(self.dest, pre_purge=True) + upload.addSource(await self.dest.save(upload, await self.source.read(upload))) + await self._purge(self.dest) + self._handleBackupDetails() + else: + break + if self.config.get(Setting.DELETE_AFTER_UPLOAD): + await self._purge(self.source) + self._handleBackupDetails() + self.source.postSync() + self.dest.postSync() + self._data_cache.saveIfDirty() + + def isWorkingThroughUpload(self): + return self.dest.isWorking() + + async def createBackup(self, options): + if not self.source.enabled(): + return + + self.estimator.refresh() + self.estimator.checkSpace(list(self.backups.values())) + created = await self.source.create(options) + backup = Backup(created) + self.backups[backup.slug()] = backup + + async def deleteBackup(self, backup, source): + if not backup.getSource(source.name()): + return + slug = backup.slug() + await source.delete(backup) + backup.removeSource(source.name()) + if backup.isDeleted(): + del self.backups[slug] + + def getNextPurges(self): + purges = {} + for source in [self.source, self.dest]: + purges[source.name()] = self._nextPurge( + source, self.backups.values(), findNext=True)[1] + return purges + + def _parseTimeOfDay(self) -> Optional[Tuple[int, int]]: + from_config = self.config.get(Setting.BACKUP_TIME_OF_DAY) + if len(from_config) == 0: + return None + parts = from_config.split(":") + if len(parts) != 2: + return None + try: + hour: int = int(parts[0]) + minute: int = int(parts[1]) + if hour < 0 or minute < 0 or hour > 23 or minute > 59: + return None + return (hour, minute) + except ValueError: + # Parse error + return None + + async def _syncBackups(self, sources: List[BackupSource], now: datetime): + for source in sources: + if source.enabled(): + # check if we have the results from this source precached + from_source: Dict[str, AbstractBackup] = None + if self.precache is not None: + from_source = self.precache.cached(source.name(), now) + if not from_source: + from_source = await source.get() + else: + from_source: Dict[str, AbstractBackup] = {} + for backup in from_source.values(): + if backup.slug() not in self.backups: + self.backups[backup.slug()] = Backup(backup) + else: + self.backups[backup.slug()].addSource(backup) + for backup in list(self.backups.values()): + if backup.slug() not in from_source: + slug = backup.slug() + backup.removeSource(source.name()) + if backup.isDeleted(): + del self.backups[slug] + self.firstSync = False + + def _buildDeleteScheme(self, source, findNext=False): + count = source.maxCount() + if findNext: + count -= 1 + if source == self.source and self.config.get(Setting.DELETE_AFTER_UPLOAD): + return DeleteAfterUploadScheme(source.name(), [self.dest.name()]) + elif self.generational_config: + return GenerationalScheme( + self.time, self.generational_config, count=count) + else: + return OldestScheme(count=count) + + def _buildNamingScheme(self): + source = max(filter(BackupSource.enabled, self.allSources()), key=BackupSource.maxCount) + return self._buildDeleteScheme(source) + + def _handleBackupDetails(self): + self._buildNamingScheme().handleNaming(self.backups.values()) + + def _nextPurge(self, source: BackupSource, backups, findNext=False): + """ + Given a list of backups, decides if one should be purged. + """ + if not source.enabled() or len(backups) == 0: + return None, None + if source.maxCount() == 0 and source.isDestination(): + # When maxCount is zero for a destination, we should never delete from it. + return None, None + if source.maxCount() == 0 and not self.config.get(Setting.DELETE_AFTER_UPLOAD): + return None, None + + scheme = self._buildDeleteScheme(source, findNext=findNext) + consider_purging = [] + for backup in backups: + source_backup = backup.getSource(source.name()) + if source_backup is not None and source_backup.considerForPurge() and not backup.ignore(): + consider_purging.append(backup) + if len(consider_purging) == 0: + return None, None + return scheme.getOldest(consider_purging) + + async def _purge(self, source: BackupSource, pre_purge=False): + while True: + purge = self._getPurgeList(source, pre_purge) + + reasons = set(map(lambda p: p[1], purge)) + if len(purge) <= 0: + return + if len(purge) != len(reasons) and (self.config.get(Setting.CONFIRM_MULTIPLE_DELETES) and not self.info.isPermitMultipleDeletes()): + raise DeleteMutlipleBackupsError(self._getPurgeStats()) + await self.deleteBackup(purge[0][0], source) + + def _getPurgeStats(self): + ret = {} + for source in [self.source, self.dest]: + ret[source.name()] = len(self._getPurgeList(source)) + return ret + + def _getPurgeList(self, source: BackupSource, pre_purge=False): + if not source.enabled(): + return [] + candidates = list(self.backups.values()) + purges = [] + while True: + reason, next_purge = self._nextPurge(source, candidates, findNext=pre_purge) + if next_purge is None: + return purges + else: + purges.append((next_purge, reason)) + candidates.remove(next_purge) diff --git a/hassio-google-drive-backup/backup/model/precache.py b/hassio-google-drive-backup/backup/model/precache.py new file mode 100644 index 0000000..af71028 --- /dev/null +++ b/hassio-google-drive-backup/backup/model/precache.py @@ -0,0 +1,15 @@ +from abc import ABC, abstractmethod +from typing import Any +from datetime import datetime + + +class Precache(ABC): + @abstractmethod + def cached(self, source: str, date: datetime) -> Any: + """For a given source and datetime, returns valid precached results if they're available""" + pass + + @abstractmethod + def clear(self): + """Clears any cached results stored""" + pass \ No newline at end of file diff --git a/hassio-google-drive-backup/backup/model/simulatedsource.py b/hassio-google-drive-backup/backup/model/simulatedsource.py new file mode 100644 index 0000000..e728920 --- /dev/null +++ b/hassio-google-drive-backup/backup/model/simulatedsource.py @@ -0,0 +1,129 @@ +from .model import CreateOptions, BackupDestination +from .backups import Backup +from .dummybackupsource import DummyBackupSource +from typing import Dict +from io import IOBase +from ..ha import BackupName +from ..logger import getLogger + +logger = getLogger(__name__) + + +class SimulatedSource(BackupDestination): + def __init__(self, name, is_destination=False): + self._name = name + self.current: Dict[str, DummyBackupSource] = {} + self.saved = [] + self.deleted = [] + self.created = [] + self._enabled = True + self._upload = True + self.index = 0 + self.max = 0 + self.backup_name = BackupName() + self.host_info = {} + self.backup_type = "Full" + self.working = False + self.needConfig = None + self.is_destination = is_destination + + def isDestination(self): + return self.is_destination + + def setEnabled(self, value): + self._enabled = value + return self + + def needsConfiguration(self) -> bool: + if self.needConfig is not None: + return self.needConfig + return super().needsConfiguration() + + def setNeedsConfiguration(self, value: bool): + self.needConfig = value + + def setUpload(self, value): + self._upload = value + return self + + def upload(self): + return self._upload + + def setMax(self, count): + self.max = count + return self + + def isWorking(self): + return self.working + + def setIsWorking(self, value): + self.working = value + + def maxCount(self) -> None: + return self.max + + def insert(self, name, date, slug=None, retain=False): + if slug is None: + slug = name + new_backup = DummyBackupSource( + name, + date, + self._name, + slug) + self.current[new_backup.slug()] = new_backup + return new_backup + + def name(self) -> str: + return self._name + + def enabled(self) -> bool: + return self._enabled + + def nameSetup(self, type, host_info): + self.backup_type = type + self.host_info = host_info + + async def create(self, options: CreateOptions) -> DummyBackupSource: + assert self.enabled + new_backup = DummyBackupSource( + self.backup_name.resolve( + self.backup_type, options.name_template, options.when, self.host_info), + options.when, + self._name, + "{0}slug{1}".format(self._name, self.index)) + self.index += 1 + self.current[new_backup.slug()] = new_backup + self.created.append(new_backup) + return new_backup + + async def get(self) -> Dict[str, DummyBackupSource]: + assert self.enabled + return self.current + + async def delete(self, backup: Backup): + assert self.enabled + assert backup.getSource(self._name) is not None + assert backup.getSource(self._name).source() is self._name + assert backup.slug() in self.current + slug = backup.slug() + self.deleted.append(backup.getSource(self._name)) + backup.removeSource(self._name) + del self.current[slug] + + async def save(self, backup: Backup, bytes: IOBase = None) -> DummyBackupSource: + assert self.enabled + assert backup.slug() not in self.current + new_backup = DummyBackupSource( + backup.name(), backup.date(), self._name, backup.slug()) + backup.addSource(new_backup) + self.current[new_backup.slug()] = new_backup + self.saved.append(new_backup) + return new_backup + + async def read(self, backup: DummyBackupSource) -> IOBase: + assert self.enabled + return None + + async def retain(self, backup: DummyBackupSource, retain: bool) -> None: + assert self.enabled + backup.getSource(self.name()).setRetained(retain) diff --git a/hassio-google-drive-backup/backup/model/simulation.py b/hassio-google-drive-backup/backup/model/simulation.py new file mode 100644 index 0000000..e69de29 diff --git a/hassio-google-drive-backup/backup/model/syncer.py b/hassio-google-drive-backup/backup/model/syncer.py new file mode 100644 index 0000000..20b814d --- /dev/null +++ b/hassio-google-drive-backup/backup/model/syncer.py @@ -0,0 +1,36 @@ +from typing import List + +from injector import inject, singleton + +from .coordinator import Coordinator +from backup.time import Time +from backup.worker import Worker, Trigger +from backup.logger import getLogger +from backup.exceptions import PleaseWait + +logger = getLogger(__name__) + + +@singleton +class Scyncer(Worker): + @inject + def __init__(self, time: Time, coord: Coordinator, triggers: List[Trigger]): + super().__init__("Sync Worker", self.checkforSync, time, 0.5) + self.coord = coord + self.triggers: List[Trigger] = triggers + self._time = time + + async def checkforSync(self): + try: + doSync = False + for trigger in self.triggers: + if await trigger.check(): + logger.debug("Sync requested by " + str(trigger.name())) + doSync = True + if doSync: + while self.coord.isSyncing(): + await self._time.sleepAsync(3) + await self.coord.sync() + except PleaseWait: + # Ignore this, since it means a sync already started (unavilable race condition) + pass diff --git a/hassio-google-drive-backup/backup/module.py b/hassio-google-drive-backup/backup/module.py new file mode 100644 index 0000000..d89fc77 --- /dev/null +++ b/hassio-google-drive-backup/backup/module.py @@ -0,0 +1,87 @@ +import socket +import sys +import aiohttp +import os +from aiohttp import ClientSession +from injector import Module, provider, singleton, multiprovider +from typing import List + +from backup.config import Config, Startable, Setting +from backup.drive import DriveSource +from backup.ha import HaSource, HaUpdater, AddonStopper +from backup.model import BackupDestination, BackupSource, Scyncer +from backup.util import Resolver +from backup.model import Coordinator, Precache, DestinationPrecache +from backup.worker import Trigger +from backup.watcher import Watcher +from backup.ui import UiServer, Restarter +from backup.logger import getLogger +from backup.debug import DebugServer +from .debugworker import DebugWorker +from .tracing_session import TracingSession +logger = getLogger(__name__) + + +class BaseModule(Module): + ''' + A module shared between tests and main + ''' + def __init__(self, override_dns=True): + self._override_dns = override_dns + + @multiprovider + @singleton + def getTriggers(self, coord: Coordinator, ha: HaSource, drive: DriveSource, watcher: Watcher, server: UiServer) -> List[Trigger]: + return [coord, ha, drive, watcher, server] + + @provider + @singleton + def getDrive(self, drive: DriveSource) -> BackupDestination: + return drive + + @provider + @singleton + def getHa(self, ha: HaSource) -> BackupSource: + return ha + + @provider + @singleton + def getPrecache(self, cache: DestinationPrecache) -> Precache: + return cache + + @multiprovider + @singleton + def getStartables(self, debug_server: DebugServer, ha_updater: HaUpdater, debugger: DebugWorker, ha_source: HaSource, + server: UiServer, restarter: Restarter, syncer: Scyncer, watcher: Watcher, stopper: AddonStopper, precache: Precache) -> List[Startable]: + # Order here matters, since its the order in which components of the addon are initialized. + return [debug_server, ha_updater, debugger, ha_source, server, restarter, syncer, watcher, stopper, precache] + + @provider + @singleton + def getSession(self, resolver: Resolver, config: Config) -> ClientSession: + conn = None + if self._override_dns: + conn = aiohttp.TCPConnector(resolver=resolver, family=socket.AF_INET) + return TracingSession(config, connector=conn) + + +class MainModule(Module): + @provider + @singleton + def getConfig(self) -> Config: + alt_config = None + index = 1 + for arg in sys.argv[1:]: + if arg == "--config": + alt_config = sys.argv[index + 1] + break + index += 1 + + if alt_config: + config = Config.withFileOverrides(alt_config) + elif "PYTEST_CURRENT_TEST" in os.environ: + config = Config() + else: + config = Config.fromFile(Setting.CONFIG_FILE_PATH.default()) + logger.overrideLevel(config.get(Setting.CONSOLE_LOG_LEVEL), config.get(Setting.LOG_LEVEL)) + return config diff --git a/hassio-google-drive-backup/backup/server/__init__.py b/hassio-google-drive-backup/backup/server/__init__.py new file mode 100644 index 0000000..e685f47 --- /dev/null +++ b/hassio-google-drive-backup/backup/server/__init__.py @@ -0,0 +1,4 @@ +# flake8: noqa +from .server import Server +from .errorstore import ErrorStore +from .cloudlogger import CloudLogger \ No newline at end of file diff --git a/hassio-google-drive-backup/backup/server/__main__.py b/hassio-google-drive-backup/backup/server/__main__.py new file mode 100644 index 0000000..7eac6d6 --- /dev/null +++ b/hassio-google-drive-backup/backup/server/__main__.py @@ -0,0 +1,27 @@ +import aiorun +from .server import Server +from backup.config import Config +from backup.module import BaseModule +from injector import Injector +from injector import provider, singleton + + +class ServerModule(BaseModule): + def __init__(self): + super().__init__(override_dns=False) + + @provider + @singleton + def getConfig(self) -> Config: + return Config.fromEnvironment() + + +async def main(): + module = ServerModule() + injector = Injector(module) + await injector.get(Server).start() + + +if __name__ == '__main__': + print("Starting") + aiorun.run(main()) diff --git a/hassio-google-drive-backup/backup/server/cloudlogger.py b/hassio-google-drive-backup/backup/server/cloudlogger.py new file mode 100644 index 0000000..ec8d609 --- /dev/null +++ b/hassio-google-drive-backup/backup/server/cloudlogger.py @@ -0,0 +1,28 @@ +import os +import json +from backup.logger import getLogger, StandardLogger +from injector import inject, singleton +from google.cloud import logging +from google.auth.exceptions import DefaultCredentialsError + +basic_logger = getLogger(__name__) + + +@singleton +class CloudLogger(StandardLogger): + @inject + def __init__(self): + super().__init__(__name__) + self.google_logger = None + if os.environ.get('GOOGLE_APPLICATION_CREDENTIALS') is not None: + try: + google_logger_client = logging.Client() + self.googler_logger = google_logger_client.logger("refresh_server") + except DefaultCredentialsError: + basic_logger.error("Unable to start Google Logger, no default credentials") + + def log_struct(self, data): + if self.google_logger is not None: + self.google_logger.log_struct(data) + else: + basic_logger.info(json.dumps(data)) diff --git a/hassio-google-drive-backup/backup/server/errorstore.py b/hassio-google-drive-backup/backup/server/errorstore.py new file mode 100644 index 0000000..ebd8de3 --- /dev/null +++ b/hassio-google-drive-backup/backup/server/errorstore.py @@ -0,0 +1,32 @@ +import firebase_admin +from firebase_admin import credentials +from firebase_admin import firestore +from datetime import datetime +from backup.config import Setting, Config +from .cloudlogger import CloudLogger +from injector import inject, singleton + + +@singleton +class ErrorStore(): + @inject + def __init__(self, logger: CloudLogger, config: Config): + try: + cred = credentials.ApplicationDefault() + firebase_admin.initialize_app(cred, { + 'projectId': config.get(Setting.SERVER_PROJECT_ID), + }) + self.db = firestore.client() + except Exception as e: + logger.log_struct({ + "error": "unable to initialize firestore, errors will not be logged to firestore. If you are running this on a developer machine, this error is normal.", + "exception": str(e) + }) + self.db = None + self.last_error = None + + def store(self, error_data): + if self.db is not None: + doc_ref = self.db.collection(u'error_reports').document(error_data.get('client', "unknown") + "-" + datetime.now().isoformat()) + doc_ref.set(error_data) + self.last_error = error_data diff --git a/hassio-google-drive-backup/backup/server/server.py b/hassio-google-drive-backup/backup/server/server.py new file mode 100644 index 0000000..b092acf --- /dev/null +++ b/hassio-google-drive-backup/backup/server/server.py @@ -0,0 +1,218 @@ +import json +import aiohttp_jinja2 +import jinja2 +import base64 +from os.path import abspath, join +from aiohttp.web import Application, json_response, Request, TCPSite, AppRunner, post, Response, static, get +from aiohttp.client_exceptions import ClientResponseError, ClientConnectorError, ServerConnectionError, ServerDisconnectedError, ServerTimeoutError +from aiohttp.web_exceptions import HTTPBadRequest, HTTPSeeOther +from backup.creds import Exchanger +from backup.config import Config, Setting, VERSION +from backup.exceptions import GoogleCredentialsExpired, ensureKey, KnownError +from injector import ClassAssistedBuilder, inject, singleton +from .errorstore import ErrorStore +from .cloudlogger import CloudLogger +from yarl import URL +from backup.config import Version +from urllib.parse import unquote + +NEW_AUTH_MINIMUM = Version(0, 101, 3) + + +@singleton +class Server(): + @inject + def __init__(self, + config: Config, + exchanger_builder: ClassAssistedBuilder[Exchanger], + logger: CloudLogger, + error_store: ErrorStore): + self.exchanger = exchanger_builder.build( + client_id=config.get(Setting.DEFAULT_DRIVE_CLIENT_ID), + client_secret=config.get(Setting.DEFAULT_DRIVE_CLIENT_SECRET), + redirect=URL(config.get(Setting.AUTHORIZATION_HOST)).with_path("/drive/authorize")) + self.logger = logger + self.config = config + self.error_store = error_store + + def base_context(self, request: Request): + return { + 'version': VERSION, + 'backgroundColor': request.query.get('bg', self.config.get(Setting.BACKGROUND_COLOR)), + 'accentColor': request.query.get('ac', self.config.get(Setting.ACCENT_COLOR)), + 'bmc_logo_path': "/static/" + VERSION + "/images/bmc.svg" + } + + async def authorize(self, request: Request): + if 'redirectbacktoken' in request.query: + version = Version.parse(request.query.get('version', "0")) + token_url = request.query.get('redirectbacktoken') + return_url = request.query.get('return', None) + state = { + 'v': str(version), + 'token': token_url, + 'return': return_url, + 'bg': self.base_context(request).get('backgroundColor'), + 'ac': self.base_context(request).get('accentColor'), + } + # Someone is trying to authenticate with the add-on, direct them to the google auth url + raise HTTPSeeOther(await self.exchanger.getAuthorizationUrl(json.dumps(state))) + elif 'state' in request.query and 'code' in request.query: + state = json.loads(unquote(request.query.get('state'))) + code = request.query.get('code') + try: + version = Version.parse(state["v"]) + creds = (await self.exchanger.exchange(code)).serialize(include_secret=False) + + if version < NEW_AUTH_MINIMUM: + # Redirect back to the addon, since this is the older addon + url = URL(state['token']).with_query({'creds': json.dumps(creds)}) + raise HTTPSeeOther(url) + + serialized_creds = str(base64.b64encode(json.dumps(creds).encode("utf-8")), "utf-8") + url = URL(state['token']).with_query({ + 'creds': serialized_creds, + 'host': state['return']}) + context = { + **self.base_context(request), + 'redirect_url': str(url), + 'credentials_serialized': serialized_creds, + } + if 'bg' in state: + context['backgroundColor'] = state['bg'] + if 'ac' in state: + context['accentColor'] = state['ac'] + return aiohttp_jinja2.render_template( + "authorize.jinja2", + request, + context) + except Exception as e: + if isinstance(e, HTTPSeeOther): + # expected, pass this thorugh + raise + self.logError(request, e) + content = "The server encountered an error while processing this request: " + str(e) + "
" + content += "Please file an issue on Home Assistant Google Backup's GitHub page so I'm aware of this problem or attempt authorizing with Google Drive again." + return Response(status=500, body=content) + else: + raise HTTPBadRequest() + + async def error(self, request: Request): + try: + self.logReport(request, await request.json()) + except BaseException as e: + self.logError(request, e) + return Response() + + async def refresh(self, request: Request): + try: + token = ensureKey('refresh_token', await request.json(), "the request payload") + creds = self.exchanger.refreshCredentials(token) + new_creds = await self.exchanger.refresh(creds) + return json_response(new_creds.serialize(include_secret=False)) + except ClientResponseError as e: + if e.status == 401: + return json_response({ + "error": "expired" + }, status=401) + else: + self.logError(request, e) + return json_response({ + "error": "Google returned HTTP {}".format(e.status) + }, status=503) + except ClientConnectorError: + return json_response({ + "error": "Couldn't connect to Google's servers" + }, status=503) + except ServerConnectionError: + return json_response({ + "error": "Couldn't connect to Google's servers" + }, status=503) + except ServerDisconnectedError: + return json_response({ + "error": "Couldn't connect to Google's servers" + }, status=503) + except ServerTimeoutError: + return json_response({ + "error": "Google's servers timed out" + }, status=503) + except GoogleCredentialsExpired: + return json_response({ + "error": "expired" + }, status=401) + except KnownError as e: + return json_response({ + "error": e.message() + }, status=503) + except Exception as e: + self.logError(request, e) + return json_response({ + "error": str(e) + }, status=500) + + @aiohttp_jinja2.template('picker.jinja2') + async def picker(self, request: Request): + version = Version.parse(request.query.get('version', "0")) + bg = request.query.get('bg', self.config.get(Setting.BACKGROUND_COLOR)) + ac = request.query.get('ac', self.config.get(Setting.ACCENT_COLOR)) + return { + **self.base_context(request), + "client_id": self.config.get(Setting.DEFAULT_DRIVE_CLIENT_ID), + "developer_key": self.config.get(Setting.DRIVE_PICKER_API_KEY), + "app_id": self.config.get(Setting.DEFAULT_DRIVE_CLIENT_ID).split("-")[0], + 'backgroundColor': bg, + 'accentColor': ac, + "do_redirect": str(version < NEW_AUTH_MINIMUM).lower() + } + + @aiohttp_jinja2.template('server-index.jinja2') + async def index(self, request: Request): + return self.base_context(request) + + async def health(self, request: Request): + return json_response({ + 'status': 'ok', + 'messages': [] + }) + + def buildApp(self, app): + path = abspath(join(__file__, "..", "..", "static")) + app.add_routes([ + static("/static/" + VERSION, path, append_version=True), + static("/drive/static/" + VERSION, path, append_version=True), + get("/drive/picker", self.picker), + get("/", self.index), + get("/drive/authorize", self.authorize), + post("/drive/refresh", self.refresh), + post("/logerror", self.error), + get("/health", self.health) + ]) + aiohttp_jinja2.setup(app, loader=jinja2.FileSystemLoader(path)) + return app + + async def start(self): + runner = AppRunner(self.buildApp(Application())) + await runner.setup() + site = TCPSite(runner, "0.0.0.0", int(self.config.get(Setting.PORT))) + await site.start() + self.logger.info("Backup Auth Server Started") + + def logError(self, request: Request, exception: Exception): + data = self.getRequestInfo(request) + data['exception'] = self.logger.formatException(exception) + self.logger.log_struct(data) + + def logReport(self, request, report): + data = self.getRequestInfo(request) + data['report'] = report + self.logger.log_struct(data) + self.error_store.store(data) + + def getRequestInfo(self, request: Request): + return { + 'client': request.headers.get('client', "unknown"), + 'version': request.headers.get('addon_version', "unknown"), + 'address': request.remote, + 'url': str(request.url), + 'length': request.content_length + } diff --git a/hassio-google-drive-backup/backup/starter.py b/hassio-google-drive-backup/backup/starter.py new file mode 100644 index 0000000..22c40ee --- /dev/null +++ b/hassio-google-drive-backup/backup/starter.py @@ -0,0 +1,24 @@ +from injector import inject, singleton +from typing import List + +from .config import Startable, Config, Setting +from .logger import getLogger + +logger = getLogger(__name__) + + +@singleton +class Starter(Startable): + @inject + def __init__(self, config: Config, startables: List[Startable]): + self.startables = startables + self.config = config + + async def start(self): + logger.overrideLevel(self.config.get(Setting.CONSOLE_LOG_LEVEL), self.config.get(Setting.LOG_LEVEL)) + for startable in self.startables: + await startable.start() + + async def stop(self): + for startable in self.startables: + await startable.stop() diff --git a/hassio-google-drive-backup/backup/static/authorize.jinja2 b/hassio-google-drive-backup/backup/static/authorize.jinja2 new file mode 100644 index 0000000..e3bc0ae --- /dev/null +++ b/hassio-google-drive-backup/backup/static/authorize.jinja2 @@ -0,0 +1,73 @@ +{% import "layouts/macros.jinja2" as macros %} +{% extends "layouts/base-server.jinja2" %} + +{% block head %} + {{ super() }} + +{% endblock %} + +{% block content %} +{% call macros.header(version) %}{% endcall %} +
+
+
+

+
+
+
+
You're almost set.
+
+
+ You've authorized the addon to connect with Google Drive and created the authorization string shown below. Most of the time + you can just click "Send Credentials" below to have them sent back to the addon, but depending on circumstances beyond the + addon's control that might just return an error. If that happens, instead just copy the authorization string below + and paste it into the addon where you clicked the "Authenticate With Google Drive" button that brought you here. +
+
+ + open_in_newSend Credentials + +
+
+
+
+ +
+
+
+
Authorization String:
+
+ +
+
+
+ +
+
+
+ Note: This string is like a password that lets the addon talk with Google Drive, so don't share it with anyone. +
+
+

+
+
+
+{% endblock %} diff --git a/hassio-google-drive-backup/backup/static/css/colpick.css b/hassio-google-drive-backup/backup/static/css/colpick.css new file mode 100644 index 0000000..1e2ecbd --- /dev/null +++ b/hassio-google-drive-backup/backup/static/css/colpick.css @@ -0,0 +1,439 @@ +/* +colpick Color Picker / colpick.com +*/ + +/*Main container*/ +.colpick { + position: absolute; + box-sizing:content-box; + width: 346px; + height: 170px; + overflow: hidden; + display: none; + font-family: Arial, Helvetica, sans-serif; + direction:ltr; + background:#ebebeb; + border: 1px solid #bbb; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; + + /*Prevents selecting text when dragging the selectors*/ + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + -o-user-select: none; + user-select: none; +} +/*Color selection box with gradients*/ +.colpick .colpick_color { + position: absolute; + left: 7px; + top: 7px; + width: 156px; + height: 156px; + overflow: hidden; + outline: 1px solid #aaa; + cursor: crosshair; +} +.colpick .colpick_color_overlay1 { + position: absolute; + left:0; + top:0; + width: 156px; + height: 156px; + -ms-filter: "progid:DXImageTransform.Microsoft.gradient(GradientType=1,startColorstr='#ffffff', endColorstr='#00ffffff')"; /* IE8 */ + background: -moz-linear-gradient(left, rgba(255,255,255,1) 0%, rgba(255,255,255,0) 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, right top, color-stop(0%,rgba(255,255,255,1)), color-stop(100%,rgba(255,255,255,0))); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(left, rgba(255,255,255,1) 0%,rgba(255,255,255,0) 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(left, rgba(255,255,255,1) 0%,rgba(255,255,255,0) 100%); /* Opera 11.10+ */ + background: -ms-linear-gradient(left, rgba(255,255,255,1) 0%,rgba(255,255,255,0) 100%); /* IE10+ */ + background: linear-gradient(to right, rgba(255,255,255,1) 0%, rgba(255,255,255,0) 100%); + filter: progid:DXImageTransform.Microsoft.gradient(GradientType=1,startColorstr='#ffffff', endColorstr='#00ffffff'); /* IE6 & IE7 */ +} +.colpick .colpick_color_overlay2 { + position: absolute; + left:0; + top:0; + width: 156px; + height: 156px; + -ms-filter: "progid:DXImageTransform.Microsoft.gradient(GradientType=0,startColorstr='#00000000', endColorstr='#000000')"; /* IE8 */ + background: -moz-linear-gradient(top, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(0,0,0,0)), color-stop(100%,rgba(0,0,0,1))); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, rgba(0,0,0,0) 0%,rgba(0,0,0,1) 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, rgba(0,0,0,0) 0%,rgba(0,0,0,1) 100%); /* Opera 11.10+ */ + background: -ms-linear-gradient(top, rgba(0,0,0,0) 0%,rgba(0,0,0,1) 100%); /* IE10+ */ + background: linear-gradient(to bottom, rgba(0,0,0,0) 0%,rgba(0,0,0,1) 100%); /* W3C */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#00000000', endColorstr='#000000',GradientType=0 ); /* IE6-9 */ +} +/*Circular color selector*/ +.colpick .colpick_selector_outer { + background:none; + position: absolute; + width: 11px; + height: 11px; + margin: -6px 0 0 -6px; + border: 1px solid black; + border-radius: 50%; +} +.colpick .colpick_selector_inner{ + position: absolute; + width: 9px; + height: 9px; + border: 1px solid white; + border-radius: 50%; +} +/*Vertical hue bar*/ +.colpick .colpick_hue { + position: absolute; + top: 6px; + left: 175px; + width: 19px; + height: 156px; + border: 1px solid #aaa; + cursor: n-resize; +} +/*Hue bar sliding indicator*/ +.colpick .colpick_hue_arrs { + position: absolute; + left: -8px; + width: 35px; + height: 7px; + margin: -7px 0 0 0; +} +.colpick .colpick_hue_larr { + position:absolute; + width: 0; + height: 0; + border-top: 6px solid transparent; + border-bottom: 6px solid transparent; + border-left: 7px solid #858585; +} +.colpick .colpick_hue_rarr { + position:absolute; + right:0; + width: 0; + height: 0; + border-top: 6px solid transparent; + border-bottom: 6px solid transparent; + border-right: 7px solid #858585; +} +/*New color box*/ +.colpick .colpick_new_color { + position: absolute; + left: 207px; + top: 6px; + width: 60px; + height: 27px; + background: #f00; + border: 1px solid #8f8f8f; +} +/*Current color box*/ +.colpick .colpick_current_color { + position: absolute; + left: 277px; + top: 6px; + width: 60px; + height: 27px; + background: #f00; + border: 1px solid #8f8f8f; +} +/*Input field containers*/ +.colpick .colpick_field, .colpick .colpick_hex_field { + position: absolute; + height: 20px; + width: 60px; + overflow:hidden; + background:#f3f3f3; + color:#b8b8b8; + font-size:12px; + border:1px solid #bdbdbd; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} +.colpick .colpick_rgb_r { + top: 40px; + left: 207px; +} +.colpick .colpick_rgb_g { + top: 67px; + left: 207px; +} +.colpick .colpick_rgb_b { + top: 94px; + left: 207px; +} +.colpick .colpick_hsb_h { + top: 40px; + left: 277px; +} +.colpick .colpick_hsb_s { + top: 67px; + left: 277px; +} +.colpick .colpick_hsb_b { + top: 94px; + left: 277px; +} +.colpick .colpick_hex_field { + width: 68px; + left: 207px; + top: 121px; +} +/*Text field container on focus*/ +.colpick .colpick_focus { + border-color: #999; +} +/*Field label container*/ +.colpick .colpick_field_letter { + position: absolute; + width: 12px; + height: 20px; + line-height: 20px; + padding-left: 4px; + background: #efefef; + border-right: 1px solid #bdbdbd; + font-weight: bold; + color:#777; +} +/*Text inputs*/ +.colpick .colpick_field input, .colpick .colpick_hex_field input { + position: absolute; + right: 11px; + margin: 0; + padding: 0; + height: 20px; + line-height: 20px; + background: transparent; + border: none; + font-size: 12px; + font-family: Arial, Helvetica, sans-serif; + color: #555; + text-align: right; + outline: none; +} +.colpick .colpick_hex_field input { + right: 4px; +} +/*Field up/down arrows*/ +.colpick .colpick_field_arrs { + position: absolute; + top: 0; + right: 0; + width: 9px; + height: 21px; + cursor: n-resize; +} +.colpick .colpick_field_uarr { + position: absolute; + top: 5px; + width: 0; + height: 0; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-bottom: 4px solid #959595; +} +.colpick .colpick_field_darr { + position: absolute; + bottom:5px; + width: 0; + height: 0; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-top: 4px solid #959595; +} +/*Submit/Select button*/ +.colpick .colpick_submit { + position: absolute; + left: 207px; + top: 149px; + width: 130px; + height: 22px; + line-height:22px; + background: #efefef; + text-align: center; + color: #555; + font-size: 12px; + font-weight:bold; + border: 1px solid #bdbdbd; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} +.colpick .colpick_submit:hover { + background:#f3f3f3; + border-color:#999; + cursor: pointer; +} + +/*full layout with no submit button*/ +.colpick.colpick_full_ns .colpick_submit, +.colpick.colpick_full_ns .colpick_current_color{ + display:none; +} +.colpick.colpick_full_ns .colpick_new_color { + width: 130px; + height: 25px; +} +.colpick.colpick_full_ns .colpick_rgb_r, +.colpick.colpick_full_ns .colpick_hsb_h { + top: 42px; +} +.colpick.colpick_full_ns .colpick_rgb_g, +.colpick.colpick_full_ns .colpick_hsb_s { + top: 73px; +} +.colpick.colpick_full_ns .colpick_rgb_b, +.colpick.colpick_full_ns .colpick_hsb_b { + top: 104px; +} +.colpick.colpick_full_ns .colpick_hex_field { + top: 135px; +} + +/*rgbhex layout*/ +.colpick.colpick_rgbhex .colpick_hsb_h, +.colpick.colpick_rgbhex .colpick_hsb_s, +.colpick.colpick_rgbhex .colpick_hsb_b { + display:none; +} +.colpick.colpick_rgbhex { + width:282px; +} +.colpick.colpick_rgbhex .colpick_field, +.colpick.colpick_rgbhex .colpick_submit { + width:68px; +} +.colpick.colpick_rgbhex .colpick_new_color { + width:34px; + border-right:none; +} +.colpick.colpick_rgbhex .colpick_current_color { + width:34px; + left:240px; + border-left:none; +} + +/*rgbhex layout, no submit button*/ +.colpick.colpick_rgbhex_ns .colpick_submit, +.colpick.colpick_rgbhex_ns .colpick_current_color{ + display:none; +} +.colpick.colpick_rgbhex_ns .colpick_new_color{ + width:68px; + border: 1px solid #8f8f8f; +} +.colpick.colpick_rgbhex_ns .colpick_rgb_r { + top: 42px; +} +.colpick.colpick_rgbhex_ns .colpick_rgb_g { + top: 73px; +} +.colpick.colpick_rgbhex_ns .colpick_rgb_b { + top: 104px; +} +.colpick.colpick_rgbhex_ns .colpick_hex_field { + top: 135px; +} + +/*hex layout*/ +.colpick.colpick_hex .colpick_hsb_h, +.colpick.colpick_hex .colpick_hsb_s, +.colpick.colpick_hex .colpick_hsb_b, +.colpick.colpick_hex .colpick_rgb_r, +.colpick.colpick_hex .colpick_rgb_g, +.colpick.colpick_hex .colpick_rgb_b { + display:none; +} +.colpick.colpick_hex { + width:206px; + height:201px; +} +.colpick.colpick_hex .colpick_hex_field { + width:72px; + height:25px; + top:168px; + left:80px; +} +.colpick.colpick_hex .colpick_hex_field div, +.colpick.colpick_hex .colpick_hex_field input { + height: 25px; + line-height: 25px; +} +.colpick.colpick_hex .colpick_new_color { + left:9px; + top:168px; + width:30px; + border-right:none; +} +.colpick.colpick_hex .colpick_current_color { + left:39px; + top:168px; + width:30px; + border-left:none; +} +.colpick.colpick_hex .colpick_submit { + left:164px; + top: 168px; + width:30px; + height:25px; + line-height: 25px; +} + +/*hex layout, no submit button*/ +.colpick.colpick_hex_ns .colpick_submit, +.colpick.colpick_hex_ns .colpick_current_color { + display:none; +} +.colpick.colpick_hex_ns .colpick_hex_field { + width:80px; +} +.colpick.colpick_hex_ns .colpick_new_color{ + width:60px; + border: 1px solid #8f8f8f; +} + +/*Dark color scheme*/ +.colpick.colpick_dark { + background: #161616; + border-color: #2a2a2a; +} +.colpick.colpick_dark .colpick_color { + outline-color: #333; +} +.colpick.colpick_dark .colpick_hue { + border-color: #555; +} +.colpick.colpick_dark .colpick_field, +.colpick.colpick_dark .colpick_hex_field { + background: #101010; + border-color: #2d2d2d; +} +.colpick.colpick_dark .colpick_field_letter { + background: #131313; + border-color: #2d2d2d; + color: #696969; +} +.colpick.colpick_dark .colpick_field input, +.colpick.colpick_dark .colpick_hex_field input { + color: #7a7a7a; +} +.colpick.colpick_dark .colpick_field_uarr { + border-bottom-color:#696969; +} +.colpick.colpick_dark .colpick_field_darr { + border-top-color:#696969; +} +.colpick.colpick_dark .colpick_focus { + border-color:#444; +} +.colpick.colpick_dark .colpick_submit { + background: #131313; + border-color:#2d2d2d; + color:#7a7a7a; +} +.colpick.colpick_dark .colpick_submit:hover { + background-color:#101010; + border-color:#444; +} \ No newline at end of file diff --git a/hassio-google-drive-backup/backup/static/css/iconfont/MaterialIcons-Regular.eot b/hassio-google-drive-backup/backup/static/css/iconfont/MaterialIcons-Regular.eot new file mode 100644 index 0000000000000000000000000000000000000000..70508ebabc9992e64f1314f866b2d7ab90438c58 GIT binary patch literal 143258 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 Hzx)3OGG_Ot literal 0 HcmV?d00001 diff --git a/hassio-google-drive-backup/backup/static/css/iconfont/MaterialIcons-Regular.ijmap b/hassio-google-drive-backup/backup/static/css/iconfont/MaterialIcons-Regular.ijmap new file mode 100644 index 0000000..d9f1d25 --- /dev/null +++ b/hassio-google-drive-backup/backup/static/css/iconfont/MaterialIcons-Regular.ijmap @@ -0,0 +1 @@ +{"icons":{"e84d":{"name":"3d Rotation"},"eb3b":{"name":"Ac Unit"},"e190":{"name":"Access Alarm"},"e191":{"name":"Access Alarms"},"e192":{"name":"Access Time"},"e84e":{"name":"Accessibility"},"e914":{"name":"Accessible"},"e84f":{"name":"Account Balance"},"e850":{"name":"Account Balance Wallet"},"e851":{"name":"Account Box"},"e853":{"name":"Account Circle"},"e60e":{"name":"Adb"},"e145":{"name":"Add"},"e439":{"name":"Add A Photo"},"e193":{"name":"Add Alarm"},"e003":{"name":"Add Alert"},"e146":{"name":"Add Box"},"e147":{"name":"Add Circle"},"e148":{"name":"Add Circle Outline"},"e567":{"name":"Add Location"},"e854":{"name":"Add Shopping Cart"},"e39d":{"name":"Add To Photos"},"e05c":{"name":"Add To Queue"},"e39e":{"name":"Adjust"},"e630":{"name":"Airline Seat Flat"},"e631":{"name":"Airline Seat Flat Angled"},"e632":{"name":"Airline Seat Individual Suite"},"e633":{"name":"Airline Seat Legroom Extra"},"e634":{"name":"Airline Seat Legroom Normal"},"e635":{"name":"Airline Seat Legroom Reduced"},"e636":{"name":"Airline Seat Recline Extra"},"e637":{"name":"Airline Seat Recline Normal"},"e195":{"name":"Airplanemode Active"},"e194":{"name":"Airplanemode Inactive"},"e055":{"name":"Airplay"},"eb3c":{"name":"Airport Shuttle"},"e855":{"name":"Alarm"},"e856":{"name":"Alarm Add"},"e857":{"name":"Alarm Off"},"e858":{"name":"Alarm On"},"e019":{"name":"Album"},"eb3d":{"name":"All Inclusive"},"e90b":{"name":"All Out"},"e859":{"name":"Android"},"e85a":{"name":"Announcement"},"e5c3":{"name":"Apps"},"e149":{"name":"Archive"},"e5c4":{"name":"Arrow Back"},"e5db":{"name":"Arrow Downward"},"e5c5":{"name":"Arrow Drop Down"},"e5c6":{"name":"Arrow Drop Down Circle"},"e5c7":{"name":"Arrow Drop Up"},"e5c8":{"name":"Arrow Forward"},"e5d8":{"name":"Arrow Upward"},"e060":{"name":"Art Track"},"e85b":{"name":"Aspect Ratio"},"e85c":{"name":"Assessment"},"e85d":{"name":"Assignment"},"e85e":{"name":"Assignment Ind"},"e85f":{"name":"Assignment Late"},"e860":{"name":"Assignment Return"},"e861":{"name":"Assignment Returned"},"e862":{"name":"Assignment Turned In"},"e39f":{"name":"Assistant"},"e3a0":{"name":"Assistant Photo"},"e226":{"name":"Attach File"},"e227":{"name":"Attach Money"},"e2bc":{"name":"Attachment"},"e3a1":{"name":"Audiotrack"},"e863":{"name":"Autorenew"},"e01b":{"name":"Av Timer"},"e14a":{"name":"Backspace"},"e864":{"name":"Backup"},"e19c":{"name":"Battery Alert"},"e1a3":{"name":"Battery Charging Full"},"e1a4":{"name":"Battery Full"},"e1a5":{"name":"Battery Std"},"e1a6":{"name":"Battery Unknown"},"eb3e":{"name":"Beach Access"},"e52d":{"name":"Beenhere"},"e14b":{"name":"Block"},"e1a7":{"name":"Bluetooth"},"e60f":{"name":"Bluetooth Audio"},"e1a8":{"name":"Bluetooth Connected"},"e1a9":{"name":"Bluetooth Disabled"},"e1aa":{"name":"Bluetooth Searching"},"e3a2":{"name":"Blur Circular"},"e3a3":{"name":"Blur Linear"},"e3a4":{"name":"Blur Off"},"e3a5":{"name":"Blur On"},"e865":{"name":"Book"},"e866":{"name":"Bookmark"},"e867":{"name":"Bookmark Border"},"e228":{"name":"Border All"},"e229":{"name":"Border Bottom"},"e22a":{"name":"Border Clear"},"e22b":{"name":"Border Color"},"e22c":{"name":"Border Horizontal"},"e22d":{"name":"Border Inner"},"e22e":{"name":"Border Left"},"e22f":{"name":"Border Outer"},"e230":{"name":"Border Right"},"e231":{"name":"Border Style"},"e232":{"name":"Border Top"},"e233":{"name":"Border Vertical"},"e06b":{"name":"Branding Watermark"},"e3a6":{"name":"Brightness 1"},"e3a7":{"name":"Brightness 2"},"e3a8":{"name":"Brightness 3"},"e3a9":{"name":"Brightness 4"},"e3aa":{"name":"Brightness 5"},"e3ab":{"name":"Brightness 6"},"e3ac":{"name":"Brightness 7"},"e1ab":{"name":"Brightness Auto"},"e1ac":{"name":"Brightness High"},"e1ad":{"name":"Brightness Low"},"e1ae":{"name":"Brightness Medium"},"e3ad":{"name":"Broken Image"},"e3ae":{"name":"Brush"},"e6dd":{"name":"Bubble Chart"},"e868":{"name":"Bug Report"},"e869":{"name":"Build"},"e43c":{"name":"Burst Mode"},"e0af":{"name":"Business"},"eb3f":{"name":"Business Center"},"e86a":{"name":"Cached"},"e7e9":{"name":"Cake"},"e0b0":{"name":"Call"},"e0b1":{"name":"Call End"},"e0b2":{"name":"Call Made"},"e0b3":{"name":"Call Merge"},"e0b4":{"name":"Call Missed"},"e0e4":{"name":"Call Missed Outgoing"},"e0b5":{"name":"Call Received"},"e0b6":{"name":"Call Split"},"e06c":{"name":"Call To Action"},"e3af":{"name":"Camera"},"e3b0":{"name":"Camera Alt"},"e8fc":{"name":"Camera Enhance"},"e3b1":{"name":"Camera Front"},"e3b2":{"name":"Camera Rear"},"e3b3":{"name":"Camera Roll"},"e5c9":{"name":"Cancel"},"e8f6":{"name":"Card Giftcard"},"e8f7":{"name":"Card Membership"},"e8f8":{"name":"Card Travel"},"eb40":{"name":"Casino"},"e307":{"name":"Cast"},"e308":{"name":"Cast Connected"},"e3b4":{"name":"Center Focus Strong"},"e3b5":{"name":"Center Focus Weak"},"e86b":{"name":"Change History"},"e0b7":{"name":"Chat"},"e0ca":{"name":"Chat Bubble"},"e0cb":{"name":"Chat Bubble Outline"},"e5ca":{"name":"Check"},"e834":{"name":"Check Box"},"e835":{"name":"Check Box Outline Blank"},"e86c":{"name":"Check Circle"},"e5cb":{"name":"Chevron Left"},"e5cc":{"name":"Chevron Right"},"eb41":{"name":"Child Care"},"eb42":{"name":"Child Friendly"},"e86d":{"name":"Chrome Reader Mode"},"e86e":{"name":"Class"},"e14c":{"name":"Clear"},"e0b8":{"name":"Clear All"},"e5cd":{"name":"Close"},"e01c":{"name":"Closed Caption"},"e2bd":{"name":"Cloud"},"e2be":{"name":"Cloud Circle"},"e2bf":{"name":"Cloud Done"},"e2c0":{"name":"Cloud Download"},"e2c1":{"name":"Cloud Off"},"e2c2":{"name":"Cloud Queue"},"e2c3":{"name":"Cloud Upload"},"e86f":{"name":"Code"},"e3b6":{"name":"Collections"},"e431":{"name":"Collections Bookmark"},"e3b7":{"name":"Color Lens"},"e3b8":{"name":"Colorize"},"e0b9":{"name":"Comment"},"e3b9":{"name":"Compare"},"e915":{"name":"Compare Arrows"},"e30a":{"name":"Computer"},"e638":{"name":"Confirmation Number"},"e0d0":{"name":"Contact Mail"},"e0cf":{"name":"Contact Phone"},"e0ba":{"name":"Contacts"},"e14d":{"name":"Content Copy"},"e14e":{"name":"Content Cut"},"e14f":{"name":"Content Paste"},"e3ba":{"name":"Control Point"},"e3bb":{"name":"Control Point Duplicate"},"e90c":{"name":"Copyright"},"e150":{"name":"Create"},"e2cc":{"name":"Create New Folder"},"e870":{"name":"Credit Card"},"e3be":{"name":"Crop"},"e3bc":{"name":"Crop 16 9"},"e3bd":{"name":"Crop 3 2"},"e3bf":{"name":"Crop 5 4"},"e3c0":{"name":"Crop 7 5"},"e3c1":{"name":"Crop Din"},"e3c2":{"name":"Crop Free"},"e3c3":{"name":"Crop Landscape"},"e3c4":{"name":"Crop Original"},"e3c5":{"name":"Crop Portrait"},"e437":{"name":"Crop Rotate"},"e3c6":{"name":"Crop Square"},"e871":{"name":"Dashboard"},"e1af":{"name":"Data Usage"},"e916":{"name":"Date Range"},"e3c7":{"name":"Dehaze"},"e872":{"name":"Delete"},"e92b":{"name":"Delete Forever"},"e16c":{"name":"Delete Sweep"},"e873":{"name":"Description"},"e30b":{"name":"Desktop Mac"},"e30c":{"name":"Desktop Windows"},"e3c8":{"name":"Details"},"e30d":{"name":"Developer Board"},"e1b0":{"name":"Developer Mode"},"e335":{"name":"Device Hub"},"e1b1":{"name":"Devices"},"e337":{"name":"Devices Other"},"e0bb":{"name":"Dialer Sip"},"e0bc":{"name":"Dialpad"},"e52e":{"name":"Directions"},"e52f":{"name":"Directions Bike"},"e532":{"name":"Directions Boat"},"e530":{"name":"Directions Bus"},"e531":{"name":"Directions Car"},"e534":{"name":"Directions Railway"},"e566":{"name":"Directions Run"},"e533":{"name":"Directions Subway"},"e535":{"name":"Directions Transit"},"e536":{"name":"Directions Walk"},"e610":{"name":"Disc Full"},"e875":{"name":"Dns"},"e612":{"name":"Do Not Disturb"},"e611":{"name":"Do Not Disturb Alt"},"e643":{"name":"Do Not Disturb Off"},"e644":{"name":"Do Not Disturb On"},"e30e":{"name":"Dock"},"e7ee":{"name":"Domain"},"e876":{"name":"Done"},"e877":{"name":"Done All"},"e917":{"name":"Donut Large"},"e918":{"name":"Donut Small"},"e151":{"name":"Drafts"},"e25d":{"name":"Drag Handle"},"e613":{"name":"Drive Eta"},"e1b2":{"name":"Dvr"},"e3c9":{"name":"Edit"},"e568":{"name":"Edit Location"},"e8fb":{"name":"Eject"},"e0be":{"name":"Email"},"e63f":{"name":"Enhanced Encryption"},"e01d":{"name":"Equalizer"},"e000":{"name":"Error"},"e001":{"name":"Error Outline"},"e926":{"name":"Euro Symbol"},"e56d":{"name":"Ev Station"},"e878":{"name":"Event"},"e614":{"name":"Event Available"},"e615":{"name":"Event Busy"},"e616":{"name":"Event Note"},"e903":{"name":"Event Seat"},"e879":{"name":"Exit To App"},"e5ce":{"name":"Expand Less"},"e5cf":{"name":"Expand More"},"e01e":{"name":"Explicit"},"e87a":{"name":"Explore"},"e3ca":{"name":"Exposure"},"e3cb":{"name":"Exposure Neg 1"},"e3cc":{"name":"Exposure Neg 2"},"e3cd":{"name":"Exposure Plus 1"},"e3ce":{"name":"Exposure Plus 2"},"e3cf":{"name":"Exposure Zero"},"e87b":{"name":"Extension"},"e87c":{"name":"Face"},"e01f":{"name":"Fast Forward"},"e020":{"name":"Fast Rewind"},"e87d":{"name":"Favorite"},"e87e":{"name":"Favorite Border"},"e06d":{"name":"Featured Play List"},"e06e":{"name":"Featured Video"},"e87f":{"name":"Feedback"},"e05d":{"name":"Fiber Dvr"},"e061":{"name":"Fiber Manual Record"},"e05e":{"name":"Fiber New"},"e06a":{"name":"Fiber Pin"},"e062":{"name":"Fiber Smart Record"},"e2c4":{"name":"File Download"},"e2c6":{"name":"File Upload"},"e3d3":{"name":"Filter"},"e3d0":{"name":"Filter 1"},"e3d1":{"name":"Filter 2"},"e3d2":{"name":"Filter 3"},"e3d4":{"name":"Filter 4"},"e3d5":{"name":"Filter 5"},"e3d6":{"name":"Filter 6"},"e3d7":{"name":"Filter 7"},"e3d8":{"name":"Filter 8"},"e3d9":{"name":"Filter 9"},"e3da":{"name":"Filter 9 Plus"},"e3db":{"name":"Filter B And W"},"e3dc":{"name":"Filter Center Focus"},"e3dd":{"name":"Filter Drama"},"e3de":{"name":"Filter Frames"},"e3df":{"name":"Filter Hdr"},"e152":{"name":"Filter List"},"e3e0":{"name":"Filter None"},"e3e2":{"name":"Filter Tilt Shift"},"e3e3":{"name":"Filter Vintage"},"e880":{"name":"Find In Page"},"e881":{"name":"Find Replace"},"e90d":{"name":"Fingerprint"},"e5dc":{"name":"First Page"},"eb43":{"name":"Fitness Center"},"e153":{"name":"Flag"},"e3e4":{"name":"Flare"},"e3e5":{"name":"Flash Auto"},"e3e6":{"name":"Flash Off"},"e3e7":{"name":"Flash On"},"e539":{"name":"Flight"},"e904":{"name":"Flight Land"},"e905":{"name":"Flight Takeoff"},"e3e8":{"name":"Flip"},"e882":{"name":"Flip To Back"},"e883":{"name":"Flip To Front"},"e2c7":{"name":"Folder"},"e2c8":{"name":"Folder Open"},"e2c9":{"name":"Folder Shared"},"e617":{"name":"Folder Special"},"e167":{"name":"Font Download"},"e234":{"name":"Format Align Center"},"e235":{"name":"Format Align Justify"},"e236":{"name":"Format Align Left"},"e237":{"name":"Format Align Right"},"e238":{"name":"Format Bold"},"e239":{"name":"Format Clear"},"e23a":{"name":"Format Color Fill"},"e23b":{"name":"Format Color Reset"},"e23c":{"name":"Format Color Text"},"e23d":{"name":"Format Indent Decrease"},"e23e":{"name":"Format Indent Increase"},"e23f":{"name":"Format Italic"},"e240":{"name":"Format Line Spacing"},"e241":{"name":"Format List Bulleted"},"e242":{"name":"Format List Numbered"},"e243":{"name":"Format Paint"},"e244":{"name":"Format Quote"},"e25e":{"name":"Format Shapes"},"e245":{"name":"Format Size"},"e246":{"name":"Format Strikethrough"},"e247":{"name":"Format Textdirection L To R"},"e248":{"name":"Format Textdirection R To L"},"e249":{"name":"Format Underlined"},"e0bf":{"name":"Forum"},"e154":{"name":"Forward"},"e056":{"name":"Forward 10"},"e057":{"name":"Forward 30"},"e058":{"name":"Forward 5"},"eb44":{"name":"Free Breakfast"},"e5d0":{"name":"Fullscreen"},"e5d1":{"name":"Fullscreen Exit"},"e24a":{"name":"Functions"},"e927":{"name":"G Translate"},"e30f":{"name":"Gamepad"},"e021":{"name":"Games"},"e90e":{"name":"Gavel"},"e155":{"name":"Gesture"},"e884":{"name":"Get App"},"e908":{"name":"Gif"},"eb45":{"name":"Golf Course"},"e1b3":{"name":"Gps Fixed"},"e1b4":{"name":"Gps Not Fixed"},"e1b5":{"name":"Gps Off"},"e885":{"name":"Grade"},"e3e9":{"name":"Gradient"},"e3ea":{"name":"Grain"},"e1b8":{"name":"Graphic Eq"},"e3eb":{"name":"Grid Off"},"e3ec":{"name":"Grid On"},"e7ef":{"name":"Group"},"e7f0":{"name":"Group Add"},"e886":{"name":"Group Work"},"e052":{"name":"Hd"},"e3ed":{"name":"Hdr Off"},"e3ee":{"name":"Hdr On"},"e3f1":{"name":"Hdr Strong"},"e3f2":{"name":"Hdr Weak"},"e310":{"name":"Headset"},"e311":{"name":"Headset Mic"},"e3f3":{"name":"Healing"},"e023":{"name":"Hearing"},"e887":{"name":"Help"},"e8fd":{"name":"Help Outline"},"e024":{"name":"High Quality"},"e25f":{"name":"Highlight"},"e888":{"name":"Highlight Off"},"e889":{"name":"History"},"e88a":{"name":"Home"},"eb46":{"name":"Hot Tub"},"e53a":{"name":"Hotel"},"e88b":{"name":"Hourglass Empty"},"e88c":{"name":"Hourglass Full"},"e902":{"name":"Http"},"e88d":{"name":"Https"},"e3f4":{"name":"Image"},"e3f5":{"name":"Image Aspect Ratio"},"e0e0":{"name":"Import Contacts"},"e0c3":{"name":"Import Export"},"e912":{"name":"Important Devices"},"e156":{"name":"Inbox"},"e909":{"name":"Indeterminate Check Box"},"e88e":{"name":"Info"},"e88f":{"name":"Info Outline"},"e890":{"name":"Input"},"e24b":{"name":"Insert Chart"},"e24c":{"name":"Insert Comment"},"e24d":{"name":"Insert Drive File"},"e24e":{"name":"Insert Emoticon"},"e24f":{"name":"Insert Invitation"},"e250":{"name":"Insert Link"},"e251":{"name":"Insert Photo"},"e891":{"name":"Invert Colors"},"e0c4":{"name":"Invert Colors Off"},"e3f6":{"name":"Iso"},"e312":{"name":"Keyboard"},"e313":{"name":"Keyboard Arrow Down"},"e314":{"name":"Keyboard Arrow Left"},"e315":{"name":"Keyboard Arrow Right"},"e316":{"name":"Keyboard Arrow Up"},"e317":{"name":"Keyboard Backspace"},"e318":{"name":"Keyboard Capslock"},"e31a":{"name":"Keyboard Hide"},"e31b":{"name":"Keyboard Return"},"e31c":{"name":"Keyboard Tab"},"e31d":{"name":"Keyboard Voice"},"eb47":{"name":"Kitchen"},"e892":{"name":"Label"},"e893":{"name":"Label Outline"},"e3f7":{"name":"Landscape"},"e894":{"name":"Language"},"e31e":{"name":"Laptop"},"e31f":{"name":"Laptop Chromebook"},"e320":{"name":"Laptop Mac"},"e321":{"name":"Laptop Windows"},"e5dd":{"name":"Last Page"},"e895":{"name":"Launch"},"e53b":{"name":"Layers"},"e53c":{"name":"Layers Clear"},"e3f8":{"name":"Leak Add"},"e3f9":{"name":"Leak Remove"},"e3fa":{"name":"Lens"},"e02e":{"name":"Library Add"},"e02f":{"name":"Library Books"},"e030":{"name":"Library Music"},"e90f":{"name":"Lightbulb Outline"},"e919":{"name":"Line Style"},"e91a":{"name":"Line Weight"},"e260":{"name":"Linear Scale"},"e157":{"name":"Link"},"e438":{"name":"Linked Camera"},"e896":{"name":"List"},"e0c6":{"name":"Live Help"},"e639":{"name":"Live Tv"},"e53f":{"name":"Local Activity"},"e53d":{"name":"Local Airport"},"e53e":{"name":"Local Atm"},"e540":{"name":"Local Bar"},"e541":{"name":"Local Cafe"},"e542":{"name":"Local Car Wash"},"e543":{"name":"Local Convenience Store"},"e556":{"name":"Local Dining"},"e544":{"name":"Local Drink"},"e545":{"name":"Local Florist"},"e546":{"name":"Local Gas Station"},"e547":{"name":"Local Grocery Store"},"e548":{"name":"Local Hospital"},"e549":{"name":"Local Hotel"},"e54a":{"name":"Local Laundry Service"},"e54b":{"name":"Local Library"},"e54c":{"name":"Local Mall"},"e54d":{"name":"Local Movies"},"e54e":{"name":"Local Offer"},"e54f":{"name":"Local Parking"},"e550":{"name":"Local Pharmacy"},"e551":{"name":"Local Phone"},"e552":{"name":"Local Pizza"},"e553":{"name":"Local Play"},"e554":{"name":"Local Post Office"},"e555":{"name":"Local Printshop"},"e557":{"name":"Local See"},"e558":{"name":"Local Shipping"},"e559":{"name":"Local Taxi"},"e7f1":{"name":"Location City"},"e1b6":{"name":"Location Disabled"},"e0c7":{"name":"Location Off"},"e0c8":{"name":"Location On"},"e1b7":{"name":"Location Searching"},"e897":{"name":"Lock"},"e898":{"name":"Lock Open"},"e899":{"name":"Lock Outline"},"e3fc":{"name":"Looks"},"e3fb":{"name":"Looks 3"},"e3fd":{"name":"Looks 4"},"e3fe":{"name":"Looks 5"},"e3ff":{"name":"Looks 6"},"e400":{"name":"Looks One"},"e401":{"name":"Looks Two"},"e028":{"name":"Loop"},"e402":{"name":"Loupe"},"e16d":{"name":"Low Priority"},"e89a":{"name":"Loyalty"},"e158":{"name":"Mail"},"e0e1":{"name":"Mail Outline"},"e55b":{"name":"Map"},"e159":{"name":"Markunread"},"e89b":{"name":"Markunread Mailbox"},"e322":{"name":"Memory"},"e5d2":{"name":"Menu"},"e252":{"name":"Merge Type"},"e0c9":{"name":"Message"},"e029":{"name":"Mic"},"e02a":{"name":"Mic None"},"e02b":{"name":"Mic Off"},"e618":{"name":"Mms"},"e253":{"name":"Mode Comment"},"e254":{"name":"Mode Edit"},"e263":{"name":"Monetization On"},"e25c":{"name":"Money Off"},"e403":{"name":"Monochrome Photos"},"e7f2":{"name":"Mood"},"e7f3":{"name":"Mood Bad"},"e619":{"name":"More"},"e5d3":{"name":"More Horiz"},"e5d4":{"name":"More Vert"},"e91b":{"name":"Motorcycle"},"e323":{"name":"Mouse"},"e168":{"name":"Move To Inbox"},"e02c":{"name":"Movie"},"e404":{"name":"Movie Creation"},"e43a":{"name":"Movie Filter"},"e6df":{"name":"Multiline Chart"},"e405":{"name":"Music Note"},"e063":{"name":"Music Video"},"e55c":{"name":"My Location"},"e406":{"name":"Nature"},"e407":{"name":"Nature People"},"e408":{"name":"Navigate Before"},"e409":{"name":"Navigate Next"},"e55d":{"name":"Navigation"},"e569":{"name":"Near Me"},"e1b9":{"name":"Network Cell"},"e640":{"name":"Network Check"},"e61a":{"name":"Network Locked"},"e1ba":{"name":"Network Wifi"},"e031":{"name":"New Releases"},"e16a":{"name":"Next Week"},"e1bb":{"name":"Nfc"},"e641":{"name":"No Encryption"},"e0cc":{"name":"No Sim"},"e033":{"name":"Not Interested"},"e06f":{"name":"Note"},"e89c":{"name":"Note Add"},"e7f4":{"name":"Notifications"},"e7f7":{"name":"Notifications Active"},"e7f5":{"name":"Notifications None"},"e7f6":{"name":"Notifications Off"},"e7f8":{"name":"Notifications Paused"},"e90a":{"name":"Offline Pin"},"e63a":{"name":"Ondemand Video"},"e91c":{"name":"Opacity"},"e89d":{"name":"Open In Browser"},"e89e":{"name":"Open In New"},"e89f":{"name":"Open With"},"e7f9":{"name":"Pages"},"e8a0":{"name":"Pageview"},"e40a":{"name":"Palette"},"e925":{"name":"Pan Tool"},"e40b":{"name":"Panorama"},"e40c":{"name":"Panorama Fish Eye"},"e40d":{"name":"Panorama Horizontal"},"e40e":{"name":"Panorama Vertical"},"e40f":{"name":"Panorama Wide Angle"},"e7fa":{"name":"Party Mode"},"e034":{"name":"Pause"},"e035":{"name":"Pause Circle Filled"},"e036":{"name":"Pause Circle Outline"},"e8a1":{"name":"Payment"},"e7fb":{"name":"People"},"e7fc":{"name":"People Outline"},"e8a2":{"name":"Perm Camera Mic"},"e8a3":{"name":"Perm Contact Calendar"},"e8a4":{"name":"Perm Data Setting"},"e8a5":{"name":"Perm Device Information"},"e8a6":{"name":"Perm Identity"},"e8a7":{"name":"Perm Media"},"e8a8":{"name":"Perm Phone Msg"},"e8a9":{"name":"Perm Scan Wifi"},"e7fd":{"name":"Person"},"e7fe":{"name":"Person Add"},"e7ff":{"name":"Person Outline"},"e55a":{"name":"Person Pin"},"e56a":{"name":"Person Pin Circle"},"e63b":{"name":"Personal Video"},"e91d":{"name":"Pets"},"e0cd":{"name":"Phone"},"e324":{"name":"Phone Android"},"e61b":{"name":"Phone Bluetooth Speaker"},"e61c":{"name":"Phone Forwarded"},"e61d":{"name":"Phone In Talk"},"e325":{"name":"Phone Iphone"},"e61e":{"name":"Phone Locked"},"e61f":{"name":"Phone Missed"},"e620":{"name":"Phone Paused"},"e326":{"name":"Phonelink"},"e0db":{"name":"Phonelink Erase"},"e0dc":{"name":"Phonelink Lock"},"e327":{"name":"Phonelink Off"},"e0dd":{"name":"Phonelink Ring"},"e0de":{"name":"Phonelink Setup"},"e410":{"name":"Photo"},"e411":{"name":"Photo Album"},"e412":{"name":"Photo Camera"},"e43b":{"name":"Photo Filter"},"e413":{"name":"Photo Library"},"e432":{"name":"Photo Size Select Actual"},"e433":{"name":"Photo Size Select Large"},"e434":{"name":"Photo Size Select Small"},"e415":{"name":"Picture As Pdf"},"e8aa":{"name":"Picture In Picture"},"e911":{"name":"Picture In Picture Alt"},"e6c4":{"name":"Pie Chart"},"e6c5":{"name":"Pie Chart Outlined"},"e55e":{"name":"Pin Drop"},"e55f":{"name":"Place"},"e037":{"name":"Play Arrow"},"e038":{"name":"Play Circle Filled"},"e039":{"name":"Play Circle Outline"},"e906":{"name":"Play For Work"},"e03b":{"name":"Playlist Add"},"e065":{"name":"Playlist Add Check"},"e05f":{"name":"Playlist Play"},"e800":{"name":"Plus One"},"e801":{"name":"Poll"},"e8ab":{"name":"Polymer"},"eb48":{"name":"Pool"},"e0ce":{"name":"Portable Wifi Off"},"e416":{"name":"Portrait"},"e63c":{"name":"Power"},"e336":{"name":"Power Input"},"e8ac":{"name":"Power Settings New"},"e91e":{"name":"Pregnant Woman"},"e0df":{"name":"Present To All"},"e8ad":{"name":"Print"},"e645":{"name":"Priority High"},"e80b":{"name":"Public"},"e255":{"name":"Publish"},"e8ae":{"name":"Query Builder"},"e8af":{"name":"Question Answer"},"e03c":{"name":"Queue"},"e03d":{"name":"Queue Music"},"e066":{"name":"Queue Play Next"},"e03e":{"name":"Radio"},"e837":{"name":"Radio Button Checked"},"e836":{"name":"Radio Button Unchecked"},"e560":{"name":"Rate Review"},"e8b0":{"name":"Receipt"},"e03f":{"name":"Recent Actors"},"e91f":{"name":"Record Voice Over"},"e8b1":{"name":"Redeem"},"e15a":{"name":"Redo"},"e5d5":{"name":"Refresh"},"e15b":{"name":"Remove"},"e15c":{"name":"Remove Circle"},"e15d":{"name":"Remove Circle Outline"},"e067":{"name":"Remove From Queue"},"e417":{"name":"Remove Red Eye"},"e928":{"name":"Remove Shopping Cart"},"e8fe":{"name":"Reorder"},"e040":{"name":"Repeat"},"e041":{"name":"Repeat One"},"e042":{"name":"Replay"},"e059":{"name":"Replay 10"},"e05a":{"name":"Replay 30"},"e05b":{"name":"Replay 5"},"e15e":{"name":"Reply"},"e15f":{"name":"Reply All"},"e160":{"name":"Report"},"e8b2":{"name":"Report Problem"},"e56c":{"name":"Restaurant"},"e561":{"name":"Restaurant Menu"},"e8b3":{"name":"Restore"},"e929":{"name":"Restore Page"},"e0d1":{"name":"Ring Volume"},"e8b4":{"name":"Room"},"eb49":{"name":"Room Service"},"e418":{"name":"Rotate 90 Degrees Ccw"},"e419":{"name":"Rotate Left"},"e41a":{"name":"Rotate Right"},"e920":{"name":"Rounded Corner"},"e328":{"name":"Router"},"e921":{"name":"Rowing"},"e0e5":{"name":"Rss Feed"},"e642":{"name":"Rv Hookup"},"e562":{"name":"Satellite"},"e161":{"name":"Save"},"e329":{"name":"Scanner"},"e8b5":{"name":"Schedule"},"e80c":{"name":"School"},"e1be":{"name":"Screen Lock Landscape"},"e1bf":{"name":"Screen Lock Portrait"},"e1c0":{"name":"Screen Lock Rotation"},"e1c1":{"name":"Screen Rotation"},"e0e2":{"name":"Screen Share"},"e623":{"name":"Sd Card"},"e1c2":{"name":"Sd Storage"},"e8b6":{"name":"Search"},"e32a":{"name":"Security"},"e162":{"name":"Select All"},"e163":{"name":"Send"},"e811":{"name":"Sentiment Dissatisfied"},"e812":{"name":"Sentiment Neutral"},"e813":{"name":"Sentiment Satisfied"},"e814":{"name":"Sentiment Very Dissatisfied"},"e815":{"name":"Sentiment Very Satisfied"},"e8b8":{"name":"Settings"},"e8b9":{"name":"Settings Applications"},"e8ba":{"name":"Settings Backup Restore"},"e8bb":{"name":"Settings Bluetooth"},"e8bd":{"name":"Settings Brightness"},"e8bc":{"name":"Settings Cell"},"e8be":{"name":"Settings Ethernet"},"e8bf":{"name":"Settings Input Antenna"},"e8c0":{"name":"Settings Input Component"},"e8c1":{"name":"Settings Input Composite"},"e8c2":{"name":"Settings Input Hdmi"},"e8c3":{"name":"Settings Input Svideo"},"e8c4":{"name":"Settings Overscan"},"e8c5":{"name":"Settings Phone"},"e8c6":{"name":"Settings Power"},"e8c7":{"name":"Settings Remote"},"e1c3":{"name":"Settings System Daydream"},"e8c8":{"name":"Settings Voice"},"e80d":{"name":"Share"},"e8c9":{"name":"Shop"},"e8ca":{"name":"Shop Two"},"e8cb":{"name":"Shopping Basket"},"e8cc":{"name":"Shopping Cart"},"e261":{"name":"Short Text"},"e6e1":{"name":"Show Chart"},"e043":{"name":"Shuffle"},"e1c8":{"name":"Signal Cellular 4 Bar"},"e1cd":{"name":"Signal Cellular Connected No Internet 4 Bar"},"e1ce":{"name":"Signal Cellular No Sim"},"e1cf":{"name":"Signal Cellular Null"},"e1d0":{"name":"Signal Cellular Off"},"e1d8":{"name":"Signal Wifi 4 Bar"},"e1d9":{"name":"Signal Wifi 4 Bar Lock"},"e1da":{"name":"Signal Wifi Off"},"e32b":{"name":"Sim Card"},"e624":{"name":"Sim Card Alert"},"e044":{"name":"Skip Next"},"e045":{"name":"Skip Previous"},"e41b":{"name":"Slideshow"},"e068":{"name":"Slow Motion Video"},"e32c":{"name":"Smartphone"},"eb4a":{"name":"Smoke Free"},"eb4b":{"name":"Smoking Rooms"},"e625":{"name":"Sms"},"e626":{"name":"Sms Failed"},"e046":{"name":"Snooze"},"e164":{"name":"Sort"},"e053":{"name":"Sort By Alpha"},"eb4c":{"name":"Spa"},"e256":{"name":"Space Bar"},"e32d":{"name":"Speaker"},"e32e":{"name":"Speaker Group"},"e8cd":{"name":"Speaker Notes"},"e92a":{"name":"Speaker Notes Off"},"e0d2":{"name":"Speaker Phone"},"e8ce":{"name":"Spellcheck"},"e838":{"name":"Star"},"e83a":{"name":"Star Border"},"e839":{"name":"Star Half"},"e8d0":{"name":"Stars"},"e0d3":{"name":"Stay Current Landscape"},"e0d4":{"name":"Stay Current Portrait"},"e0d5":{"name":"Stay Primary Landscape"},"e0d6":{"name":"Stay Primary Portrait"},"e047":{"name":"Stop"},"e0e3":{"name":"Stop Screen Share"},"e1db":{"name":"Storage"},"e8d1":{"name":"Store"},"e563":{"name":"Store Mall Directory"},"e41c":{"name":"Straighten"},"e56e":{"name":"Streetview"},"e257":{"name":"Strikethrough S"},"e41d":{"name":"Style"},"e5d9":{"name":"Subdirectory Arrow Left"},"e5da":{"name":"Subdirectory Arrow Right"},"e8d2":{"name":"Subject"},"e064":{"name":"Subscriptions"},"e048":{"name":"Subtitles"},"e56f":{"name":"Subway"},"e8d3":{"name":"Supervisor Account"},"e049":{"name":"Surround Sound"},"e0d7":{"name":"Swap Calls"},"e8d4":{"name":"Swap Horiz"},"e8d5":{"name":"Swap Vert"},"e8d6":{"name":"Swap Vertical Circle"},"e41e":{"name":"Switch Camera"},"e41f":{"name":"Switch Video"},"e627":{"name":"Sync"},"e628":{"name":"Sync Disabled"},"e629":{"name":"Sync Problem"},"e62a":{"name":"System Update"},"e8d7":{"name":"System Update Alt"},"e8d8":{"name":"Tab"},"e8d9":{"name":"Tab Unselected"},"e32f":{"name":"Tablet"},"e330":{"name":"Tablet Android"},"e331":{"name":"Tablet Mac"},"e420":{"name":"Tag Faces"},"e62b":{"name":"Tap And Play"},"e564":{"name":"Terrain"},"e262":{"name":"Text Fields"},"e165":{"name":"Text Format"},"e0d8":{"name":"Textsms"},"e421":{"name":"Texture"},"e8da":{"name":"Theaters"},"e8db":{"name":"Thumb Down"},"e8dc":{"name":"Thumb Up"},"e8dd":{"name":"Thumbs Up Down"},"e62c":{"name":"Time To Leave"},"e422":{"name":"Timelapse"},"e922":{"name":"Timeline"},"e425":{"name":"Timer"},"e423":{"name":"Timer 10"},"e424":{"name":"Timer 3"},"e426":{"name":"Timer Off"},"e264":{"name":"Title"},"e8de":{"name":"Toc"},"e8df":{"name":"Today"},"e8e0":{"name":"Toll"},"e427":{"name":"Tonality"},"e913":{"name":"Touch App"},"e332":{"name":"Toys"},"e8e1":{"name":"Track Changes"},"e565":{"name":"Traffic"},"e570":{"name":"Train"},"e571":{"name":"Tram"},"e572":{"name":"Transfer Within A Station"},"e428":{"name":"Transform"},"e8e2":{"name":"Translate"},"e8e3":{"name":"Trending Down"},"e8e4":{"name":"Trending Flat"},"e8e5":{"name":"Trending Up"},"e429":{"name":"Tune"},"e8e6":{"name":"Turned In"},"e8e7":{"name":"Turned In Not"},"e333":{"name":"Tv"},"e169":{"name":"Unarchive"},"e166":{"name":"Undo"},"e5d6":{"name":"Unfold Less"},"e5d7":{"name":"Unfold More"},"e923":{"name":"Update"},"e1e0":{"name":"Usb"},"e8e8":{"name":"Verified User"},"e258":{"name":"Vertical Align Bottom"},"e259":{"name":"Vertical Align Center"},"e25a":{"name":"Vertical Align Top"},"e62d":{"name":"Vibration"},"e070":{"name":"Video Call"},"e071":{"name":"Video Label"},"e04a":{"name":"Video Library"},"e04b":{"name":"Videocam"},"e04c":{"name":"Videocam Off"},"e338":{"name":"Videogame Asset"},"e8e9":{"name":"View Agenda"},"e8ea":{"name":"View Array"},"e8eb":{"name":"View Carousel"},"e8ec":{"name":"View Column"},"e42a":{"name":"View Comfy"},"e42b":{"name":"View Compact"},"e8ed":{"name":"View Day"},"e8ee":{"name":"View Headline"},"e8ef":{"name":"View List"},"e8f0":{"name":"View Module"},"e8f1":{"name":"View Quilt"},"e8f2":{"name":"View Stream"},"e8f3":{"name":"View Week"},"e435":{"name":"Vignette"},"e8f4":{"name":"Visibility"},"e8f5":{"name":"Visibility Off"},"e62e":{"name":"Voice Chat"},"e0d9":{"name":"Voicemail"},"e04d":{"name":"Volume Down"},"e04e":{"name":"Volume Mute"},"e04f":{"name":"Volume Off"},"e050":{"name":"Volume Up"},"e0da":{"name":"Vpn Key"},"e62f":{"name":"Vpn Lock"},"e1bc":{"name":"Wallpaper"},"e002":{"name":"Warning"},"e334":{"name":"Watch"},"e924":{"name":"Watch Later"},"e42c":{"name":"Wb Auto"},"e42d":{"name":"Wb Cloudy"},"e42e":{"name":"Wb Incandescent"},"e436":{"name":"Wb Iridescent"},"e430":{"name":"Wb Sunny"},"e63d":{"name":"Wc"},"e051":{"name":"Web"},"e069":{"name":"Web Asset"},"e16b":{"name":"Weekend"},"e80e":{"name":"Whatshot"},"e1bd":{"name":"Widgets"},"e63e":{"name":"Wifi"},"e1e1":{"name":"Wifi Lock"},"e1e2":{"name":"Wifi Tethering"},"e8f9":{"name":"Work"},"e25b":{"name":"Wrap Text"},"e8fa":{"name":"Youtube Searched For"},"e8ff":{"name":"Zoom In"},"e900":{"name":"Zoom Out"},"e56b":{"name":"Zoom Out Map"}}} \ No newline at end of file diff --git a/hassio-google-drive-backup/backup/static/css/iconfont/MaterialIcons-Regular.svg b/hassio-google-drive-backup/backup/static/css/iconfont/MaterialIcons-Regular.svg new file mode 100644 index 0000000..a449327 --- /dev/null +++ b/hassio-google-drive-backup/backup/static/css/iconfont/MaterialIcons-Regular.svg @@ -0,0 +1,2373 @@ + + + + + +Created by FontForge 20151118 at Mon Feb 8 11:58:02 2016 + By shyndman +Copyright 2015 Google, Inc. All Rights Reserved. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/hassio-google-drive-backup/backup/static/css/iconfont/MaterialIcons-Regular.ttf b/hassio-google-drive-backup/backup/static/css/iconfont/MaterialIcons-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..7015564ad166a3e9d88c82f17829f0cc01ebe29a GIT binary patch literal 128180 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 ohyj3!`#ee%L;DTjx@8!5k5~VH0QmdE^#A|> literal 0 HcmV?d00001 diff --git a/hassio-google-drive-backup/backup/static/css/iconfont/MaterialIcons-Regular.woff b/hassio-google-drive-backup/backup/static/css/iconfont/MaterialIcons-Regular.woff new file mode 100644 index 0000000000000000000000000000000000000000..b648a3eea2d16b6ce783906d6b7d5f251b9eb56c GIT binary patch literal 57620 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

wF7DI|Kx|V1sBQ9FYs>m5C)C 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 BJ$(QG literal 0 HcmV?d00001 diff --git a/hassio-google-drive-backup/backup/static/css/iconfont/MaterialIcons-Regular.woff2 b/hassio-google-drive-backup/backup/static/css/iconfont/MaterialIcons-Regular.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..9fa211252080046a23b2449dbdced6abc2b0bb34 GIT binary patch literal 44300 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 QW8}wPUeo^@jR61v08@RD2LJ#7 literal 0 HcmV?d00001 diff --git a/hassio-google-drive-backup/backup/static/css/iconfont/codepoints b/hassio-google-drive-backup/backup/static/css/iconfont/codepoints new file mode 100644 index 0000000..3c8b075 --- /dev/null +++ b/hassio-google-drive-backup/backup/static/css/iconfont/codepoints @@ -0,0 +1,932 @@ +3d_rotation e84d +ac_unit eb3b +access_alarm e190 +access_alarms e191 +access_time e192 +accessibility e84e +accessible e914 +account_balance e84f +account_balance_wallet e850 +account_box e851 +account_circle e853 +adb e60e +add e145 +add_a_photo e439 +add_alarm e193 +add_alert e003 +add_box e146 +add_circle e147 +add_circle_outline e148 +add_location e567 +add_shopping_cart e854 +add_to_photos e39d +add_to_queue e05c +adjust e39e +airline_seat_flat e630 +airline_seat_flat_angled e631 +airline_seat_individual_suite e632 +airline_seat_legroom_extra e633 +airline_seat_legroom_normal e634 +airline_seat_legroom_reduced e635 +airline_seat_recline_extra e636 +airline_seat_recline_normal e637 +airplanemode_active e195 +airplanemode_inactive e194 +airplay e055 +airport_shuttle eb3c +alarm e855 +alarm_add e856 +alarm_off e857 +alarm_on e858 +album e019 +all_inclusive eb3d +all_out e90b +android e859 +announcement e85a +apps e5c3 +archive e149 +arrow_back e5c4 +arrow_downward e5db +arrow_drop_down e5c5 +arrow_drop_down_circle e5c6 +arrow_drop_up e5c7 +arrow_forward e5c8 +arrow_upward e5d8 +art_track e060 +aspect_ratio e85b +assessment e85c +assignment e85d +assignment_ind e85e +assignment_late e85f +assignment_return e860 +assignment_returned e861 +assignment_turned_in e862 +assistant e39f +assistant_photo e3a0 +attach_file e226 +attach_money e227 +attachment e2bc +audiotrack e3a1 +autorenew e863 +av_timer e01b +backspace e14a +backup e864 +battery_alert e19c +battery_charging_full e1a3 +battery_full e1a4 +battery_std e1a5 +battery_unknown e1a6 +beach_access eb3e +beenhere e52d +block e14b +bluetooth e1a7 +bluetooth_audio e60f +bluetooth_connected e1a8 +bluetooth_disabled e1a9 +bluetooth_searching e1aa +blur_circular e3a2 +blur_linear e3a3 +blur_off e3a4 +blur_on e3a5 +book e865 +bookmark e866 +bookmark_border e867 +border_all e228 +border_bottom e229 +border_clear e22a +border_color e22b +border_horizontal e22c +border_inner e22d +border_left e22e +border_outer e22f +border_right e230 +border_style e231 +border_top e232 +border_vertical e233 +branding_watermark e06b +brightness_1 e3a6 +brightness_2 e3a7 +brightness_3 e3a8 +brightness_4 e3a9 +brightness_5 e3aa +brightness_6 e3ab +brightness_7 e3ac +brightness_auto e1ab +brightness_high e1ac +brightness_low e1ad +brightness_medium e1ae +broken_image e3ad +brush e3ae +bubble_chart e6dd +bug_report e868 +build e869 +burst_mode e43c +business e0af +business_center eb3f +cached e86a +cake e7e9 +call e0b0 +call_end e0b1 +call_made e0b2 +call_merge e0b3 +call_missed e0b4 +call_missed_outgoing e0e4 +call_received e0b5 +call_split e0b6 +call_to_action e06c +camera e3af +camera_alt e3b0 +camera_enhance e8fc +camera_front e3b1 +camera_rear e3b2 +camera_roll e3b3 +cancel e5c9 +card_giftcard e8f6 +card_membership e8f7 +card_travel e8f8 +casino eb40 +cast e307 +cast_connected e308 +center_focus_strong e3b4 +center_focus_weak e3b5 +change_history e86b +chat e0b7 +chat_bubble e0ca +chat_bubble_outline e0cb +check e5ca +check_box e834 +check_box_outline_blank e835 +check_circle e86c +chevron_left e5cb +chevron_right e5cc +child_care eb41 +child_friendly eb42 +chrome_reader_mode e86d +class e86e +clear e14c +clear_all e0b8 +close e5cd +closed_caption e01c +cloud e2bd +cloud_circle e2be +cloud_done e2bf +cloud_download e2c0 +cloud_off e2c1 +cloud_queue e2c2 +cloud_upload e2c3 +code e86f +collections e3b6 +collections_bookmark e431 +color_lens e3b7 +colorize e3b8 +comment e0b9 +compare e3b9 +compare_arrows e915 +computer e30a +confirmation_number e638 +contact_mail e0d0 +contact_phone e0cf +contacts e0ba +content_copy e14d +content_cut e14e +content_paste e14f +control_point e3ba +control_point_duplicate e3bb +copyright e90c +create e150 +create_new_folder e2cc +credit_card e870 +crop e3be +crop_16_9 e3bc +crop_3_2 e3bd +crop_5_4 e3bf +crop_7_5 e3c0 +crop_din e3c1 +crop_free e3c2 +crop_landscape e3c3 +crop_original e3c4 +crop_portrait e3c5 +crop_rotate e437 +crop_square e3c6 +dashboard e871 +data_usage e1af +date_range e916 +dehaze e3c7 +delete e872 +delete_forever e92b +delete_sweep e16c +description e873 +desktop_mac e30b +desktop_windows e30c +details e3c8 +developer_board e30d +developer_mode e1b0 +device_hub e335 +devices e1b1 +devices_other e337 +dialer_sip e0bb +dialpad e0bc +directions e52e +directions_bike e52f +directions_boat e532 +directions_bus e530 +directions_car e531 +directions_railway e534 +directions_run e566 +directions_subway e533 +directions_transit e535 +directions_walk e536 +disc_full e610 +dns e875 +do_not_disturb e612 +do_not_disturb_alt e611 +do_not_disturb_off e643 +do_not_disturb_on e644 +dock e30e +domain e7ee +done e876 +done_all e877 +donut_large e917 +donut_small e918 +drafts e151 +drag_handle e25d +drive_eta e613 +dvr e1b2 +edit e3c9 +edit_location e568 +eject e8fb +email e0be +enhanced_encryption e63f +equalizer e01d +error e000 +error_outline e001 +euro_symbol e926 +ev_station e56d +event e878 +event_available e614 +event_busy e615 +event_note e616 +event_seat e903 +exit_to_app e879 +expand_less e5ce +expand_more e5cf +explicit e01e +explore e87a +exposure e3ca +exposure_neg_1 e3cb +exposure_neg_2 e3cc +exposure_plus_1 e3cd +exposure_plus_2 e3ce +exposure_zero e3cf +extension e87b +face e87c +fast_forward e01f +fast_rewind e020 +favorite e87d +favorite_border e87e +featured_play_list e06d +featured_video e06e +feedback e87f +fiber_dvr e05d +fiber_manual_record e061 +fiber_new e05e +fiber_pin e06a +fiber_smart_record e062 +file_download e2c4 +file_upload e2c6 +filter e3d3 +filter_1 e3d0 +filter_2 e3d1 +filter_3 e3d2 +filter_4 e3d4 +filter_5 e3d5 +filter_6 e3d6 +filter_7 e3d7 +filter_8 e3d8 +filter_9 e3d9 +filter_9_plus e3da +filter_b_and_w e3db +filter_center_focus e3dc +filter_drama e3dd +filter_frames e3de +filter_hdr e3df +filter_list e152 +filter_none e3e0 +filter_tilt_shift e3e2 +filter_vintage e3e3 +find_in_page e880 +find_replace e881 +fingerprint e90d +first_page e5dc +fitness_center eb43 +flag e153 +flare e3e4 +flash_auto e3e5 +flash_off e3e6 +flash_on e3e7 +flight e539 +flight_land e904 +flight_takeoff e905 +flip e3e8 +flip_to_back e882 +flip_to_front e883 +folder e2c7 +folder_open e2c8 +folder_shared e2c9 +folder_special e617 +font_download e167 +format_align_center e234 +format_align_justify e235 +format_align_left e236 +format_align_right e237 +format_bold e238 +format_clear e239 +format_color_fill e23a +format_color_reset e23b +format_color_text e23c +format_indent_decrease e23d +format_indent_increase e23e +format_italic e23f +format_line_spacing e240 +format_list_bulleted e241 +format_list_numbered e242 +format_paint e243 +format_quote e244 +format_shapes e25e +format_size e245 +format_strikethrough e246 +format_textdirection_l_to_r e247 +format_textdirection_r_to_l e248 +format_underlined e249 +forum e0bf +forward e154 +forward_10 e056 +forward_30 e057 +forward_5 e058 +free_breakfast eb44 +fullscreen e5d0 +fullscreen_exit e5d1 +functions e24a +g_translate e927 +gamepad e30f +games e021 +gavel e90e +gesture e155 +get_app e884 +gif e908 +golf_course eb45 +gps_fixed e1b3 +gps_not_fixed e1b4 +gps_off e1b5 +grade e885 +gradient e3e9 +grain e3ea +graphic_eq e1b8 +grid_off e3eb +grid_on e3ec +group e7ef +group_add e7f0 +group_work e886 +hd e052 +hdr_off e3ed +hdr_on e3ee +hdr_strong e3f1 +hdr_weak e3f2 +headset e310 +headset_mic e311 +healing e3f3 +hearing e023 +help e887 +help_outline e8fd +high_quality e024 +highlight e25f +highlight_off e888 +history e889 +home e88a +hot_tub eb46 +hotel e53a +hourglass_empty e88b +hourglass_full e88c +http e902 +https e88d +image e3f4 +image_aspect_ratio e3f5 +import_contacts e0e0 +import_export e0c3 +important_devices e912 +inbox e156 +indeterminate_check_box e909 +info e88e +info_outline e88f +input e890 +insert_chart e24b +insert_comment e24c +insert_drive_file e24d +insert_emoticon e24e +insert_invitation e24f +insert_link e250 +insert_photo e251 +invert_colors e891 +invert_colors_off e0c4 +iso e3f6 +keyboard e312 +keyboard_arrow_down e313 +keyboard_arrow_left e314 +keyboard_arrow_right e315 +keyboard_arrow_up e316 +keyboard_backspace e317 +keyboard_capslock e318 +keyboard_hide e31a +keyboard_return e31b +keyboard_tab e31c +keyboard_voice e31d +kitchen eb47 +label e892 +label_outline e893 +landscape e3f7 +language e894 +laptop e31e +laptop_chromebook e31f +laptop_mac e320 +laptop_windows e321 +last_page e5dd +launch e895 +layers e53b +layers_clear e53c +leak_add e3f8 +leak_remove e3f9 +lens e3fa +library_add e02e +library_books e02f +library_music e030 +lightbulb_outline e90f +line_style e919 +line_weight e91a +linear_scale e260 +link e157 +linked_camera e438 +list e896 +live_help e0c6 +live_tv e639 +local_activity e53f +local_airport e53d +local_atm e53e +local_bar e540 +local_cafe e541 +local_car_wash e542 +local_convenience_store e543 +local_dining e556 +local_drink e544 +local_florist e545 +local_gas_station e546 +local_grocery_store e547 +local_hospital e548 +local_hotel e549 +local_laundry_service e54a +local_library e54b +local_mall e54c +local_movies e54d +local_offer e54e +local_parking e54f +local_pharmacy e550 +local_phone e551 +local_pizza e552 +local_play e553 +local_post_office e554 +local_printshop e555 +local_see e557 +local_shipping e558 +local_taxi e559 +location_city e7f1 +location_disabled e1b6 +location_off e0c7 +location_on e0c8 +location_searching e1b7 +lock e897 +lock_open e898 +lock_outline e899 +looks e3fc +looks_3 e3fb +looks_4 e3fd +looks_5 e3fe +looks_6 e3ff +looks_one e400 +looks_two e401 +loop e028 +loupe e402 +low_priority e16d +loyalty e89a +mail e158 +mail_outline e0e1 +map e55b +markunread e159 +markunread_mailbox e89b +memory e322 +menu e5d2 +merge_type e252 +message e0c9 +mic e029 +mic_none e02a +mic_off e02b +mms e618 +mode_comment e253 +mode_edit e254 +monetization_on e263 +money_off e25c +monochrome_photos e403 +mood e7f2 +mood_bad e7f3 +more e619 +more_horiz e5d3 +more_vert e5d4 +motorcycle e91b +mouse e323 +move_to_inbox e168 +movie e02c +movie_creation e404 +movie_filter e43a +multiline_chart e6df +music_note e405 +music_video e063 +my_location e55c +nature e406 +nature_people e407 +navigate_before e408 +navigate_next e409 +navigation e55d +near_me e569 +network_cell e1b9 +network_check e640 +network_locked e61a +network_wifi e1ba +new_releases e031 +next_week e16a +nfc e1bb +no_encryption e641 +no_sim e0cc +not_interested e033 +note e06f +note_add e89c +notifications e7f4 +notifications_active e7f7 +notifications_none e7f5 +notifications_off e7f6 +notifications_paused e7f8 +offline_pin e90a +ondemand_video e63a +opacity e91c +open_in_browser e89d +open_in_new e89e +open_with e89f +pages e7f9 +pageview e8a0 +palette e40a +pan_tool e925 +panorama e40b +panorama_fish_eye e40c +panorama_horizontal e40d +panorama_vertical e40e +panorama_wide_angle e40f +party_mode e7fa +pause e034 +pause_circle_filled e035 +pause_circle_outline e036 +payment e8a1 +people e7fb +people_outline e7fc +perm_camera_mic e8a2 +perm_contact_calendar e8a3 +perm_data_setting e8a4 +perm_device_information e8a5 +perm_identity e8a6 +perm_media e8a7 +perm_phone_msg e8a8 +perm_scan_wifi e8a9 +person e7fd +person_add e7fe +person_outline e7ff +person_pin e55a +person_pin_circle e56a +personal_video e63b +pets e91d +phone e0cd +phone_android e324 +phone_bluetooth_speaker e61b +phone_forwarded e61c +phone_in_talk e61d +phone_iphone e325 +phone_locked e61e +phone_missed e61f +phone_paused e620 +phonelink e326 +phonelink_erase e0db +phonelink_lock e0dc +phonelink_off e327 +phonelink_ring e0dd +phonelink_setup e0de +photo e410 +photo_album e411 +photo_camera e412 +photo_filter e43b +photo_library e413 +photo_size_select_actual e432 +photo_size_select_large e433 +photo_size_select_small e434 +picture_as_pdf e415 +picture_in_picture e8aa +picture_in_picture_alt e911 +pie_chart e6c4 +pie_chart_outlined e6c5 +pin_drop e55e +place e55f +play_arrow e037 +play_circle_filled e038 +play_circle_outline e039 +play_for_work e906 +playlist_add e03b +playlist_add_check e065 +playlist_play e05f +plus_one e800 +poll e801 +polymer e8ab +pool eb48 +portable_wifi_off e0ce +portrait e416 +power e63c +power_input e336 +power_settings_new e8ac +pregnant_woman e91e +present_to_all e0df +print e8ad +priority_high e645 +public e80b +publish e255 +query_builder e8ae +question_answer e8af +queue e03c +queue_music e03d +queue_play_next e066 +radio e03e +radio_button_checked e837 +radio_button_unchecked e836 +rate_review e560 +receipt e8b0 +recent_actors e03f +record_voice_over e91f +redeem e8b1 +redo e15a +refresh e5d5 +remove e15b +remove_circle e15c +remove_circle_outline e15d +remove_from_queue e067 +remove_red_eye e417 +remove_shopping_cart e928 +reorder e8fe +repeat e040 +repeat_one e041 +replay e042 +replay_10 e059 +replay_30 e05a +replay_5 e05b +reply e15e +reply_all e15f +report e160 +report_problem e8b2 +restaurant e56c +restaurant_menu e561 +restore e8b3 +restore_page e929 +ring_volume e0d1 +room e8b4 +room_service eb49 +rotate_90_degrees_ccw e418 +rotate_left e419 +rotate_right e41a +rounded_corner e920 +router e328 +rowing e921 +rss_feed e0e5 +rv_hookup e642 +satellite e562 +save e161 +scanner e329 +schedule e8b5 +school e80c +screen_lock_landscape e1be +screen_lock_portrait e1bf +screen_lock_rotation e1c0 +screen_rotation e1c1 +screen_share e0e2 +sd_card e623 +sd_storage e1c2 +search e8b6 +security e32a +select_all e162 +send e163 +sentiment_dissatisfied e811 +sentiment_neutral e812 +sentiment_satisfied e813 +sentiment_very_dissatisfied e814 +sentiment_very_satisfied e815 +settings e8b8 +settings_applications e8b9 +settings_backup_restore e8ba +settings_bluetooth e8bb +settings_brightness e8bd +settings_cell e8bc +settings_ethernet e8be +settings_input_antenna e8bf +settings_input_component e8c0 +settings_input_composite e8c1 +settings_input_hdmi e8c2 +settings_input_svideo e8c3 +settings_overscan e8c4 +settings_phone e8c5 +settings_power e8c6 +settings_remote e8c7 +settings_system_daydream e1c3 +settings_voice e8c8 +share e80d +shop e8c9 +shop_two e8ca +shopping_basket e8cb +shopping_cart e8cc +short_text e261 +show_chart e6e1 +shuffle e043 +signal_cellular_4_bar e1c8 +signal_cellular_connected_no_internet_4_bar e1cd +signal_cellular_no_sim e1ce +signal_cellular_null e1cf +signal_cellular_off e1d0 +signal_wifi_4_bar e1d8 +signal_wifi_4_bar_lock e1d9 +signal_wifi_off e1da +sim_card e32b +sim_card_alert e624 +skip_next e044 +skip_previous e045 +slideshow e41b +slow_motion_video e068 +smartphone e32c +smoke_free eb4a +smoking_rooms eb4b +sms e625 +sms_failed e626 +snooze e046 +sort e164 +sort_by_alpha e053 +spa eb4c +space_bar e256 +speaker e32d +speaker_group e32e +speaker_notes e8cd +speaker_notes_off e92a +speaker_phone e0d2 +spellcheck e8ce +star e838 +star_border e83a +star_half e839 +stars e8d0 +stay_current_landscape e0d3 +stay_current_portrait e0d4 +stay_primary_landscape e0d5 +stay_primary_portrait e0d6 +stop e047 +stop_screen_share e0e3 +storage e1db +store e8d1 +store_mall_directory e563 +straighten e41c +streetview e56e +strikethrough_s e257 +style e41d +subdirectory_arrow_left e5d9 +subdirectory_arrow_right e5da +subject e8d2 +subscriptions e064 +subtitles e048 +subway e56f +supervisor_account e8d3 +surround_sound e049 +swap_calls e0d7 +swap_horiz e8d4 +swap_vert e8d5 +swap_vertical_circle e8d6 +switch_camera e41e +switch_video e41f +sync e627 +sync_disabled e628 +sync_problem e629 +system_update e62a +system_update_alt e8d7 +tab e8d8 +tab_unselected e8d9 +tablet e32f +tablet_android e330 +tablet_mac e331 +tag_faces e420 +tap_and_play e62b +terrain e564 +text_fields e262 +text_format e165 +textsms e0d8 +texture e421 +theaters e8da +thumb_down e8db +thumb_up e8dc +thumbs_up_down e8dd +time_to_leave e62c +timelapse e422 +timeline e922 +timer e425 +timer_10 e423 +timer_3 e424 +timer_off e426 +title e264 +toc e8de +today e8df +toll e8e0 +tonality e427 +touch_app e913 +toys e332 +track_changes e8e1 +traffic e565 +train e570 +tram e571 +transfer_within_a_station e572 +transform e428 +translate e8e2 +trending_down e8e3 +trending_flat e8e4 +trending_up e8e5 +tune e429 +turned_in e8e6 +turned_in_not e8e7 +tv e333 +unarchive e169 +undo e166 +unfold_less e5d6 +unfold_more e5d7 +update e923 +usb e1e0 +verified_user e8e8 +vertical_align_bottom e258 +vertical_align_center e259 +vertical_align_top e25a +vibration e62d +video_call e070 +video_label e071 +video_library e04a +videocam e04b +videocam_off e04c +videogame_asset e338 +view_agenda e8e9 +view_array e8ea +view_carousel e8eb +view_column e8ec +view_comfy e42a +view_compact e42b +view_day e8ed +view_headline e8ee +view_list e8ef +view_module e8f0 +view_quilt e8f1 +view_stream e8f2 +view_week e8f3 +vignette e435 +visibility e8f4 +visibility_off e8f5 +voice_chat e62e +voicemail e0d9 +volume_down e04d +volume_mute e04e +volume_off e04f +volume_up e050 +vpn_key e0da +vpn_lock e62f +wallpaper e1bc +warning e002 +watch e334 +watch_later e924 +wb_auto e42c +wb_cloudy e42d +wb_incandescent e42e +wb_iridescent e436 +wb_sunny e430 +wc e63d +web e051 +web_asset e069 +weekend e16b +whatshot e80e +widgets e1bd +wifi e63e +wifi_lock e1e1 +wifi_tethering e1e2 +work e8f9 +wrap_text e25b +youtube_searched_for e8fa +zoom_in e8ff +zoom_out e900 +zoom_out_map e56b diff --git a/hassio-google-drive-backup/backup/static/css/iconfont/material-icons.css b/hassio-google-drive-backup/backup/static/css/iconfont/material-icons.css new file mode 100644 index 0000000..2270c09 --- /dev/null +++ b/hassio-google-drive-backup/backup/static/css/iconfont/material-icons.css @@ -0,0 +1,36 @@ +@font-face { + font-family: 'Material Icons'; + font-style: normal; + font-weight: 400; + src: url(MaterialIcons-Regular.eot); /* For IE6-8 */ + src: local('Material Icons'), + local('MaterialIcons-Regular'), + url(MaterialIcons-Regular.woff2) format('woff2'), + url(MaterialIcons-Regular.woff) format('woff'), + url(MaterialIcons-Regular.ttf) format('truetype'); +} + +.material-icons { + font-family: 'Material Icons'; + font-weight: normal; + font-style: normal; + font-size: 24px; /* Preferred icon size */ + display: inline-block; + line-height: 1; + text-transform: none; + letter-spacing: normal; + word-wrap: normal; + white-space: nowrap; + direction: ltr; + + /* Support for all WebKit browsers. */ + -webkit-font-smoothing: antialiased; + /* Support for Safari and Chrome. */ + text-rendering: optimizeLegibility; + + /* Support for Firefox. */ + -moz-osx-font-smoothing: grayscale; + + /* Support for IE. */ + font-feature-settings: 'liga'; +} diff --git a/hassio-google-drive-backup/backup/static/css/icons.css b/hassio-google-drive-backup/backup/static/css/icons.css new file mode 100644 index 0000000..d6ba092 --- /dev/null +++ b/hassio-google-drive-backup/backup/static/css/icons.css @@ -0,0 +1,36 @@ +@font-face { + font-family: 'Material Icons'; + font-style: normal; + font-weight: 400; + src: url(iconfont/MaterialIcons-Regular.eot); /* For IE6-8 */ + src: local('Material Icons'), + local('MaterialIcons-Regular'), + url(iconfont/MaterialIcons-Regular.woff2) format('woff2'), + url(iconfont/MaterialIcons-Regular.woff) format('woff'), + url(iconfont/MaterialIcons-Regular.ttf) format('truetype'); +} + +.material-icons { + font-family: 'Material Icons'; + font-weight: normal; + font-style: normal; + font-size: 24px; /* Preferred icon size */ + display: inline-block; + line-height: 1; + text-transform: none; + letter-spacing: normal; + word-wrap: normal; + white-space: nowrap; + direction: ltr; + + /* Support for all WebKit browsers. */ + -webkit-font-smoothing: antialiased; + /* Support for Safari and Chrome. */ + text-rendering: optimizeLegibility; + + /* Support for Firefox. */ + -moz-osx-font-smoothing: grayscale; + + /* Support for IE. */ + font-feature-settings: 'liga'; +} \ No newline at end of file diff --git a/hassio-google-drive-backup/backup/static/css/materialize.css b/hassio-google-drive-backup/backup/static/css/materialize.css new file mode 100644 index 0000000..0873b38 --- /dev/null +++ b/hassio-google-drive-backup/backup/static/css/materialize.css @@ -0,0 +1,9058 @@ +/*! + * Materialize v1.0.0-rc.1 (http://materializecss.com) + * Copyright 2014-2017 Materialize + * MIT License (https://raw.githubusercontent.com/Dogfalo/materialize/master/LICENSE) + */ +.materialize-red { + background-color: #e51c23 !important; +} + +.materialize-red-text { + color: #e51c23 !important; +} + +.materialize-red.lighten-5 { + background-color: #fdeaeb !important; +} + +.materialize-red-text.text-lighten-5 { + color: #fdeaeb !important; +} + +.materialize-red.lighten-4 { + background-color: #f8c1c3 !important; +} + +.materialize-red-text.text-lighten-4 { + color: #f8c1c3 !important; +} + +.materialize-red.lighten-3 { + background-color: #f3989b !important; +} + +.materialize-red-text.text-lighten-3 { + color: #f3989b !important; +} + +.materialize-red.lighten-2 { + background-color: #ee6e73 !important; +} + +.materialize-red-text.text-lighten-2 { + color: #ee6e73 !important; +} + +.materialize-red.lighten-1 { + background-color: #ea454b !important; +} + +.materialize-red-text.text-lighten-1 { + color: #ea454b !important; +} + +.materialize-red.darken-1 { + background-color: #d0181e !important; +} + +.materialize-red-text.text-darken-1 { + color: #d0181e !important; +} + +.materialize-red.darken-2 { + background-color: #b9151b !important; +} + +.materialize-red-text.text-darken-2 { + color: #b9151b !important; +} + +.materialize-red.darken-3 { + background-color: #a21318 !important; +} + +.materialize-red-text.text-darken-3 { + color: #a21318 !important; +} + +.materialize-red.darken-4 { + background-color: #8b1014 !important; +} + +.materialize-red-text.text-darken-4 { + color: #8b1014 !important; +} + +.red { + background-color: #F44336 !important; +} + +.red-text { + color: #F44336 !important; +} + +.red.lighten-5 { + background-color: #FFEBEE !important; +} + +.red-text.text-lighten-5 { + color: #FFEBEE !important; +} + +.red.lighten-4 { + background-color: #FFCDD2 !important; +} + +.red-text.text-lighten-4 { + color: #FFCDD2 !important; +} + +.red.lighten-3 { + background-color: #EF9A9A !important; +} + +.red-text.text-lighten-3 { + color: #EF9A9A !important; +} + +.red.lighten-2 { + background-color: #E57373 !important; +} + +.red-text.text-lighten-2 { + color: #E57373 !important; +} + +.red.lighten-1 { + background-color: #EF5350 !important; +} + +.red-text.text-lighten-1 { + color: #EF5350 !important; +} + +.red.darken-1 { + background-color: #E53935 !important; +} + +.red-text.text-darken-1 { + color: #E53935 !important; +} + +.red.darken-2 { + background-color: #D32F2F !important; +} + +.red-text.text-darken-2 { + color: #D32F2F !important; +} + +.red.darken-3 { + background-color: #C62828 !important; +} + +.red-text.text-darken-3 { + color: #C62828 !important; +} + +.red.darken-4 { + background-color: #B71C1C !important; +} + +.red-text.text-darken-4 { + color: #B71C1C !important; +} + +.red.accent-1 { + background-color: #FF8A80 !important; +} + +.red-text.text-accent-1 { + color: #FF8A80 !important; +} + +.red.accent-2 { + background-color: #FF5252 !important; +} + +.red-text.text-accent-2 { + color: #FF5252 !important; +} + +.red.accent-3 { + background-color: #FF1744 !important; +} + +.red-text.text-accent-3 { + color: #FF1744 !important; +} + +.red.accent-4 { + background-color: #D50000 !important; +} + +.red-text.text-accent-4 { + color: #D50000 !important; +} + +.pink { + background-color: #e91e63 !important; +} + +.pink-text { + color: #e91e63 !important; +} + +.pink.lighten-5 { + background-color: #fce4ec !important; +} + +.pink-text.text-lighten-5 { + color: #fce4ec !important; +} + +.pink.lighten-4 { + background-color: #f8bbd0 !important; +} + +.pink-text.text-lighten-4 { + color: #f8bbd0 !important; +} + +.pink.lighten-3 { + background-color: #f48fb1 !important; +} + +.pink-text.text-lighten-3 { + color: #f48fb1 !important; +} + +.pink.lighten-2 { + background-color: #f06292 !important; +} + +.pink-text.text-lighten-2 { + color: #f06292 !important; +} + +.pink.lighten-1 { + background-color: #ec407a !important; +} + +.pink-text.text-lighten-1 { + color: #ec407a !important; +} + +.pink.darken-1 { + background-color: #d81b60 !important; +} + +.pink-text.text-darken-1 { + color: #d81b60 !important; +} + +.pink.darken-2 { + background-color: #c2185b !important; +} + +.pink-text.text-darken-2 { + color: #c2185b !important; +} + +.pink.darken-3 { + background-color: #ad1457 !important; +} + +.pink-text.text-darken-3 { + color: #ad1457 !important; +} + +.pink.darken-4 { + background-color: #880e4f !important; +} + +.pink-text.text-darken-4 { + color: #880e4f !important; +} + +.pink.accent-1 { + background-color: #ff80ab !important; +} + +.pink-text.text-accent-1 { + color: #ff80ab !important; +} + +.pink.accent-2 { + background-color: #ff4081 !important; +} + +.pink-text.text-accent-2 { + color: #ff4081 !important; +} + +.pink.accent-3 { + background-color: #f50057 !important; +} + +.pink-text.text-accent-3 { + color: #f50057 !important; +} + +.pink.accent-4 { + background-color: #c51162 !important; +} + +.pink-text.text-accent-4 { + color: #c51162 !important; +} + +.purple { + background-color: #9c27b0 !important; +} + +.purple-text { + color: #9c27b0 !important; +} + +.purple.lighten-5 { + background-color: #f3e5f5 !important; +} + +.purple-text.text-lighten-5 { + color: #f3e5f5 !important; +} + +.purple.lighten-4 { + background-color: #e1bee7 !important; +} + +.purple-text.text-lighten-4 { + color: #e1bee7 !important; +} + +.purple.lighten-3 { + background-color: #ce93d8 !important; +} + +.purple-text.text-lighten-3 { + color: #ce93d8 !important; +} + +.purple.lighten-2 { + background-color: #ba68c8 !important; +} + +.purple-text.text-lighten-2 { + color: #ba68c8 !important; +} + +.purple.lighten-1 { + background-color: #ab47bc !important; +} + +.purple-text.text-lighten-1 { + color: #ab47bc !important; +} + +.purple.darken-1 { + background-color: #8e24aa !important; +} + +.purple-text.text-darken-1 { + color: #8e24aa !important; +} + +.purple.darken-2 { + background-color: #7b1fa2 !important; +} + +.purple-text.text-darken-2 { + color: #7b1fa2 !important; +} + +.purple.darken-3 { + background-color: #6a1b9a !important; +} + +.purple-text.text-darken-3 { + color: #6a1b9a !important; +} + +.purple.darken-4 { + background-color: #4a148c !important; +} + +.purple-text.text-darken-4 { + color: #4a148c !important; +} + +.purple.accent-1 { + background-color: #ea80fc !important; +} + +.purple-text.text-accent-1 { + color: #ea80fc !important; +} + +.purple.accent-2 { + background-color: #e040fb !important; +} + +.purple-text.text-accent-2 { + color: #e040fb !important; +} + +.purple.accent-3 { + background-color: #d500f9 !important; +} + +.purple-text.text-accent-3 { + color: #d500f9 !important; +} + +.purple.accent-4 { + background-color: #aa00ff !important; +} + +.purple-text.text-accent-4 { + color: #aa00ff !important; +} + +.deep-purple { + background-color: #673ab7 !important; +} + +.deep-purple-text { + color: #673ab7 !important; +} + +.deep-purple.lighten-5 { + background-color: #ede7f6 !important; +} + +.deep-purple-text.text-lighten-5 { + color: #ede7f6 !important; +} + +.deep-purple.lighten-4 { + background-color: #d1c4e9 !important; +} + +.deep-purple-text.text-lighten-4 { + color: #d1c4e9 !important; +} + +.deep-purple.lighten-3 { + background-color: #b39ddb !important; +} + +.deep-purple-text.text-lighten-3 { + color: #b39ddb !important; +} + +.deep-purple.lighten-2 { + background-color: #9575cd !important; +} + +.deep-purple-text.text-lighten-2 { + color: #9575cd !important; +} + +.deep-purple.lighten-1 { + background-color: #7e57c2 !important; +} + +.deep-purple-text.text-lighten-1 { + color: #7e57c2 !important; +} + +.deep-purple.darken-1 { + background-color: #5e35b1 !important; +} + +.deep-purple-text.text-darken-1 { + color: #5e35b1 !important; +} + +.deep-purple.darken-2 { + background-color: #512da8 !important; +} + +.deep-purple-text.text-darken-2 { + color: #512da8 !important; +} + +.deep-purple.darken-3 { + background-color: #4527a0 !important; +} + +.deep-purple-text.text-darken-3 { + color: #4527a0 !important; +} + +.deep-purple.darken-4 { + background-color: #311b92 !important; +} + +.deep-purple-text.text-darken-4 { + color: #311b92 !important; +} + +.deep-purple.accent-1 { + background-color: #b388ff !important; +} + +.deep-purple-text.text-accent-1 { + color: #b388ff !important; +} + +.deep-purple.accent-2 { + background-color: #7c4dff !important; +} + +.deep-purple-text.text-accent-2 { + color: #7c4dff !important; +} + +.deep-purple.accent-3 { + background-color: #651fff !important; +} + +.deep-purple-text.text-accent-3 { + color: #651fff !important; +} + +.deep-purple.accent-4 { + background-color: #6200ea !important; +} + +.deep-purple-text.text-accent-4 { + color: #6200ea !important; +} + +.indigo { + background-color: #3f51b5 !important; +} + +.indigo-text { + color: #3f51b5 !important; +} + +.indigo.lighten-5 { + background-color: #e8eaf6 !important; +} + +.indigo-text.text-lighten-5 { + color: #e8eaf6 !important; +} + +.indigo.lighten-4 { + background-color: #c5cae9 !important; +} + +.indigo-text.text-lighten-4 { + color: #c5cae9 !important; +} + +.indigo.lighten-3 { + background-color: #9fa8da !important; +} + +.indigo-text.text-lighten-3 { + color: #9fa8da !important; +} + +.indigo.lighten-2 { + background-color: #7986cb !important; +} + +.indigo-text.text-lighten-2 { + color: #7986cb !important; +} + +.indigo.lighten-1 { + background-color: #5c6bc0 !important; +} + +.indigo-text.text-lighten-1 { + color: #5c6bc0 !important; +} + +.indigo.darken-1 { + background-color: #3949ab !important; +} + +.indigo-text.text-darken-1 { + color: #3949ab !important; +} + +.indigo.darken-2 { + background-color: #303f9f !important; +} + +.indigo-text.text-darken-2 { + color: #303f9f !important; +} + +.indigo.darken-3 { + background-color: #283593 !important; +} + +.indigo-text.text-darken-3 { + color: #283593 !important; +} + +.indigo.darken-4 { + background-color: #1a237e !important; +} + +.indigo-text.text-darken-4 { + color: #1a237e !important; +} + +.indigo.accent-1 { + background-color: #8c9eff !important; +} + +.indigo-text.text-accent-1 { + color: #8c9eff !important; +} + +.indigo.accent-2 { + background-color: #536dfe !important; +} + +.indigo-text.text-accent-2 { + color: #536dfe !important; +} + +.indigo.accent-3 { + background-color: #3d5afe !important; +} + +.indigo-text.text-accent-3 { + color: #3d5afe !important; +} + +.indigo.accent-4 { + background-color: #304ffe !important; +} + +.indigo-text.text-accent-4 { + color: #304ffe !important; +} + +.blue { + background-color: #2196F3 !important; +} + +.blue-text { + color: #2196F3 !important; +} + +.blue.lighten-5 { + background-color: #E3F2FD !important; +} + +.blue-text.text-lighten-5 { + color: #E3F2FD !important; +} + +.blue.lighten-4 { + background-color: #BBDEFB !important; +} + +.blue-text.text-lighten-4 { + color: #BBDEFB !important; +} + +.blue.lighten-3 { + background-color: #90CAF9 !important; +} + +.blue-text.text-lighten-3 { + color: #90CAF9 !important; +} + +.blue.lighten-2 { + background-color: #64B5F6 !important; +} + +.blue-text.text-lighten-2 { + color: #64B5F6 !important; +} + +.blue.lighten-1 { + background-color: #42A5F5 !important; +} + +.blue-text.text-lighten-1 { + color: #42A5F5 !important; +} + +.blue.darken-1 { + background-color: #1E88E5 !important; +} + +.blue-text.text-darken-1 { + color: #1E88E5 !important; +} + +.blue.darken-2 { + background-color: #1976D2 !important; +} + +.blue-text.text-darken-2 { + color: #1976D2 !important; +} + +.blue.darken-3 { + background-color: #1565C0 !important; +} + +.blue-text.text-darken-3 { + color: #1565C0 !important; +} + +.blue.darken-4 { + background-color: #0D47A1 !important; +} + +.blue-text.text-darken-4 { + color: #0D47A1 !important; +} + +.blue.accent-1 { + background-color: #82B1FF !important; +} + +.blue-text.text-accent-1 { + color: #82B1FF !important; +} + +.blue.accent-2 { + background-color: #448AFF !important; +} + +.blue-text.text-accent-2 { + color: #448AFF !important; +} + +.blue.accent-3 { + background-color: #2979FF !important; +} + +.blue-text.text-accent-3 { + color: #2979FF !important; +} + +.blue.accent-4 { + background-color: #2962FF !important; +} + +.blue-text.text-accent-4 { + color: #2962FF !important; +} + +.light-blue { + background-color: #03a9f4 !important; +} + +.light-blue-text { + color: #03a9f4 !important; +} + +.light-blue.lighten-5 { + background-color: #e1f5fe !important; +} + +.light-blue-text.text-lighten-5 { + color: #e1f5fe !important; +} + +.light-blue.lighten-4 { + background-color: #b3e5fc !important; +} + +.light-blue-text.text-lighten-4 { + color: #b3e5fc !important; +} + +.light-blue.lighten-3 { + background-color: #81d4fa !important; +} + +.light-blue-text.text-lighten-3 { + color: #81d4fa !important; +} + +.light-blue.lighten-2 { + background-color: #4fc3f7 !important; +} + +.light-blue-text.text-lighten-2 { + color: #4fc3f7 !important; +} + +.light-blue.lighten-1 { + background-color: #29b6f6 !important; +} + +.light-blue-text.text-lighten-1 { + color: #29b6f6 !important; +} + +.light-blue.darken-1 { + background-color: #039be5 !important; +} + +.light-blue-text.text-darken-1 { + color: #039be5 !important; +} + +.light-blue.darken-2 { + background-color: #0288d1 !important; +} + +.light-blue-text.text-darken-2 { + color: #0288d1 !important; +} + +.light-blue.darken-3 { + background-color: #0277bd !important; +} + +.light-blue-text.text-darken-3 { + color: #0277bd !important; +} + +.light-blue.darken-4 { + background-color: #01579b !important; +} + +.light-blue-text.text-darken-4 { + color: #01579b !important; +} + +.light-blue.accent-1 { + background-color: #80d8ff !important; +} + +.light-blue-text.text-accent-1 { + color: #80d8ff !important; +} + +.light-blue.accent-2 { + background-color: #40c4ff !important; +} + +.light-blue-text.text-accent-2 { + color: #40c4ff !important; +} + +.light-blue.accent-3 { + background-color: #00b0ff !important; +} + +.light-blue-text.text-accent-3 { + color: #00b0ff !important; +} + +.light-blue.accent-4 { + background-color: #0091ea !important; +} + +.light-blue-text.text-accent-4 { + color: #0091ea !important; +} + +.cyan { + background-color: #00bcd4 !important; +} + +.cyan-text { + color: #00bcd4 !important; +} + +.cyan.lighten-5 { + background-color: #e0f7fa !important; +} + +.cyan-text.text-lighten-5 { + color: #e0f7fa !important; +} + +.cyan.lighten-4 { + background-color: #b2ebf2 !important; +} + +.cyan-text.text-lighten-4 { + color: #b2ebf2 !important; +} + +.cyan.lighten-3 { + background-color: #80deea !important; +} + +.cyan-text.text-lighten-3 { + color: #80deea !important; +} + +.cyan.lighten-2 { + background-color: #4dd0e1 !important; +} + +.cyan-text.text-lighten-2 { + color: #4dd0e1 !important; +} + +.cyan.lighten-1 { + background-color: #26c6da !important; +} + +.cyan-text.text-lighten-1 { + color: #26c6da !important; +} + +.cyan.darken-1 { + background-color: #00acc1 !important; +} + +.cyan-text.text-darken-1 { + color: #00acc1 !important; +} + +.cyan.darken-2 { + background-color: #0097a7 !important; +} + +.cyan-text.text-darken-2 { + color: #0097a7 !important; +} + +.cyan.darken-3 { + background-color: #00838f !important; +} + +.cyan-text.text-darken-3 { + color: #00838f !important; +} + +.cyan.darken-4 { + background-color: #006064 !important; +} + +.cyan-text.text-darken-4 { + color: #006064 !important; +} + +.cyan.accent-1 { + background-color: #84ffff !important; +} + +.cyan-text.text-accent-1 { + color: #84ffff !important; +} + +.cyan.accent-2 { + background-color: #18ffff !important; +} + +.cyan-text.text-accent-2 { + color: #18ffff !important; +} + +.cyan.accent-3 { + background-color: #00e5ff !important; +} + +.cyan-text.text-accent-3 { + color: #00e5ff !important; +} + +.cyan.accent-4 { + background-color: #00b8d4 !important; +} + +.cyan-text.text-accent-4 { + color: #00b8d4 !important; +} + +.teal { + background-color: #009688 !important; +} + +.teal-text { + color: #009688 !important; +} + +.teal.lighten-5 { + background-color: #e0f2f1 !important; +} + +.teal-text.text-lighten-5 { + color: #e0f2f1 !important; +} + +.teal.lighten-4 { + background-color: #b2dfdb !important; +} + +.teal-text.text-lighten-4 { + color: #b2dfdb !important; +} + +.teal.lighten-3 { + background-color: #80cbc4 !important; +} + +.teal-text.text-lighten-3 { + color: #80cbc4 !important; +} + +.teal.lighten-2 { + background-color: #4db6ac !important; +} + +.teal-text.text-lighten-2 { + color: #4db6ac !important; +} + +.teal.lighten-1 { + background-color: #26a69a !important; +} + +.teal-text.text-lighten-1 { + color: #26a69a !important; +} + +.teal.darken-1 { + background-color: #00897b !important; +} + +.teal-text.text-darken-1 { + color: #00897b !important; +} + +.teal.darken-2 { + background-color: #00796b !important; +} + +.teal-text.text-darken-2 { + color: #00796b !important; +} + +.teal.darken-3 { + background-color: #00695c !important; +} + +.teal-text.text-darken-3 { + color: #00695c !important; +} + +.teal.darken-4 { + background-color: #004d40 !important; +} + +.teal-text.text-darken-4 { + color: #004d40 !important; +} + +.teal.accent-1 { + background-color: #a7ffeb !important; +} + +.teal-text.text-accent-1 { + color: #a7ffeb !important; +} + +.teal.accent-2 { + background-color: #64ffda !important; +} + +.teal-text.text-accent-2 { + color: #64ffda !important; +} + +.teal.accent-3 { + background-color: #1de9b6 !important; +} + +.teal-text.text-accent-3 { + color: #1de9b6 !important; +} + +.teal.accent-4 { + background-color: #00bfa5 !important; +} + +.teal-text.text-accent-4 { + color: #00bfa5 !important; +} + +.green { + background-color: #4CAF50 !important; +} + +.green-text { + color: #4CAF50 !important; +} + +.green.lighten-5 { + background-color: #E8F5E9 !important; +} + +.green-text.text-lighten-5 { + color: #E8F5E9 !important; +} + +.green.lighten-4 { + background-color: #C8E6C9 !important; +} + +.green-text.text-lighten-4 { + color: #C8E6C9 !important; +} + +.green.lighten-3 { + background-color: #A5D6A7 !important; +} + +.green-text.text-lighten-3 { + color: #A5D6A7 !important; +} + +.green.lighten-2 { + background-color: #81C784 !important; +} + +.green-text.text-lighten-2 { + color: #81C784 !important; +} + +.green.lighten-1 { + background-color: #66BB6A !important; +} + +.green-text.text-lighten-1 { + color: #66BB6A !important; +} + +.green.darken-1 { + background-color: #43A047 !important; +} + +.green-text.text-darken-1 { + color: #43A047 !important; +} + +.green.darken-2 { + background-color: #388E3C !important; +} + +.green-text.text-darken-2 { + color: #388E3C !important; +} + +.green.darken-3 { + background-color: #2E7D32 !important; +} + +.green-text.text-darken-3 { + color: #2E7D32 !important; +} + +.green.darken-4 { + background-color: #1B5E20 !important; +} + +.green-text.text-darken-4 { + color: #1B5E20 !important; +} + +.green.accent-1 { + background-color: #B9F6CA !important; +} + +.green-text.text-accent-1 { + color: #B9F6CA !important; +} + +.green.accent-2 { + background-color: #69F0AE !important; +} + +.green-text.text-accent-2 { + color: #69F0AE !important; +} + +.green.accent-3 { + background-color: #00E676 !important; +} + +.green-text.text-accent-3 { + color: #00E676 !important; +} + +.green.accent-4 { + background-color: #00C853 !important; +} + +.green-text.text-accent-4 { + color: #00C853 !important; +} + +.light-green { + background-color: #8bc34a !important; +} + +.light-green-text { + color: #8bc34a !important; +} + +.light-green.lighten-5 { + background-color: #f1f8e9 !important; +} + +.light-green-text.text-lighten-5 { + color: #f1f8e9 !important; +} + +.light-green.lighten-4 { + background-color: #dcedc8 !important; +} + +.light-green-text.text-lighten-4 { + color: #dcedc8 !important; +} + +.light-green.lighten-3 { + background-color: #c5e1a5 !important; +} + +.light-green-text.text-lighten-3 { + color: #c5e1a5 !important; +} + +.light-green.lighten-2 { + background-color: #aed581 !important; +} + +.light-green-text.text-lighten-2 { + color: #aed581 !important; +} + +.light-green.lighten-1 { + background-color: #9ccc65 !important; +} + +.light-green-text.text-lighten-1 { + color: #9ccc65 !important; +} + +.light-green.darken-1 { + background-color: #7cb342 !important; +} + +.light-green-text.text-darken-1 { + color: #7cb342 !important; +} + +.light-green.darken-2 { + background-color: #689f38 !important; +} + +.light-green-text.text-darken-2 { + color: #689f38 !important; +} + +.light-green.darken-3 { + background-color: #558b2f !important; +} + +.light-green-text.text-darken-3 { + color: #558b2f !important; +} + +.light-green.darken-4 { + background-color: #33691e !important; +} + +.light-green-text.text-darken-4 { + color: #33691e !important; +} + +.light-green.accent-1 { + background-color: #ccff90 !important; +} + +.light-green-text.text-accent-1 { + color: #ccff90 !important; +} + +.light-green.accent-2 { + background-color: #b2ff59 !important; +} + +.light-green-text.text-accent-2 { + color: #b2ff59 !important; +} + +.light-green.accent-3 { + background-color: #76ff03 !important; +} + +.light-green-text.text-accent-3 { + color: #76ff03 !important; +} + +.light-green.accent-4 { + background-color: #64dd17 !important; +} + +.light-green-text.text-accent-4 { + color: #64dd17 !important; +} + +.lime { + background-color: #cddc39 !important; +} + +.lime-text { + color: #cddc39 !important; +} + +.lime.lighten-5 { + background-color: #f9fbe7 !important; +} + +.lime-text.text-lighten-5 { + color: #f9fbe7 !important; +} + +.lime.lighten-4 { + background-color: #f0f4c3 !important; +} + +.lime-text.text-lighten-4 { + color: #f0f4c3 !important; +} + +.lime.lighten-3 { + background-color: #e6ee9c !important; +} + +.lime-text.text-lighten-3 { + color: #e6ee9c !important; +} + +.lime.lighten-2 { + background-color: #dce775 !important; +} + +.lime-text.text-lighten-2 { + color: #dce775 !important; +} + +.lime.lighten-1 { + background-color: #d4e157 !important; +} + +.lime-text.text-lighten-1 { + color: #d4e157 !important; +} + +.lime.darken-1 { + background-color: #c0ca33 !important; +} + +.lime-text.text-darken-1 { + color: #c0ca33 !important; +} + +.lime.darken-2 { + background-color: #afb42b !important; +} + +.lime-text.text-darken-2 { + color: #afb42b !important; +} + +.lime.darken-3 { + background-color: #9e9d24 !important; +} + +.lime-text.text-darken-3 { + color: #9e9d24 !important; +} + +.lime.darken-4 { + background-color: #827717 !important; +} + +.lime-text.text-darken-4 { + color: #827717 !important; +} + +.lime.accent-1 { + background-color: #f4ff81 !important; +} + +.lime-text.text-accent-1 { + color: #f4ff81 !important; +} + +.lime.accent-2 { + background-color: #eeff41 !important; +} + +.lime-text.text-accent-2 { + color: #eeff41 !important; +} + +.lime.accent-3 { + background-color: #c6ff00 !important; +} + +.lime-text.text-accent-3 { + color: #c6ff00 !important; +} + +.lime.accent-4 { + background-color: #aeea00 !important; +} + +.lime-text.text-accent-4 { + color: #aeea00 !important; +} + +.yellow { + background-color: #ffeb3b !important; +} + +.yellow-text { + color: #ffeb3b !important; +} + +.yellow.lighten-5 { + background-color: #fffde7 !important; +} + +.yellow-text.text-lighten-5 { + color: #fffde7 !important; +} + +.yellow.lighten-4 { + background-color: #fff9c4 !important; +} + +.yellow-text.text-lighten-4 { + color: #fff9c4 !important; +} + +.yellow.lighten-3 { + background-color: #fff59d !important; +} + +.yellow-text.text-lighten-3 { + color: #fff59d !important; +} + +.yellow.lighten-2 { + background-color: #fff176 !important; +} + +.yellow-text.text-lighten-2 { + color: #fff176 !important; +} + +.yellow.lighten-1 { + background-color: #ffee58 !important; +} + +.yellow-text.text-lighten-1 { + color: #ffee58 !important; +} + +.yellow.darken-1 { + background-color: #fdd835 !important; +} + +.yellow-text.text-darken-1 { + color: #fdd835 !important; +} + +.yellow.darken-2 { + background-color: #fbc02d !important; +} + +.yellow-text.text-darken-2 { + color: #fbc02d !important; +} + +.yellow.darken-3 { + background-color: #f9a825 !important; +} + +.yellow-text.text-darken-3 { + color: #f9a825 !important; +} + +.yellow.darken-4 { + background-color: #f57f17 !important; +} + +.yellow-text.text-darken-4 { + color: #f57f17 !important; +} + +.yellow.accent-1 { + background-color: #ffff8d !important; +} + +.yellow-text.text-accent-1 { + color: #ffff8d !important; +} + +.yellow.accent-2 { + background-color: #ffff00 !important; +} + +.yellow-text.text-accent-2 { + color: #ffff00 !important; +} + +.yellow.accent-3 { + background-color: #ffea00 !important; +} + +.yellow-text.text-accent-3 { + color: #ffea00 !important; +} + +.yellow.accent-4 { + background-color: #ffd600 !important; +} + +.yellow-text.text-accent-4 { + color: #ffd600 !important; +} + +.amber { + background-color: #ffc107 !important; +} + +.amber-text { + color: #ffc107 !important; +} + +.amber.lighten-5 { + background-color: #fff8e1 !important; +} + +.amber-text.text-lighten-5 { + color: #fff8e1 !important; +} + +.amber.lighten-4 { + background-color: #ffecb3 !important; +} + +.amber-text.text-lighten-4 { + color: #ffecb3 !important; +} + +.amber.lighten-3 { + background-color: #ffe082 !important; +} + +.amber-text.text-lighten-3 { + color: #ffe082 !important; +} + +.amber.lighten-2 { + background-color: #ffd54f !important; +} + +.amber-text.text-lighten-2 { + color: #ffd54f !important; +} + +.amber.lighten-1 { + background-color: #ffca28 !important; +} + +.amber-text.text-lighten-1 { + color: #ffca28 !important; +} + +.amber.darken-1 { + background-color: #ffb300 !important; +} + +.amber-text.text-darken-1 { + color: #ffb300 !important; +} + +.amber.darken-2 { + background-color: #ffa000 !important; +} + +.amber-text.text-darken-2 { + color: #ffa000 !important; +} + +.amber.darken-3 { + background-color: #ff8f00 !important; +} + +.amber-text.text-darken-3 { + color: #ff8f00 !important; +} + +.amber.darken-4 { + background-color: #ff6f00 !important; +} + +.amber-text.text-darken-4 { + color: #ff6f00 !important; +} + +.amber.accent-1 { + background-color: #ffe57f !important; +} + +.amber-text.text-accent-1 { + color: #ffe57f !important; +} + +.amber.accent-2 { + background-color: #ffd740 !important; +} + +.amber-text.text-accent-2 { + color: #ffd740 !important; +} + +.amber.accent-3 { + background-color: #ffc400 !important; +} + +.amber-text.text-accent-3 { + color: #ffc400 !important; +} + +.amber.accent-4 { + background-color: #ffab00 !important; +} + +.amber-text.text-accent-4 { + color: #ffab00 !important; +} + +.orange { + background-color: #ff9800 !important; +} + +.orange-text { + color: #ff9800 !important; +} + +.orange.lighten-5 { + background-color: #fff3e0 !important; +} + +.orange-text.text-lighten-5 { + color: #fff3e0 !important; +} + +.orange.lighten-4 { + background-color: #ffe0b2 !important; +} + +.orange-text.text-lighten-4 { + color: #ffe0b2 !important; +} + +.orange.lighten-3 { + background-color: #ffcc80 !important; +} + +.orange-text.text-lighten-3 { + color: #ffcc80 !important; +} + +.orange.lighten-2 { + background-color: #ffb74d !important; +} + +.orange-text.text-lighten-2 { + color: #ffb74d !important; +} + +.orange.lighten-1 { + background-color: #ffa726 !important; +} + +.orange-text.text-lighten-1 { + color: #ffa726 !important; +} + +.orange.darken-1 { + background-color: #fb8c00 !important; +} + +.orange-text.text-darken-1 { + color: #fb8c00 !important; +} + +.orange.darken-2 { + background-color: #f57c00 !important; +} + +.orange-text.text-darken-2 { + color: #f57c00 !important; +} + +.orange.darken-3 { + background-color: #ef6c00 !important; +} + +.orange-text.text-darken-3 { + color: #ef6c00 !important; +} + +.orange.darken-4 { + background-color: #e65100 !important; +} + +.orange-text.text-darken-4 { + color: #e65100 !important; +} + +.orange.accent-1 { + background-color: #ffd180 !important; +} + +.orange-text.text-accent-1 { + color: #ffd180 !important; +} + +.orange.accent-2 { + background-color: #ffab40 !important; +} + +.orange-text.text-accent-2 { + color: #ffab40 !important; +} + +.orange.accent-3 { + background-color: #ff9100 !important; +} + +.orange-text.text-accent-3 { + color: #ff9100 !important; +} + +.orange.accent-4 { + background-color: #ff6d00 !important; +} + +.orange-text.text-accent-4 { + color: #ff6d00 !important; +} + +.deep-orange { + background-color: #ff5722 !important; +} + +.deep-orange-text { + color: #ff5722 !important; +} + +.deep-orange.lighten-5 { + background-color: #fbe9e7 !important; +} + +.deep-orange-text.text-lighten-5 { + color: #fbe9e7 !important; +} + +.deep-orange.lighten-4 { + background-color: #ffccbc !important; +} + +.deep-orange-text.text-lighten-4 { + color: #ffccbc !important; +} + +.deep-orange.lighten-3 { + background-color: #ffab91 !important; +} + +.deep-orange-text.text-lighten-3 { + color: #ffab91 !important; +} + +.deep-orange.lighten-2 { + background-color: #ff8a65 !important; +} + +.deep-orange-text.text-lighten-2 { + color: #ff8a65 !important; +} + +.deep-orange.lighten-1 { + background-color: #ff7043 !important; +} + +.deep-orange-text.text-lighten-1 { + color: #ff7043 !important; +} + +.deep-orange.darken-1 { + background-color: #f4511e !important; +} + +.deep-orange-text.text-darken-1 { + color: #f4511e !important; +} + +.deep-orange.darken-2 { + background-color: #e64a19 !important; +} + +.deep-orange-text.text-darken-2 { + color: #e64a19 !important; +} + +.deep-orange.darken-3 { + background-color: #d84315 !important; +} + +.deep-orange-text.text-darken-3 { + color: #d84315 !important; +} + +.deep-orange.darken-4 { + background-color: #bf360c !important; +} + +.deep-orange-text.text-darken-4 { + color: #bf360c !important; +} + +.deep-orange.accent-1 { + background-color: #ff9e80 !important; +} + +.deep-orange-text.text-accent-1 { + color: #ff9e80 !important; +} + +.deep-orange.accent-2 { + background-color: #ff6e40 !important; +} + +.deep-orange-text.text-accent-2 { + color: #ff6e40 !important; +} + +.deep-orange.accent-3 { + background-color: #ff3d00 !important; +} + +.deep-orange-text.text-accent-3 { + color: #ff3d00 !important; +} + +.deep-orange.accent-4 { + background-color: #dd2c00 !important; +} + +.deep-orange-text.text-accent-4 { + color: #dd2c00 !important; +} + +.brown { + background-color: #795548 !important; +} + +.brown-text { + color: #795548 !important; +} + +.brown.lighten-5 { + background-color: #efebe9 !important; +} + +.brown-text.text-lighten-5 { + color: #efebe9 !important; +} + +.brown.lighten-4 { + background-color: #d7ccc8 !important; +} + +.brown-text.text-lighten-4 { + color: #d7ccc8 !important; +} + +.brown.lighten-3 { + background-color: #bcaaa4 !important; +} + +.brown-text.text-lighten-3 { + color: #bcaaa4 !important; +} + +.brown.lighten-2 { + background-color: #a1887f !important; +} + +.brown-text.text-lighten-2 { + color: #a1887f !important; +} + +.brown.lighten-1 { + background-color: #8d6e63 !important; +} + +.brown-text.text-lighten-1 { + color: #8d6e63 !important; +} + +.brown.darken-1 { + background-color: #6d4c41 !important; +} + +.brown-text.text-darken-1 { + color: #6d4c41 !important; +} + +.brown.darken-2 { + background-color: #5d4037 !important; +} + +.brown-text.text-darken-2 { + color: #5d4037 !important; +} + +.brown.darken-3 { + background-color: #4e342e !important; +} + +.brown-text.text-darken-3 { + color: #4e342e !important; +} + +.brown.darken-4 { + background-color: #3e2723 !important; +} + +.brown-text.text-darken-4 { + color: #3e2723 !important; +} + +.blue-grey { + background-color: #607d8b !important; +} + +.blue-grey-text { + color: #607d8b !important; +} + +.blue-grey.lighten-5 { + background-color: #eceff1 !important; +} + +.blue-grey-text.text-lighten-5 { + color: #eceff1 !important; +} + +.blue-grey.lighten-4 { + background-color: #cfd8dc !important; +} + +.blue-grey-text.text-lighten-4 { + color: #cfd8dc !important; +} + +.blue-grey.lighten-3 { + background-color: #b0bec5 !important; +} + +.blue-grey-text.text-lighten-3 { + color: #b0bec5 !important; +} + +.blue-grey.lighten-2 { + background-color: #90a4ae !important; +} + +.blue-grey-text.text-lighten-2 { + color: #90a4ae !important; +} + +.blue-grey.lighten-1 { + background-color: #78909c !important; +} + +.blue-grey-text.text-lighten-1 { + color: #78909c !important; +} + +.blue-grey.darken-1 { + background-color: #546e7a !important; +} + +.blue-grey-text.text-darken-1 { + color: #546e7a !important; +} + +.blue-grey.darken-2 { + background-color: #455a64 !important; +} + +.blue-grey-text.text-darken-2 { + color: #455a64 !important; +} + +.blue-grey.darken-3 { + background-color: #37474f !important; +} + +.blue-grey-text.text-darken-3 { + color: #37474f !important; +} + +.blue-grey.darken-4 { + background-color: #263238 !important; +} + +.blue-grey-text.text-darken-4 { + color: #263238 !important; +} + +.grey { + background-color: #9e9e9e !important; +} + +.grey-text { + color: #9e9e9e !important; +} + +.grey.lighten-5 { + background-color: #fafafa !important; +} + +.grey-text.text-lighten-5 { + color: #fafafa !important; +} + +.grey.lighten-4 { + background-color: #f5f5f5 !important; +} + +.grey-text.text-lighten-4 { + color: #f5f5f5 !important; +} + +.grey.lighten-3 { + background-color: #eeeeee !important; +} + +.grey-text.text-lighten-3 { + color: #eeeeee !important; +} + +.grey.lighten-2 { + background-color: #e0e0e0 !important; +} + +.grey-text.text-lighten-2 { + color: #e0e0e0 !important; +} + +.grey.lighten-1 { + background-color: #bdbdbd !important; +} + +.grey-text.text-lighten-1 { + color: #bdbdbd !important; +} + +.grey.darken-1 { + background-color: #757575 !important; +} + +.grey-text.text-darken-1 { + color: #757575 !important; +} + +.grey.darken-2 { + background-color: #616161 !important; +} + +.grey-text.text-darken-2 { + color: #616161 !important; +} + +.grey.darken-3 { + background-color: #424242 !important; +} + +.grey-text.text-darken-3 { + color: #424242 !important; +} + +.grey.darken-4 { + background-color: #212121 !important; +} + +.grey-text.text-darken-4 { + color: #212121 !important; +} + +.black { + background-color: #000000 !important; +} + +.black-text { + color: #000000 !important; +} + +.white { + background-color: #FFFFFF !important; +} + +.white-text { + color: #FFFFFF !important; +} + +.transparent { + background-color: transparent !important; +} + +.transparent-text { + color: transparent !important; +} + +/*! normalize.css v7.0.0 | MIT License | github.com/necolas/normalize.css */ +/* Document + ========================================================================== */ +/** + * 1. Correct the line height in all browsers. + * 2. Prevent adjustments of font size after orientation changes in + * IE on Windows Phone and in iOS. + */ +html { + line-height: 1.15; + /* 1 */ + -ms-text-size-adjust: 100%; + /* 2 */ + -webkit-text-size-adjust: 100%; + /* 2 */ +} + +/* Sections + ========================================================================== */ +/** + * Remove the margin in all browsers (opinionated). + */ +body { + margin: 0; +} + +/** + * Add the correct display in IE 9-. + */ +article, +aside, +footer, +header, +nav, +section { + display: block; +} + +/** + * Correct the font size and margin on `h1` elements within `section` and + * `article` contexts in Chrome, Firefox, and Safari. + */ +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +/* Grouping content + ========================================================================== */ +/** + * Add the correct display in IE 9-. + * 1. Add the correct display in IE. + */ +figcaption, +figure, +main { + /* 1 */ + display: block; +} + +/** + * Add the correct margin in IE 8. + */ +figure { + margin: 1em 40px; +} + +/** + * 1. Add the correct box sizing in Firefox. + * 2. Show the overflow in Edge and IE. + */ +hr { + -webkit-box-sizing: content-box; + box-sizing: content-box; + /* 1 */ + height: 0; + /* 1 */ + overflow: visible; + /* 2 */ +} + +/** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ +pre { + font-family: monospace, monospace; + /* 1 */ + font-size: 1em; + /* 2 */ +} + +/* Text-level semantics + ========================================================================== */ +/** + * 1. Remove the gray background on active links in IE 10. + * 2. Remove gaps in links underline in iOS 8+ and Safari 8+. + */ +a { + background-color: transparent; + /* 1 */ + -webkit-text-decoration-skip: objects; + /* 2 */ +} + +/** + * 1. Remove the bottom border in Chrome 57- and Firefox 39-. + * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. + */ +abbr[title] { + border-bottom: none; + /* 1 */ + text-decoration: underline; + /* 2 */ + text-decoration: underline dotted; + /* 2 */ +} + +/** + * Prevent the duplicate application of `bolder` by the next rule in Safari 6. + */ +b, +strong { + font-weight: inherit; +} + +/** + * Add the correct font weight in Chrome, Edge, and Safari. + */ +b, +strong { + font-weight: bolder; +} + +/** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ +code, +kbd, +samp { + font-family: monospace, monospace; + /* 1 */ + font-size: 1em; + /* 2 */ +} + +/** + * Add the correct font style in Android 4.3-. + */ +dfn { + font-style: italic; +} + +/** + * Add the correct background and color in IE 9-. + */ +mark { + background-color: #ff0; + color: #000; +} + +/** + * Add the correct font size in all browsers. + */ +small { + font-size: 80%; +} + +/** + * Prevent `sub` and `sup` elements from affecting the line height in + * all browsers. + */ +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* Embedded content + ========================================================================== */ +/** + * Add the correct display in IE 9-. + */ +audio, +video { + display: inline-block; +} + +/** + * Add the correct display in iOS 4-7. + */ +audio:not([controls]) { + display: none; + height: 0; +} + +/** + * Remove the border on images inside links in IE 10-. + */ +img { + border-style: none; +} + +/** + * Hide the overflow in IE. + */ +svg:not(:root) { + overflow: hidden; +} + +/* Forms + ========================================================================== */ +/** + * 1. Change the font styles in all browsers (opinionated). + * 2. Remove the margin in Firefox and Safari. + */ +button, +input, +optgroup, +select, +textarea { + font-family: sans-serif; + /* 1 */ + font-size: 100%; + /* 1 */ + line-height: 1.15; + /* 1 */ + margin: 0; + /* 2 */ +} + +/** + * Show the overflow in IE. + * 1. Show the overflow in Edge. + */ +button, +input { + /* 1 */ + overflow: visible; +} + +/** + * Remove the inheritance of text transform in Edge, Firefox, and IE. + * 1. Remove the inheritance of text transform in Firefox. + */ +button, +select { + /* 1 */ + text-transform: none; +} + +/** + * 1. Prevent a WebKit bug where (2) destroys native `audio` and `video` + * controls in Android 4. + * 2. Correct the inability to style clickable types in iOS and Safari. + */ +button, +html [type="button"], +[type="reset"], +[type="submit"] { + -webkit-appearance: button; + /* 2 */ +} + +/** + * Remove the inner border and padding in Firefox. + */ +button::-moz-focus-inner, +[type="button"]::-moz-focus-inner, +[type="reset"]::-moz-focus-inner, +[type="submit"]::-moz-focus-inner { + border-style: none; + padding: 0; +} + +/** + * Restore the focus styles unset by the previous rule. + */ +button:-moz-focusring, +[type="button"]:-moz-focusring, +[type="reset"]:-moz-focusring, +[type="submit"]:-moz-focusring { + outline: 1px dotted ButtonText; +} + +/** + * Correct the padding in Firefox. + */ +fieldset { + padding: 0.35em 0.75em 0.625em; +} + +/** + * 1. Correct the text wrapping in Edge and IE. + * 2. Correct the color inheritance from `fieldset` elements in IE. + * 3. Remove the padding so developers are not caught out when they zero out + * `fieldset` elements in all browsers. + */ +legend { + -webkit-box-sizing: border-box; + box-sizing: border-box; + /* 1 */ + color: inherit; + /* 2 */ + display: table; + /* 1 */ + max-width: 100%; + /* 1 */ + padding: 0; + /* 3 */ + white-space: normal; + /* 1 */ +} + +/** + * 1. Add the correct display in IE 9-. + * 2. Add the correct vertical alignment in Chrome, Firefox, and Opera. + */ +progress { + display: inline-block; + /* 1 */ + vertical-align: baseline; + /* 2 */ +} + +/** + * Remove the default vertical scrollbar in IE. + */ +textarea { + overflow: auto; +} + +/** + * 1. Add the correct box sizing in IE 10-. + * 2. Remove the padding in IE 10-. + */ +[type="checkbox"], +[type="radio"] { + -webkit-box-sizing: border-box; + box-sizing: border-box; + /* 1 */ + padding: 0; + /* 2 */ +} + +/** + * Correct the cursor style of increment and decrement buttons in Chrome. + */ +[type="number"]::-webkit-inner-spin-button, +[type="number"]::-webkit-outer-spin-button { + height: auto; +} + +/** + * 1. Correct the odd appearance in Chrome and Safari. + * 2. Correct the outline style in Safari. + */ +[type="search"] { + -webkit-appearance: textfield; + /* 1 */ + outline-offset: -2px; + /* 2 */ +} + +/** + * Remove the inner padding and cancel buttons in Chrome and Safari on macOS. + */ +[type="search"]::-webkit-search-cancel-button, +[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +/** + * 1. Correct the inability to style clickable types in iOS and Safari. + * 2. Change font properties to `inherit` in Safari. + */ +::-webkit-file-upload-button { + -webkit-appearance: button; + /* 1 */ + font: inherit; + /* 2 */ +} + +/* Interactive + ========================================================================== */ +/* + * Add the correct display in IE 9-. + * 1. Add the correct display in Edge, IE, and Firefox. + */ +details, +menu { + display: block; +} + +/* + * Add the correct display in all browsers. + */ +summary { + display: list-item; +} + +/* Scripting + ========================================================================== */ +/** + * Add the correct display in IE 9-. + */ +canvas { + display: inline-block; +} + +/** + * Add the correct display in IE. + */ +template { + display: none; +} + +/* Hidden + ========================================================================== */ +/** + * Add the correct display in IE 10-. + */ +[hidden] { + display: none; +} + +html { + -webkit-box-sizing: border-box; + box-sizing: border-box; +} + +*, *:before, *:after { + -webkit-box-sizing: inherit; + box-sizing: inherit; +} + +button, +input, +optgroup, +select, +textarea { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; +} + +ul:not(.browser-default) { + padding-left: 0; + list-style-type: none; +} + +ul:not(.browser-default) > li { + list-style-type: none; +} + +a { + color: #039be5; + text-decoration: none; + -webkit-tap-highlight-color: transparent; +} + +.valign-wrapper { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-align: center; + -webkit-align-items: center; + -ms-flex-align: center; + align-items: center; +} + +.clearfix { + clear: both; +} + +.z-depth-0 { + -webkit-box-shadow: none !important; + box-shadow: none !important; +} + +/* 2dp elevation modified*/ +.z-depth-1, nav, .card-panel, .card, .toast, .btn, .btn-large, .btn-small, .btn-floating, .dropdown-content, .collapsible, .sidenav { + -webkit-box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.12), 0 1px 5px 0 rgba(0, 0, 0, 0.2); + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.12), 0 1px 5px 0 rgba(0, 0, 0, 0.2); +} + +.z-depth-1-half, .btn:hover, .btn-large:hover, .btn-small:hover, .btn-floating:hover { + -webkit-box-shadow: 0 3px 3px 0 rgba(0, 0, 0, 0.14), 0 1px 7px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -1px rgba(0, 0, 0, 0.2); + box-shadow: 0 3px 3px 0 rgba(0, 0, 0, 0.14), 0 1px 7px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -1px rgba(0, 0, 0, 0.2); +} + +/* 6dp elevation modified*/ +.z-depth-2 { + -webkit-box-shadow: 0 4px 5px 0 rgba(0, 0, 0, 0.14), 0 1px 10px 0 rgba(0, 0, 0, 0.12), 0 2px 4px -1px rgba(0, 0, 0, 0.3); + box-shadow: 0 4px 5px 0 rgba(0, 0, 0, 0.14), 0 1px 10px 0 rgba(0, 0, 0, 0.12), 0 2px 4px -1px rgba(0, 0, 0, 0.3); +} + +/* 12dp elevation modified*/ +.z-depth-3 { + -webkit-box-shadow: 0 8px 17px 2px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12), 0 5px 5px -3px rgba(0, 0, 0, 0.2); + box-shadow: 0 8px 17px 2px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12), 0 5px 5px -3px rgba(0, 0, 0, 0.2); +} + +/* 16dp elevation */ +.z-depth-4 { + -webkit-box-shadow: 0 16px 24px 2px rgba(0, 0, 0, 0.14), 0 6px 30px 5px rgba(0, 0, 0, 0.12), 0 8px 10px -7px rgba(0, 0, 0, 0.2); + box-shadow: 0 16px 24px 2px rgba(0, 0, 0, 0.14), 0 6px 30px 5px rgba(0, 0, 0, 0.12), 0 8px 10px -7px rgba(0, 0, 0, 0.2); +} + +/* 24dp elevation */ +.z-depth-5, .modal { + -webkit-box-shadow: 0 24px 38px 3px rgba(0, 0, 0, 0.14), 0 9px 46px 8px rgba(0, 0, 0, 0.12), 0 11px 15px -7px rgba(0, 0, 0, 0.2); + box-shadow: 0 24px 38px 3px rgba(0, 0, 0, 0.14), 0 9px 46px 8px rgba(0, 0, 0, 0.12), 0 11px 15px -7px rgba(0, 0, 0, 0.2); +} + +.hoverable { + -webkit-transition: -webkit-box-shadow .25s; + transition: -webkit-box-shadow .25s; + transition: box-shadow .25s; + transition: box-shadow .25s, -webkit-box-shadow .25s; +} + +.hoverable:hover { + -webkit-box-shadow: 0 8px 17px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19); + box-shadow: 0 8px 17px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19); +} + +.divider { + height: 1px; + overflow: hidden; + background-color: #e0e0e0; +} + +blockquote { + margin: 20px 0; + padding-left: 1.5rem; + border-left: 5px solid #ee6e73; +} + +i { + line-height: inherit; +} + +i.left { + float: left; + margin-right: 15px; +} + +i.right { + float: right; + margin-left: 15px; +} + +i.tiny { + font-size: 1rem; +} + +i.small { + font-size: 2rem; +} + +i.medium { + font-size: 4rem; +} + +i.large { + font-size: 6rem; +} + +img.responsive-img, +video.responsive-video { + max-width: 100%; + height: auto; +} + +.pagination li { + display: inline-block; + border-radius: 2px; + text-align: center; + vertical-align: top; + height: 30px; +} + +.pagination li a { + color: #444; + display: inline-block; + font-size: 1.2rem; + padding: 0 10px; + line-height: 30px; +} + +.pagination li.active a { + color: #fff; +} + +.pagination li.active { + background-color: #ee6e73; +} + +.pagination li.disabled a { + cursor: default; + color: #999; +} + +.pagination li i { + font-size: 2rem; +} + +.pagination li.pages ul li { + display: inline-block; + float: none; +} + +@media only screen and (max-width: 992px) { + .pagination { + width: 100%; + } + .pagination li.prev, + .pagination li.next { + width: 10%; + } + .pagination li.pages { + width: 80%; + overflow: hidden; + white-space: nowrap; + } +} + +.breadcrumb { + font-size: 18px; + color: rgba(255, 255, 255, 0.7); +} + +.breadcrumb i, +.breadcrumb [class^="mdi-"], .breadcrumb [class*="mdi-"], +.breadcrumb i.material-icons { + display: inline-block; + float: left; + font-size: 24px; +} + +.breadcrumb:before { + content: '\E5CC'; + color: rgba(255, 255, 255, 0.7); + vertical-align: top; + display: inline-block; + font-family: 'Material Icons'; + font-weight: normal; + font-style: normal; + font-size: 25px; + margin: 0 10px 0 8px; + -webkit-font-smoothing: antialiased; +} + +.breadcrumb:first-child:before { + display: none; +} + +.breadcrumb:last-child { + color: #fff; +} + +.parallax-container { + position: relative; + overflow: hidden; + height: 500px; +} + +.parallax-container .parallax { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: -1; +} + +.parallax-container .parallax img { + opacity: 0; + position: absolute; + left: 50%; + bottom: 0; + min-width: 100%; + min-height: 100%; + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + -webkit-transform: translateX(-50%); + transform: translateX(-50%); +} + +.pin-top, .pin-bottom { + position: relative; +} + +.pinned { + position: fixed !important; +} + +/********************* + Transition Classes +**********************/ +ul.staggered-list li { + opacity: 0; +} + +.fade-in { + opacity: 0; + -webkit-transform-origin: 0 50%; + transform-origin: 0 50%; +} + +/********************* + Media Query Classes +**********************/ +@media only screen and (max-width: 600px) { + .hide-on-small-only, .hide-on-small-and-down { + display: none !important; + } +} + +@media only screen and (max-width: 992px) { + .hide-on-med-and-down { + display: none !important; + } +} + +@media only screen and (min-width: 601px) { + .hide-on-med-and-up { + display: none !important; + } +} + +@media only screen and (min-width: 600px) and (max-width: 992px) { + .hide-on-med-only { + display: none !important; + } +} + +@media only screen and (min-width: 993px) { + .hide-on-large-only { + display: none !important; + } +} + +@media only screen and (min-width: 1201px) { + .hide-on-extra-large-only { + display: none !important; + } +} + +@media only screen and (min-width: 1201px) { + .show-on-extra-large { + display: block !important; + } +} + +@media only screen and (min-width: 993px) { + .show-on-large { + display: block !important; + } +} + +@media only screen and (min-width: 600px) and (max-width: 992px) { + .show-on-medium { + display: block !important; + } +} + +@media only screen and (max-width: 600px) { + .show-on-small { + display: block !important; + } +} + +@media only screen and (min-width: 601px) { + .show-on-medium-and-up { + display: block !important; + } +} + +@media only screen and (max-width: 992px) { + .show-on-medium-and-down { + display: block !important; + } +} + +@media only screen and (max-width: 600px) { + .center-on-small-only { + text-align: center; + } +} + +.page-footer { + padding-top: 20px; + color: #fff; + background-color: #ee6e73; +} + +.page-footer .footer-copyright { + overflow: hidden; + min-height: 50px; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-align: center; + -webkit-align-items: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + padding: 10px 0px; + color: rgba(255, 255, 255, 0.8); + background-color: rgba(51, 51, 51, 0.08); +} + +table, th, td { + border: none; +} + +table { + width: 100%; + display: table; + border-collapse: collapse; + border-spacing: 0; +} + +table.striped tr { + border-bottom: none; +} + +table.striped > tbody > tr:nth-child(odd) { + background-color: rgba(242, 242, 242, 0.5); +} + +table.striped > tbody > tr > td { + border-radius: 0; +} + +table.highlight > tbody > tr { + -webkit-transition: background-color .25s ease; + transition: background-color .25s ease; +} + +table.highlight > tbody > tr:hover { + background-color: rgba(242, 242, 242, 0.5); +} + +table.centered thead tr th, table.centered tbody tr td { + text-align: center; +} + +tr { + border-bottom: 1px solid rgba(0, 0, 0, 0.12); +} + +td, th { + padding: 15px 5px; + display: table-cell; + text-align: left; + vertical-align: middle; + border-radius: 2px; +} + +@media only screen and (max-width: 992px) { + table.responsive-table { + width: 100%; + border-collapse: collapse; + border-spacing: 0; + display: block; + position: relative; + /* sort out borders */ + } + table.responsive-table td:empty:before { + content: '\00a0'; + } + table.responsive-table th, + table.responsive-table td { + margin: 0; + vertical-align: top; + } + table.responsive-table th { + text-align: left; + } + table.responsive-table thead { + display: block; + float: left; + } + table.responsive-table thead tr { + display: block; + padding: 0 10px 0 0; + } + table.responsive-table thead tr th::before { + content: "\00a0"; + } + table.responsive-table tbody { + display: block; + width: auto; + position: relative; + overflow-x: auto; + white-space: nowrap; + } + table.responsive-table tbody tr { + display: inline-block; + vertical-align: top; + } + table.responsive-table th { + display: block; + text-align: right; + } + table.responsive-table td { + display: block; + min-height: 1.25em; + text-align: left; + } + table.responsive-table tr { + border-bottom: none; + padding: 0 10px; + } + table.responsive-table thead { + border: 0; + border-right: 1px solid rgba(0, 0, 0, 0.12); + } +} + +.collection { + margin: 0.5rem 0 1rem 0; + border: 1px solid #e0e0e0; + border-radius: 2px; + overflow: hidden; + position: relative; +} + +.collection .collection-item { + background-color: #fff; + line-height: 1.5rem; + padding: 10px 20px; + margin: 0; + border-bottom: 1px solid #e0e0e0; +} + +.collection .collection-item.avatar { + min-height: 84px; + padding-left: 72px; + position: relative; +} + +.collection .collection-item.avatar:not(.circle-clipper) > .circle, +.collection .collection-item.avatar :not(.circle-clipper) > .circle { + position: absolute; + width: 42px; + height: 42px; + overflow: hidden; + left: 15px; + display: inline-block; + vertical-align: middle; +} + +.collection .collection-item.avatar i.circle { + font-size: 18px; + line-height: 42px; + color: #fff; + background-color: #999; + text-align: center; +} + +.collection .collection-item.avatar .title { + font-size: 16px; +} + +.collection .collection-item.avatar p { + margin: 0; +} + +.collection .collection-item.avatar .secondary-content { + position: absolute; + top: 16px; + right: 16px; +} + +.collection .collection-item:last-child { + border-bottom: none; +} + +.collection .collection-item.active { + background-color: #26a69a; + color: #eafaf9; +} + +.collection .collection-item.active .secondary-content { + color: #fff; +} + +.collection a.collection-item { + display: block; + -webkit-transition: .25s; + transition: .25s; + color: #26a69a; +} + +.collection a.collection-item:not(.active):hover { + background-color: #ddd; +} + +.collection.with-header .collection-header { + background-color: #fff; + border-bottom: 1px solid #e0e0e0; + padding: 10px 20px; +} + +.collection.with-header .collection-item { + padding-left: 30px; +} + +.collection.with-header .collection-item.avatar { + padding-left: 72px; +} + +.secondary-content { + float: right; + color: #26a69a; +} + +.collapsible .collection { + margin: 0; + border: none; +} + +.video-container { + position: relative; + padding-bottom: 56.25%; + height: 0; + overflow: hidden; +} + +.video-container iframe, .video-container object, .video-container embed { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +.progress { + position: relative; + height: 4px; + display: block; + width: 100%; + background-color: #acece6; + border-radius: 2px; + margin: 0.5rem 0 1rem 0; + overflow: hidden; +} + +.progress .determinate { + position: absolute; + top: 0; + left: 0; + bottom: 0; + background-color: #26a69a; + -webkit-transition: width .3s linear; + transition: width .3s linear; +} + +.progress .indeterminate { + background-color: #26a69a; +} + +.progress .indeterminate:before { + content: ''; + position: absolute; + background-color: inherit; + top: 0; + left: 0; + bottom: 0; + will-change: left, right; + -webkit-animation: indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite; + animation: indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite; +} + +.progress .indeterminate:after { + content: ''; + position: absolute; + background-color: inherit; + top: 0; + left: 0; + bottom: 0; + will-change: left, right; + -webkit-animation: indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) infinite; + animation: indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) infinite; + -webkit-animation-delay: 1.15s; + animation-delay: 1.15s; +} + +@-webkit-keyframes indeterminate { + 0% { + left: -35%; + right: 100%; + } + 60% { + left: 100%; + right: -90%; + } + 100% { + left: 100%; + right: -90%; + } +} + +@keyframes indeterminate { + 0% { + left: -35%; + right: 100%; + } + 60% { + left: 100%; + right: -90%; + } + 100% { + left: 100%; + right: -90%; + } +} + +@-webkit-keyframes indeterminate-short { + 0% { + left: -200%; + right: 100%; + } + 60% { + left: 107%; + right: -8%; + } + 100% { + left: 107%; + right: -8%; + } +} + +@keyframes indeterminate-short { + 0% { + left: -200%; + right: 100%; + } + 60% { + left: 107%; + right: -8%; + } + 100% { + left: 107%; + right: -8%; + } +} + +/******************* + Utility Classes +*******************/ +.hide { + display: none !important; +} + +.left-align { + text-align: left; +} + +.right-align { + text-align: right; +} + +.center, .center-align { + text-align: center; +} + +.left { + float: left !important; +} + +.right { + float: right !important; +} + +.no-select, input[type=range], +input[type=range] + .thumb { + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.circle { + border-radius: 50%; +} + +.center-block { + display: block; + margin-left: auto; + margin-right: auto; +} + +.truncate { + display: block; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.no-padding { + padding: 0 !important; +} + +span.badge { + min-width: 3rem; + padding: 0 6px; + margin-left: 14px; + text-align: center; + font-size: 1rem; + line-height: 22px; + height: 22px; + color: #757575; + float: right; + -webkit-box-sizing: border-box; + box-sizing: border-box; +} + +span.badge.new { + font-weight: 300; + font-size: 0.8rem; + color: #fff; + background-color: #26a69a; + border-radius: 2px; +} + +span.badge.new:after { + content: " new"; +} + +span.badge[data-badge-caption]::after { + content: " " attr(data-badge-caption); +} + +nav ul a span.badge { + display: inline-block; + float: none; + margin-left: 4px; + line-height: 22px; + height: 22px; + -webkit-font-smoothing: auto; +} + +.collection-item span.badge { + margin-top: calc(0.75rem - 11px); +} + +.collapsible span.badge { + margin-left: auto; +} + +.sidenav span.badge { + margin-top: calc(24px - 11px); +} + +table span.badge { + display: inline-block; + float: none; + margin-left: auto; +} + +/* This is needed for some mobile phones to display the Google Icon font properly */ +.material-icons { + text-rendering: optimizeLegibility; + -webkit-font-feature-settings: 'liga'; + -moz-font-feature-settings: 'liga'; + font-feature-settings: 'liga'; +} + +.container { + margin: 0 auto; + max-width: 1280px; + width: 90%; +} + +@media only screen and (min-width: 601px) { + .container { + width: 85%; + } +} + +@media only screen and (min-width: 993px) { + .container { + width: 70%; + } +} + +.col .row { + margin-left: -0.75rem; + margin-right: -0.75rem; +} + +.section { + padding-top: 1rem; + padding-bottom: 1rem; +} + +.section.no-pad { + padding: 0; +} + +.section.no-pad-bot { + padding-bottom: 0; +} + +.section.no-pad-top { + padding-top: 0; +} + +.row { + margin-left: auto; + margin-right: auto; + margin-bottom: 20px; +} + +.row:after { + content: ""; + display: table; + clear: both; +} + +.row .col { + float: left; + -webkit-box-sizing: border-box; + box-sizing: border-box; + padding: 0 0.75rem; + min-height: 1px; +} + +.row .col[class*="push-"], .row .col[class*="pull-"] { + position: relative; +} + +.row .col.s1 { + width: 8.3333333333%; + margin-left: auto; + left: auto; + right: auto; +} + +.row .col.s2 { + width: 16.6666666667%; + margin-left: auto; + left: auto; + right: auto; +} + +.row .col.s3 { + width: 25%; + margin-left: auto; + left: auto; + right: auto; +} + +.row .col.s4 { + width: 33.3333333333%; + margin-left: auto; + left: auto; + right: auto; +} + +.row .col.s5 { + width: 41.6666666667%; + margin-left: auto; + left: auto; + right: auto; +} + +.row .col.s6 { + width: 50%; + margin-left: auto; + left: auto; + right: auto; +} + +.row .col.s7 { + width: 58.3333333333%; + margin-left: auto; + left: auto; + right: auto; +} + +.row .col.s8 { + width: 66.6666666667%; + margin-left: auto; + left: auto; + right: auto; +} + +.row .col.s9 { + width: 75%; + margin-left: auto; + left: auto; + right: auto; +} + +.row .col.s10 { + width: 83.3333333333%; + margin-left: auto; + left: auto; + right: auto; +} + +.row .col.s11 { + width: 91.6666666667%; + margin-left: auto; + left: auto; + right: auto; +} + +.row .col.s12 { + width: 100%; + margin-left: auto; + left: auto; + right: auto; +} + +.row .col.offset-s1 { + margin-left: 8.3333333333%; +} + +.row .col.pull-s1 { + right: 8.3333333333%; +} + +.row .col.push-s1 { + left: 8.3333333333%; +} + +.row .col.offset-s2 { + margin-left: 16.6666666667%; +} + +.row .col.pull-s2 { + right: 16.6666666667%; +} + +.row .col.push-s2 { + left: 16.6666666667%; +} + +.row .col.offset-s3 { + margin-left: 25%; +} + +.row .col.pull-s3 { + right: 25%; +} + +.row .col.push-s3 { + left: 25%; +} + +.row .col.offset-s4 { + margin-left: 33.3333333333%; +} + +.row .col.pull-s4 { + right: 33.3333333333%; +} + +.row .col.push-s4 { + left: 33.3333333333%; +} + +.row .col.offset-s5 { + margin-left: 41.6666666667%; +} + +.row .col.pull-s5 { + right: 41.6666666667%; +} + +.row .col.push-s5 { + left: 41.6666666667%; +} + +.row .col.offset-s6 { + margin-left: 50%; +} + +.row .col.pull-s6 { + right: 50%; +} + +.row .col.push-s6 { + left: 50%; +} + +.row .col.offset-s7 { + margin-left: 58.3333333333%; +} + +.row .col.pull-s7 { + right: 58.3333333333%; +} + +.row .col.push-s7 { + left: 58.3333333333%; +} + +.row .col.offset-s8 { + margin-left: 66.6666666667%; +} + +.row .col.pull-s8 { + right: 66.6666666667%; +} + +.row .col.push-s8 { + left: 66.6666666667%; +} + +.row .col.offset-s9 { + margin-left: 75%; +} + +.row .col.pull-s9 { + right: 75%; +} + +.row .col.push-s9 { + left: 75%; +} + +.row .col.offset-s10 { + margin-left: 83.3333333333%; +} + +.row .col.pull-s10 { + right: 83.3333333333%; +} + +.row .col.push-s10 { + left: 83.3333333333%; +} + +.row .col.offset-s11 { + margin-left: 91.6666666667%; +} + +.row .col.pull-s11 { + right: 91.6666666667%; +} + +.row .col.push-s11 { + left: 91.6666666667%; +} + +.row .col.offset-s12 { + margin-left: 100%; +} + +.row .col.pull-s12 { + right: 100%; +} + +.row .col.push-s12 { + left: 100%; +} + +@media only screen and (min-width: 601px) { + .row .col.m1 { + width: 8.3333333333%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.m2 { + width: 16.6666666667%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.m3 { + width: 25%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.m4 { + width: 33.3333333333%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.m5 { + width: 41.6666666667%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.m6 { + width: 50%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.m7 { + width: 58.3333333333%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.m8 { + width: 66.6666666667%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.m9 { + width: 75%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.m10 { + width: 83.3333333333%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.m11 { + width: 91.6666666667%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.m12 { + width: 100%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.offset-m1 { + margin-left: 8.3333333333%; + } + .row .col.pull-m1 { + right: 8.3333333333%; + } + .row .col.push-m1 { + left: 8.3333333333%; + } + .row .col.offset-m2 { + margin-left: 16.6666666667%; + } + .row .col.pull-m2 { + right: 16.6666666667%; + } + .row .col.push-m2 { + left: 16.6666666667%; + } + .row .col.offset-m3 { + margin-left: 25%; + } + .row .col.pull-m3 { + right: 25%; + } + .row .col.push-m3 { + left: 25%; + } + .row .col.offset-m4 { + margin-left: 33.3333333333%; + } + .row .col.pull-m4 { + right: 33.3333333333%; + } + .row .col.push-m4 { + left: 33.3333333333%; + } + .row .col.offset-m5 { + margin-left: 41.6666666667%; + } + .row .col.pull-m5 { + right: 41.6666666667%; + } + .row .col.push-m5 { + left: 41.6666666667%; + } + .row .col.offset-m6 { + margin-left: 50%; + } + .row .col.pull-m6 { + right: 50%; + } + .row .col.push-m6 { + left: 50%; + } + .row .col.offset-m7 { + margin-left: 58.3333333333%; + } + .row .col.pull-m7 { + right: 58.3333333333%; + } + .row .col.push-m7 { + left: 58.3333333333%; + } + .row .col.offset-m8 { + margin-left: 66.6666666667%; + } + .row .col.pull-m8 { + right: 66.6666666667%; + } + .row .col.push-m8 { + left: 66.6666666667%; + } + .row .col.offset-m9 { + margin-left: 75%; + } + .row .col.pull-m9 { + right: 75%; + } + .row .col.push-m9 { + left: 75%; + } + .row .col.offset-m10 { + margin-left: 83.3333333333%; + } + .row .col.pull-m10 { + right: 83.3333333333%; + } + .row .col.push-m10 { + left: 83.3333333333%; + } + .row .col.offset-m11 { + margin-left: 91.6666666667%; + } + .row .col.pull-m11 { + right: 91.6666666667%; + } + .row .col.push-m11 { + left: 91.6666666667%; + } + .row .col.offset-m12 { + margin-left: 100%; + } + .row .col.pull-m12 { + right: 100%; + } + .row .col.push-m12 { + left: 100%; + } +} + +@media only screen and (min-width: 993px) { + .row .col.l1 { + width: 8.3333333333%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.l2 { + width: 16.6666666667%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.l3 { + width: 25%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.l4 { + width: 33.3333333333%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.l5 { + width: 41.6666666667%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.l6 { + width: 50%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.l7 { + width: 58.3333333333%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.l8 { + width: 66.6666666667%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.l9 { + width: 75%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.l10 { + width: 83.3333333333%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.l11 { + width: 91.6666666667%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.l12 { + width: 100%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.offset-l1 { + margin-left: 8.3333333333%; + } + .row .col.pull-l1 { + right: 8.3333333333%; + } + .row .col.push-l1 { + left: 8.3333333333%; + } + .row .col.offset-l2 { + margin-left: 16.6666666667%; + } + .row .col.pull-l2 { + right: 16.6666666667%; + } + .row .col.push-l2 { + left: 16.6666666667%; + } + .row .col.offset-l3 { + margin-left: 25%; + } + .row .col.pull-l3 { + right: 25%; + } + .row .col.push-l3 { + left: 25%; + } + .row .col.offset-l4 { + margin-left: 33.3333333333%; + } + .row .col.pull-l4 { + right: 33.3333333333%; + } + .row .col.push-l4 { + left: 33.3333333333%; + } + .row .col.offset-l5 { + margin-left: 41.6666666667%; + } + .row .col.pull-l5 { + right: 41.6666666667%; + } + .row .col.push-l5 { + left: 41.6666666667%; + } + .row .col.offset-l6 { + margin-left: 50%; + } + .row .col.pull-l6 { + right: 50%; + } + .row .col.push-l6 { + left: 50%; + } + .row .col.offset-l7 { + margin-left: 58.3333333333%; + } + .row .col.pull-l7 { + right: 58.3333333333%; + } + .row .col.push-l7 { + left: 58.3333333333%; + } + .row .col.offset-l8 { + margin-left: 66.6666666667%; + } + .row .col.pull-l8 { + right: 66.6666666667%; + } + .row .col.push-l8 { + left: 66.6666666667%; + } + .row .col.offset-l9 { + margin-left: 75%; + } + .row .col.pull-l9 { + right: 75%; + } + .row .col.push-l9 { + left: 75%; + } + .row .col.offset-l10 { + margin-left: 83.3333333333%; + } + .row .col.pull-l10 { + right: 83.3333333333%; + } + .row .col.push-l10 { + left: 83.3333333333%; + } + .row .col.offset-l11 { + margin-left: 91.6666666667%; + } + .row .col.pull-l11 { + right: 91.6666666667%; + } + .row .col.push-l11 { + left: 91.6666666667%; + } + .row .col.offset-l12 { + margin-left: 100%; + } + .row .col.pull-l12 { + right: 100%; + } + .row .col.push-l12 { + left: 100%; + } +} + +@media only screen and (min-width: 1201px) { + .row .col.xl1 { + width: 8.3333333333%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.xl2 { + width: 16.6666666667%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.xl3 { + width: 25%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.xl4 { + width: 33.3333333333%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.xl5 { + width: 41.6666666667%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.xl6 { + width: 50%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.xl7 { + width: 58.3333333333%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.xl8 { + width: 66.6666666667%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.xl9 { + width: 75%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.xl10 { + width: 83.3333333333%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.xl11 { + width: 91.6666666667%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.xl12 { + width: 100%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.offset-xl1 { + margin-left: 8.3333333333%; + } + .row .col.pull-xl1 { + right: 8.3333333333%; + } + .row .col.push-xl1 { + left: 8.3333333333%; + } + .row .col.offset-xl2 { + margin-left: 16.6666666667%; + } + .row .col.pull-xl2 { + right: 16.6666666667%; + } + .row .col.push-xl2 { + left: 16.6666666667%; + } + .row .col.offset-xl3 { + margin-left: 25%; + } + .row .col.pull-xl3 { + right: 25%; + } + .row .col.push-xl3 { + left: 25%; + } + .row .col.offset-xl4 { + margin-left: 33.3333333333%; + } + .row .col.pull-xl4 { + right: 33.3333333333%; + } + .row .col.push-xl4 { + left: 33.3333333333%; + } + .row .col.offset-xl5 { + margin-left: 41.6666666667%; + } + .row .col.pull-xl5 { + right: 41.6666666667%; + } + .row .col.push-xl5 { + left: 41.6666666667%; + } + .row .col.offset-xl6 { + margin-left: 50%; + } + .row .col.pull-xl6 { + right: 50%; + } + .row .col.push-xl6 { + left: 50%; + } + .row .col.offset-xl7 { + margin-left: 58.3333333333%; + } + .row .col.pull-xl7 { + right: 58.3333333333%; + } + .row .col.push-xl7 { + left: 58.3333333333%; + } + .row .col.offset-xl8 { + margin-left: 66.6666666667%; + } + .row .col.pull-xl8 { + right: 66.6666666667%; + } + .row .col.push-xl8 { + left: 66.6666666667%; + } + .row .col.offset-xl9 { + margin-left: 75%; + } + .row .col.pull-xl9 { + right: 75%; + } + .row .col.push-xl9 { + left: 75%; + } + .row .col.offset-xl10 { + margin-left: 83.3333333333%; + } + .row .col.pull-xl10 { + right: 83.3333333333%; + } + .row .col.push-xl10 { + left: 83.3333333333%; + } + .row .col.offset-xl11 { + margin-left: 91.6666666667%; + } + .row .col.pull-xl11 { + right: 91.6666666667%; + } + .row .col.push-xl11 { + left: 91.6666666667%; + } + .row .col.offset-xl12 { + margin-left: 100%; + } + .row .col.pull-xl12 { + right: 100%; + } + .row .col.push-xl12 { + left: 100%; + } +} + +nav { + color: #fff; + background-color: #ee6e73; + width: 100%; + height: 56px; + line-height: 56px; +} + +nav.nav-extended { + height: auto; +} + +nav.nav-extended .nav-wrapper { + min-height: 56px; + height: auto; +} + +nav.nav-extended .nav-content { + position: relative; + line-height: normal; +} + +nav a { + color: #fff; +} + +nav i, +nav [class^="mdi-"], nav [class*="mdi-"], +nav i.material-icons { + display: block; + font-size: 24px; + height: 56px; + line-height: 56px; +} + +nav .nav-wrapper { + position: relative; + height: 100%; +} + +@media only screen and (min-width: 993px) { + nav a.sidenav-trigger { + display: none; + } +} + +nav .sidenav-trigger { + float: left; + position: relative; + z-index: 1; + height: 56px; + margin: 0 18px; +} + +nav .sidenav-trigger i { + height: 56px; + line-height: 56px; +} + +nav .brand-logo { + position: absolute; + color: #fff; + display: inline-block; + font-size: 2.1rem; + padding: 0; +} + +nav .brand-logo.center { + left: 50%; + -webkit-transform: translateX(-50%); + transform: translateX(-50%); +} + +@media only screen and (max-width: 992px) { + nav .brand-logo { + left: 50%; + -webkit-transform: translateX(-50%); + transform: translateX(-50%); + } + nav .brand-logo.left, nav .brand-logo.right { + padding: 0; + -webkit-transform: none; + transform: none; + } + nav .brand-logo.left { + left: 0.5rem; + } + nav .brand-logo.right { + right: 0.5rem; + left: auto; + } +} + +nav .brand-logo.right { + right: 0.5rem; + padding: 0; +} + +nav .brand-logo i, +nav .brand-logo [class^="mdi-"], nav .brand-logo [class*="mdi-"], +nav .brand-logo i.material-icons { + float: left; + margin-right: 15px; +} + +nav .nav-title { + display: inline-block; + font-size: 32px; + padding: 28px 0; +} + +nav ul { + margin: 0; +} + +nav ul li { + -webkit-transition: background-color .3s; + transition: background-color .3s; + float: left; + padding: 0; +} + +nav ul li.active { + background-color: rgba(0, 0, 0, 0.1); +} + +nav ul a { + -webkit-transition: background-color .3s; + transition: background-color .3s; + font-size: 1rem; + color: #fff; + display: block; + padding: 0 15px; + cursor: pointer; +} + +nav ul a.btn, nav ul a.btn-large, nav ul a.btn-small, nav ul a.btn-large, nav ul a.btn-flat, nav ul a.btn-floating { + margin-top: -2px; + margin-left: 15px; + margin-right: 15px; +} + +nav ul a.btn > .material-icons, nav ul a.btn-large > .material-icons, nav ul a.btn-small > .material-icons, nav ul a.btn-large > .material-icons, nav ul a.btn-flat > .material-icons, nav ul a.btn-floating > .material-icons { + height: inherit; + line-height: inherit; +} + +nav ul a:hover { + background-color: rgba(0, 0, 0, 0.1); +} + +nav ul.left { + float: left; +} + +nav form { + height: 100%; +} + +nav .input-field { + margin: 0; + height: 100%; +} + +nav .input-field input { + height: 100%; + font-size: 1.2rem; + border: none; + padding-left: 2rem; +} + +nav .input-field input:focus, nav .input-field input[type=text]:valid, nav .input-field input[type=password]:valid, nav .input-field input[type=email]:valid, nav .input-field input[type=url]:valid, nav .input-field input[type=date]:valid { + border: none; + -webkit-box-shadow: none; + box-shadow: none; +} + +nav .input-field label { + top: 0; + left: 0; +} + +nav .input-field label i { + color: rgba(255, 255, 255, 0.7); + -webkit-transition: color .3s; + transition: color .3s; +} + +nav .input-field label.active i { + color: #fff; +} + +.navbar-fixed { + position: relative; + height: 56px; + z-index: 997; +} + +.navbar-fixed nav { + position: fixed; +} + +@media only screen and (min-width: 601px) { + nav.nav-extended .nav-wrapper { + min-height: 64px; + } + nav, nav .nav-wrapper i, nav a.sidenav-trigger, nav a.sidenav-trigger i { + height: 64px; + line-height: 64px; + } + .navbar-fixed { + height: 64px; + } +} + +a { + text-decoration: none; +} + +html { + line-height: 1.5; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; + font-weight: normal; + color: rgba(0, 0, 0, 0.87); +} + +@media only screen and (min-width: 0) { + html { + font-size: 14px; + } +} + +@media only screen and (min-width: 992px) { + html { + font-size: 14.5px; + } +} + +@media only screen and (min-width: 1200px) { + html { + font-size: 15px; + } +} + +h1, h2, h3, h4, h5, h6 { + font-weight: 400; + line-height: 1.3; +} + +h1 a, h2 a, h3 a, h4 a, h5 a, h6 a { + font-weight: inherit; +} + +h1 { + font-size: 4.2rem; + line-height: 110%; + margin: 2.8rem 0 1.68rem 0; +} + +h2 { + font-size: 3.56rem; + line-height: 110%; + margin: 2.3733333333rem 0 1.424rem 0; +} + +h3 { + font-size: 2.92rem; + line-height: 110%; + margin: 1.9466666667rem 0 1.168rem 0; +} + +h4 { + font-size: 2.28rem; + line-height: 110%; + margin: 1.52rem 0 0.912rem 0; +} + +h5 { + font-size: 1.64rem; + line-height: 110%; + margin: 1.0933333333rem 0 0.656rem 0; +} + +h6 { + font-size: 1.15rem; + line-height: 110%; + margin: 0.7666666667rem 0 0.46rem 0; +} + +em { + font-style: italic; +} + +strong { + font-weight: 500; +} + +small { + font-size: 75%; +} + +.light { + font-weight: 300; +} + +.thin { + font-weight: 200; +} + +@media only screen and (min-width: 360px) { + .flow-text { + font-size: 1.2rem; + } +} + +@media only screen and (min-width: 390px) { + .flow-text { + font-size: 1.224rem; + } +} + +@media only screen and (min-width: 420px) { + .flow-text { + font-size: 1.248rem; + } +} + +@media only screen and (min-width: 450px) { + .flow-text { + font-size: 1.272rem; + } +} + +@media only screen and (min-width: 480px) { + .flow-text { + font-size: 1.296rem; + } +} + +@media only screen and (min-width: 510px) { + .flow-text { + font-size: 1.32rem; + } +} + +@media only screen and (min-width: 540px) { + .flow-text { + font-size: 1.344rem; + } +} + +@media only screen and (min-width: 570px) { + .flow-text { + font-size: 1.368rem; + } +} + +@media only screen and (min-width: 600px) { + .flow-text { + font-size: 1.392rem; + } +} + +@media only screen and (min-width: 630px) { + .flow-text { + font-size: 1.416rem; + } +} + +@media only screen and (min-width: 660px) { + .flow-text { + font-size: 1.44rem; + } +} + +@media only screen and (min-width: 690px) { + .flow-text { + font-size: 1.464rem; + } +} + +@media only screen and (min-width: 720px) { + .flow-text { + font-size: 1.488rem; + } +} + +@media only screen and (min-width: 750px) { + .flow-text { + font-size: 1.512rem; + } +} + +@media only screen and (min-width: 780px) { + .flow-text { + font-size: 1.536rem; + } +} + +@media only screen and (min-width: 810px) { + .flow-text { + font-size: 1.56rem; + } +} + +@media only screen and (min-width: 840px) { + .flow-text { + font-size: 1.584rem; + } +} + +@media only screen and (min-width: 870px) { + .flow-text { + font-size: 1.608rem; + } +} + +@media only screen and (min-width: 900px) { + .flow-text { + font-size: 1.632rem; + } +} + +@media only screen and (min-width: 930px) { + .flow-text { + font-size: 1.656rem; + } +} + +@media only screen and (min-width: 960px) { + .flow-text { + font-size: 1.68rem; + } +} + +@media only screen and (max-width: 360px) { + .flow-text { + font-size: 1.2rem; + } +} + +.scale-transition { + -webkit-transition: -webkit-transform 0.3s cubic-bezier(0.53, 0.01, 0.36, 1.63) !important; + transition: -webkit-transform 0.3s cubic-bezier(0.53, 0.01, 0.36, 1.63) !important; + transition: transform 0.3s cubic-bezier(0.53, 0.01, 0.36, 1.63) !important; + transition: transform 0.3s cubic-bezier(0.53, 0.01, 0.36, 1.63), -webkit-transform 0.3s cubic-bezier(0.53, 0.01, 0.36, 1.63) !important; +} + +.scale-transition.scale-out { + -webkit-transform: scale(0); + transform: scale(0); + -webkit-transition: -webkit-transform .2s !important; + transition: -webkit-transform .2s !important; + transition: transform .2s !important; + transition: transform .2s, -webkit-transform .2s !important; +} + +.scale-transition.scale-in { + -webkit-transform: scale(1); + transform: scale(1); +} + +.card-panel { + -webkit-transition: -webkit-box-shadow .25s; + transition: -webkit-box-shadow .25s; + transition: box-shadow .25s; + transition: box-shadow .25s, -webkit-box-shadow .25s; + padding: 24px; + margin: 0.5rem 0 1rem 0; + border-radius: 2px; + background-color: #fff; +} + +.card { + position: relative; + margin: 0.5rem 0 1rem 0; + background-color: #fff; + -webkit-transition: -webkit-box-shadow .25s; + transition: -webkit-box-shadow .25s; + transition: box-shadow .25s; + transition: box-shadow .25s, -webkit-box-shadow .25s; + border-radius: 2px; +} + +.card .card-title { + font-size: 24px; + font-weight: 300; +} + +.card .card-title.activator { + cursor: pointer; +} + +.card.small, .card.medium, .card.large { + position: relative; +} + +.card.small .card-image, .card.medium .card-image, .card.large .card-image { + max-height: 60%; + overflow: hidden; +} + +.card.small .card-image + .card-content, .card.medium .card-image + .card-content, .card.large .card-image + .card-content { + max-height: 40%; +} + +.card.small .card-content, .card.medium .card-content, .card.large .card-content { + max-height: 100%; + overflow: hidden; +} + +.card.small .card-action, .card.medium .card-action, .card.large .card-action { + position: absolute; + bottom: 0; + left: 0; + right: 0; +} + +.card.small { + height: 300px; +} + +.card.medium { + height: 400px; +} + +.card.large { + height: 500px; +} + +.card.horizontal { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; +} + +.card.horizontal.small .card-image, .card.horizontal.medium .card-image, .card.horizontal.large .card-image { + height: 100%; + max-height: none; + overflow: visible; +} + +.card.horizontal.small .card-image img, .card.horizontal.medium .card-image img, .card.horizontal.large .card-image img { + height: 100%; +} + +.card.horizontal .card-image { + max-width: 50%; +} + +.card.horizontal .card-image img { + border-radius: 2px 0 0 2px; + max-width: 100%; + width: auto; +} + +.card.horizontal .card-stacked { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-flex: 1; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + position: relative; +} + +.card.horizontal .card-stacked .card-content { + -webkit-box-flex: 1; + -webkit-flex-grow: 1; + -ms-flex-positive: 1; + flex-grow: 1; +} + +.card.sticky-action .card-action { + z-index: 2; +} + +.card.sticky-action .card-reveal { + z-index: 1; + padding-bottom: 64px; +} + +.card .card-image { + position: relative; +} + +.card .card-image img { + display: block; + border-radius: 2px 2px 0 0; + position: relative; + left: 0; + right: 0; + top: 0; + bottom: 0; + width: 100%; +} + +.card .card-image .card-title { + color: #fff; + position: absolute; + bottom: 0; + left: 0; + max-width: 100%; + padding: 24px; +} + +.card .card-content { + padding: 24px; + border-radius: 0 0 2px 2px; +} + +.card .card-content p { + margin: 0; +} + +.card .card-content .card-title { + display: block; + line-height: 32px; + margin-bottom: 8px; +} + +.card .card-content .card-title i { + line-height: 32px; +} + +.card .card-action { + background-color: inherit; + border-top: 1px solid rgba(160, 160, 160, 0.2); + position: relative; + padding: 16px 24px; +} + +.card .card-action:last-child { + border-radius: 0 0 2px 2px; +} + +.card .card-action a:not(.btn):not(.btn-large):not(.btn-small):not(.btn-large):not(.btn-floating) { + color: #ffab40; + margin-right: 24px; + -webkit-transition: color .3s ease; + transition: color .3s ease; + text-transform: uppercase; +} + +.card .card-action a:not(.btn):not(.btn-large):not(.btn-small):not(.btn-large):not(.btn-floating):hover { + color: #ffd8a6; +} + +.card .card-reveal { + padding: 24px; + position: absolute; + background-color: #fff; + width: 100%; + overflow-y: auto; + left: 0; + top: 100%; + height: 100%; + z-index: 3; + display: none; +} + +.card .card-reveal .card-title { + cursor: pointer; + display: block; +} + +#toast-container { + display: block; + position: fixed; + z-index: 10000; +} + +@media only screen and (max-width: 600px) { + #toast-container { + min-width: 100%; + bottom: 0%; + } +} + +@media only screen and (min-width: 601px) and (max-width: 992px) { + #toast-container { + left: 5%; + bottom: 7%; + max-width: 90%; + } +} + +@media only screen and (min-width: 993px) { + #toast-container { + top: 10%; + right: 7%; + max-width: 86%; + } +} + +.toast { + border-radius: 2px; + top: 35px; + width: auto; + margin-top: 10px; + position: relative; + max-width: 100%; + height: auto; + min-height: 48px; + line-height: 1.5em; + background-color: #323232; + padding: 10px 25px; + font-size: 1.1rem; + font-weight: 300; + color: #fff; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-align: center; + -webkit-align-items: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + cursor: default; +} + +.toast .toast-action { + color: #eeff41; + font-weight: 500; + margin-right: -25px; + margin-left: 3rem; +} + +.toast.rounded { + border-radius: 24px; +} + +@media only screen and (max-width: 600px) { + .toast { + width: 100%; + border-radius: 0; + } +} + +.tabs { + position: relative; + overflow-x: auto; + overflow-y: hidden; + height: 48px; + width: 100%; + background-color: #fff; + margin: 0 auto; + white-space: nowrap; +} + +.tabs.tabs-transparent { + background-color: transparent; +} + +.tabs.tabs-transparent .tab a, +.tabs.tabs-transparent .tab.disabled a, +.tabs.tabs-transparent .tab.disabled a:hover { + color: rgba(255, 255, 255, 0.7); +} + +.tabs.tabs-transparent .tab a:hover, +.tabs.tabs-transparent .tab a.active { + color: #fff; +} + +.tabs.tabs-transparent .indicator { + background-color: #fff; +} + +.tabs.tabs-fixed-width { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; +} + +.tabs.tabs-fixed-width .tab { + -webkit-box-flex: 1; + -webkit-flex-grow: 1; + -ms-flex-positive: 1; + flex-grow: 1; +} + +.tabs .tab { + display: inline-block; + text-align: center; + line-height: 48px; + height: 48px; + padding: 0; + margin: 0; + text-transform: uppercase; +} + +.tabs .tab a { + color: rgba(238, 110, 115, 0.7); + display: block; + width: 100%; + height: 100%; + padding: 0 24px; + font-size: 14px; + text-overflow: ellipsis; + overflow: hidden; + -webkit-transition: color .28s ease, background-color .28s ease; + transition: color .28s ease, background-color .28s ease; +} + +.tabs .tab a:focus, .tabs .tab a:focus.active { + background-color: rgba(246, 178, 181, 0.2); + outline: none; +} + +.tabs .tab a:hover, .tabs .tab a.active { + background-color: transparent; + color: #ee6e73; +} + +.tabs .tab.disabled a, +.tabs .tab.disabled a:hover { + color: rgba(238, 110, 115, 0.4); + cursor: default; +} + +.tabs .indicator { + position: absolute; + bottom: 0; + height: 2px; + background-color: #f6b2b5; + will-change: left, right; +} + +@media only screen and (max-width: 992px) { + .tabs { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + } + .tabs .tab { + -webkit-box-flex: 1; + -webkit-flex-grow: 1; + -ms-flex-positive: 1; + flex-grow: 1; + } + .tabs .tab a { + padding: 0 12px; + } +} + +.material-tooltip { + padding: 10px 8px; + font-size: 1rem; + z-index: 2000; + background-color: transparent; + border-radius: 2px; + color: #fff; + min-height: 36px; + line-height: 120%; + opacity: 0; + position: absolute; + text-align: center; + max-width: calc(100% - 4px); + overflow: hidden; + left: 0; + top: 0; + pointer-events: none; + visibility: hidden; + background-color: #323232; +} + +.backdrop { + position: absolute; + opacity: 0; + height: 7px; + width: 14px; + border-radius: 0 0 50% 50%; + background-color: #323232; + z-index: -1; + -webkit-transform-origin: 50% 0%; + transform-origin: 50% 0%; + visibility: hidden; +} + +.btn, .btn-large, .btn-small, +.btn-flat { + border: none; + border-radius: 2px; + display: inline-block; + height: 36px; + line-height: 36px; + padding: 0 16px; + text-transform: uppercase; + vertical-align: middle; + -webkit-tap-highlight-color: transparent; +} + +.btn.disabled, .disabled.btn-large, .disabled.btn-small, +.btn-floating.disabled, +.btn-large.disabled, +.btn-small.disabled, +.btn-flat.disabled, +.btn:disabled, +.btn-large:disabled, +.btn-small:disabled, +.btn-floating:disabled, +.btn-large:disabled, +.btn-small:disabled, +.btn-flat:disabled, +.btn[disabled], +[disabled].btn-large, +[disabled].btn-small, +.btn-floating[disabled], +.btn-large[disabled], +.btn-small[disabled], +.btn-flat[disabled] { + pointer-events: none; + background-color: #DFDFDF !important; + -webkit-box-shadow: none; + box-shadow: none; + color: #9F9F9F !important; + cursor: default; +} + +.btn.disabled:hover, .disabled.btn-large:hover, .disabled.btn-small:hover, +.btn-floating.disabled:hover, +.btn-large.disabled:hover, +.btn-small.disabled:hover, +.btn-flat.disabled:hover, +.btn:disabled:hover, +.btn-large:disabled:hover, +.btn-small:disabled:hover, +.btn-floating:disabled:hover, +.btn-large:disabled:hover, +.btn-small:disabled:hover, +.btn-flat:disabled:hover, +.btn[disabled]:hover, +[disabled].btn-large:hover, +[disabled].btn-small:hover, +.btn-floating[disabled]:hover, +.btn-large[disabled]:hover, +.btn-small[disabled]:hover, +.btn-flat[disabled]:hover { + background-color: #DFDFDF !important; + color: #9F9F9F !important; +} + +.btn, .btn-large, .btn-small, +.btn-floating, +.btn-large, +.btn-small, +.btn-flat { + font-size: 14px; + outline: 0; +} + +.btn i, .btn-large i, .btn-small i, +.btn-floating i, +.btn-large i, +.btn-small i, +.btn-flat i { + font-size: 1.3rem; + line-height: inherit; +} + +.btn:focus, .btn-large:focus, .btn-small:focus, +.btn-floating:focus { + background-color: #1d7d74; +} + +.btn, .btn-large, .btn-small { + text-decoration: none; + color: #fff; + background-color: #26a69a; + text-align: center; + letter-spacing: .5px; + -webkit-transition: background-color .2s ease-out; + transition: background-color .2s ease-out; + cursor: pointer; +} + +.btn:hover, .btn-large:hover, .btn-small:hover { + background-color: #2bbbad; +} + +.btn-floating { + display: inline-block; + color: #fff; + position: relative; + overflow: hidden; + z-index: 1; + width: 40px; + height: 40px; + line-height: 40px; + padding: 0; + background-color: #26a69a; + border-radius: 50%; + -webkit-transition: background-color .3s; + transition: background-color .3s; + cursor: pointer; + vertical-align: middle; +} + +.btn-floating:hover { + background-color: #26a69a; +} + +.btn-floating:before { + border-radius: 0; +} + +.btn-floating.btn-large { + width: 56px; + height: 56px; + padding: 0; +} + +.btn-floating.btn-large.halfway-fab { + bottom: -28px; +} + +.btn-floating.btn-large i { + line-height: 56px; +} + +.btn-floating.btn-small { + width: 32.4px; + height: 32.4px; +} + +.btn-floating.btn-small.halfway-fab { + bottom: -16.2px; +} + +.btn-floating.btn-small i { + line-height: 32.4px; +} + +.btn-floating.halfway-fab { + position: absolute; + right: 24px; + bottom: -20px; +} + +.btn-floating.halfway-fab.left { + right: auto; + left: 24px; +} + +.btn-floating i { + width: inherit; + display: inline-block; + text-align: center; + color: #fff; + font-size: 1.6rem; + line-height: 40px; +} + +button.btn-floating { + border: none; +} + +.fixed-action-btn { + position: fixed; + right: 23px; + bottom: 23px; + padding-top: 15px; + margin-bottom: 0; + z-index: 997; +} + +.fixed-action-btn.active ul { + visibility: visible; +} + +.fixed-action-btn.direction-left, .fixed-action-btn.direction-right { + padding: 0 0 0 15px; +} + +.fixed-action-btn.direction-left ul, .fixed-action-btn.direction-right ul { + text-align: right; + right: 64px; + top: 50%; + -webkit-transform: translateY(-50%); + transform: translateY(-50%); + height: 100%; + left: auto; + /*width 100% only goes to width of button container */ + width: 500px; +} + +.fixed-action-btn.direction-left ul li, .fixed-action-btn.direction-right ul li { + display: inline-block; + margin: 7.5px 15px 0 0; +} + +.fixed-action-btn.direction-right { + padding: 0 15px 0 0; +} + +.fixed-action-btn.direction-right ul { + text-align: left; + direction: rtl; + left: 64px; + right: auto; +} + +.fixed-action-btn.direction-right ul li { + margin: 7.5px 0 0 15px; +} + +.fixed-action-btn.direction-bottom { + padding: 0 0 15px 0; +} + +.fixed-action-btn.direction-bottom ul { + top: 64px; + bottom: auto; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: reverse; + -webkit-flex-direction: column-reverse; + -ms-flex-direction: column-reverse; + flex-direction: column-reverse; +} + +.fixed-action-btn.direction-bottom ul li { + margin: 15px 0 0 0; +} + +.fixed-action-btn.toolbar { + padding: 0; + height: 56px; +} + +.fixed-action-btn.toolbar.active > a i { + opacity: 0; +} + +.fixed-action-btn.toolbar ul { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + top: 0; + bottom: 0; + z-index: 1; +} + +.fixed-action-btn.toolbar ul li { + -webkit-box-flex: 1; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + display: inline-block; + margin: 0; + height: 100%; + -webkit-transition: none; + transition: none; +} + +.fixed-action-btn.toolbar ul li a { + display: block; + overflow: hidden; + position: relative; + width: 100%; + height: 100%; + background-color: transparent; + -webkit-box-shadow: none; + box-shadow: none; + color: #fff; + line-height: 56px; + z-index: 1; +} + +.fixed-action-btn.toolbar ul li a i { + line-height: inherit; +} + +.fixed-action-btn ul { + left: 0; + right: 0; + text-align: center; + position: absolute; + bottom: 64px; + margin: 0; + visibility: hidden; +} + +.fixed-action-btn ul li { + margin-bottom: 15px; +} + +.fixed-action-btn ul a.btn-floating { + opacity: 0; +} + +.fixed-action-btn .fab-backdrop { + position: absolute; + top: 0; + left: 0; + z-index: -1; + width: 40px; + height: 40px; + background-color: #26a69a; + border-radius: 50%; + -webkit-transform: scale(0); + transform: scale(0); +} + +.btn-flat { + -webkit-box-shadow: none; + box-shadow: none; + background-color: transparent; + color: #343434; + cursor: pointer; + -webkit-transition: background-color .2s; + transition: background-color .2s; +} + +.btn-flat:focus, .btn-flat:hover { + -webkit-box-shadow: none; + box-shadow: none; +} + +.btn-flat:focus { + background-color: rgba(0, 0, 0, 0.1); +} + +.btn-flat.disabled, .btn-flat.btn-flat[disabled] { + background-color: transparent !important; + color: #b3b2b2 !important; + cursor: default; +} + +.btn-large { + height: 54px; + line-height: 54px; + font-size: 15px; + padding: 0 28px; +} + +.btn-large i { + font-size: 1.6rem; +} + +.btn-small { + height: 32.4px; + line-height: 32.4px; + font-size: 13px; +} + +.btn-small i { + font-size: 1.2rem; +} + +.btn-block { + display: block; +} + +.dropdown-content { + background-color: #fff; + margin: 0; + display: none; + min-width: 100px; + overflow-y: auto; + opacity: 0; + position: absolute; + left: 0; + top: 0; + z-index: 9999; + -webkit-transform-origin: 0 0; + transform-origin: 0 0; +} + +.dropdown-content:focus { + outline: 0; +} + +.dropdown-content li { + clear: both; + color: rgba(0, 0, 0, 0.87); + cursor: pointer; + min-height: 50px; + line-height: 1.5rem; + width: 100%; + text-align: left; +} + +.dropdown-content li:hover, .dropdown-content li.active { + background-color: #eee; +} + +.dropdown-content li:focus { + outline: none; +} + +.dropdown-content li.divider { + min-height: 0; + height: 1px; +} + +.dropdown-content li > a, .dropdown-content li > span { + font-size: 16px; + color: #26a69a; + display: block; + line-height: 22px; + padding: 14px 16px; +} + +.dropdown-content li > span > label { + top: 1px; + left: 0; + height: 18px; +} + +.dropdown-content li > a > i { + height: inherit; + line-height: inherit; + float: left; + margin: 0 24px 0 0; + width: 24px; +} + +body.keyboard-focused .dropdown-content li:focus { + background-color: #dadada; +} + +.input-field.col .dropdown-content [type="checkbox"] + label { + top: 1px; + left: 0; + height: 18px; + -webkit-transform: none; + transform: none; +} + +.dropdown-trigger { + cursor: pointer; +} + +/*! + * Waves v0.6.0 + * http://fian.my.id/Waves + * + * Copyright 2014 Alfiana E. Sibuea and other contributors + * Released under the MIT license + * https://github.com/fians/Waves/blob/master/LICENSE + */ +.waves-effect { + position: relative; + cursor: pointer; + display: inline-block; + overflow: hidden; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + -webkit-tap-highlight-color: transparent; + vertical-align: middle; + z-index: 1; + -webkit-transition: .3s ease-out; + transition: .3s ease-out; +} + +.waves-effect .waves-ripple { + position: absolute; + border-radius: 50%; + width: 20px; + height: 20px; + margin-top: -10px; + margin-left: -10px; + opacity: 0; + background: rgba(0, 0, 0, 0.2); + -webkit-transition: all 0.7s ease-out; + transition: all 0.7s ease-out; + -webkit-transition-property: opacity, -webkit-transform; + transition-property: opacity, -webkit-transform; + transition-property: transform, opacity; + transition-property: transform, opacity, -webkit-transform; + -webkit-transform: scale(0); + transform: scale(0); + pointer-events: none; +} + +.waves-effect.waves-light .waves-ripple { + background-color: rgba(255, 255, 255, 0.45); +} + +.waves-effect.waves-red .waves-ripple { + background-color: rgba(244, 67, 54, 0.7); +} + +.waves-effect.waves-yellow .waves-ripple { + background-color: rgba(255, 235, 59, 0.7); +} + +.waves-effect.waves-orange .waves-ripple { + background-color: rgba(255, 152, 0, 0.7); +} + +.waves-effect.waves-purple .waves-ripple { + background-color: rgba(156, 39, 176, 0.7); +} + +.waves-effect.waves-green .waves-ripple { + background-color: rgba(76, 175, 80, 0.7); +} + +.waves-effect.waves-teal .waves-ripple { + background-color: rgba(0, 150, 136, 0.7); +} + +.waves-effect input[type="button"], .waves-effect input[type="reset"], .waves-effect input[type="submit"] { + border: 0; + font-style: normal; + font-size: inherit; + text-transform: inherit; + background: none; +} + +.waves-effect img { + position: relative; + z-index: -1; +} + +.waves-notransition { + -webkit-transition: none !important; + transition: none !important; +} + +.waves-circle { + -webkit-transform: translateZ(0); + transform: translateZ(0); + -webkit-mask-image: -webkit-radial-gradient(circle, white 100%, black 100%); +} + +.waves-input-wrapper { + border-radius: 0.2em; + vertical-align: bottom; +} + +.waves-input-wrapper .waves-button-input { + position: relative; + top: 0; + left: 0; + z-index: 1; +} + +.waves-circle { + text-align: center; + width: 2.5em; + height: 2.5em; + line-height: 2.5em; + border-radius: 50%; + -webkit-mask-image: none; +} + +.waves-block { + display: block; +} + +/* Firefox Bug: link not triggered */ +.waves-effect .waves-ripple { + z-index: -1; +} + +.modal { + display: none; + position: fixed; + left: 0; + right: 0; + background-color: #fafafa; + padding: 0; + max-height: 70%; + width: 55%; + margin: auto; + overflow-y: auto; + border-radius: 2px; + will-change: top, opacity; +} + +@media only screen and (max-width: 992px) { + .modal { + width: 80%; + } +} + +.modal h1, .modal h2, .modal h3, .modal h4 { + margin-top: 0; +} + +.modal .modal-content { + padding: 24px; +} + +.modal .modal-close { + cursor: pointer; +} + +.modal .modal-footer { + border-radius: 0 0 2px 2px; + background-color: #fafafa; + padding: 4px 6px; + height: 56px; + width: 100%; + text-align: right; +} + +.modal .modal-footer .btn, .modal .modal-footer .btn-large, .modal .modal-footer .btn-small, .modal .modal-footer .btn-flat { + margin: 6px 0; +} + +.modal-overlay { + position: fixed; + z-index: 999; + top: -25%; + left: 0; + bottom: 0; + right: 0; + height: 125%; + width: 100%; + background: #000; + display: none; + will-change: opacity; +} + +.modal.modal-fixed-footer { + padding: 0; + height: 70%; +} + +.modal.modal-fixed-footer .modal-content { + position: absolute; + height: calc(100% - 56px); + max-height: 100%; + width: 100%; + overflow-y: auto; +} + +.modal.modal-fixed-footer .modal-footer { + border-top: 1px solid rgba(0, 0, 0, 0.1); + position: absolute; + bottom: 0; +} + +.modal.bottom-sheet { + top: auto; + bottom: -100%; + margin: 0; + width: 100%; + max-height: 45%; + border-radius: 0; + will-change: bottom, opacity; +} + +.collapsible { + border-top: 1px solid #ddd; + border-right: 1px solid #ddd; + border-left: 1px solid #ddd; + margin: 0.5rem 0 1rem 0; +} + +.collapsible-header { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + cursor: pointer; + -webkit-tap-highlight-color: transparent; + line-height: 1.5; + padding: 1rem; + background-color: #fff; + border-bottom: 1px solid #ddd; +} + +.collapsible-header:focus { + outline: 0; +} + +.collapsible-header i { + width: 2rem; + font-size: 1.6rem; + display: inline-block; + text-align: center; + margin-right: 1rem; +} + +.keyboard-focused .collapsible-header:focus { + background-color: #eee; +} + +.collapsible-body { + display: none; + border-bottom: 1px solid #ddd; + -webkit-box-sizing: border-box; + box-sizing: border-box; + padding: 2rem; +} + +.sidenav .collapsible, +.sidenav.fixed .collapsible { + border: none; + -webkit-box-shadow: none; + box-shadow: none; +} + +.sidenav .collapsible li, +.sidenav.fixed .collapsible li { + padding: 0; +} + +.sidenav .collapsible-header, +.sidenav.fixed .collapsible-header { + background-color: transparent; + border: none; + line-height: inherit; + height: inherit; + padding: 0 16px; +} + +.sidenav .collapsible-header:hover, +.sidenav.fixed .collapsible-header:hover { + background-color: rgba(0, 0, 0, 0.05); +} + +.sidenav .collapsible-header i, +.sidenav.fixed .collapsible-header i { + line-height: inherit; +} + +.sidenav .collapsible-body, +.sidenav.fixed .collapsible-body { + border: 0; + background-color: #fff; +} + +.sidenav .collapsible-body li a, +.sidenav.fixed .collapsible-body li a { + padding: 0 23.5px 0 31px; +} + +.collapsible.popout { + border: none; + -webkit-box-shadow: none; + box-shadow: none; +} + +.collapsible.popout > li { + -webkit-box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 2px 10px 0 rgba(0, 0, 0, 0.12); + box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 2px 10px 0 rgba(0, 0, 0, 0.12); + margin: 0 24px; + -webkit-transition: margin 0.35s cubic-bezier(0.25, 0.46, 0.45, 0.94); + transition: margin 0.35s cubic-bezier(0.25, 0.46, 0.45, 0.94); +} + +.collapsible.popout > li.active { + -webkit-box-shadow: 0 5px 11px 0 rgba(0, 0, 0, 0.18), 0 4px 15px 0 rgba(0, 0, 0, 0.15); + box-shadow: 0 5px 11px 0 rgba(0, 0, 0, 0.18), 0 4px 15px 0 rgba(0, 0, 0, 0.15); + margin: 16px 0; +} + +.chip { + display: inline-block; + height: 32px; + font-size: 13px; + font-weight: 500; + color: rgba(0, 0, 0, 0.6); + line-height: 32px; + padding: 0 12px; + border-radius: 16px; + background-color: #e4e4e4; + margin-bottom: 5px; + margin-right: 5px; +} + +.chip:focus { + outline: none; + background-color: #26a69a; + color: #fff; +} + +.chip > img { + float: left; + margin: 0 8px 0 -12px; + height: 32px; + width: 32px; + border-radius: 50%; +} + +.chip .close { + cursor: pointer; + float: right; + font-size: 16px; + line-height: 32px; + padding-left: 8px; +} + +.chips { + border: none; + border-bottom: 1px solid #9e9e9e; + -webkit-box-shadow: none; + box-shadow: none; + margin: 0 0 8px 0; + min-height: 45px; + outline: none; + -webkit-transition: all .3s; + transition: all .3s; +} + +.chips.focus { + border-bottom: 1px solid #26a69a; + -webkit-box-shadow: 0 1px 0 0 #26a69a; + box-shadow: 0 1px 0 0 #26a69a; +} + +.chips:hover { + cursor: text; +} + +.chips .input { + background: none; + border: 0; + color: rgba(0, 0, 0, 0.6); + display: inline-block; + font-size: 16px; + height: 3rem; + line-height: 32px; + outline: 0; + margin: 0; + padding: 0 !important; + width: 120px !important; +} + +.chips .input:focus { + border: 0 !important; + -webkit-box-shadow: none !important; + box-shadow: none !important; +} + +.chips .autocomplete-content { + margin-top: 0; + margin-bottom: 0; +} + +.prefix ~ .chips { + margin-left: 3rem; + width: 92%; + width: calc(100% - 3rem); +} + +.chips:empty ~ label { + font-size: 0.8rem; + -webkit-transform: translateY(-140%); + transform: translateY(-140%); +} + +.materialboxed { + display: block; + cursor: -webkit-zoom-in; + cursor: zoom-in; + position: relative; + -webkit-transition: opacity .4s; + transition: opacity .4s; + -webkit-backface-visibility: hidden; +} + +.materialboxed:hover:not(.active) { + opacity: .8; +} + +.materialboxed.active { + cursor: -webkit-zoom-out; + cursor: zoom-out; +} + +#materialbox-overlay { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + background-color: #292929; + z-index: 1000; + will-change: opacity; +} + +.materialbox-caption { + position: fixed; + display: none; + color: #fff; + line-height: 50px; + bottom: 0; + left: 0; + width: 100%; + text-align: center; + padding: 0% 15%; + height: 50px; + z-index: 1000; + -webkit-font-smoothing: antialiased; +} + +select:focus { + outline: 1px solid #c9f3ef; +} + +button:focus { + outline: none; + background-color: #2ab7a9; +} + +label { + font-size: 0.8rem; + color: #9e9e9e; +} + +/* Text Inputs + Textarea + ========================================================================== */ +/* Style Placeholders */ +::-webkit-input-placeholder { + color: #d1d1d1; +} +::-moz-placeholder { + color: #d1d1d1; +} +:-ms-input-placeholder { + color: #d1d1d1; +} +::placeholder { + color: #d1d1d1; +} + +/* Text inputs */ +input:not([type]), +input[type=text]:not(.browser-default), +input[type=password]:not(.browser-default), +input[type=email]:not(.browser-default), +input[type=url]:not(.browser-default), +input[type=time]:not(.browser-default), +input[type=date]:not(.browser-default), +input[type=datetime]:not(.browser-default), +input[type=datetime-local]:not(.browser-default), +input[type=tel]:not(.browser-default), +input[type=number]:not(.browser-default), +input[type=search]:not(.browser-default), +textarea.materialize-textarea { + background-color: transparent; + border: none; + border-bottom: 1px solid #9e9e9e; + border-radius: 0; + outline: none; + height: 3rem; + width: 100%; + font-size: 16px; + margin: 0 0 8px 0; + padding: 0; + -webkit-box-shadow: none; + box-shadow: none; + -webkit-box-sizing: content-box; + box-sizing: content-box; + -webkit-transition: border .3s, -webkit-box-shadow .3s; + transition: border .3s, -webkit-box-shadow .3s; + transition: box-shadow .3s, border .3s; + transition: box-shadow .3s, border .3s, -webkit-box-shadow .3s; +} + +input:not([type]):disabled, input:not([type])[readonly="readonly"], +input[type=text]:not(.browser-default):disabled, +input[type=text]:not(.browser-default)[readonly="readonly"], +input[type=password]:not(.browser-default):disabled, +input[type=password]:not(.browser-default)[readonly="readonly"], +input[type=email]:not(.browser-default):disabled, +input[type=email]:not(.browser-default)[readonly="readonly"], +input[type=url]:not(.browser-default):disabled, +input[type=url]:not(.browser-default)[readonly="readonly"], +input[type=time]:not(.browser-default):disabled, +input[type=time]:not(.browser-default)[readonly="readonly"], +input[type=date]:not(.browser-default):disabled, +input[type=date]:not(.browser-default)[readonly="readonly"], +input[type=datetime]:not(.browser-default):disabled, +input[type=datetime]:not(.browser-default)[readonly="readonly"], +input[type=datetime-local]:not(.browser-default):disabled, +input[type=datetime-local]:not(.browser-default)[readonly="readonly"], +input[type=tel]:not(.browser-default):disabled, +input[type=tel]:not(.browser-default)[readonly="readonly"], +input[type=number]:not(.browser-default):disabled, +input[type=number]:not(.browser-default)[readonly="readonly"], +input[type=search]:not(.browser-default):disabled, +input[type=search]:not(.browser-default)[readonly="readonly"], +textarea.materialize-textarea:disabled, +textarea.materialize-textarea[readonly="readonly"] { + color: rgba(0, 0, 0, 0.42); + border-bottom: 1px dotted rgba(0, 0, 0, 0.42); +} + +input:not([type]):disabled + label, +input:not([type])[readonly="readonly"] + label, +input[type=text]:not(.browser-default):disabled + label, +input[type=text]:not(.browser-default)[readonly="readonly"] + label, +input[type=password]:not(.browser-default):disabled + label, +input[type=password]:not(.browser-default)[readonly="readonly"] + label, +input[type=email]:not(.browser-default):disabled + label, +input[type=email]:not(.browser-default)[readonly="readonly"] + label, +input[type=url]:not(.browser-default):disabled + label, +input[type=url]:not(.browser-default)[readonly="readonly"] + label, +input[type=time]:not(.browser-default):disabled + label, +input[type=time]:not(.browser-default)[readonly="readonly"] + label, +input[type=date]:not(.browser-default):disabled + label, +input[type=date]:not(.browser-default)[readonly="readonly"] + label, +input[type=datetime]:not(.browser-default):disabled + label, +input[type=datetime]:not(.browser-default)[readonly="readonly"] + label, +input[type=datetime-local]:not(.browser-default):disabled + label, +input[type=datetime-local]:not(.browser-default)[readonly="readonly"] + label, +input[type=tel]:not(.browser-default):disabled + label, +input[type=tel]:not(.browser-default)[readonly="readonly"] + label, +input[type=number]:not(.browser-default):disabled + label, +input[type=number]:not(.browser-default)[readonly="readonly"] + label, +input[type=search]:not(.browser-default):disabled + label, +input[type=search]:not(.browser-default)[readonly="readonly"] + label, +textarea.materialize-textarea:disabled + label, +textarea.materialize-textarea[readonly="readonly"] + label { + color: rgba(0, 0, 0, 0.42); +} + +input:not([type]):focus:not([readonly]), +input[type=text]:not(.browser-default):focus:not([readonly]), +input[type=password]:not(.browser-default):focus:not([readonly]), +input[type=email]:not(.browser-default):focus:not([readonly]), +input[type=url]:not(.browser-default):focus:not([readonly]), +input[type=time]:not(.browser-default):focus:not([readonly]), +input[type=date]:not(.browser-default):focus:not([readonly]), +input[type=datetime]:not(.browser-default):focus:not([readonly]), +input[type=datetime-local]:not(.browser-default):focus:not([readonly]), +input[type=tel]:not(.browser-default):focus:not([readonly]), +input[type=number]:not(.browser-default):focus:not([readonly]), +input[type=search]:not(.browser-default):focus:not([readonly]), +textarea.materialize-textarea:focus:not([readonly]) { + border-bottom: 1px solid #26a69a; + -webkit-box-shadow: 0 1px 0 0 #26a69a; + box-shadow: 0 1px 0 0 #26a69a; +} + +input:not([type]):focus:not([readonly]) + label, +input[type=text]:not(.browser-default):focus:not([readonly]) + label, +input[type=password]:not(.browser-default):focus:not([readonly]) + label, +input[type=email]:not(.browser-default):focus:not([readonly]) + label, +input[type=url]:not(.browser-default):focus:not([readonly]) + label, +input[type=time]:not(.browser-default):focus:not([readonly]) + label, +input[type=date]:not(.browser-default):focus:not([readonly]) + label, +input[type=datetime]:not(.browser-default):focus:not([readonly]) + label, +input[type=datetime-local]:not(.browser-default):focus:not([readonly]) + label, +input[type=tel]:not(.browser-default):focus:not([readonly]) + label, +input[type=number]:not(.browser-default):focus:not([readonly]) + label, +input[type=search]:not(.browser-default):focus:not([readonly]) + label, +textarea.materialize-textarea:focus:not([readonly]) + label { + color: #26a69a; +} + +input:not([type]):focus.valid ~ label, +input[type=text]:not(.browser-default):focus.valid ~ label, +input[type=password]:not(.browser-default):focus.valid ~ label, +input[type=email]:not(.browser-default):focus.valid ~ label, +input[type=url]:not(.browser-default):focus.valid ~ label, +input[type=time]:not(.browser-default):focus.valid ~ label, +input[type=date]:not(.browser-default):focus.valid ~ label, +input[type=datetime]:not(.browser-default):focus.valid ~ label, +input[type=datetime-local]:not(.browser-default):focus.valid ~ label, +input[type=tel]:not(.browser-default):focus.valid ~ label, +input[type=number]:not(.browser-default):focus.valid ~ label, +input[type=search]:not(.browser-default):focus.valid ~ label, +textarea.materialize-textarea:focus.valid ~ label { + color: #4CAF50; +} + +input:not([type]):focus.invalid ~ label, +input[type=text]:not(.browser-default):focus.invalid ~ label, +input[type=password]:not(.browser-default):focus.invalid ~ label, +input[type=email]:not(.browser-default):focus.invalid ~ label, +input[type=url]:not(.browser-default):focus.invalid ~ label, +input[type=time]:not(.browser-default):focus.invalid ~ label, +input[type=date]:not(.browser-default):focus.invalid ~ label, +input[type=datetime]:not(.browser-default):focus.invalid ~ label, +input[type=datetime-local]:not(.browser-default):focus.invalid ~ label, +input[type=tel]:not(.browser-default):focus.invalid ~ label, +input[type=number]:not(.browser-default):focus.invalid ~ label, +input[type=search]:not(.browser-default):focus.invalid ~ label, +textarea.materialize-textarea:focus.invalid ~ label { + color: #F44336; +} + +input:not([type]).validate + label, +input[type=text]:not(.browser-default).validate + label, +input[type=password]:not(.browser-default).validate + label, +input[type=email]:not(.browser-default).validate + label, +input[type=url]:not(.browser-default).validate + label, +input[type=time]:not(.browser-default).validate + label, +input[type=date]:not(.browser-default).validate + label, +input[type=datetime]:not(.browser-default).validate + label, +input[type=datetime-local]:not(.browser-default).validate + label, +input[type=tel]:not(.browser-default).validate + label, +input[type=number]:not(.browser-default).validate + label, +input[type=search]:not(.browser-default).validate + label, +textarea.materialize-textarea.validate + label { + width: 100%; +} + +/* Validation Sass Placeholders */ +input.valid:not([type]), input.valid:not([type]):focus, +input[type=text].valid:not(.browser-default), +input[type=text].valid:not(.browser-default):focus, +input[type=password].valid:not(.browser-default), +input[type=password].valid:not(.browser-default):focus, +input[type=email].valid:not(.browser-default), +input[type=email].valid:not(.browser-default):focus, +input[type=url].valid:not(.browser-default), +input[type=url].valid:not(.browser-default):focus, +input[type=time].valid:not(.browser-default), +input[type=time].valid:not(.browser-default):focus, +input[type=date].valid:not(.browser-default), +input[type=date].valid:not(.browser-default):focus, +input[type=datetime].valid:not(.browser-default), +input[type=datetime].valid:not(.browser-default):focus, +input[type=datetime-local].valid:not(.browser-default), +input[type=datetime-local].valid:not(.browser-default):focus, +input[type=tel].valid:not(.browser-default), +input[type=tel].valid:not(.browser-default):focus, +input[type=number].valid:not(.browser-default), +input[type=number].valid:not(.browser-default):focus, +input[type=search].valid:not(.browser-default), +input[type=search].valid:not(.browser-default):focus, +textarea.materialize-textarea.valid, +textarea.materialize-textarea.valid:focus, .select-wrapper.valid > input.select-dropdown { + border-bottom: 1px solid #4CAF50; + -webkit-box-shadow: 0 1px 0 0 #4CAF50; + box-shadow: 0 1px 0 0 #4CAF50; +} + +input.invalid:not([type]), input.invalid:not([type]):focus, +input[type=text].invalid:not(.browser-default), +input[type=text].invalid:not(.browser-default):focus, +input[type=password].invalid:not(.browser-default), +input[type=password].invalid:not(.browser-default):focus, +input[type=email].invalid:not(.browser-default), +input[type=email].invalid:not(.browser-default):focus, +input[type=url].invalid:not(.browser-default), +input[type=url].invalid:not(.browser-default):focus, +input[type=time].invalid:not(.browser-default), +input[type=time].invalid:not(.browser-default):focus, +input[type=date].invalid:not(.browser-default), +input[type=date].invalid:not(.browser-default):focus, +input[type=datetime].invalid:not(.browser-default), +input[type=datetime].invalid:not(.browser-default):focus, +input[type=datetime-local].invalid:not(.browser-default), +input[type=datetime-local].invalid:not(.browser-default):focus, +input[type=tel].invalid:not(.browser-default), +input[type=tel].invalid:not(.browser-default):focus, +input[type=number].invalid:not(.browser-default), +input[type=number].invalid:not(.browser-default):focus, +input[type=search].invalid:not(.browser-default), +input[type=search].invalid:not(.browser-default):focus, +textarea.materialize-textarea.invalid, +textarea.materialize-textarea.invalid:focus, .select-wrapper.invalid > input.select-dropdown, +.select-wrapper.invalid > input.select-dropdown:focus { + border-bottom: 1px solid #F44336; + -webkit-box-shadow: 0 1px 0 0 #F44336; + box-shadow: 0 1px 0 0 #F44336; +} + +input:not([type]).valid ~ .helper-text[data-success], +input:not([type]):focus.valid ~ .helper-text[data-success], +input:not([type]).invalid ~ .helper-text[data-error], +input:not([type]):focus.invalid ~ .helper-text[data-error], +input[type=text]:not(.browser-default).valid ~ .helper-text[data-success], +input[type=text]:not(.browser-default):focus.valid ~ .helper-text[data-success], +input[type=text]:not(.browser-default).invalid ~ .helper-text[data-error], +input[type=text]:not(.browser-default):focus.invalid ~ .helper-text[data-error], +input[type=password]:not(.browser-default).valid ~ .helper-text[data-success], +input[type=password]:not(.browser-default):focus.valid ~ .helper-text[data-success], +input[type=password]:not(.browser-default).invalid ~ .helper-text[data-error], +input[type=password]:not(.browser-default):focus.invalid ~ .helper-text[data-error], +input[type=email]:not(.browser-default).valid ~ .helper-text[data-success], +input[type=email]:not(.browser-default):focus.valid ~ .helper-text[data-success], +input[type=email]:not(.browser-default).invalid ~ .helper-text[data-error], +input[type=email]:not(.browser-default):focus.invalid ~ .helper-text[data-error], +input[type=url]:not(.browser-default).valid ~ .helper-text[data-success], +input[type=url]:not(.browser-default):focus.valid ~ .helper-text[data-success], +input[type=url]:not(.browser-default).invalid ~ .helper-text[data-error], +input[type=url]:not(.browser-default):focus.invalid ~ .helper-text[data-error], +input[type=time]:not(.browser-default).valid ~ .helper-text[data-success], +input[type=time]:not(.browser-default):focus.valid ~ .helper-text[data-success], +input[type=time]:not(.browser-default).invalid ~ .helper-text[data-error], +input[type=time]:not(.browser-default):focus.invalid ~ .helper-text[data-error], +input[type=date]:not(.browser-default).valid ~ .helper-text[data-success], +input[type=date]:not(.browser-default):focus.valid ~ .helper-text[data-success], +input[type=date]:not(.browser-default).invalid ~ .helper-text[data-error], +input[type=date]:not(.browser-default):focus.invalid ~ .helper-text[data-error], +input[type=datetime]:not(.browser-default).valid ~ .helper-text[data-success], +input[type=datetime]:not(.browser-default):focus.valid ~ .helper-text[data-success], +input[type=datetime]:not(.browser-default).invalid ~ .helper-text[data-error], +input[type=datetime]:not(.browser-default):focus.invalid ~ .helper-text[data-error], +input[type=datetime-local]:not(.browser-default).valid ~ .helper-text[data-success], +input[type=datetime-local]:not(.browser-default):focus.valid ~ .helper-text[data-success], +input[type=datetime-local]:not(.browser-default).invalid ~ .helper-text[data-error], +input[type=datetime-local]:not(.browser-default):focus.invalid ~ .helper-text[data-error], +input[type=tel]:not(.browser-default).valid ~ .helper-text[data-success], +input[type=tel]:not(.browser-default):focus.valid ~ .helper-text[data-success], +input[type=tel]:not(.browser-default).invalid ~ .helper-text[data-error], +input[type=tel]:not(.browser-default):focus.invalid ~ .helper-text[data-error], +input[type=number]:not(.browser-default).valid ~ .helper-text[data-success], +input[type=number]:not(.browser-default):focus.valid ~ .helper-text[data-success], +input[type=number]:not(.browser-default).invalid ~ .helper-text[data-error], +input[type=number]:not(.browser-default):focus.invalid ~ .helper-text[data-error], +input[type=search]:not(.browser-default).valid ~ .helper-text[data-success], +input[type=search]:not(.browser-default):focus.valid ~ .helper-text[data-success], +input[type=search]:not(.browser-default).invalid ~ .helper-text[data-error], +input[type=search]:not(.browser-default):focus.invalid ~ .helper-text[data-error], +textarea.materialize-textarea.valid ~ .helper-text[data-success], +textarea.materialize-textarea:focus.valid ~ .helper-text[data-success], +textarea.materialize-textarea.invalid ~ .helper-text[data-error], +textarea.materialize-textarea:focus.invalid ~ .helper-text[data-error], .select-wrapper.valid .helper-text[data-success], +.select-wrapper.invalid ~ .helper-text[data-error] { + color: transparent; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + pointer-events: none; +} + +input:not([type]).valid ~ .helper-text:after, +input:not([type]):focus.valid ~ .helper-text:after, +input[type=text]:not(.browser-default).valid ~ .helper-text:after, +input[type=text]:not(.browser-default):focus.valid ~ .helper-text:after, +input[type=password]:not(.browser-default).valid ~ .helper-text:after, +input[type=password]:not(.browser-default):focus.valid ~ .helper-text:after, +input[type=email]:not(.browser-default).valid ~ .helper-text:after, +input[type=email]:not(.browser-default):focus.valid ~ .helper-text:after, +input[type=url]:not(.browser-default).valid ~ .helper-text:after, +input[type=url]:not(.browser-default):focus.valid ~ .helper-text:after, +input[type=time]:not(.browser-default).valid ~ .helper-text:after, +input[type=time]:not(.browser-default):focus.valid ~ .helper-text:after, +input[type=date]:not(.browser-default).valid ~ .helper-text:after, +input[type=date]:not(.browser-default):focus.valid ~ .helper-text:after, +input[type=datetime]:not(.browser-default).valid ~ .helper-text:after, +input[type=datetime]:not(.browser-default):focus.valid ~ .helper-text:after, +input[type=datetime-local]:not(.browser-default).valid ~ .helper-text:after, +input[type=datetime-local]:not(.browser-default):focus.valid ~ .helper-text:after, +input[type=tel]:not(.browser-default).valid ~ .helper-text:after, +input[type=tel]:not(.browser-default):focus.valid ~ .helper-text:after, +input[type=number]:not(.browser-default).valid ~ .helper-text:after, +input[type=number]:not(.browser-default):focus.valid ~ .helper-text:after, +input[type=search]:not(.browser-default).valid ~ .helper-text:after, +input[type=search]:not(.browser-default):focus.valid ~ .helper-text:after, +textarea.materialize-textarea.valid ~ .helper-text:after, +textarea.materialize-textarea:focus.valid ~ .helper-text:after, .select-wrapper.valid ~ .helper-text:after { + content: attr(data-success); + color: #4CAF50; +} + +input:not([type]).invalid ~ .helper-text:after, +input:not([type]):focus.invalid ~ .helper-text:after, +input[type=text]:not(.browser-default).invalid ~ .helper-text:after, +input[type=text]:not(.browser-default):focus.invalid ~ .helper-text:after, +input[type=password]:not(.browser-default).invalid ~ .helper-text:after, +input[type=password]:not(.browser-default):focus.invalid ~ .helper-text:after, +input[type=email]:not(.browser-default).invalid ~ .helper-text:after, +input[type=email]:not(.browser-default):focus.invalid ~ .helper-text:after, +input[type=url]:not(.browser-default).invalid ~ .helper-text:after, +input[type=url]:not(.browser-default):focus.invalid ~ .helper-text:after, +input[type=time]:not(.browser-default).invalid ~ .helper-text:after, +input[type=time]:not(.browser-default):focus.invalid ~ .helper-text:after, +input[type=date]:not(.browser-default).invalid ~ .helper-text:after, +input[type=date]:not(.browser-default):focus.invalid ~ .helper-text:after, +input[type=datetime]:not(.browser-default).invalid ~ .helper-text:after, +input[type=datetime]:not(.browser-default):focus.invalid ~ .helper-text:after, +input[type=datetime-local]:not(.browser-default).invalid ~ .helper-text:after, +input[type=datetime-local]:not(.browser-default):focus.invalid ~ .helper-text:after, +input[type=tel]:not(.browser-default).invalid ~ .helper-text:after, +input[type=tel]:not(.browser-default):focus.invalid ~ .helper-text:after, +input[type=number]:not(.browser-default).invalid ~ .helper-text:after, +input[type=number]:not(.browser-default):focus.invalid ~ .helper-text:after, +input[type=search]:not(.browser-default).invalid ~ .helper-text:after, +input[type=search]:not(.browser-default):focus.invalid ~ .helper-text:after, +textarea.materialize-textarea.invalid ~ .helper-text:after, +textarea.materialize-textarea:focus.invalid ~ .helper-text:after, .select-wrapper.invalid ~ .helper-text:after { + content: attr(data-error); + color: #F44336; +} + +input:not([type]) + label:after, +input[type=text]:not(.browser-default) + label:after, +input[type=password]:not(.browser-default) + label:after, +input[type=email]:not(.browser-default) + label:after, +input[type=url]:not(.browser-default) + label:after, +input[type=time]:not(.browser-default) + label:after, +input[type=date]:not(.browser-default) + label:after, +input[type=datetime]:not(.browser-default) + label:after, +input[type=datetime-local]:not(.browser-default) + label:after, +input[type=tel]:not(.browser-default) + label:after, +input[type=number]:not(.browser-default) + label:after, +input[type=search]:not(.browser-default) + label:after, +textarea.materialize-textarea + label:after, .select-wrapper + label:after { + display: block; + content: ""; + position: absolute; + top: 100%; + left: 0; + opacity: 0; + -webkit-transition: .2s opacity ease-out, .2s color ease-out; + transition: .2s opacity ease-out, .2s color ease-out; +} + +.input-field { + position: relative; + margin-top: 1rem; + margin-bottom: 1rem; +} + +.input-field.inline { + display: inline-block; + vertical-align: middle; + margin-left: 5px; +} + +.input-field.inline input, +.input-field.inline .select-dropdown { + margin-bottom: 1rem; +} + +.input-field.col label { + left: 0.75rem; +} + +.input-field.col .prefix ~ label, +.input-field.col .prefix ~ .validate ~ label { + width: calc(100% - 3rem - 1.5rem); +} + +.input-field > label { + color: #9e9e9e; + position: absolute; + top: 0; + left: 0; + font-size: 1rem; + cursor: text; + -webkit-transition: color .2s ease-out, -webkit-transform .2s ease-out; + transition: color .2s ease-out, -webkit-transform .2s ease-out; + transition: transform .2s ease-out, color .2s ease-out; + transition: transform .2s ease-out, color .2s ease-out, -webkit-transform .2s ease-out; + -webkit-transform-origin: 0% 100%; + transform-origin: 0% 100%; + text-align: initial; + -webkit-transform: translateY(12px); + transform: translateY(12px); +} + +.input-field > label:not(.label-icon).active { + -webkit-transform: translateY(-14px) scale(0.8); + transform: translateY(-14px) scale(0.8); + -webkit-transform-origin: 0 0; + transform-origin: 0 0; +} + +.input-field > input[type]:-webkit-autofill:not(.browser-default) + label, +.input-field > input[type=date]:not(.browser-default) + label, +.input-field > input[type=time]:not(.browser-default) + label { + -webkit-transform: translateY(-14px) scale(0.8); + transform: translateY(-14px) scale(0.8); + -webkit-transform-origin: 0 0; + transform-origin: 0 0; +} + +.input-field .helper-text { + position: relative; + min-height: 18px; + display: block; + font-size: 12px; + color: rgba(0, 0, 0, 0.54); +} + +.input-field .helper-text::after { + opacity: 1; + position: absolute; + top: 0; + left: 0; +} + +.input-field .prefix { + position: absolute; + width: 3rem; + font-size: 2rem; + -webkit-transition: color .2s; + transition: color .2s; + top: 0.5rem; +} + +.input-field .prefix.active { + color: #26a69a; +} + +.input-field .prefix ~ input, +.input-field .prefix ~ textarea, +.input-field .prefix ~ label, +.input-field .prefix ~ .validate ~ label, +.input-field .prefix ~ .helper-text, +.input-field .prefix ~ .autocomplete-content { + margin-left: 3rem; + width: 92%; + width: calc(100% - 3rem); +} + +.input-field .prefix ~ label { + margin-left: 3rem; +} + +@media only screen and (max-width: 992px) { + .input-field .prefix ~ input { + width: 86%; + width: calc(100% - 3rem); + } +} + +@media only screen and (max-width: 600px) { + .input-field .prefix ~ input { + width: 80%; + width: calc(100% - 3rem); + } +} + +/* Search Field */ +.input-field input[type=search] { + display: block; + line-height: inherit; + -webkit-transition: .3s background-color; + transition: .3s background-color; +} + +.nav-wrapper .input-field input[type=search] { + height: inherit; + padding-left: 4rem; + width: calc(100% - 4rem); + border: 0; + -webkit-box-shadow: none; + box-shadow: none; +} + +.input-field input[type=search]:focus:not(.browser-default) { + background-color: #fff; + border: 0; + -webkit-box-shadow: none; + box-shadow: none; + color: #444; +} + +.input-field input[type=search]:focus:not(.browser-default) + label i, +.input-field input[type=search]:focus:not(.browser-default) ~ .mdi-navigation-close, +.input-field input[type=search]:focus:not(.browser-default) ~ .material-icons { + color: #444; +} + +.input-field input[type=search] + .label-icon { + -webkit-transform: none; + transform: none; + left: 1rem; +} + +.input-field input[type=search] ~ .mdi-navigation-close, +.input-field input[type=search] ~ .material-icons { + position: absolute; + top: 0; + right: 1rem; + color: transparent; + cursor: pointer; + font-size: 2rem; + -webkit-transition: .3s color; + transition: .3s color; +} + +/* Textarea */ +textarea { + width: 100%; + height: 3rem; + background-color: transparent; +} + +textarea.materialize-textarea { + line-height: normal; + overflow-y: hidden; + /* prevents scroll bar flash */ + padding: .8rem 0 .8rem 0; + /* prevents text jump on Enter keypress */ + resize: none; + min-height: 3rem; + -webkit-box-sizing: border-box; + box-sizing: border-box; +} + +.hiddendiv { + visibility: hidden; + white-space: pre-wrap; + word-wrap: break-word; + overflow-wrap: break-word; + /* future version of deprecated 'word-wrap' */ + padding-top: 1.2rem; + /* prevents text jump on Enter keypress */ + position: absolute; + top: 0; + z-index: -1; +} + +/* Autocomplete */ +.autocomplete-content li .highlight { + color: #444; +} + +.autocomplete-content li img { + height: 40px; + width: 40px; + margin: 5px 15px; +} + +/* Character Counter */ +.character-counter { + min-height: 18px; +} + +/* Radio Buttons + ========================================================================== */ +[type="radio"]:not(:checked), +[type="radio"]:checked { + position: absolute; + opacity: 0; + pointer-events: none; +} + +[type="radio"]:not(:checked) + span, +[type="radio"]:checked + span { + position: relative; + padding-left: 35px; + cursor: pointer; + display: inline-block; + height: 25px; + line-height: 25px; + font-size: 1rem; + -webkit-transition: .28s ease; + transition: .28s ease; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +[type="radio"] + span:before, +[type="radio"] + span:after { + content: ''; + position: absolute; + left: 0; + top: 0; + margin: 4px; + width: 16px; + height: 16px; + z-index: 0; + -webkit-transition: .28s ease; + transition: .28s ease; +} + +/* Unchecked styles */ +[type="radio"]:not(:checked) + span:before, +[type="radio"]:not(:checked) + span:after, +[type="radio"]:checked + span:before, +[type="radio"]:checked + span:after, +[type="radio"].with-gap:checked + span:before, +[type="radio"].with-gap:checked + span:after { + border-radius: 50%; +} + +[type="radio"]:not(:checked) + span:before, +[type="radio"]:not(:checked) + span:after { + border: 2px solid #5a5a5a; +} + +[type="radio"]:not(:checked) + span:after { + -webkit-transform: scale(0); + transform: scale(0); +} + +/* Checked styles */ +[type="radio"]:checked + span:before { + border: 2px solid transparent; +} + +[type="radio"]:checked + span:after, +[type="radio"].with-gap:checked + span:before, +[type="radio"].with-gap:checked + span:after { + border: 2px solid #26a69a; +} + +[type="radio"]:checked + span:after, +[type="radio"].with-gap:checked + span:after { + background-color: #26a69a; +} + +[type="radio"]:checked + span:after { + -webkit-transform: scale(1.02); + transform: scale(1.02); +} + +/* Radio With gap */ +[type="radio"].with-gap:checked + span:after { + -webkit-transform: scale(0.5); + transform: scale(0.5); +} + +/* Focused styles */ +[type="radio"].tabbed:focus + span:before { + -webkit-box-shadow: 0 0 0 10px rgba(0, 0, 0, 0.1); + box-shadow: 0 0 0 10px rgba(0, 0, 0, 0.1); +} + +/* Disabled Radio With gap */ +[type="radio"].with-gap:disabled:checked + span:before { + border: 2px solid rgba(0, 0, 0, 0.42); +} + +[type="radio"].with-gap:disabled:checked + span:after { + border: none; + background-color: rgba(0, 0, 0, 0.42); +} + +/* Disabled style */ +[type="radio"]:disabled:not(:checked) + span:before, +[type="radio"]:disabled:checked + span:before { + background-color: transparent; + border-color: rgba(0, 0, 0, 0.42); +} + +[type="radio"]:disabled + span { + color: rgba(0, 0, 0, 0.42); +} + +[type="radio"]:disabled:not(:checked) + span:before { + border-color: rgba(0, 0, 0, 0.42); +} + +[type="radio"]:disabled:checked + span:after { + background-color: rgba(0, 0, 0, 0.42); + border-color: #949494; +} + +/* Checkboxes + ========================================================================== */ +/* Remove default checkbox */ +[type="checkbox"]:not(:checked), +[type="checkbox"]:checked { + position: absolute; + opacity: 0; + pointer-events: none; +} + +[type="checkbox"] { + /* checkbox aspect */ +} + +[type="checkbox"] + span:not(.lever) { + position: relative; + padding-left: 35px; + cursor: pointer; + display: inline-block; + height: 25px; + line-height: 25px; + font-size: 1rem; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +[type="checkbox"] + span:not(.lever):before, +[type="checkbox"]:not(.filled-in) + span:not(.lever):after { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 18px; + height: 18px; + z-index: 0; + border: 2px solid #5a5a5a; + border-radius: 1px; + margin-top: 3px; + -webkit-transition: .2s; + transition: .2s; +} + +[type="checkbox"]:not(.filled-in) + span:not(.lever):after { + border: 0; + -webkit-transform: scale(0); + transform: scale(0); +} + +[type="checkbox"]:not(:checked):disabled + span:not(.lever):before { + border: none; + background-color: rgba(0, 0, 0, 0.42); +} + +[type="checkbox"].tabbed:focus + span:not(.lever):after { + -webkit-transform: scale(1); + transform: scale(1); + border: 0; + border-radius: 50%; + -webkit-box-shadow: 0 0 0 10px rgba(0, 0, 0, 0.1); + box-shadow: 0 0 0 10px rgba(0, 0, 0, 0.1); + background-color: rgba(0, 0, 0, 0.1); +} + +[type="checkbox"]:checked + span:not(.lever):before { + top: -4px; + left: -5px; + width: 12px; + height: 22px; + border-top: 2px solid transparent; + border-left: 2px solid transparent; + border-right: 2px solid #26a69a; + border-bottom: 2px solid #26a69a; + -webkit-transform: rotate(40deg); + transform: rotate(40deg); + -webkit-backface-visibility: hidden; + backface-visibility: hidden; + -webkit-transform-origin: 100% 100%; + transform-origin: 100% 100%; +} + +[type="checkbox"]:checked:disabled + span:before { + border-right: 2px solid rgba(0, 0, 0, 0.42); + border-bottom: 2px solid rgba(0, 0, 0, 0.42); +} + +/* Indeterminate checkbox */ +[type="checkbox"]:indeterminate + span:not(.lever):before { + top: -11px; + left: -12px; + width: 10px; + height: 22px; + border-top: none; + border-left: none; + border-right: 2px solid #26a69a; + border-bottom: none; + -webkit-transform: rotate(90deg); + transform: rotate(90deg); + -webkit-backface-visibility: hidden; + backface-visibility: hidden; + -webkit-transform-origin: 100% 100%; + transform-origin: 100% 100%; +} + +[type="checkbox"]:indeterminate:disabled + span:not(.lever):before { + border-right: 2px solid rgba(0, 0, 0, 0.42); + background-color: transparent; +} + +[type="checkbox"].filled-in + span:not(.lever):after { + border-radius: 2px; +} + +[type="checkbox"].filled-in + span:not(.lever):before, +[type="checkbox"].filled-in + span:not(.lever):after { + content: ''; + left: 0; + position: absolute; + /* .1s delay is for check animation */ + -webkit-transition: border .25s, background-color .25s, width .20s .1s, height .20s .1s, top .20s .1s, left .20s .1s; + transition: border .25s, background-color .25s, width .20s .1s, height .20s .1s, top .20s .1s, left .20s .1s; + z-index: 1; +} + +[type="checkbox"].filled-in:not(:checked) + span:not(.lever):before { + width: 0; + height: 0; + border: 3px solid transparent; + left: 6px; + top: 10px; + -webkit-transform: rotateZ(37deg); + transform: rotateZ(37deg); + -webkit-transform-origin: 100% 100%; + transform-origin: 100% 100%; +} + +[type="checkbox"].filled-in:not(:checked) + span:not(.lever):after { + height: 20px; + width: 20px; + background-color: transparent; + border: 2px solid #5a5a5a; + top: 0px; + z-index: 0; +} + +[type="checkbox"].filled-in:checked + span:not(.lever):before { + top: 0; + left: 1px; + width: 8px; + height: 13px; + border-top: 2px solid transparent; + border-left: 2px solid transparent; + border-right: 2px solid #fff; + border-bottom: 2px solid #fff; + -webkit-transform: rotateZ(37deg); + transform: rotateZ(37deg); + -webkit-transform-origin: 100% 100%; + transform-origin: 100% 100%; +} + +[type="checkbox"].filled-in:checked + span:not(.lever):after { + top: 0; + width: 20px; + height: 20px; + border: 2px solid #26a69a; + background-color: #26a69a; + z-index: 0; +} + +[type="checkbox"].filled-in.tabbed:focus + span:not(.lever):after { + border-radius: 2px; + border-color: #5a5a5a; + background-color: rgba(0, 0, 0, 0.1); +} + +[type="checkbox"].filled-in.tabbed:checked:focus + span:not(.lever):after { + border-radius: 2px; + background-color: #26a69a; + border-color: #26a69a; +} + +[type="checkbox"].filled-in:disabled:not(:checked) + span:not(.lever):before { + background-color: transparent; + border: 2px solid transparent; +} + +[type="checkbox"].filled-in:disabled:not(:checked) + span:not(.lever):after { + border-color: transparent; + background-color: #949494; +} + +[type="checkbox"].filled-in:disabled:checked + span:not(.lever):before { + background-color: transparent; +} + +[type="checkbox"].filled-in:disabled:checked + span:not(.lever):after { + background-color: #949494; + border-color: #949494; +} + +/* Switch + ========================================================================== */ +.switch, +.switch * { + -webkit-tap-highlight-color: transparent; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.switch label { + cursor: pointer; +} + +.switch label input[type=checkbox] { + opacity: 0; + width: 0; + height: 0; +} + +.switch label input[type=checkbox]:checked + .lever { + background-color: #84c7c1; +} + +.switch label input[type=checkbox]:checked + .lever:before, .switch label input[type=checkbox]:checked + .lever:after { + left: 18px; +} + +.switch label input[type=checkbox]:checked + .lever:after { + background-color: #26a69a; +} + +.switch label .lever { + content: ""; + display: inline-block; + position: relative; + width: 36px; + height: 14px; + background-color: rgba(0, 0, 0, 0.38); + border-radius: 15px; + margin-right: 10px; + -webkit-transition: background 0.3s ease; + transition: background 0.3s ease; + vertical-align: middle; + margin: 0 16px; +} + +.switch label .lever:before, .switch label .lever:after { + content: ""; + position: absolute; + display: inline-block; + width: 20px; + height: 20px; + border-radius: 50%; + left: 0; + top: -3px; + -webkit-transition: left 0.3s ease, background .3s ease, -webkit-box-shadow 0.1s ease, -webkit-transform .1s ease; + transition: left 0.3s ease, background .3s ease, -webkit-box-shadow 0.1s ease, -webkit-transform .1s ease; + transition: left 0.3s ease, background .3s ease, box-shadow 0.1s ease, transform .1s ease; + transition: left 0.3s ease, background .3s ease, box-shadow 0.1s ease, transform .1s ease, -webkit-box-shadow 0.1s ease, -webkit-transform .1s ease; +} + +.switch label .lever:before { + background-color: rgba(38, 166, 154, 0.15); +} + +.switch label .lever:after { + background-color: #F1F1F1; + -webkit-box-shadow: 0px 3px 1px -2px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 1px 5px 0px rgba(0, 0, 0, 0.12); + box-shadow: 0px 3px 1px -2px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 1px 5px 0px rgba(0, 0, 0, 0.12); +} + +input[type=checkbox]:checked:not(:disabled) ~ .lever:active::before, +input[type=checkbox]:checked:not(:disabled).tabbed:focus ~ .lever::before { + -webkit-transform: scale(2.4); + transform: scale(2.4); + background-color: rgba(38, 166, 154, 0.15); +} + +input[type=checkbox]:not(:disabled) ~ .lever:active:before, +input[type=checkbox]:not(:disabled).tabbed:focus ~ .lever::before { + -webkit-transform: scale(2.4); + transform: scale(2.4); + background-color: rgba(0, 0, 0, 0.08); +} + +.switch input[type=checkbox][disabled] + .lever { + cursor: default; + background-color: rgba(0, 0, 0, 0.12); +} + +.switch label input[type=checkbox][disabled] + .lever:after, +.switch label input[type=checkbox][disabled]:checked + .lever:after { + background-color: #949494; +} + +/* Select Field + ========================================================================== */ +select { + display: none; +} + +select.browser-default { + display: block; +} + +select { + background-color: rgba(255, 255, 255, 0.9); + width: 100%; + padding: 5px; + border: 1px solid #f2f2f2; + border-radius: 2px; + height: 3rem; +} + +.select-label { + position: absolute; +} + +.select-wrapper { + position: relative; +} + +.select-wrapper.valid + label, +.select-wrapper.invalid + label { + width: 100%; + pointer-events: none; +} + +.select-wrapper input.select-dropdown { + position: relative; + cursor: pointer; + background-color: transparent; + border: none; + border-bottom: 1px solid #9e9e9e; + outline: none; + height: 3rem; + line-height: 3rem; + width: 100%; + font-size: 16px; + margin: 0 0 8px 0; + padding: 0; + display: block; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + z-index: 1; +} + +.select-wrapper input.select-dropdown:focus { + border-bottom: 1px solid #26a69a; +} + +.select-wrapper .caret { + position: absolute; + right: 0; + top: 0; + bottom: 0; + margin: auto 0; + z-index: 0; + fill: rgba(0, 0, 0, 0.87); +} + +.select-wrapper + label { + position: absolute; + top: -26px; + font-size: 0.8rem; +} + +select:disabled { + color: rgba(0, 0, 0, 0.42); +} + +.select-wrapper.disabled + label { + color: rgba(0, 0, 0, 0.42); +} + +.select-wrapper.disabled .caret { + fill: rgba(0, 0, 0, 0.42); +} + +.select-wrapper input.select-dropdown:disabled { + color: rgba(0, 0, 0, 0.42); + cursor: default; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.select-wrapper i { + color: rgba(0, 0, 0, 0.3); +} + +.select-dropdown li.disabled, +.select-dropdown li.disabled > span, +.select-dropdown li.optgroup { + color: rgba(0, 0, 0, 0.3); + background-color: transparent; +} + +body.keyboard-focused .select-dropdown.dropdown-content li:focus { + background-color: rgba(0, 0, 0, 0.08); +} + +.select-dropdown.dropdown-content li:hover { + background-color: rgba(0, 0, 0, 0.08); +} + +.select-dropdown.dropdown-content li.selected { + background-color: rgba(0, 0, 0, 0.03); +} + +.prefix ~ .select-wrapper { + margin-left: 3rem; + width: 92%; + width: calc(100% - 3rem); +} + +.prefix ~ label { + margin-left: 3rem; +} + +.select-dropdown li img { + height: 40px; + width: 40px; + margin: 5px 15px; + float: right; +} + +.select-dropdown li.optgroup { + border-top: 1px solid #eee; +} + +.select-dropdown li.optgroup.selected > span { + color: rgba(0, 0, 0, 0.7); +} + +.select-dropdown li.optgroup > span { + color: rgba(0, 0, 0, 0.4); +} + +.select-dropdown li.optgroup ~ li.optgroup-option { + padding-left: 1rem; +} + +/* File Input + ========================================================================== */ +.file-field { + position: relative; +} + +.file-field .file-path-wrapper { + overflow: hidden; + padding-left: 10px; +} + +.file-field input.file-path { + width: 100%; +} + +.file-field .btn, .file-field .btn-large, .file-field .btn-small { + float: left; + height: 3rem; + line-height: 3rem; +} + +.file-field span { + cursor: pointer; +} + +.file-field input[type=file] { + position: absolute; + top: 0; + right: 0; + left: 0; + bottom: 0; + width: 100%; + margin: 0; + padding: 0; + font-size: 20px; + cursor: pointer; + opacity: 0; + filter: alpha(opacity=0); +} + +.file-field input[type=file]::-webkit-file-upload-button { + display: none; +} + +/* Range + ========================================================================== */ +.range-field { + position: relative; +} + +input[type=range], +input[type=range] + .thumb { + cursor: pointer; +} + +input[type=range] { + position: relative; + background-color: transparent; + border: none; + outline: none; + width: 100%; + margin: 15px 0; + padding: 0; +} + +input[type=range]:focus { + outline: none; +} + +input[type=range] + .thumb { + position: absolute; + top: 10px; + left: 0; + border: none; + height: 0; + width: 0; + border-radius: 50%; + background-color: #26a69a; + margin-left: 7px; + -webkit-transform-origin: 50% 50%; + transform-origin: 50% 50%; + -webkit-transform: rotate(-45deg); + transform: rotate(-45deg); +} + +input[type=range] + .thumb .value { + display: block; + width: 30px; + text-align: center; + color: #26a69a; + font-size: 0; + -webkit-transform: rotate(45deg); + transform: rotate(45deg); +} + +input[type=range] + .thumb.active { + border-radius: 50% 50% 50% 0; +} + +input[type=range] + .thumb.active .value { + color: #fff; + margin-left: -1px; + margin-top: 8px; + font-size: 10px; +} + +input[type=range] { + -webkit-appearance: none; +} + +input[type=range]::-webkit-slider-runnable-track { + height: 3px; + background: #c2c0c2; + border: none; +} + +input[type=range]::-webkit-slider-thumb { + border: none; + height: 14px; + width: 14px; + border-radius: 50%; + background: #26a69a; + -webkit-transition: -webkit-box-shadow .3s; + transition: -webkit-box-shadow .3s; + transition: box-shadow .3s; + transition: box-shadow .3s, -webkit-box-shadow .3s; + -webkit-appearance: none; + background-color: #26a69a; + -webkit-transform-origin: 50% 50%; + transform-origin: 50% 50%; + margin: -5px 0 0 0; +} + +.keyboard-focused input[type=range]:focus:not(.active)::-webkit-slider-thumb { + -webkit-box-shadow: 0 0 0 10px rgba(38, 166, 154, 0.26); + box-shadow: 0 0 0 10px rgba(38, 166, 154, 0.26); +} + +input[type=range] { + /* fix for FF unable to apply focus style bug */ + border: 1px solid white; + /*required for proper track sizing in FF*/ +} + +input[type=range]::-moz-range-track { + height: 3px; + background: #c2c0c2; + border: none; +} + +input[type=range]::-moz-focus-inner { + border: 0; +} + +input[type=range]::-moz-range-thumb { + border: none; + height: 14px; + width: 14px; + border-radius: 50%; + background: #26a69a; + -webkit-transition: -webkit-box-shadow .3s; + transition: -webkit-box-shadow .3s; + transition: box-shadow .3s; + transition: box-shadow .3s, -webkit-box-shadow .3s; + margin-top: -5px; +} + +input[type=range]:-moz-focusring { + outline: 1px solid #fff; + outline-offset: -1px; +} + +.keyboard-focused input[type=range]:focus:not(.active)::-moz-range-thumb { + box-shadow: 0 0 0 10px rgba(38, 166, 154, 0.26); +} + +input[type=range]::-ms-track { + height: 3px; + background: transparent; + border-color: transparent; + border-width: 6px 0; + /*remove default tick marks*/ + color: transparent; +} + +input[type=range]::-ms-fill-lower { + background: #777; +} + +input[type=range]::-ms-fill-upper { + background: #ddd; +} + +input[type=range]::-ms-thumb { + border: none; + height: 14px; + width: 14px; + border-radius: 50%; + background: #26a69a; + -webkit-transition: -webkit-box-shadow .3s; + transition: -webkit-box-shadow .3s; + transition: box-shadow .3s; + transition: box-shadow .3s, -webkit-box-shadow .3s; +} + +.keyboard-focused input[type=range]:focus:not(.active)::-ms-thumb { + box-shadow: 0 0 0 10px rgba(38, 166, 154, 0.26); +} + +/*************** + Nav List +***************/ +.table-of-contents.fixed { + position: fixed; +} + +.table-of-contents li { + padding: 2px 0; +} + +.table-of-contents a { + display: inline-block; + font-weight: 300; + color: #757575; + padding-left: 16px; + height: 1.5rem; + line-height: 1.5rem; + letter-spacing: .4; + display: inline-block; +} + +.table-of-contents a:hover { + color: #a8a8a8; + padding-left: 15px; + border-left: 1px solid #ee6e73; +} + +.table-of-contents a.active { + font-weight: 500; + padding-left: 14px; + border-left: 2px solid #ee6e73; +} + +.sidenav { + position: fixed; + width: 300px; + left: 0; + top: 0; + margin: 0; + -webkit-transform: translateX(-100%); + transform: translateX(-100%); + height: 100%; + height: calc(100% + 60px); + height: -moz-calc(100%); + padding-bottom: 60px; + background-color: #fff; + z-index: 999; + overflow-y: auto; + will-change: transform; + -webkit-backface-visibility: hidden; + backface-visibility: hidden; + -webkit-transform: translateX(-105%); + transform: translateX(-105%); +} + +.sidenav.right-aligned { + right: 0; + -webkit-transform: translateX(105%); + transform: translateX(105%); + left: auto; + -webkit-transform: translateX(100%); + transform: translateX(100%); +} + +.sidenav .collapsible { + margin: 0; +} + +.sidenav li { + float: none; + line-height: 48px; +} + +.sidenav li.active { + background-color: rgba(0, 0, 0, 0.05); +} + +.sidenav li > a { + color: rgba(0, 0, 0, 0.87); + display: block; + font-size: 14px; + font-weight: 500; + height: 48px; + line-height: 48px; + padding: 0 32px; +} + +.sidenav li > a:hover { + background-color: rgba(0, 0, 0, 0.05); +} + +.sidenav li > a.btn, .sidenav li > a.btn-large, .sidenav li > a.btn-small, .sidenav li > a.btn-large, .sidenav li > a.btn-flat, .sidenav li > a.btn-floating { + margin: 10px 15px; +} + +.sidenav li > a.btn, .sidenav li > a.btn-large, .sidenav li > a.btn-small, .sidenav li > a.btn-large, .sidenav li > a.btn-floating { + color: #fff; +} + +.sidenav li > a.btn-flat { + color: #343434; +} + +.sidenav li > a.btn:hover, .sidenav li > a.btn-large:hover, .sidenav li > a.btn-small:hover, .sidenav li > a.btn-large:hover { + background-color: #2bbbad; +} + +.sidenav li > a.btn-floating:hover { + background-color: #26a69a; +} + +.sidenav li > a > i, +.sidenav li > a > [class^="mdi-"], .sidenav li > a li > a > [class*="mdi-"], +.sidenav li > a > i.material-icons { + float: left; + height: 48px; + line-height: 48px; + margin: 0 32px 0 0; + width: 24px; + color: rgba(0, 0, 0, 0.54); +} + +.sidenav .divider { + margin: 8px 0 0 0; +} + +.sidenav .subheader { + cursor: initial; + pointer-events: none; + color: rgba(0, 0, 0, 0.54); + font-size: 14px; + font-weight: 500; + line-height: 48px; +} + +.sidenav .subheader:hover { + background-color: transparent; +} + +.sidenav .user-view { + position: relative; + padding: 32px 32px 0; + margin-bottom: 8px; +} + +.sidenav .user-view > a { + height: auto; + padding: 0; +} + +.sidenav .user-view > a:hover { + background-color: transparent; +} + +.sidenav .user-view .background { + overflow: hidden; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: -1; +} + +.sidenav .user-view .circle, .sidenav .user-view .name, .sidenav .user-view .email { + display: block; +} + +.sidenav .user-view .circle { + height: 64px; + width: 64px; +} + +.sidenav .user-view .name, +.sidenav .user-view .email { + font-size: 14px; + line-height: 24px; +} + +.sidenav .user-view .name { + margin-top: 16px; + font-weight: 500; +} + +.sidenav .user-view .email { + padding-bottom: 16px; + font-weight: 400; +} + +.drag-target { + height: 100%; + width: 10px; + position: fixed; + top: 0; + z-index: 998; +} + +.drag-target.right-aligned { + right: 0; +} + +.sidenav.sidenav-fixed { + left: 0; + -webkit-transform: translateX(0); + transform: translateX(0); + position: fixed; +} + +.sidenav.sidenav-fixed.right-aligned { + right: 0; + left: auto; +} + +@media only screen and (max-width: 992px) { + .sidenav.sidenav-fixed { + -webkit-transform: translateX(-105%); + transform: translateX(-105%); + } + .sidenav.sidenav-fixed.right-aligned { + -webkit-transform: translateX(105%); + transform: translateX(105%); + } + .sidenav > a { + padding: 0 16px; + } + .sidenav .user-view { + padding: 16px 16px 0; + } +} + +.sidenav .collapsible-body > ul:not(.collapsible) > li.active, +.sidenav.sidenav-fixed .collapsible-body > ul:not(.collapsible) > li.active { + background-color: #ee6e73; +} + +.sidenav .collapsible-body > ul:not(.collapsible) > li.active a, +.sidenav.sidenav-fixed .collapsible-body > ul:not(.collapsible) > li.active a { + color: #fff; +} + +.sidenav .collapsible-body { + padding: 0; +} + +.sidenav-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + opacity: 0; + height: 120vh; + background-color: rgba(0, 0, 0, 0.5); + z-index: 997; + display: none; +} + +/* + @license + Copyright (c) 2014 The Polymer Project Authors. All rights reserved. + This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt + The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt + The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt + Code distributed by Google as part of the polymer project is also + subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt + */ +/**************************/ +/* STYLES FOR THE SPINNER */ +/**************************/ +/* + * Constants: + * STROKEWIDTH = 3px + * ARCSIZE = 270 degrees (amount of circle the arc takes up) + * ARCTIME = 1333ms (time it takes to expand and contract arc) + * ARCSTARTROT = 216 degrees (how much the start location of the arc + * should rotate each time, 216 gives us a + * 5 pointed star shape (it's 360/5 * 3). + * For a 7 pointed star, we might do + * 360/7 * 3 = 154.286) + * CONTAINERWIDTH = 28px + * SHRINK_TIME = 400ms + */ +.preloader-wrapper { + display: inline-block; + position: relative; + width: 50px; + height: 50px; +} + +.preloader-wrapper.small { + width: 36px; + height: 36px; +} + +.preloader-wrapper.big { + width: 64px; + height: 64px; +} + +.preloader-wrapper.active { + /* duration: 360 * ARCTIME / (ARCSTARTROT + (360-ARCSIZE)) */ + -webkit-animation: container-rotate 1568ms linear infinite; + animation: container-rotate 1568ms linear infinite; +} + +@-webkit-keyframes container-rotate { + to { + -webkit-transform: rotate(360deg); + } +} + +@keyframes container-rotate { + to { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} + +.spinner-layer { + position: absolute; + width: 100%; + height: 100%; + opacity: 0; + border-color: #26a69a; +} + +.spinner-blue, +.spinner-blue-only { + border-color: #4285f4; +} + +.spinner-red, +.spinner-red-only { + border-color: #db4437; +} + +.spinner-yellow, +.spinner-yellow-only { + border-color: #f4b400; +} + +.spinner-green, +.spinner-green-only { + border-color: #0f9d58; +} + +/** + * IMPORTANT NOTE ABOUT CSS ANIMATION PROPERTIES (keanulee): + * + * iOS Safari (tested on iOS 8.1) does not handle animation-delay very well - it doesn't + * guarantee that the animation will start _exactly_ after that value. So we avoid using + * animation-delay and instead set custom keyframes for each color (as redundant as it + * seems). + * + * We write out each animation in full (instead of separating animation-name, + * animation-duration, etc.) because under the polyfill, Safari does not recognize those + * specific properties properly, treats them as -webkit-animation, and overrides the + * other animation rules. See https://github.com/Polymer/platform/issues/53. + */ +.active .spinner-layer.spinner-blue { + /* durations: 4 * ARCTIME */ + -webkit-animation: fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both, blue-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both; + animation: fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both, blue-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both; +} + +.active .spinner-layer.spinner-red { + /* durations: 4 * ARCTIME */ + -webkit-animation: fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both, red-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both; + animation: fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both, red-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both; +} + +.active .spinner-layer.spinner-yellow { + /* durations: 4 * ARCTIME */ + -webkit-animation: fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both, yellow-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both; + animation: fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both, yellow-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both; +} + +.active .spinner-layer.spinner-green { + /* durations: 4 * ARCTIME */ + -webkit-animation: fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both, green-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both; + animation: fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both, green-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both; +} + +.active .spinner-layer, +.active .spinner-layer.spinner-blue-only, +.active .spinner-layer.spinner-red-only, +.active .spinner-layer.spinner-yellow-only, +.active .spinner-layer.spinner-green-only { + /* durations: 4 * ARCTIME */ + opacity: 1; + -webkit-animation: fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both; + animation: fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both; +} + +@-webkit-keyframes fill-unfill-rotate { + 12.5% { + -webkit-transform: rotate(135deg); + } + /* 0.5 * ARCSIZE */ + 25% { + -webkit-transform: rotate(270deg); + } + /* 1 * ARCSIZE */ + 37.5% { + -webkit-transform: rotate(405deg); + } + /* 1.5 * ARCSIZE */ + 50% { + -webkit-transform: rotate(540deg); + } + /* 2 * ARCSIZE */ + 62.5% { + -webkit-transform: rotate(675deg); + } + /* 2.5 * ARCSIZE */ + 75% { + -webkit-transform: rotate(810deg); + } + /* 3 * ARCSIZE */ + 87.5% { + -webkit-transform: rotate(945deg); + } + /* 3.5 * ARCSIZE */ + to { + -webkit-transform: rotate(1080deg); + } + /* 4 * ARCSIZE */ +} + +@keyframes fill-unfill-rotate { + 12.5% { + -webkit-transform: rotate(135deg); + transform: rotate(135deg); + } + /* 0.5 * ARCSIZE */ + 25% { + -webkit-transform: rotate(270deg); + transform: rotate(270deg); + } + /* 1 * ARCSIZE */ + 37.5% { + -webkit-transform: rotate(405deg); + transform: rotate(405deg); + } + /* 1.5 * ARCSIZE */ + 50% { + -webkit-transform: rotate(540deg); + transform: rotate(540deg); + } + /* 2 * ARCSIZE */ + 62.5% { + -webkit-transform: rotate(675deg); + transform: rotate(675deg); + } + /* 2.5 * ARCSIZE */ + 75% { + -webkit-transform: rotate(810deg); + transform: rotate(810deg); + } + /* 3 * ARCSIZE */ + 87.5% { + -webkit-transform: rotate(945deg); + transform: rotate(945deg); + } + /* 3.5 * ARCSIZE */ + to { + -webkit-transform: rotate(1080deg); + transform: rotate(1080deg); + } + /* 4 * ARCSIZE */ +} + +@-webkit-keyframes blue-fade-in-out { + from { + opacity: 1; + } + 25% { + opacity: 1; + } + 26% { + opacity: 0; + } + 89% { + opacity: 0; + } + 90% { + opacity: 1; + } + 100% { + opacity: 1; + } +} + +@keyframes blue-fade-in-out { + from { + opacity: 1; + } + 25% { + opacity: 1; + } + 26% { + opacity: 0; + } + 89% { + opacity: 0; + } + 90% { + opacity: 1; + } + 100% { + opacity: 1; + } +} + +@-webkit-keyframes red-fade-in-out { + from { + opacity: 0; + } + 15% { + opacity: 0; + } + 25% { + opacity: 1; + } + 50% { + opacity: 1; + } + 51% { + opacity: 0; + } +} + +@keyframes red-fade-in-out { + from { + opacity: 0; + } + 15% { + opacity: 0; + } + 25% { + opacity: 1; + } + 50% { + opacity: 1; + } + 51% { + opacity: 0; + } +} + +@-webkit-keyframes yellow-fade-in-out { + from { + opacity: 0; + } + 40% { + opacity: 0; + } + 50% { + opacity: 1; + } + 75% { + opacity: 1; + } + 76% { + opacity: 0; + } +} + +@keyframes yellow-fade-in-out { + from { + opacity: 0; + } + 40% { + opacity: 0; + } + 50% { + opacity: 1; + } + 75% { + opacity: 1; + } + 76% { + opacity: 0; + } +} + +@-webkit-keyframes green-fade-in-out { + from { + opacity: 0; + } + 65% { + opacity: 0; + } + 75% { + opacity: 1; + } + 90% { + opacity: 1; + } + 100% { + opacity: 0; + } +} + +@keyframes green-fade-in-out { + from { + opacity: 0; + } + 65% { + opacity: 0; + } + 75% { + opacity: 1; + } + 90% { + opacity: 1; + } + 100% { + opacity: 0; + } +} + +/** + * Patch the gap that appear between the two adjacent div.circle-clipper while the + * spinner is rotating (appears on Chrome 38, Safari 7.1, and IE 11). + */ +.gap-patch { + position: absolute; + top: 0; + left: 45%; + width: 10%; + height: 100%; + overflow: hidden; + border-color: inherit; +} + +.gap-patch .circle { + width: 1000%; + left: -450%; +} + +.circle-clipper { + display: inline-block; + position: relative; + width: 50%; + height: 100%; + overflow: hidden; + border-color: inherit; +} + +.circle-clipper .circle { + width: 200%; + height: 100%; + border-width: 3px; + /* STROKEWIDTH */ + border-style: solid; + border-color: inherit; + border-bottom-color: transparent !important; + border-radius: 50%; + -webkit-animation: none; + animation: none; + position: absolute; + top: 0; + right: 0; + bottom: 0; +} + +.circle-clipper.left .circle { + left: 0; + border-right-color: transparent !important; + -webkit-transform: rotate(129deg); + transform: rotate(129deg); +} + +.circle-clipper.right .circle { + left: -100%; + border-left-color: transparent !important; + -webkit-transform: rotate(-129deg); + transform: rotate(-129deg); +} + +.active .circle-clipper.left .circle { + /* duration: ARCTIME */ + -webkit-animation: left-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both; + animation: left-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both; +} + +.active .circle-clipper.right .circle { + /* duration: ARCTIME */ + -webkit-animation: right-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both; + animation: right-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both; +} + +@-webkit-keyframes left-spin { + from { + -webkit-transform: rotate(130deg); + } + 50% { + -webkit-transform: rotate(-5deg); + } + to { + -webkit-transform: rotate(130deg); + } +} + +@keyframes left-spin { + from { + -webkit-transform: rotate(130deg); + transform: rotate(130deg); + } + 50% { + -webkit-transform: rotate(-5deg); + transform: rotate(-5deg); + } + to { + -webkit-transform: rotate(130deg); + transform: rotate(130deg); + } +} + +@-webkit-keyframes right-spin { + from { + -webkit-transform: rotate(-130deg); + } + 50% { + -webkit-transform: rotate(5deg); + } + to { + -webkit-transform: rotate(-130deg); + } +} + +@keyframes right-spin { + from { + -webkit-transform: rotate(-130deg); + transform: rotate(-130deg); + } + 50% { + -webkit-transform: rotate(5deg); + transform: rotate(5deg); + } + to { + -webkit-transform: rotate(-130deg); + transform: rotate(-130deg); + } +} + +#spinnerContainer.cooldown { + /* duration: SHRINK_TIME */ + -webkit-animation: container-rotate 1568ms linear infinite, fade-out 400ms cubic-bezier(0.4, 0, 0.2, 1); + animation: container-rotate 1568ms linear infinite, fade-out 400ms cubic-bezier(0.4, 0, 0.2, 1); +} + +@-webkit-keyframes fade-out { + from { + opacity: 1; + } + to { + opacity: 0; + } +} + +@keyframes fade-out { + from { + opacity: 1; + } + to { + opacity: 0; + } +} + +.slider { + position: relative; + height: 400px; + width: 100%; +} + +.slider.fullscreen { + height: 100%; + width: 100%; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; +} + +.slider.fullscreen ul.slides { + height: 100%; +} + +.slider.fullscreen ul.indicators { + z-index: 2; + bottom: 30px; +} + +.slider .slides { + background-color: #9e9e9e; + margin: 0; + height: 400px; +} + +.slider .slides li { + opacity: 0; + position: absolute; + top: 0; + left: 0; + z-index: 1; + width: 100%; + height: inherit; + overflow: hidden; +} + +.slider .slides li img { + height: 100%; + width: 100%; + background-size: cover; + background-position: center; +} + +.slider .slides li .caption { + color: #fff; + position: absolute; + top: 15%; + left: 15%; + width: 70%; + opacity: 0; +} + +.slider .slides li .caption p { + color: #e0e0e0; +} + +.slider .slides li.active { + z-index: 2; +} + +.slider .indicators { + position: absolute; + text-align: center; + left: 0; + right: 0; + bottom: 0; + margin: 0; +} + +.slider .indicators .indicator-item { + display: inline-block; + position: relative; + cursor: pointer; + height: 16px; + width: 16px; + margin: 0 12px; + background-color: #e0e0e0; + -webkit-transition: background-color .3s; + transition: background-color .3s; + border-radius: 50%; +} + +.slider .indicators .indicator-item.active { + background-color: #4CAF50; +} + +.carousel { + overflow: hidden; + position: relative; + width: 100%; + height: 400px; + -webkit-perspective: 500px; + perspective: 500px; + -webkit-transform-style: preserve-3d; + transform-style: preserve-3d; + -webkit-transform-origin: 0% 50%; + transform-origin: 0% 50%; +} + +.carousel.carousel-slider { + top: 0; + left: 0; +} + +.carousel.carousel-slider .carousel-fixed-item { + position: absolute; + left: 0; + right: 0; + bottom: 20px; + z-index: 1; +} + +.carousel.carousel-slider .carousel-fixed-item.with-indicators { + bottom: 68px; +} + +.carousel.carousel-slider .carousel-item { + width: 100%; + height: 100%; + min-height: 400px; + position: absolute; + top: 0; + left: 0; +} + +.carousel.carousel-slider .carousel-item h2 { + font-size: 24px; + font-weight: 500; + line-height: 32px; +} + +.carousel.carousel-slider .carousel-item p { + font-size: 15px; +} + +.carousel .carousel-item { + visibility: hidden; + width: 200px; + height: 200px; + position: absolute; + top: 0; + left: 0; +} + +.carousel .carousel-item > img { + width: 100%; +} + +.carousel .indicators { + position: absolute; + text-align: center; + left: 0; + right: 0; + bottom: 0; + margin: 0; +} + +.carousel .indicators .indicator-item { + display: inline-block; + position: relative; + cursor: pointer; + height: 8px; + width: 8px; + margin: 24px 4px; + background-color: rgba(255, 255, 255, 0.5); + -webkit-transition: background-color .3s; + transition: background-color .3s; + border-radius: 50%; +} + +.carousel .indicators .indicator-item.active { + background-color: #fff; +} + +.carousel.scrolling .carousel-item .materialboxed, +.carousel .carousel-item:not(.active) .materialboxed { + pointer-events: none; +} + +.tap-target-wrapper { + width: 800px; + height: 800px; + position: fixed; + z-index: 1000; + visibility: hidden; + -webkit-transition: visibility 0s .3s; + transition: visibility 0s .3s; +} + +.tap-target-wrapper.open { + visibility: visible; + -webkit-transition: visibility 0s; + transition: visibility 0s; +} + +.tap-target-wrapper.open .tap-target { + -webkit-transform: scale(1); + transform: scale(1); + opacity: .95; + -webkit-transition: opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1), -webkit-transform 0.3s cubic-bezier(0.42, 0, 0.58, 1); + transition: opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1), -webkit-transform 0.3s cubic-bezier(0.42, 0, 0.58, 1); + transition: transform 0.3s cubic-bezier(0.42, 0, 0.58, 1), opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1); + transition: transform 0.3s cubic-bezier(0.42, 0, 0.58, 1), opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1), -webkit-transform 0.3s cubic-bezier(0.42, 0, 0.58, 1); +} + +.tap-target-wrapper.open .tap-target-wave::before { + -webkit-transform: scale(1); + transform: scale(1); +} + +.tap-target-wrapper.open .tap-target-wave::after { + visibility: visible; + -webkit-animation: pulse-animation 1s cubic-bezier(0.24, 0, 0.38, 1) infinite; + animation: pulse-animation 1s cubic-bezier(0.24, 0, 0.38, 1) infinite; + -webkit-transition: opacity .3s, visibility 0s 1s, -webkit-transform .3s; + transition: opacity .3s, visibility 0s 1s, -webkit-transform .3s; + transition: opacity .3s, transform .3s, visibility 0s 1s; + transition: opacity .3s, transform .3s, visibility 0s 1s, -webkit-transform .3s; +} + +.tap-target { + position: absolute; + font-size: 1rem; + border-radius: 50%; + background-color: #ee6e73; + -webkit-box-shadow: 0 20px 20px 0 rgba(0, 0, 0, 0.14), 0 10px 50px 0 rgba(0, 0, 0, 0.12), 0 30px 10px -20px rgba(0, 0, 0, 0.2); + box-shadow: 0 20px 20px 0 rgba(0, 0, 0, 0.14), 0 10px 50px 0 rgba(0, 0, 0, 0.12), 0 30px 10px -20px rgba(0, 0, 0, 0.2); + width: 100%; + height: 100%; + opacity: 0; + -webkit-transform: scale(0); + transform: scale(0); + -webkit-transition: opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1), -webkit-transform 0.3s cubic-bezier(0.42, 0, 0.58, 1); + transition: opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1), -webkit-transform 0.3s cubic-bezier(0.42, 0, 0.58, 1); + transition: transform 0.3s cubic-bezier(0.42, 0, 0.58, 1), opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1); + transition: transform 0.3s cubic-bezier(0.42, 0, 0.58, 1), opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1), -webkit-transform 0.3s cubic-bezier(0.42, 0, 0.58, 1); +} + +.tap-target-content { + position: relative; + display: table-cell; +} + +.tap-target-wave { + position: absolute; + border-radius: 50%; + z-index: 10001; +} + +.tap-target-wave::before, .tap-target-wave::after { + content: ''; + display: block; + position: absolute; + width: 100%; + height: 100%; + border-radius: 50%; + background-color: #ffffff; +} + +.tap-target-wave::before { + -webkit-transform: scale(0); + transform: scale(0); + -webkit-transition: -webkit-transform .3s; + transition: -webkit-transform .3s; + transition: transform .3s; + transition: transform .3s, -webkit-transform .3s; +} + +.tap-target-wave::after { + visibility: hidden; + -webkit-transition: opacity .3s, visibility 0s, -webkit-transform .3s; + transition: opacity .3s, visibility 0s, -webkit-transform .3s; + transition: opacity .3s, transform .3s, visibility 0s; + transition: opacity .3s, transform .3s, visibility 0s, -webkit-transform .3s; + z-index: -1; +} + +.tap-target-origin { + top: 50%; + left: 50%; + -webkit-transform: translate(-50%, -50%); + transform: translate(-50%, -50%); + z-index: 10002; + position: absolute !important; +} + +.tap-target-origin:not(.btn):not(.btn-large):not(.btn-small), .tap-target-origin:not(.btn):not(.btn-large):not(.btn-small):hover { + background: none; +} + +@media only screen and (max-width: 600px) { + .tap-target, .tap-target-wrapper { + width: 600px; + height: 600px; + } +} + +.pulse { + overflow: visible; + position: relative; +} + +.pulse::before { + content: ''; + display: block; + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + background-color: inherit; + border-radius: inherit; + -webkit-transition: opacity .3s, -webkit-transform .3s; + transition: opacity .3s, -webkit-transform .3s; + transition: opacity .3s, transform .3s; + transition: opacity .3s, transform .3s, -webkit-transform .3s; + -webkit-animation: pulse-animation 1s cubic-bezier(0.24, 0, 0.38, 1) infinite; + animation: pulse-animation 1s cubic-bezier(0.24, 0, 0.38, 1) infinite; + z-index: -1; +} + +@-webkit-keyframes pulse-animation { + 0% { + opacity: 1; + -webkit-transform: scale(1); + transform: scale(1); + } + 50% { + opacity: 0; + -webkit-transform: scale(1.5); + transform: scale(1.5); + } + 100% { + opacity: 0; + -webkit-transform: scale(1.5); + transform: scale(1.5); + } +} + +@keyframes pulse-animation { + 0% { + opacity: 1; + -webkit-transform: scale(1); + transform: scale(1); + } + 50% { + opacity: 0; + -webkit-transform: scale(1.5); + transform: scale(1.5); + } + 100% { + opacity: 0; + -webkit-transform: scale(1.5); + transform: scale(1.5); + } +} + +/* Modal */ +.datepicker-modal { + max-width: 325px; + min-width: 300px; + max-height: none; +} + +.datepicker-container.modal-content { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + padding: 0; +} + +.datepicker-controls { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + width: 280px; + margin: 0 auto; +} + +.datepicker-controls .selects-container { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; +} + +.datepicker-controls .select-wrapper input { + border-bottom: none; + text-align: center; + margin: 0; +} + +.datepicker-controls .select-wrapper input:focus { + border-bottom: none; +} + +.datepicker-controls .select-wrapper .caret { + display: none; +} + +.datepicker-controls .select-year input { + width: 50px; +} + +.datepicker-controls .select-month input { + width: 70px; +} + +.month-prev, .month-next { + margin-top: 4px; + cursor: pointer; + background-color: transparent; + border: none; +} + +/* Date Display */ +.datepicker-date-display { + -webkit-box-flex: 1; + -webkit-flex: 1 auto; + -ms-flex: 1 auto; + flex: 1 auto; + background-color: #26a69a; + color: #fff; + padding: 20px 22px; + font-weight: 500; +} + +.datepicker-date-display .year-text { + display: block; + font-size: 1.5rem; + line-height: 25px; + color: rgba(255, 255, 255, 0.7); +} + +.datepicker-date-display .date-text { + display: block; + font-size: 2.8rem; + line-height: 47px; + font-weight: 500; +} + +/* Calendar */ +.datepicker-calendar-container { + -webkit-box-flex: 2.5; + -webkit-flex: 2.5 auto; + -ms-flex: 2.5 auto; + flex: 2.5 auto; +} + +.datepicker-table { + width: 280px; + font-size: 1rem; + margin: 0 auto; +} + +.datepicker-table thead { + border-bottom: none; +} + +.datepicker-table th { + padding: 10px 5px; + text-align: center; +} + +.datepicker-table tr { + border: none; +} + +.datepicker-table abbr { + text-decoration: none; + color: #999; +} + +.datepicker-table td { + border-radius: 50%; + padding: 0; +} + +.datepicker-table td.is-today { + color: #26a69a; +} + +.datepicker-table td.is-selected { + background-color: #26a69a; + color: #fff; +} + +.datepicker-table td.is-outside-current-month, .datepicker-table td.is-disabled { + color: rgba(0, 0, 0, 0.3); + pointer-events: none; +} + +.datepicker-day-button { + background-color: transparent; + border: none; + line-height: 38px; + display: block; + width: 100%; + border-radius: 50%; + padding: 0 5px; + cursor: pointer; + color: inherit; +} + +.datepicker-day-button:focus { + background-color: rgba(43, 161, 150, 0.25); +} + +/* Footer */ +.datepicker-footer { + width: 280px; + margin: 0 auto; + padding-bottom: 5px; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; +} + +.datepicker-cancel, +.datepicker-clear, +.datepicker-today, +.datepicker-done { + color: #26a69a; + padding: 0 1rem; +} + +.datepicker-clear { + color: #F44336; +} + +/* Media Queries */ +@media only screen and (min-width: 601px) { + .datepicker-modal { + max-width: 625px; + } + .datepicker-container.modal-content { + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + } + .datepicker-date-display { + -webkit-box-flex: 0; + -webkit-flex: 0 1 270px; + -ms-flex: 0 1 270px; + flex: 0 1 270px; + } + .datepicker-controls, + .datepicker-table, + .datepicker-footer { + width: 320px; + } + .datepicker-day-button { + line-height: 44px; + } +} + +/* Timepicker Containers */ +.timepicker-modal { + max-width: 325px; + max-height: none; +} + +.timepicker-container.modal-content { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + padding: 0; +} + +.text-primary { + color: white; +} + +/* Clock Digital Display */ +.timepicker-digital-display { + -webkit-box-flex: 1; + -webkit-flex: 1 auto; + -ms-flex: 1 auto; + flex: 1 auto; + background-color: #26a69a; + padding: 10px; + font-weight: 300; +} + +.timepicker-text-container { + font-size: 4rem; + font-weight: bold; + text-align: center; + color: rgba(255, 255, 255, 0.6); + font-weight: 400; + position: relative; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.timepicker-span-hours, +.timepicker-span-minutes, +.timepicker-span-am-pm div { + cursor: pointer; +} + +.timepicker-span-hours { + margin-right: 3px; +} + +.timepicker-span-minutes { + margin-left: 3px; +} + +.timepicker-display-am-pm { + font-size: 1.3rem; + position: absolute; + right: 1rem; + bottom: 1rem; + font-weight: 400; +} + +/* Analog Clock Display */ +.timepicker-analog-display { + -webkit-box-flex: 2.5; + -webkit-flex: 2.5 auto; + -ms-flex: 2.5 auto; + flex: 2.5 auto; +} + +.timepicker-plate { + background-color: #eee; + border-radius: 50%; + width: 270px; + height: 270px; + overflow: visible; + position: relative; + margin: auto; + margin-top: 25px; + margin-bottom: 5px; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.timepicker-canvas, +.timepicker-dial { + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; +} + +.timepicker-minutes { + visibility: hidden; +} + +.timepicker-tick { + border-radius: 50%; + color: rgba(0, 0, 0, 0.87); + line-height: 40px; + text-align: center; + width: 40px; + height: 40px; + position: absolute; + cursor: pointer; + font-size: 15px; +} + +.timepicker-tick.active, +.timepicker-tick:hover { + background-color: rgba(38, 166, 154, 0.25); +} + +.timepicker-dial { + -webkit-transition: opacity 350ms, -webkit-transform 350ms; + transition: opacity 350ms, -webkit-transform 350ms; + transition: transform 350ms, opacity 350ms; + transition: transform 350ms, opacity 350ms, -webkit-transform 350ms; +} + +.timepicker-dial-out { + opacity: 0; +} + +.timepicker-dial-out.timepicker-hours { + -webkit-transform: scale(1.1, 1.1); + transform: scale(1.1, 1.1); +} + +.timepicker-dial-out.timepicker-minutes { + -webkit-transform: scale(0.8, 0.8); + transform: scale(0.8, 0.8); +} + +.timepicker-canvas { + -webkit-transition: opacity 175ms; + transition: opacity 175ms; +} + +.timepicker-canvas line { + stroke: #26a69a; + stroke-width: 4; + stroke-linecap: round; +} + +.timepicker-canvas-out { + opacity: 0.25; +} + +.timepicker-canvas-bearing { + stroke: none; + fill: #26a69a; +} + +.timepicker-canvas-bg { + stroke: none; + fill: #26a69a; +} + +/* Footer */ +.timepicker-footer { + margin: 0 auto; + padding: 5px 1rem; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; +} + +.timepicker-clear { + color: #F44336; +} + +.timepicker-close { + color: #26a69a; +} + +.timepicker-clear, +.timepicker-close { + padding: 0 20px; +} + +/* Media Queries */ +@media only screen and (min-width: 601px) { + .timepicker-modal { + max-width: 600px; + } + .timepicker-container.modal-content { + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + } + .timepicker-text-container { + top: 32%; + } + .timepicker-display-am-pm { + position: relative; + right: auto; + bottom: auto; + text-align: center; + margin-top: 1.2rem; + } +} diff --git a/hassio-google-drive-backup/backup/static/css/materialize.min.css b/hassio-google-drive-backup/backup/static/css/materialize.min.css new file mode 100644 index 0000000..b3dc954 --- /dev/null +++ b/hassio-google-drive-backup/backup/static/css/materialize.min.css @@ -0,0 +1,13 @@ +/*! + * Materialize v1.0.0-rc.1 (http://materializecss.com) + * Copyright 2014-2017 Materialize + * MIT License (https://raw.githubusercontent.com/Dogfalo/materialize/master/LICENSE) + */ +.materialize-red{background-color:#e51c23 !important}.materialize-red-text{color:#e51c23 !important}.materialize-red.lighten-5{background-color:#fdeaeb !important}.materialize-red-text.text-lighten-5{color:#fdeaeb !important}.materialize-red.lighten-4{background-color:#f8c1c3 !important}.materialize-red-text.text-lighten-4{color:#f8c1c3 !important}.materialize-red.lighten-3{background-color:#f3989b !important}.materialize-red-text.text-lighten-3{color:#f3989b !important}.materialize-red.lighten-2{background-color:#ee6e73 !important}.materialize-red-text.text-lighten-2{color:#ee6e73 !important}.materialize-red.lighten-1{background-color:#ea454b !important}.materialize-red-text.text-lighten-1{color:#ea454b !important}.materialize-red.darken-1{background-color:#d0181e !important}.materialize-red-text.text-darken-1{color:#d0181e !important}.materialize-red.darken-2{background-color:#b9151b !important}.materialize-red-text.text-darken-2{color:#b9151b !important}.materialize-red.darken-3{background-color:#a21318 !important}.materialize-red-text.text-darken-3{color:#a21318 !important}.materialize-red.darken-4{background-color:#8b1014 !important}.materialize-red-text.text-darken-4{color:#8b1014 !important}.red{background-color:#F44336 !important}.red-text{color:#F44336 !important}.red.lighten-5{background-color:#FFEBEE !important}.red-text.text-lighten-5{color:#FFEBEE !important}.red.lighten-4{background-color:#FFCDD2 !important}.red-text.text-lighten-4{color:#FFCDD2 !important}.red.lighten-3{background-color:#EF9A9A !important}.red-text.text-lighten-3{color:#EF9A9A !important}.red.lighten-2{background-color:#E57373 !important}.red-text.text-lighten-2{color:#E57373 !important}.red.lighten-1{background-color:#EF5350 !important}.red-text.text-lighten-1{color:#EF5350 !important}.red.darken-1{background-color:#E53935 !important}.red-text.text-darken-1{color:#E53935 !important}.red.darken-2{background-color:#D32F2F !important}.red-text.text-darken-2{color:#D32F2F !important}.red.darken-3{background-color:#C62828 !important}.red-text.text-darken-3{color:#C62828 !important}.red.darken-4{background-color:#B71C1C !important}.red-text.text-darken-4{color:#B71C1C !important}.red.accent-1{background-color:#FF8A80 !important}.red-text.text-accent-1{color:#FF8A80 !important}.red.accent-2{background-color:#FF5252 !important}.red-text.text-accent-2{color:#FF5252 !important}.red.accent-3{background-color:#FF1744 !important}.red-text.text-accent-3{color:#FF1744 !important}.red.accent-4{background-color:#D50000 !important}.red-text.text-accent-4{color:#D50000 !important}.pink{background-color:#e91e63 !important}.pink-text{color:#e91e63 !important}.pink.lighten-5{background-color:#fce4ec !important}.pink-text.text-lighten-5{color:#fce4ec !important}.pink.lighten-4{background-color:#f8bbd0 !important}.pink-text.text-lighten-4{color:#f8bbd0 !important}.pink.lighten-3{background-color:#f48fb1 !important}.pink-text.text-lighten-3{color:#f48fb1 !important}.pink.lighten-2{background-color:#f06292 !important}.pink-text.text-lighten-2{color:#f06292 !important}.pink.lighten-1{background-color:#ec407a !important}.pink-text.text-lighten-1{color:#ec407a !important}.pink.darken-1{background-color:#d81b60 !important}.pink-text.text-darken-1{color:#d81b60 !important}.pink.darken-2{background-color:#c2185b !important}.pink-text.text-darken-2{color:#c2185b !important}.pink.darken-3{background-color:#ad1457 !important}.pink-text.text-darken-3{color:#ad1457 !important}.pink.darken-4{background-color:#880e4f !important}.pink-text.text-darken-4{color:#880e4f !important}.pink.accent-1{background-color:#ff80ab !important}.pink-text.text-accent-1{color:#ff80ab !important}.pink.accent-2{background-color:#ff4081 !important}.pink-text.text-accent-2{color:#ff4081 !important}.pink.accent-3{background-color:#f50057 !important}.pink-text.text-accent-3{color:#f50057 !important}.pink.accent-4{background-color:#c51162 !important}.pink-text.text-accent-4{color:#c51162 !important}.purple{background-color:#9c27b0 !important}.purple-text{color:#9c27b0 !important}.purple.lighten-5{background-color:#f3e5f5 !important}.purple-text.text-lighten-5{color:#f3e5f5 !important}.purple.lighten-4{background-color:#e1bee7 !important}.purple-text.text-lighten-4{color:#e1bee7 !important}.purple.lighten-3{background-color:#ce93d8 !important}.purple-text.text-lighten-3{color:#ce93d8 !important}.purple.lighten-2{background-color:#ba68c8 !important}.purple-text.text-lighten-2{color:#ba68c8 !important}.purple.lighten-1{background-color:#ab47bc !important}.purple-text.text-lighten-1{color:#ab47bc !important}.purple.darken-1{background-color:#8e24aa !important}.purple-text.text-darken-1{color:#8e24aa !important}.purple.darken-2{background-color:#7b1fa2 !important}.purple-text.text-darken-2{color:#7b1fa2 !important}.purple.darken-3{background-color:#6a1b9a !important}.purple-text.text-darken-3{color:#6a1b9a !important}.purple.darken-4{background-color:#4a148c !important}.purple-text.text-darken-4{color:#4a148c !important}.purple.accent-1{background-color:#ea80fc !important}.purple-text.text-accent-1{color:#ea80fc !important}.purple.accent-2{background-color:#e040fb !important}.purple-text.text-accent-2{color:#e040fb !important}.purple.accent-3{background-color:#d500f9 !important}.purple-text.text-accent-3{color:#d500f9 !important}.purple.accent-4{background-color:#a0f !important}.purple-text.text-accent-4{color:#a0f !important}.deep-purple{background-color:#673ab7 !important}.deep-purple-text{color:#673ab7 !important}.deep-purple.lighten-5{background-color:#ede7f6 !important}.deep-purple-text.text-lighten-5{color:#ede7f6 !important}.deep-purple.lighten-4{background-color:#d1c4e9 !important}.deep-purple-text.text-lighten-4{color:#d1c4e9 !important}.deep-purple.lighten-3{background-color:#b39ddb !important}.deep-purple-text.text-lighten-3{color:#b39ddb !important}.deep-purple.lighten-2{background-color:#9575cd !important}.deep-purple-text.text-lighten-2{color:#9575cd !important}.deep-purple.lighten-1{background-color:#7e57c2 !important}.deep-purple-text.text-lighten-1{color:#7e57c2 !important}.deep-purple.darken-1{background-color:#5e35b1 !important}.deep-purple-text.text-darken-1{color:#5e35b1 !important}.deep-purple.darken-2{background-color:#512da8 !important}.deep-purple-text.text-darken-2{color:#512da8 !important}.deep-purple.darken-3{background-color:#4527a0 !important}.deep-purple-text.text-darken-3{color:#4527a0 !important}.deep-purple.darken-4{background-color:#311b92 !important}.deep-purple-text.text-darken-4{color:#311b92 !important}.deep-purple.accent-1{background-color:#b388ff !important}.deep-purple-text.text-accent-1{color:#b388ff !important}.deep-purple.accent-2{background-color:#7c4dff !important}.deep-purple-text.text-accent-2{color:#7c4dff !important}.deep-purple.accent-3{background-color:#651fff !important}.deep-purple-text.text-accent-3{color:#651fff !important}.deep-purple.accent-4{background-color:#6200ea !important}.deep-purple-text.text-accent-4{color:#6200ea !important}.indigo{background-color:#3f51b5 !important}.indigo-text{color:#3f51b5 !important}.indigo.lighten-5{background-color:#e8eaf6 !important}.indigo-text.text-lighten-5{color:#e8eaf6 !important}.indigo.lighten-4{background-color:#c5cae9 !important}.indigo-text.text-lighten-4{color:#c5cae9 !important}.indigo.lighten-3{background-color:#9fa8da !important}.indigo-text.text-lighten-3{color:#9fa8da !important}.indigo.lighten-2{background-color:#7986cb !important}.indigo-text.text-lighten-2{color:#7986cb !important}.indigo.lighten-1{background-color:#5c6bc0 !important}.indigo-text.text-lighten-1{color:#5c6bc0 !important}.indigo.darken-1{background-color:#3949ab !important}.indigo-text.text-darken-1{color:#3949ab !important}.indigo.darken-2{background-color:#303f9f !important}.indigo-text.text-darken-2{color:#303f9f !important}.indigo.darken-3{background-color:#283593 !important}.indigo-text.text-darken-3{color:#283593 !important}.indigo.darken-4{background-color:#1a237e !important}.indigo-text.text-darken-4{color:#1a237e !important}.indigo.accent-1{background-color:#8c9eff !important}.indigo-text.text-accent-1{color:#8c9eff !important}.indigo.accent-2{background-color:#536dfe !important}.indigo-text.text-accent-2{color:#536dfe !important}.indigo.accent-3{background-color:#3d5afe !important}.indigo-text.text-accent-3{color:#3d5afe !important}.indigo.accent-4{background-color:#304ffe !important}.indigo-text.text-accent-4{color:#304ffe !important}.blue{background-color:#2196F3 !important}.blue-text{color:#2196F3 !important}.blue.lighten-5{background-color:#E3F2FD !important}.blue-text.text-lighten-5{color:#E3F2FD !important}.blue.lighten-4{background-color:#BBDEFB !important}.blue-text.text-lighten-4{color:#BBDEFB !important}.blue.lighten-3{background-color:#90CAF9 !important}.blue-text.text-lighten-3{color:#90CAF9 !important}.blue.lighten-2{background-color:#64B5F6 !important}.blue-text.text-lighten-2{color:#64B5F6 !important}.blue.lighten-1{background-color:#42A5F5 !important}.blue-text.text-lighten-1{color:#42A5F5 !important}.blue.darken-1{background-color:#1E88E5 !important}.blue-text.text-darken-1{color:#1E88E5 !important}.blue.darken-2{background-color:#1976D2 !important}.blue-text.text-darken-2{color:#1976D2 !important}.blue.darken-3{background-color:#1565C0 !important}.blue-text.text-darken-3{color:#1565C0 !important}.blue.darken-4{background-color:#0D47A1 !important}.blue-text.text-darken-4{color:#0D47A1 !important}.blue.accent-1{background-color:#82B1FF !important}.blue-text.text-accent-1{color:#82B1FF !important}.blue.accent-2{background-color:#448AFF !important}.blue-text.text-accent-2{color:#448AFF !important}.blue.accent-3{background-color:#2979FF !important}.blue-text.text-accent-3{color:#2979FF !important}.blue.accent-4{background-color:#2962FF !important}.blue-text.text-accent-4{color:#2962FF !important}.light-blue{background-color:#03a9f4 !important}.light-blue-text{color:#03a9f4 !important}.light-blue.lighten-5{background-color:#e1f5fe !important}.light-blue-text.text-lighten-5{color:#e1f5fe !important}.light-blue.lighten-4{background-color:#b3e5fc !important}.light-blue-text.text-lighten-4{color:#b3e5fc !important}.light-blue.lighten-3{background-color:#81d4fa !important}.light-blue-text.text-lighten-3{color:#81d4fa !important}.light-blue.lighten-2{background-color:#4fc3f7 !important}.light-blue-text.text-lighten-2{color:#4fc3f7 !important}.light-blue.lighten-1{background-color:#29b6f6 !important}.light-blue-text.text-lighten-1{color:#29b6f6 !important}.light-blue.darken-1{background-color:#039be5 !important}.light-blue-text.text-darken-1{color:#039be5 !important}.light-blue.darken-2{background-color:#0288d1 !important}.light-blue-text.text-darken-2{color:#0288d1 !important}.light-blue.darken-3{background-color:#0277bd !important}.light-blue-text.text-darken-3{color:#0277bd !important}.light-blue.darken-4{background-color:#01579b !important}.light-blue-text.text-darken-4{color:#01579b !important}.light-blue.accent-1{background-color:#80d8ff !important}.light-blue-text.text-accent-1{color:#80d8ff !important}.light-blue.accent-2{background-color:#40c4ff !important}.light-blue-text.text-accent-2{color:#40c4ff !important}.light-blue.accent-3{background-color:#00b0ff !important}.light-blue-text.text-accent-3{color:#00b0ff !important}.light-blue.accent-4{background-color:#0091ea !important}.light-blue-text.text-accent-4{color:#0091ea !important}.cyan{background-color:#00bcd4 !important}.cyan-text{color:#00bcd4 !important}.cyan.lighten-5{background-color:#e0f7fa !important}.cyan-text.text-lighten-5{color:#e0f7fa !important}.cyan.lighten-4{background-color:#b2ebf2 !important}.cyan-text.text-lighten-4{color:#b2ebf2 !important}.cyan.lighten-3{background-color:#80deea !important}.cyan-text.text-lighten-3{color:#80deea !important}.cyan.lighten-2{background-color:#4dd0e1 !important}.cyan-text.text-lighten-2{color:#4dd0e1 !important}.cyan.lighten-1{background-color:#26c6da !important}.cyan-text.text-lighten-1{color:#26c6da !important}.cyan.darken-1{background-color:#00acc1 !important}.cyan-text.text-darken-1{color:#00acc1 !important}.cyan.darken-2{background-color:#0097a7 !important}.cyan-text.text-darken-2{color:#0097a7 !important}.cyan.darken-3{background-color:#00838f !important}.cyan-text.text-darken-3{color:#00838f !important}.cyan.darken-4{background-color:#006064 !important}.cyan-text.text-darken-4{color:#006064 !important}.cyan.accent-1{background-color:#84ffff !important}.cyan-text.text-accent-1{color:#84ffff !important}.cyan.accent-2{background-color:#18ffff !important}.cyan-text.text-accent-2{color:#18ffff !important}.cyan.accent-3{background-color:#00e5ff !important}.cyan-text.text-accent-3{color:#00e5ff !important}.cyan.accent-4{background-color:#00b8d4 !important}.cyan-text.text-accent-4{color:#00b8d4 !important}.teal{background-color:#009688 !important}.teal-text{color:#009688 !important}.teal.lighten-5{background-color:#e0f2f1 !important}.teal-text.text-lighten-5{color:#e0f2f1 !important}.teal.lighten-4{background-color:#b2dfdb !important}.teal-text.text-lighten-4{color:#b2dfdb !important}.teal.lighten-3{background-color:#80cbc4 !important}.teal-text.text-lighten-3{color:#80cbc4 !important}.teal.lighten-2{background-color:#4db6ac !important}.teal-text.text-lighten-2{color:#4db6ac !important}.teal.lighten-1{background-color:#26a69a !important}.teal-text.text-lighten-1{color:#26a69a !important}.teal.darken-1{background-color:#00897b !important}.teal-text.text-darken-1{color:#00897b !important}.teal.darken-2{background-color:#00796b !important}.teal-text.text-darken-2{color:#00796b !important}.teal.darken-3{background-color:#00695c !important}.teal-text.text-darken-3{color:#00695c !important}.teal.darken-4{background-color:#004d40 !important}.teal-text.text-darken-4{color:#004d40 !important}.teal.accent-1{background-color:#a7ffeb !important}.teal-text.text-accent-1{color:#a7ffeb !important}.teal.accent-2{background-color:#64ffda !important}.teal-text.text-accent-2{color:#64ffda !important}.teal.accent-3{background-color:#1de9b6 !important}.teal-text.text-accent-3{color:#1de9b6 !important}.teal.accent-4{background-color:#00bfa5 !important}.teal-text.text-accent-4{color:#00bfa5 !important}.green{background-color:#4CAF50 !important}.green-text{color:#4CAF50 !important}.green.lighten-5{background-color:#E8F5E9 !important}.green-text.text-lighten-5{color:#E8F5E9 !important}.green.lighten-4{background-color:#C8E6C9 !important}.green-text.text-lighten-4{color:#C8E6C9 !important}.green.lighten-3{background-color:#A5D6A7 !important}.green-text.text-lighten-3{color:#A5D6A7 !important}.green.lighten-2{background-color:#81C784 !important}.green-text.text-lighten-2{color:#81C784 !important}.green.lighten-1{background-color:#66BB6A !important}.green-text.text-lighten-1{color:#66BB6A !important}.green.darken-1{background-color:#43A047 !important}.green-text.text-darken-1{color:#43A047 !important}.green.darken-2{background-color:#388E3C !important}.green-text.text-darken-2{color:#388E3C !important}.green.darken-3{background-color:#2E7D32 !important}.green-text.text-darken-3{color:#2E7D32 !important}.green.darken-4{background-color:#1B5E20 !important}.green-text.text-darken-4{color:#1B5E20 !important}.green.accent-1{background-color:#B9F6CA !important}.green-text.text-accent-1{color:#B9F6CA !important}.green.accent-2{background-color:#69F0AE !important}.green-text.text-accent-2{color:#69F0AE !important}.green.accent-3{background-color:#00E676 !important}.green-text.text-accent-3{color:#00E676 !important}.green.accent-4{background-color:#00C853 !important}.green-text.text-accent-4{color:#00C853 !important}.light-green{background-color:#8bc34a !important}.light-green-text{color:#8bc34a !important}.light-green.lighten-5{background-color:#f1f8e9 !important}.light-green-text.text-lighten-5{color:#f1f8e9 !important}.light-green.lighten-4{background-color:#dcedc8 !important}.light-green-text.text-lighten-4{color:#dcedc8 !important}.light-green.lighten-3{background-color:#c5e1a5 !important}.light-green-text.text-lighten-3{color:#c5e1a5 !important}.light-green.lighten-2{background-color:#aed581 !important}.light-green-text.text-lighten-2{color:#aed581 !important}.light-green.lighten-1{background-color:#9ccc65 !important}.light-green-text.text-lighten-1{color:#9ccc65 !important}.light-green.darken-1{background-color:#7cb342 !important}.light-green-text.text-darken-1{color:#7cb342 !important}.light-green.darken-2{background-color:#689f38 !important}.light-green-text.text-darken-2{color:#689f38 !important}.light-green.darken-3{background-color:#558b2f !important}.light-green-text.text-darken-3{color:#558b2f !important}.light-green.darken-4{background-color:#33691e !important}.light-green-text.text-darken-4{color:#33691e !important}.light-green.accent-1{background-color:#ccff90 !important}.light-green-text.text-accent-1{color:#ccff90 !important}.light-green.accent-2{background-color:#b2ff59 !important}.light-green-text.text-accent-2{color:#b2ff59 !important}.light-green.accent-3{background-color:#76ff03 !important}.light-green-text.text-accent-3{color:#76ff03 !important}.light-green.accent-4{background-color:#64dd17 !important}.light-green-text.text-accent-4{color:#64dd17 !important}.lime{background-color:#cddc39 !important}.lime-text{color:#cddc39 !important}.lime.lighten-5{background-color:#f9fbe7 !important}.lime-text.text-lighten-5{color:#f9fbe7 !important}.lime.lighten-4{background-color:#f0f4c3 !important}.lime-text.text-lighten-4{color:#f0f4c3 !important}.lime.lighten-3{background-color:#e6ee9c !important}.lime-text.text-lighten-3{color:#e6ee9c !important}.lime.lighten-2{background-color:#dce775 !important}.lime-text.text-lighten-2{color:#dce775 !important}.lime.lighten-1{background-color:#d4e157 !important}.lime-text.text-lighten-1{color:#d4e157 !important}.lime.darken-1{background-color:#c0ca33 !important}.lime-text.text-darken-1{color:#c0ca33 !important}.lime.darken-2{background-color:#afb42b !important}.lime-text.text-darken-2{color:#afb42b !important}.lime.darken-3{background-color:#9e9d24 !important}.lime-text.text-darken-3{color:#9e9d24 !important}.lime.darken-4{background-color:#827717 !important}.lime-text.text-darken-4{color:#827717 !important}.lime.accent-1{background-color:#f4ff81 !important}.lime-text.text-accent-1{color:#f4ff81 !important}.lime.accent-2{background-color:#eeff41 !important}.lime-text.text-accent-2{color:#eeff41 !important}.lime.accent-3{background-color:#c6ff00 !important}.lime-text.text-accent-3{color:#c6ff00 !important}.lime.accent-4{background-color:#aeea00 !important}.lime-text.text-accent-4{color:#aeea00 !important}.yellow{background-color:#ffeb3b !important}.yellow-text{color:#ffeb3b !important}.yellow.lighten-5{background-color:#fffde7 !important}.yellow-text.text-lighten-5{color:#fffde7 !important}.yellow.lighten-4{background-color:#fff9c4 !important}.yellow-text.text-lighten-4{color:#fff9c4 !important}.yellow.lighten-3{background-color:#fff59d !important}.yellow-text.text-lighten-3{color:#fff59d !important}.yellow.lighten-2{background-color:#fff176 !important}.yellow-text.text-lighten-2{color:#fff176 !important}.yellow.lighten-1{background-color:#ffee58 !important}.yellow-text.text-lighten-1{color:#ffee58 !important}.yellow.darken-1{background-color:#fdd835 !important}.yellow-text.text-darken-1{color:#fdd835 !important}.yellow.darken-2{background-color:#fbc02d !important}.yellow-text.text-darken-2{color:#fbc02d !important}.yellow.darken-3{background-color:#f9a825 !important}.yellow-text.text-darken-3{color:#f9a825 !important}.yellow.darken-4{background-color:#f57f17 !important}.yellow-text.text-darken-4{color:#f57f17 !important}.yellow.accent-1{background-color:#ffff8d !important}.yellow-text.text-accent-1{color:#ffff8d !important}.yellow.accent-2{background-color:#ff0 !important}.yellow-text.text-accent-2{color:#ff0 !important}.yellow.accent-3{background-color:#ffea00 !important}.yellow-text.text-accent-3{color:#ffea00 !important}.yellow.accent-4{background-color:#ffd600 !important}.yellow-text.text-accent-4{color:#ffd600 !important}.amber{background-color:#ffc107 !important}.amber-text{color:#ffc107 !important}.amber.lighten-5{background-color:#fff8e1 !important}.amber-text.text-lighten-5{color:#fff8e1 !important}.amber.lighten-4{background-color:#ffecb3 !important}.amber-text.text-lighten-4{color:#ffecb3 !important}.amber.lighten-3{background-color:#ffe082 !important}.amber-text.text-lighten-3{color:#ffe082 !important}.amber.lighten-2{background-color:#ffd54f !important}.amber-text.text-lighten-2{color:#ffd54f !important}.amber.lighten-1{background-color:#ffca28 !important}.amber-text.text-lighten-1{color:#ffca28 !important}.amber.darken-1{background-color:#ffb300 !important}.amber-text.text-darken-1{color:#ffb300 !important}.amber.darken-2{background-color:#ffa000 !important}.amber-text.text-darken-2{color:#ffa000 !important}.amber.darken-3{background-color:#ff8f00 !important}.amber-text.text-darken-3{color:#ff8f00 !important}.amber.darken-4{background-color:#ff6f00 !important}.amber-text.text-darken-4{color:#ff6f00 !important}.amber.accent-1{background-color:#ffe57f !important}.amber-text.text-accent-1{color:#ffe57f !important}.amber.accent-2{background-color:#ffd740 !important}.amber-text.text-accent-2{color:#ffd740 !important}.amber.accent-3{background-color:#ffc400 !important}.amber-text.text-accent-3{color:#ffc400 !important}.amber.accent-4{background-color:#ffab00 !important}.amber-text.text-accent-4{color:#ffab00 !important}.orange{background-color:#ff9800 !important}.orange-text{color:#ff9800 !important}.orange.lighten-5{background-color:#fff3e0 !important}.orange-text.text-lighten-5{color:#fff3e0 !important}.orange.lighten-4{background-color:#ffe0b2 !important}.orange-text.text-lighten-4{color:#ffe0b2 !important}.orange.lighten-3{background-color:#ffcc80 !important}.orange-text.text-lighten-3{color:#ffcc80 !important}.orange.lighten-2{background-color:#ffb74d !important}.orange-text.text-lighten-2{color:#ffb74d !important}.orange.lighten-1{background-color:#ffa726 !important}.orange-text.text-lighten-1{color:#ffa726 !important}.orange.darken-1{background-color:#fb8c00 !important}.orange-text.text-darken-1{color:#fb8c00 !important}.orange.darken-2{background-color:#f57c00 !important}.orange-text.text-darken-2{color:#f57c00 !important}.orange.darken-3{background-color:#ef6c00 !important}.orange-text.text-darken-3{color:#ef6c00 !important}.orange.darken-4{background-color:#e65100 !important}.orange-text.text-darken-4{color:#e65100 !important}.orange.accent-1{background-color:#ffd180 !important}.orange-text.text-accent-1{color:#ffd180 !important}.orange.accent-2{background-color:#ffab40 !important}.orange-text.text-accent-2{color:#ffab40 !important}.orange.accent-3{background-color:#ff9100 !important}.orange-text.text-accent-3{color:#ff9100 !important}.orange.accent-4{background-color:#ff6d00 !important}.orange-text.text-accent-4{color:#ff6d00 !important}.deep-orange{background-color:#ff5722 !important}.deep-orange-text{color:#ff5722 !important}.deep-orange.lighten-5{background-color:#fbe9e7 !important}.deep-orange-text.text-lighten-5{color:#fbe9e7 !important}.deep-orange.lighten-4{background-color:#ffccbc !important}.deep-orange-text.text-lighten-4{color:#ffccbc !important}.deep-orange.lighten-3{background-color:#ffab91 !important}.deep-orange-text.text-lighten-3{color:#ffab91 !important}.deep-orange.lighten-2{background-color:#ff8a65 !important}.deep-orange-text.text-lighten-2{color:#ff8a65 !important}.deep-orange.lighten-1{background-color:#ff7043 !important}.deep-orange-text.text-lighten-1{color:#ff7043 !important}.deep-orange.darken-1{background-color:#f4511e !important}.deep-orange-text.text-darken-1{color:#f4511e !important}.deep-orange.darken-2{background-color:#e64a19 !important}.deep-orange-text.text-darken-2{color:#e64a19 !important}.deep-orange.darken-3{background-color:#d84315 !important}.deep-orange-text.text-darken-3{color:#d84315 !important}.deep-orange.darken-4{background-color:#bf360c !important}.deep-orange-text.text-darken-4{color:#bf360c !important}.deep-orange.accent-1{background-color:#ff9e80 !important}.deep-orange-text.text-accent-1{color:#ff9e80 !important}.deep-orange.accent-2{background-color:#ff6e40 !important}.deep-orange-text.text-accent-2{color:#ff6e40 !important}.deep-orange.accent-3{background-color:#ff3d00 !important}.deep-orange-text.text-accent-3{color:#ff3d00 !important}.deep-orange.accent-4{background-color:#dd2c00 !important}.deep-orange-text.text-accent-4{color:#dd2c00 !important}.brown{background-color:#795548 !important}.brown-text{color:#795548 !important}.brown.lighten-5{background-color:#efebe9 !important}.brown-text.text-lighten-5{color:#efebe9 !important}.brown.lighten-4{background-color:#d7ccc8 !important}.brown-text.text-lighten-4{color:#d7ccc8 !important}.brown.lighten-3{background-color:#bcaaa4 !important}.brown-text.text-lighten-3{color:#bcaaa4 !important}.brown.lighten-2{background-color:#a1887f !important}.brown-text.text-lighten-2{color:#a1887f !important}.brown.lighten-1{background-color:#8d6e63 !important}.brown-text.text-lighten-1{color:#8d6e63 !important}.brown.darken-1{background-color:#6d4c41 !important}.brown-text.text-darken-1{color:#6d4c41 !important}.brown.darken-2{background-color:#5d4037 !important}.brown-text.text-darken-2{color:#5d4037 !important}.brown.darken-3{background-color:#4e342e !important}.brown-text.text-darken-3{color:#4e342e !important}.brown.darken-4{background-color:#3e2723 !important}.brown-text.text-darken-4{color:#3e2723 !important}.blue-grey{background-color:#607d8b !important}.blue-grey-text{color:#607d8b !important}.blue-grey.lighten-5{background-color:#eceff1 !important}.blue-grey-text.text-lighten-5{color:#eceff1 !important}.blue-grey.lighten-4{background-color:#cfd8dc !important}.blue-grey-text.text-lighten-4{color:#cfd8dc !important}.blue-grey.lighten-3{background-color:#b0bec5 !important}.blue-grey-text.text-lighten-3{color:#b0bec5 !important}.blue-grey.lighten-2{background-color:#90a4ae !important}.blue-grey-text.text-lighten-2{color:#90a4ae !important}.blue-grey.lighten-1{background-color:#78909c !important}.blue-grey-text.text-lighten-1{color:#78909c !important}.blue-grey.darken-1{background-color:#546e7a !important}.blue-grey-text.text-darken-1{color:#546e7a !important}.blue-grey.darken-2{background-color:#455a64 !important}.blue-grey-text.text-darken-2{color:#455a64 !important}.blue-grey.darken-3{background-color:#37474f !important}.blue-grey-text.text-darken-3{color:#37474f !important}.blue-grey.darken-4{background-color:#263238 !important}.blue-grey-text.text-darken-4{color:#263238 !important}.grey{background-color:#9e9e9e !important}.grey-text{color:#9e9e9e !important}.grey.lighten-5{background-color:#fafafa !important}.grey-text.text-lighten-5{color:#fafafa !important}.grey.lighten-4{background-color:#f5f5f5 !important}.grey-text.text-lighten-4{color:#f5f5f5 !important}.grey.lighten-3{background-color:#eee !important}.grey-text.text-lighten-3{color:#eee !important}.grey.lighten-2{background-color:#e0e0e0 !important}.grey-text.text-lighten-2{color:#e0e0e0 !important}.grey.lighten-1{background-color:#bdbdbd !important}.grey-text.text-lighten-1{color:#bdbdbd !important}.grey.darken-1{background-color:#757575 !important}.grey-text.text-darken-1{color:#757575 !important}.grey.darken-2{background-color:#616161 !important}.grey-text.text-darken-2{color:#616161 !important}.grey.darken-3{background-color:#424242 !important}.grey-text.text-darken-3{color:#424242 !important}.grey.darken-4{background-color:#212121 !important}.grey-text.text-darken-4{color:#212121 !important}.black{background-color:#000 !important}.black-text{color:#000 !important}.white{background-color:#fff !important}.white-text{color:#fff !important}.transparent{background-color:transparent !important}.transparent-text{color:transparent !important}/*! normalize.css v7.0.0 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,footer,header,nav,section{display:block}h1{font-size:2em;margin:0.67em 0}figcaption,figure,main{display:block}figure{margin:1em 40px}hr{-webkit-box-sizing:content-box;box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace, monospace;font-size:1em}a{background-color:transparent;-webkit-text-decoration-skip:objects}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:inherit}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace, monospace;font-size:1em}dfn{font-style:italic}mark{background-color:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-0.25em}sup{top:-0.5em}audio,video{display:inline-block}audio:not([controls]){display:none;height:0}img{border-style:none}svg:not(:root){overflow:hidden}button,input,optgroup,select,textarea{font-family:sans-serif;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}button,html [type="button"],[type="reset"],[type="submit"]{-webkit-appearance:button}button::-moz-focus-inner,[type="button"]::-moz-focus-inner,[type="reset"]::-moz-focus-inner,[type="submit"]::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type="button"]:-moz-focusring,[type="reset"]:-moz-focusring,[type="submit"]:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:0.35em 0.75em 0.625em}legend{-webkit-box-sizing:border-box;box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{display:inline-block;vertical-align:baseline}textarea{overflow:auto}[type="checkbox"],[type="radio"]{-webkit-box-sizing:border-box;box-sizing:border-box;padding:0}[type="number"]::-webkit-inner-spin-button,[type="number"]::-webkit-outer-spin-button{height:auto}[type="search"]{-webkit-appearance:textfield;outline-offset:-2px}[type="search"]::-webkit-search-cancel-button,[type="search"]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details,menu{display:block}summary{display:list-item}canvas{display:inline-block}template{display:none}[hidden]{display:none}html{-webkit-box-sizing:border-box;box-sizing:border-box}*,*:before,*:after{-webkit-box-sizing:inherit;box-sizing:inherit}button,input,optgroup,select,textarea{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif}ul:not(.browser-default){padding-left:0;list-style-type:none}ul:not(.browser-default)>li{list-style-type:none}a{color:#039be5;text-decoration:none;-webkit-tap-highlight-color:transparent}.valign-wrapper{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.clearfix{clear:both}.z-depth-0{-webkit-box-shadow:none !important;box-shadow:none !important}.z-depth-1,nav,.card-panel,.card,.toast,.btn,.btn-large,.btn-small,.btn-floating,.dropdown-content,.collapsible,.sidenav{-webkit-box-shadow:0 2px 2px 0 rgba(0,0,0,0.14),0 3px 1px -2px rgba(0,0,0,0.12),0 1px 5px 0 rgba(0,0,0,0.2);box-shadow:0 2px 2px 0 rgba(0,0,0,0.14),0 3px 1px -2px rgba(0,0,0,0.12),0 1px 5px 0 rgba(0,0,0,0.2)}.z-depth-1-half,.btn:hover,.btn-large:hover,.btn-small:hover,.btn-floating:hover{-webkit-box-shadow:0 3px 3px 0 rgba(0,0,0,0.14),0 1px 7px 0 rgba(0,0,0,0.12),0 3px 1px -1px rgba(0,0,0,0.2);box-shadow:0 3px 3px 0 rgba(0,0,0,0.14),0 1px 7px 0 rgba(0,0,0,0.12),0 3px 1px -1px rgba(0,0,0,0.2)}.z-depth-2{-webkit-box-shadow:0 4px 5px 0 rgba(0,0,0,0.14),0 1px 10px 0 rgba(0,0,0,0.12),0 2px 4px -1px rgba(0,0,0,0.3);box-shadow:0 4px 5px 0 rgba(0,0,0,0.14),0 1px 10px 0 rgba(0,0,0,0.12),0 2px 4px -1px rgba(0,0,0,0.3)}.z-depth-3{-webkit-box-shadow:0 8px 17px 2px rgba(0,0,0,0.14),0 3px 14px 2px rgba(0,0,0,0.12),0 5px 5px -3px rgba(0,0,0,0.2);box-shadow:0 8px 17px 2px rgba(0,0,0,0.14),0 3px 14px 2px rgba(0,0,0,0.12),0 5px 5px -3px rgba(0,0,0,0.2)}.z-depth-4{-webkit-box-shadow:0 16px 24px 2px rgba(0,0,0,0.14),0 6px 30px 5px rgba(0,0,0,0.12),0 8px 10px -7px rgba(0,0,0,0.2);box-shadow:0 16px 24px 2px rgba(0,0,0,0.14),0 6px 30px 5px rgba(0,0,0,0.12),0 8px 10px -7px rgba(0,0,0,0.2)}.z-depth-5,.modal{-webkit-box-shadow:0 24px 38px 3px rgba(0,0,0,0.14),0 9px 46px 8px rgba(0,0,0,0.12),0 11px 15px -7px rgba(0,0,0,0.2);box-shadow:0 24px 38px 3px rgba(0,0,0,0.14),0 9px 46px 8px rgba(0,0,0,0.12),0 11px 15px -7px rgba(0,0,0,0.2)}.hoverable{-webkit-transition:-webkit-box-shadow .25s;transition:-webkit-box-shadow .25s;transition:box-shadow .25s;transition:box-shadow .25s, -webkit-box-shadow .25s}.hoverable:hover{-webkit-box-shadow:0 8px 17px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19);box-shadow:0 8px 17px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19)}.divider{height:1px;overflow:hidden;background-color:#e0e0e0}blockquote{margin:20px 0;padding-left:1.5rem;border-left:5px solid #ee6e73}i{line-height:inherit}i.left{float:left;margin-right:15px}i.right{float:right;margin-left:15px}i.tiny{font-size:1rem}i.small{font-size:2rem}i.medium{font-size:4rem}i.large{font-size:6rem}img.responsive-img,video.responsive-video{max-width:100%;height:auto}.pagination li{display:inline-block;border-radius:2px;text-align:center;vertical-align:top;height:30px}.pagination li a{color:#444;display:inline-block;font-size:1.2rem;padding:0 10px;line-height:30px}.pagination li.active a{color:#fff}.pagination li.active{background-color:#ee6e73}.pagination li.disabled a{cursor:default;color:#999}.pagination li i{font-size:2rem}.pagination li.pages ul li{display:inline-block;float:none}@media only screen and (max-width: 992px){.pagination{width:100%}.pagination li.prev,.pagination li.next{width:10%}.pagination li.pages{width:80%;overflow:hidden;white-space:nowrap}}.breadcrumb{font-size:18px;color:rgba(255,255,255,0.7)}.breadcrumb i,.breadcrumb [class^="mdi-"],.breadcrumb [class*="mdi-"],.breadcrumb i.material-icons{display:inline-block;float:left;font-size:24px}.breadcrumb:before{content:'\E5CC';color:rgba(255,255,255,0.7);vertical-align:top;display:inline-block;font-family:'Material Icons';font-weight:normal;font-style:normal;font-size:25px;margin:0 10px 0 8px;-webkit-font-smoothing:antialiased}.breadcrumb:first-child:before{display:none}.breadcrumb:last-child{color:#fff}.parallax-container{position:relative;overflow:hidden;height:500px}.parallax-container .parallax{position:absolute;top:0;left:0;right:0;bottom:0;z-index:-1}.parallax-container .parallax img{opacity:0;position:absolute;left:50%;bottom:0;min-width:100%;min-height:100%;-webkit-transform:translate3d(0, 0, 0);transform:translate3d(0, 0, 0);-webkit-transform:translateX(-50%);transform:translateX(-50%)}.pin-top,.pin-bottom{position:relative}.pinned{position:fixed !important}ul.staggered-list li{opacity:0}.fade-in{opacity:0;-webkit-transform-origin:0 50%;transform-origin:0 50%}@media only screen and (max-width: 600px){.hide-on-small-only,.hide-on-small-and-down{display:none !important}}@media only screen and (max-width: 992px){.hide-on-med-and-down{display:none !important}}@media only screen and (min-width: 601px){.hide-on-med-and-up{display:none !important}}@media only screen and (min-width: 600px) and (max-width: 992px){.hide-on-med-only{display:none !important}}@media only screen and (min-width: 993px){.hide-on-large-only{display:none !important}}@media only screen and (min-width: 1201px){.hide-on-extra-large-only{display:none !important}}@media only screen and (min-width: 1201px){.show-on-extra-large{display:block !important}}@media only screen and (min-width: 993px){.show-on-large{display:block !important}}@media only screen and (min-width: 600px) and (max-width: 992px){.show-on-medium{display:block !important}}@media only screen and (max-width: 600px){.show-on-small{display:block !important}}@media only screen and (min-width: 601px){.show-on-medium-and-up{display:block !important}}@media only screen and (max-width: 992px){.show-on-medium-and-down{display:block !important}}@media only screen and (max-width: 600px){.center-on-small-only{text-align:center}}.page-footer{padding-top:20px;color:#fff;background-color:#ee6e73}.page-footer .footer-copyright{overflow:hidden;min-height:50px;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;padding:10px 0px;color:rgba(255,255,255,0.8);background-color:rgba(51,51,51,0.08)}table,th,td{border:none}table{width:100%;display:table;border-collapse:collapse;border-spacing:0}table.striped tr{border-bottom:none}table.striped>tbody>tr:nth-child(odd){background-color:rgba(242,242,242,0.5)}table.striped>tbody>tr>td{border-radius:0}table.highlight>tbody>tr{-webkit-transition:background-color .25s ease;transition:background-color .25s ease}table.highlight>tbody>tr:hover{background-color:rgba(242,242,242,0.5)}table.centered thead tr th,table.centered tbody tr td{text-align:center}tr{border-bottom:1px solid rgba(0,0,0,0.12)}td,th{padding:15px 5px;display:table-cell;text-align:left;vertical-align:middle;border-radius:2px}@media only screen and (max-width: 992px){table.responsive-table{width:100%;border-collapse:collapse;border-spacing:0;display:block;position:relative}table.responsive-table td:empty:before{content:'\00a0'}table.responsive-table th,table.responsive-table td{margin:0;vertical-align:top}table.responsive-table th{text-align:left}table.responsive-table thead{display:block;float:left}table.responsive-table thead tr{display:block;padding:0 10px 0 0}table.responsive-table thead tr th::before{content:"\00a0"}table.responsive-table tbody{display:block;width:auto;position:relative;overflow-x:auto;white-space:nowrap}table.responsive-table tbody tr{display:inline-block;vertical-align:top}table.responsive-table th{display:block;text-align:right}table.responsive-table td{display:block;min-height:1.25em;text-align:left}table.responsive-table tr{border-bottom:none;padding:0 10px}table.responsive-table thead{border:0;border-right:1px solid rgba(0,0,0,0.12)}}.collection{margin:.5rem 0 1rem 0;border:1px solid #e0e0e0;border-radius:2px;overflow:hidden;position:relative}.collection .collection-item{background-color:#fff;line-height:1.5rem;padding:10px 20px;margin:0;border-bottom:1px solid #e0e0e0}.collection .collection-item.avatar{min-height:84px;padding-left:72px;position:relative}.collection .collection-item.avatar:not(.circle-clipper)>.circle,.collection .collection-item.avatar :not(.circle-clipper)>.circle{position:absolute;width:42px;height:42px;overflow:hidden;left:15px;display:inline-block;vertical-align:middle}.collection .collection-item.avatar i.circle{font-size:18px;line-height:42px;color:#fff;background-color:#999;text-align:center}.collection .collection-item.avatar .title{font-size:16px}.collection .collection-item.avatar p{margin:0}.collection .collection-item.avatar .secondary-content{position:absolute;top:16px;right:16px}.collection .collection-item:last-child{border-bottom:none}.collection .collection-item.active{background-color:#26a69a;color:#eafaf9}.collection .collection-item.active .secondary-content{color:#fff}.collection a.collection-item{display:block;-webkit-transition:.25s;transition:.25s;color:#26a69a}.collection a.collection-item:not(.active):hover{background-color:#ddd}.collection.with-header .collection-header{background-color:#fff;border-bottom:1px solid #e0e0e0;padding:10px 20px}.collection.with-header .collection-item{padding-left:30px}.collection.with-header .collection-item.avatar{padding-left:72px}.secondary-content{float:right;color:#26a69a}.collapsible .collection{margin:0;border:none}.video-container{position:relative;padding-bottom:56.25%;height:0;overflow:hidden}.video-container iframe,.video-container object,.video-container embed{position:absolute;top:0;left:0;width:100%;height:100%}.progress{position:relative;height:4px;display:block;width:100%;background-color:#acece6;border-radius:2px;margin:.5rem 0 1rem 0;overflow:hidden}.progress .determinate{position:absolute;top:0;left:0;bottom:0;background-color:#26a69a;-webkit-transition:width .3s linear;transition:width .3s linear}.progress .indeterminate{background-color:#26a69a}.progress .indeterminate:before{content:'';position:absolute;background-color:inherit;top:0;left:0;bottom:0;will-change:left, right;-webkit-animation:indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite;animation:indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite}.progress .indeterminate:after{content:'';position:absolute;background-color:inherit;top:0;left:0;bottom:0;will-change:left, right;-webkit-animation:indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) infinite;animation:indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) infinite;-webkit-animation-delay:1.15s;animation-delay:1.15s}@-webkit-keyframes indeterminate{0%{left:-35%;right:100%}60%{left:100%;right:-90%}100%{left:100%;right:-90%}}@keyframes indeterminate{0%{left:-35%;right:100%}60%{left:100%;right:-90%}100%{left:100%;right:-90%}}@-webkit-keyframes indeterminate-short{0%{left:-200%;right:100%}60%{left:107%;right:-8%}100%{left:107%;right:-8%}}@keyframes indeterminate-short{0%{left:-200%;right:100%}60%{left:107%;right:-8%}100%{left:107%;right:-8%}}.hide{display:none !important}.left-align{text-align:left}.right-align{text-align:right}.center,.center-align{text-align:center}.left{float:left !important}.right{float:right !important}.no-select,input[type=range],input[type=range]+.thumb{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.circle{border-radius:50%}.center-block{display:block;margin-left:auto;margin-right:auto}.truncate{display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.no-padding{padding:0 !important}span.badge{min-width:3rem;padding:0 6px;margin-left:14px;text-align:center;font-size:1rem;line-height:22px;height:22px;color:#757575;float:right;-webkit-box-sizing:border-box;box-sizing:border-box}span.badge.new{font-weight:300;font-size:0.8rem;color:#fff;background-color:#26a69a;border-radius:2px}span.badge.new:after{content:" new"}span.badge[data-badge-caption]::after{content:" " attr(data-badge-caption)}nav ul a span.badge{display:inline-block;float:none;margin-left:4px;line-height:22px;height:22px;-webkit-font-smoothing:auto}.collection-item span.badge{margin-top:calc(.75rem - 11px)}.collapsible span.badge{margin-left:auto}.sidenav span.badge{margin-top:calc(24px - 11px)}table span.badge{display:inline-block;float:none;margin-left:auto}.material-icons{text-rendering:optimizeLegibility;-webkit-font-feature-settings:'liga';-moz-font-feature-settings:'liga';font-feature-settings:'liga'}.container{margin:0 auto;max-width:1280px;width:90%}@media only screen and (min-width: 601px){.container{width:85%}}@media only screen and (min-width: 993px){.container{width:70%}}.col .row{margin-left:-.75rem;margin-right:-.75rem}.section{padding-top:1rem;padding-bottom:1rem}.section.no-pad{padding:0}.section.no-pad-bot{padding-bottom:0}.section.no-pad-top{padding-top:0}.row{margin-left:auto;margin-right:auto;margin-bottom:20px}.row:after{content:"";display:table;clear:both}.row .col{float:left;-webkit-box-sizing:border-box;box-sizing:border-box;padding:0 .75rem;min-height:1px}.row .col[class*="push-"],.row .col[class*="pull-"]{position:relative}.row .col.s1{width:8.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.s2{width:16.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.s3{width:25%;margin-left:auto;left:auto;right:auto}.row .col.s4{width:33.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.s5{width:41.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.s6{width:50%;margin-left:auto;left:auto;right:auto}.row .col.s7{width:58.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.s8{width:66.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.s9{width:75%;margin-left:auto;left:auto;right:auto}.row .col.s10{width:83.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.s11{width:91.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.s12{width:100%;margin-left:auto;left:auto;right:auto}.row .col.offset-s1{margin-left:8.3333333333%}.row .col.pull-s1{right:8.3333333333%}.row .col.push-s1{left:8.3333333333%}.row .col.offset-s2{margin-left:16.6666666667%}.row .col.pull-s2{right:16.6666666667%}.row .col.push-s2{left:16.6666666667%}.row .col.offset-s3{margin-left:25%}.row .col.pull-s3{right:25%}.row .col.push-s3{left:25%}.row .col.offset-s4{margin-left:33.3333333333%}.row .col.pull-s4{right:33.3333333333%}.row .col.push-s4{left:33.3333333333%}.row .col.offset-s5{margin-left:41.6666666667%}.row .col.pull-s5{right:41.6666666667%}.row .col.push-s5{left:41.6666666667%}.row .col.offset-s6{margin-left:50%}.row .col.pull-s6{right:50%}.row .col.push-s6{left:50%}.row .col.offset-s7{margin-left:58.3333333333%}.row .col.pull-s7{right:58.3333333333%}.row .col.push-s7{left:58.3333333333%}.row .col.offset-s8{margin-left:66.6666666667%}.row .col.pull-s8{right:66.6666666667%}.row .col.push-s8{left:66.6666666667%}.row .col.offset-s9{margin-left:75%}.row .col.pull-s9{right:75%}.row .col.push-s9{left:75%}.row .col.offset-s10{margin-left:83.3333333333%}.row .col.pull-s10{right:83.3333333333%}.row .col.push-s10{left:83.3333333333%}.row .col.offset-s11{margin-left:91.6666666667%}.row .col.pull-s11{right:91.6666666667%}.row .col.push-s11{left:91.6666666667%}.row .col.offset-s12{margin-left:100%}.row .col.pull-s12{right:100%}.row .col.push-s12{left:100%}@media only screen and (min-width: 601px){.row .col.m1{width:8.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.m2{width:16.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.m3{width:25%;margin-left:auto;left:auto;right:auto}.row .col.m4{width:33.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.m5{width:41.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.m6{width:50%;margin-left:auto;left:auto;right:auto}.row .col.m7{width:58.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.m8{width:66.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.m9{width:75%;margin-left:auto;left:auto;right:auto}.row .col.m10{width:83.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.m11{width:91.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.m12{width:100%;margin-left:auto;left:auto;right:auto}.row .col.offset-m1{margin-left:8.3333333333%}.row .col.pull-m1{right:8.3333333333%}.row .col.push-m1{left:8.3333333333%}.row .col.offset-m2{margin-left:16.6666666667%}.row .col.pull-m2{right:16.6666666667%}.row .col.push-m2{left:16.6666666667%}.row .col.offset-m3{margin-left:25%}.row .col.pull-m3{right:25%}.row .col.push-m3{left:25%}.row .col.offset-m4{margin-left:33.3333333333%}.row .col.pull-m4{right:33.3333333333%}.row .col.push-m4{left:33.3333333333%}.row .col.offset-m5{margin-left:41.6666666667%}.row .col.pull-m5{right:41.6666666667%}.row .col.push-m5{left:41.6666666667%}.row .col.offset-m6{margin-left:50%}.row .col.pull-m6{right:50%}.row .col.push-m6{left:50%}.row .col.offset-m7{margin-left:58.3333333333%}.row .col.pull-m7{right:58.3333333333%}.row .col.push-m7{left:58.3333333333%}.row .col.offset-m8{margin-left:66.6666666667%}.row .col.pull-m8{right:66.6666666667%}.row .col.push-m8{left:66.6666666667%}.row .col.offset-m9{margin-left:75%}.row .col.pull-m9{right:75%}.row .col.push-m9{left:75%}.row .col.offset-m10{margin-left:83.3333333333%}.row .col.pull-m10{right:83.3333333333%}.row .col.push-m10{left:83.3333333333%}.row .col.offset-m11{margin-left:91.6666666667%}.row .col.pull-m11{right:91.6666666667%}.row .col.push-m11{left:91.6666666667%}.row .col.offset-m12{margin-left:100%}.row .col.pull-m12{right:100%}.row .col.push-m12{left:100%}}@media only screen and (min-width: 993px){.row .col.l1{width:8.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.l2{width:16.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.l3{width:25%;margin-left:auto;left:auto;right:auto}.row .col.l4{width:33.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.l5{width:41.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.l6{width:50%;margin-left:auto;left:auto;right:auto}.row .col.l7{width:58.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.l8{width:66.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.l9{width:75%;margin-left:auto;left:auto;right:auto}.row .col.l10{width:83.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.l11{width:91.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.l12{width:100%;margin-left:auto;left:auto;right:auto}.row .col.offset-l1{margin-left:8.3333333333%}.row .col.pull-l1{right:8.3333333333%}.row .col.push-l1{left:8.3333333333%}.row .col.offset-l2{margin-left:16.6666666667%}.row .col.pull-l2{right:16.6666666667%}.row .col.push-l2{left:16.6666666667%}.row .col.offset-l3{margin-left:25%}.row .col.pull-l3{right:25%}.row .col.push-l3{left:25%}.row .col.offset-l4{margin-left:33.3333333333%}.row .col.pull-l4{right:33.3333333333%}.row .col.push-l4{left:33.3333333333%}.row .col.offset-l5{margin-left:41.6666666667%}.row .col.pull-l5{right:41.6666666667%}.row .col.push-l5{left:41.6666666667%}.row .col.offset-l6{margin-left:50%}.row .col.pull-l6{right:50%}.row .col.push-l6{left:50%}.row .col.offset-l7{margin-left:58.3333333333%}.row .col.pull-l7{right:58.3333333333%}.row .col.push-l7{left:58.3333333333%}.row .col.offset-l8{margin-left:66.6666666667%}.row .col.pull-l8{right:66.6666666667%}.row .col.push-l8{left:66.6666666667%}.row .col.offset-l9{margin-left:75%}.row .col.pull-l9{right:75%}.row .col.push-l9{left:75%}.row .col.offset-l10{margin-left:83.3333333333%}.row .col.pull-l10{right:83.3333333333%}.row .col.push-l10{left:83.3333333333%}.row .col.offset-l11{margin-left:91.6666666667%}.row .col.pull-l11{right:91.6666666667%}.row .col.push-l11{left:91.6666666667%}.row .col.offset-l12{margin-left:100%}.row .col.pull-l12{right:100%}.row .col.push-l12{left:100%}}@media only screen and (min-width: 1201px){.row .col.xl1{width:8.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.xl2{width:16.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.xl3{width:25%;margin-left:auto;left:auto;right:auto}.row .col.xl4{width:33.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.xl5{width:41.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.xl6{width:50%;margin-left:auto;left:auto;right:auto}.row .col.xl7{width:58.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.xl8{width:66.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.xl9{width:75%;margin-left:auto;left:auto;right:auto}.row .col.xl10{width:83.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.xl11{width:91.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.xl12{width:100%;margin-left:auto;left:auto;right:auto}.row .col.offset-xl1{margin-left:8.3333333333%}.row .col.pull-xl1{right:8.3333333333%}.row .col.push-xl1{left:8.3333333333%}.row .col.offset-xl2{margin-left:16.6666666667%}.row .col.pull-xl2{right:16.6666666667%}.row .col.push-xl2{left:16.6666666667%}.row .col.offset-xl3{margin-left:25%}.row .col.pull-xl3{right:25%}.row .col.push-xl3{left:25%}.row .col.offset-xl4{margin-left:33.3333333333%}.row .col.pull-xl4{right:33.3333333333%}.row .col.push-xl4{left:33.3333333333%}.row .col.offset-xl5{margin-left:41.6666666667%}.row .col.pull-xl5{right:41.6666666667%}.row .col.push-xl5{left:41.6666666667%}.row .col.offset-xl6{margin-left:50%}.row .col.pull-xl6{right:50%}.row .col.push-xl6{left:50%}.row .col.offset-xl7{margin-left:58.3333333333%}.row .col.pull-xl7{right:58.3333333333%}.row .col.push-xl7{left:58.3333333333%}.row .col.offset-xl8{margin-left:66.6666666667%}.row .col.pull-xl8{right:66.6666666667%}.row .col.push-xl8{left:66.6666666667%}.row .col.offset-xl9{margin-left:75%}.row .col.pull-xl9{right:75%}.row .col.push-xl9{left:75%}.row .col.offset-xl10{margin-left:83.3333333333%}.row .col.pull-xl10{right:83.3333333333%}.row .col.push-xl10{left:83.3333333333%}.row .col.offset-xl11{margin-left:91.6666666667%}.row .col.pull-xl11{right:91.6666666667%}.row .col.push-xl11{left:91.6666666667%}.row .col.offset-xl12{margin-left:100%}.row .col.pull-xl12{right:100%}.row .col.push-xl12{left:100%}}nav{color:#fff;background-color:#ee6e73;width:100%;height:56px;line-height:56px}nav.nav-extended{height:auto}nav.nav-extended .nav-wrapper{min-height:56px;height:auto}nav.nav-extended .nav-content{position:relative;line-height:normal}nav a{color:#fff}nav i,nav [class^="mdi-"],nav [class*="mdi-"],nav i.material-icons{display:block;font-size:24px;height:56px;line-height:56px}nav .nav-wrapper{position:relative;height:100%}@media only screen and (min-width: 993px){nav a.sidenav-trigger{display:none}}nav .sidenav-trigger{float:left;position:relative;z-index:1;height:56px;margin:0 18px}nav .sidenav-trigger i{height:56px;line-height:56px}nav .brand-logo{position:absolute;color:#fff;display:inline-block;font-size:2.1rem;padding:0}nav .brand-logo.center{left:50%;-webkit-transform:translateX(-50%);transform:translateX(-50%)}@media only screen and (max-width: 992px){nav .brand-logo{left:50%;-webkit-transform:translateX(-50%);transform:translateX(-50%)}nav .brand-logo.left,nav .brand-logo.right{padding:0;-webkit-transform:none;transform:none}nav .brand-logo.left{left:0.5rem}nav .brand-logo.right{right:0.5rem;left:auto}}nav .brand-logo.right{right:0.5rem;padding:0}nav .brand-logo i,nav .brand-logo [class^="mdi-"],nav .brand-logo [class*="mdi-"],nav .brand-logo i.material-icons{float:left;margin-right:15px}nav .nav-title{display:inline-block;font-size:32px;padding:28px 0}nav ul{margin:0}nav ul li{-webkit-transition:background-color .3s;transition:background-color .3s;float:left;padding:0}nav ul li.active{background-color:rgba(0,0,0,0.1)}nav ul a{-webkit-transition:background-color .3s;transition:background-color .3s;font-size:1rem;color:#fff;display:block;padding:0 15px;cursor:pointer}nav ul a.btn,nav ul a.btn-large,nav ul a.btn-small,nav ul a.btn-large,nav ul a.btn-flat,nav ul a.btn-floating{margin-top:-2px;margin-left:15px;margin-right:15px}nav ul a.btn>.material-icons,nav ul a.btn-large>.material-icons,nav ul a.btn-small>.material-icons,nav ul a.btn-large>.material-icons,nav ul a.btn-flat>.material-icons,nav ul a.btn-floating>.material-icons{height:inherit;line-height:inherit}nav ul a:hover{background-color:rgba(0,0,0,0.1)}nav ul.left{float:left}nav form{height:100%}nav .input-field{margin:0;height:100%}nav .input-field input{height:100%;font-size:1.2rem;border:none;padding-left:2rem}nav .input-field input:focus,nav .input-field input[type=text]:valid,nav .input-field input[type=password]:valid,nav .input-field input[type=email]:valid,nav .input-field input[type=url]:valid,nav .input-field input[type=date]:valid{border:none;-webkit-box-shadow:none;box-shadow:none}nav .input-field label{top:0;left:0}nav .input-field label i{color:rgba(255,255,255,0.7);-webkit-transition:color .3s;transition:color .3s}nav .input-field label.active i{color:#fff}.navbar-fixed{position:relative;height:56px;z-index:997}.navbar-fixed nav{position:fixed}@media only screen and (min-width: 601px){nav.nav-extended .nav-wrapper{min-height:64px}nav,nav .nav-wrapper i,nav a.sidenav-trigger,nav a.sidenav-trigger i{height:64px;line-height:64px}.navbar-fixed{height:64px}}a{text-decoration:none}html{line-height:1.5;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;font-weight:normal;color:rgba(0,0,0,0.87)}@media only screen and (min-width: 0){html{font-size:14px}}@media only screen and (min-width: 992px){html{font-size:14.5px}}@media only screen and (min-width: 1200px){html{font-size:15px}}h1,h2,h3,h4,h5,h6{font-weight:400;line-height:1.3}h1 a,h2 a,h3 a,h4 a,h5 a,h6 a{font-weight:inherit}h1{font-size:4.2rem;line-height:110%;margin:2.8rem 0 1.68rem 0}h2{font-size:3.56rem;line-height:110%;margin:2.3733333333rem 0 1.424rem 0}h3{font-size:2.92rem;line-height:110%;margin:1.9466666667rem 0 1.168rem 0}h4{font-size:2.28rem;line-height:110%;margin:1.52rem 0 .912rem 0}h5{font-size:1.64rem;line-height:110%;margin:1.0933333333rem 0 .656rem 0}h6{font-size:1.15rem;line-height:110%;margin:.7666666667rem 0 .46rem 0}em{font-style:italic}strong{font-weight:500}small{font-size:75%}.light{font-weight:300}.thin{font-weight:200}@media only screen and (min-width: 360px){.flow-text{font-size:1.2rem}}@media only screen and (min-width: 390px){.flow-text{font-size:1.224rem}}@media only screen and (min-width: 420px){.flow-text{font-size:1.248rem}}@media only screen and (min-width: 450px){.flow-text{font-size:1.272rem}}@media only screen and (min-width: 480px){.flow-text{font-size:1.296rem}}@media only screen and (min-width: 510px){.flow-text{font-size:1.32rem}}@media only screen and (min-width: 540px){.flow-text{font-size:1.344rem}}@media only screen and (min-width: 570px){.flow-text{font-size:1.368rem}}@media only screen and (min-width: 600px){.flow-text{font-size:1.392rem}}@media only screen and (min-width: 630px){.flow-text{font-size:1.416rem}}@media only screen and (min-width: 660px){.flow-text{font-size:1.44rem}}@media only screen and (min-width: 690px){.flow-text{font-size:1.464rem}}@media only screen and (min-width: 720px){.flow-text{font-size:1.488rem}}@media only screen and (min-width: 750px){.flow-text{font-size:1.512rem}}@media only screen and (min-width: 780px){.flow-text{font-size:1.536rem}}@media only screen and (min-width: 810px){.flow-text{font-size:1.56rem}}@media only screen and (min-width: 840px){.flow-text{font-size:1.584rem}}@media only screen and (min-width: 870px){.flow-text{font-size:1.608rem}}@media only screen and (min-width: 900px){.flow-text{font-size:1.632rem}}@media only screen and (min-width: 930px){.flow-text{font-size:1.656rem}}@media only screen and (min-width: 960px){.flow-text{font-size:1.68rem}}@media only screen and (max-width: 360px){.flow-text{font-size:1.2rem}}.scale-transition{-webkit-transition:-webkit-transform 0.3s cubic-bezier(0.53, 0.01, 0.36, 1.63) !important;transition:-webkit-transform 0.3s cubic-bezier(0.53, 0.01, 0.36, 1.63) !important;transition:transform 0.3s cubic-bezier(0.53, 0.01, 0.36, 1.63) !important;transition:transform 0.3s cubic-bezier(0.53, 0.01, 0.36, 1.63), -webkit-transform 0.3s cubic-bezier(0.53, 0.01, 0.36, 1.63) !important}.scale-transition.scale-out{-webkit-transform:scale(0);transform:scale(0);-webkit-transition:-webkit-transform .2s !important;transition:-webkit-transform .2s !important;transition:transform .2s !important;transition:transform .2s, -webkit-transform .2s !important}.scale-transition.scale-in{-webkit-transform:scale(1);transform:scale(1)}.card-panel{-webkit-transition:-webkit-box-shadow .25s;transition:-webkit-box-shadow .25s;transition:box-shadow .25s;transition:box-shadow .25s, -webkit-box-shadow .25s;padding:24px;margin:.5rem 0 1rem 0;border-radius:2px;background-color:#fff}.card{position:relative;margin:.5rem 0 1rem 0;background-color:#fff;-webkit-transition:-webkit-box-shadow .25s;transition:-webkit-box-shadow .25s;transition:box-shadow .25s;transition:box-shadow .25s, -webkit-box-shadow .25s;border-radius:2px}.card .card-title{font-size:24px;font-weight:300}.card .card-title.activator{cursor:pointer}.card.small,.card.medium,.card.large{position:relative}.card.small .card-image,.card.medium .card-image,.card.large .card-image{max-height:60%;overflow:hidden}.card.small .card-image+.card-content,.card.medium .card-image+.card-content,.card.large .card-image+.card-content{max-height:40%}.card.small .card-content,.card.medium .card-content,.card.large .card-content{max-height:100%;overflow:hidden}.card.small .card-action,.card.medium .card-action,.card.large .card-action{position:absolute;bottom:0;left:0;right:0}.card.small{height:300px}.card.medium{height:400px}.card.large{height:500px}.card.horizontal{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex}.card.horizontal.small .card-image,.card.horizontal.medium .card-image,.card.horizontal.large .card-image{height:100%;max-height:none;overflow:visible}.card.horizontal.small .card-image img,.card.horizontal.medium .card-image img,.card.horizontal.large .card-image img{height:100%}.card.horizontal .card-image{max-width:50%}.card.horizontal .card-image img{border-radius:2px 0 0 2px;max-width:100%;width:auto}.card.horizontal .card-stacked{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-box-flex:1;-webkit-flex:1;-ms-flex:1;flex:1;position:relative}.card.horizontal .card-stacked .card-content{-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1}.card.sticky-action .card-action{z-index:2}.card.sticky-action .card-reveal{z-index:1;padding-bottom:64px}.card .card-image{position:relative}.card .card-image img{display:block;border-radius:2px 2px 0 0;position:relative;left:0;right:0;top:0;bottom:0;width:100%}.card .card-image .card-title{color:#fff;position:absolute;bottom:0;left:0;max-width:100%;padding:24px}.card .card-content{padding:24px;border-radius:0 0 2px 2px}.card .card-content p{margin:0}.card .card-content .card-title{display:block;line-height:32px;margin-bottom:8px}.card .card-content .card-title i{line-height:32px}.card .card-action{background-color:inherit;border-top:1px solid rgba(160,160,160,0.2);position:relative;padding:16px 24px}.card .card-action:last-child{border-radius:0 0 2px 2px}.card .card-action a:not(.btn):not(.btn-large):not(.btn-small):not(.btn-large):not(.btn-floating){color:#ffab40;margin-right:24px;-webkit-transition:color .3s ease;transition:color .3s ease;text-transform:uppercase}.card .card-action a:not(.btn):not(.btn-large):not(.btn-small):not(.btn-large):not(.btn-floating):hover{color:#ffd8a6}.card .card-reveal{padding:24px;position:absolute;background-color:#fff;width:100%;overflow-y:auto;left:0;top:100%;height:100%;z-index:3;display:none}.card .card-reveal .card-title{cursor:pointer;display:block}#toast-container{display:block;position:fixed;z-index:10000}@media only screen and (max-width: 600px){#toast-container{min-width:100%;bottom:0%}}@media only screen and (min-width: 601px) and (max-width: 992px){#toast-container{left:5%;bottom:7%;max-width:90%}}@media only screen and (min-width: 993px){#toast-container{top:10%;right:7%;max-width:86%}}.toast{border-radius:2px;top:35px;width:auto;margin-top:10px;position:relative;max-width:100%;height:auto;min-height:48px;line-height:1.5em;background-color:#323232;padding:10px 25px;font-size:1.1rem;font-weight:300;color:#fff;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;cursor:default}.toast .toast-action{color:#eeff41;font-weight:500;margin-right:-25px;margin-left:3rem}.toast.rounded{border-radius:24px}@media only screen and (max-width: 600px){.toast{width:100%;border-radius:0}}.tabs{position:relative;overflow-x:auto;overflow-y:hidden;height:48px;width:100%;background-color:#fff;margin:0 auto;white-space:nowrap}.tabs.tabs-transparent{background-color:transparent}.tabs.tabs-transparent .tab a,.tabs.tabs-transparent .tab.disabled a,.tabs.tabs-transparent .tab.disabled a:hover{color:rgba(255,255,255,0.7)}.tabs.tabs-transparent .tab a:hover,.tabs.tabs-transparent .tab a.active{color:#fff}.tabs.tabs-transparent .indicator{background-color:#fff}.tabs.tabs-fixed-width{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex}.tabs.tabs-fixed-width .tab{-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1}.tabs .tab{display:inline-block;text-align:center;line-height:48px;height:48px;padding:0;margin:0;text-transform:uppercase}.tabs .tab a{color:rgba(238,110,115,0.7);display:block;width:100%;height:100%;padding:0 24px;font-size:14px;text-overflow:ellipsis;overflow:hidden;-webkit-transition:color .28s ease, background-color .28s ease;transition:color .28s ease, background-color .28s ease}.tabs .tab a:focus,.tabs .tab a:focus.active{background-color:rgba(246,178,181,0.2);outline:none}.tabs .tab a:hover,.tabs .tab a.active{background-color:transparent;color:#ee6e73}.tabs .tab.disabled a,.tabs .tab.disabled a:hover{color:rgba(238,110,115,0.4);cursor:default}.tabs .indicator{position:absolute;bottom:0;height:2px;background-color:#f6b2b5;will-change:left, right}@media only screen and (max-width: 992px){.tabs{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex}.tabs .tab{-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1}.tabs .tab a{padding:0 12px}}.material-tooltip{padding:10px 8px;font-size:1rem;z-index:2000;background-color:transparent;border-radius:2px;color:#fff;min-height:36px;line-height:120%;opacity:0;position:absolute;text-align:center;max-width:calc(100% - 4px);overflow:hidden;left:0;top:0;pointer-events:none;visibility:hidden;background-color:#323232}.backdrop{position:absolute;opacity:0;height:7px;width:14px;border-radius:0 0 50% 50%;background-color:#323232;z-index:-1;-webkit-transform-origin:50% 0%;transform-origin:50% 0%;visibility:hidden}.btn,.btn-large,.btn-small,.btn-flat{border:none;border-radius:2px;display:inline-block;height:36px;line-height:36px;padding:0 16px;text-transform:uppercase;vertical-align:middle;-webkit-tap-highlight-color:transparent}.btn.disabled,.disabled.btn-large,.disabled.btn-small,.btn-floating.disabled,.btn-large.disabled,.btn-small.disabled,.btn-flat.disabled,.btn:disabled,.btn-large:disabled,.btn-small:disabled,.btn-floating:disabled,.btn-large:disabled,.btn-small:disabled,.btn-flat:disabled,.btn[disabled],[disabled].btn-large,[disabled].btn-small,.btn-floating[disabled],.btn-large[disabled],.btn-small[disabled],.btn-flat[disabled]{pointer-events:none;background-color:#DFDFDF !important;-webkit-box-shadow:none;box-shadow:none;color:#9F9F9F !important;cursor:default}.btn.disabled:hover,.disabled.btn-large:hover,.disabled.btn-small:hover,.btn-floating.disabled:hover,.btn-large.disabled:hover,.btn-small.disabled:hover,.btn-flat.disabled:hover,.btn:disabled:hover,.btn-large:disabled:hover,.btn-small:disabled:hover,.btn-floating:disabled:hover,.btn-large:disabled:hover,.btn-small:disabled:hover,.btn-flat:disabled:hover,.btn[disabled]:hover,[disabled].btn-large:hover,[disabled].btn-small:hover,.btn-floating[disabled]:hover,.btn-large[disabled]:hover,.btn-small[disabled]:hover,.btn-flat[disabled]:hover{background-color:#DFDFDF !important;color:#9F9F9F !important}.btn,.btn-large,.btn-small,.btn-floating,.btn-large,.btn-small,.btn-flat{font-size:14px;outline:0}.btn i,.btn-large i,.btn-small i,.btn-floating i,.btn-large i,.btn-small i,.btn-flat i{font-size:1.3rem;line-height:inherit}.btn:focus,.btn-large:focus,.btn-small:focus,.btn-floating:focus{background-color:#1d7d74}.btn,.btn-large,.btn-small{text-decoration:none;color:#fff;background-color:#26a69a;text-align:center;letter-spacing:.5px;-webkit-transition:background-color .2s ease-out;transition:background-color .2s ease-out;cursor:pointer}.btn:hover,.btn-large:hover,.btn-small:hover{background-color:#2bbbad}.btn-floating{display:inline-block;color:#fff;position:relative;overflow:hidden;z-index:1;width:40px;height:40px;line-height:40px;padding:0;background-color:#26a69a;border-radius:50%;-webkit-transition:background-color .3s;transition:background-color .3s;cursor:pointer;vertical-align:middle}.btn-floating:hover{background-color:#26a69a}.btn-floating:before{border-radius:0}.btn-floating.btn-large{width:56px;height:56px;padding:0}.btn-floating.btn-large.halfway-fab{bottom:-28px}.btn-floating.btn-large i{line-height:56px}.btn-floating.btn-small{width:32.4px;height:32.4px}.btn-floating.btn-small.halfway-fab{bottom:-16.2px}.btn-floating.btn-small i{line-height:32.4px}.btn-floating.halfway-fab{position:absolute;right:24px;bottom:-20px}.btn-floating.halfway-fab.left{right:auto;left:24px}.btn-floating i{width:inherit;display:inline-block;text-align:center;color:#fff;font-size:1.6rem;line-height:40px}button.btn-floating{border:none}.fixed-action-btn{position:fixed;right:23px;bottom:23px;padding-top:15px;margin-bottom:0;z-index:997}.fixed-action-btn.active ul{visibility:visible}.fixed-action-btn.direction-left,.fixed-action-btn.direction-right{padding:0 0 0 15px}.fixed-action-btn.direction-left ul,.fixed-action-btn.direction-right ul{text-align:right;right:64px;top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%);height:100%;left:auto;width:500px}.fixed-action-btn.direction-left ul li,.fixed-action-btn.direction-right ul li{display:inline-block;margin:7.5px 15px 0 0}.fixed-action-btn.direction-right{padding:0 15px 0 0}.fixed-action-btn.direction-right ul{text-align:left;direction:rtl;left:64px;right:auto}.fixed-action-btn.direction-right ul li{margin:7.5px 0 0 15px}.fixed-action-btn.direction-bottom{padding:0 0 15px 0}.fixed-action-btn.direction-bottom ul{top:64px;bottom:auto;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:reverse;-webkit-flex-direction:column-reverse;-ms-flex-direction:column-reverse;flex-direction:column-reverse}.fixed-action-btn.direction-bottom ul li{margin:15px 0 0 0}.fixed-action-btn.toolbar{padding:0;height:56px}.fixed-action-btn.toolbar.active>a i{opacity:0}.fixed-action-btn.toolbar ul{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;top:0;bottom:0;z-index:1}.fixed-action-btn.toolbar ul li{-webkit-box-flex:1;-webkit-flex:1;-ms-flex:1;flex:1;display:inline-block;margin:0;height:100%;-webkit-transition:none;transition:none}.fixed-action-btn.toolbar ul li a{display:block;overflow:hidden;position:relative;width:100%;height:100%;background-color:transparent;-webkit-box-shadow:none;box-shadow:none;color:#fff;line-height:56px;z-index:1}.fixed-action-btn.toolbar ul li a i{line-height:inherit}.fixed-action-btn ul{left:0;right:0;text-align:center;position:absolute;bottom:64px;margin:0;visibility:hidden}.fixed-action-btn ul li{margin-bottom:15px}.fixed-action-btn ul a.btn-floating{opacity:0}.fixed-action-btn .fab-backdrop{position:absolute;top:0;left:0;z-index:-1;width:40px;height:40px;background-color:#26a69a;border-radius:50%;-webkit-transform:scale(0);transform:scale(0)}.btn-flat{-webkit-box-shadow:none;box-shadow:none;background-color:transparent;color:#343434;cursor:pointer;-webkit-transition:background-color .2s;transition:background-color .2s}.btn-flat:focus,.btn-flat:hover{-webkit-box-shadow:none;box-shadow:none}.btn-flat:focus{background-color:rgba(0,0,0,0.1)}.btn-flat.disabled,.btn-flat.btn-flat[disabled]{background-color:transparent !important;color:#b3b2b2 !important;cursor:default}.btn-large{height:54px;line-height:54px;font-size:15px;padding:0 28px}.btn-large i{font-size:1.6rem}.btn-small{height:32.4px;line-height:32.4px;font-size:13px}.btn-small i{font-size:1.2rem}.btn-block{display:block}.dropdown-content{background-color:#fff;margin:0;display:none;min-width:100px;overflow-y:auto;opacity:0;position:absolute;left:0;top:0;z-index:9999;-webkit-transform-origin:0 0;transform-origin:0 0}.dropdown-content:focus{outline:0}.dropdown-content li{clear:both;color:rgba(0,0,0,0.87);cursor:pointer;min-height:50px;line-height:1.5rem;width:100%;text-align:left}.dropdown-content li:hover,.dropdown-content li.active{background-color:#eee}.dropdown-content li:focus{outline:none}.dropdown-content li.divider{min-height:0;height:1px}.dropdown-content li>a,.dropdown-content li>span{font-size:16px;color:#26a69a;display:block;line-height:22px;padding:14px 16px}.dropdown-content li>span>label{top:1px;left:0;height:18px}.dropdown-content li>a>i{height:inherit;line-height:inherit;float:left;margin:0 24px 0 0;width:24px}body.keyboard-focused .dropdown-content li:focus{background-color:#dadada}.input-field.col .dropdown-content [type="checkbox"]+label{top:1px;left:0;height:18px;-webkit-transform:none;transform:none}.dropdown-trigger{cursor:pointer}/*! + * Waves v0.6.0 + * http://fian.my.id/Waves + * + * Copyright 2014 Alfiana E. Sibuea and other contributors + * Released under the MIT license + * https://github.com/fians/Waves/blob/master/LICENSE + */.waves-effect{position:relative;cursor:pointer;display:inline-block;overflow:hidden;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent;vertical-align:middle;z-index:1;-webkit-transition:.3s ease-out;transition:.3s ease-out}.waves-effect .waves-ripple{position:absolute;border-radius:50%;width:20px;height:20px;margin-top:-10px;margin-left:-10px;opacity:0;background:rgba(0,0,0,0.2);-webkit-transition:all 0.7s ease-out;transition:all 0.7s ease-out;-webkit-transition-property:opacity, -webkit-transform;transition-property:opacity, -webkit-transform;transition-property:transform, opacity;transition-property:transform, opacity, -webkit-transform;-webkit-transform:scale(0);transform:scale(0);pointer-events:none}.waves-effect.waves-light .waves-ripple{background-color:rgba(255,255,255,0.45)}.waves-effect.waves-red .waves-ripple{background-color:rgba(244,67,54,0.7)}.waves-effect.waves-yellow .waves-ripple{background-color:rgba(255,235,59,0.7)}.waves-effect.waves-orange .waves-ripple{background-color:rgba(255,152,0,0.7)}.waves-effect.waves-purple .waves-ripple{background-color:rgba(156,39,176,0.7)}.waves-effect.waves-green .waves-ripple{background-color:rgba(76,175,80,0.7)}.waves-effect.waves-teal .waves-ripple{background-color:rgba(0,150,136,0.7)}.waves-effect input[type="button"],.waves-effect input[type="reset"],.waves-effect input[type="submit"]{border:0;font-style:normal;font-size:inherit;text-transform:inherit;background:none}.waves-effect img{position:relative;z-index:-1}.waves-notransition{-webkit-transition:none !important;transition:none !important}.waves-circle{-webkit-transform:translateZ(0);transform:translateZ(0);-webkit-mask-image:-webkit-radial-gradient(circle, white 100%, black 100%)}.waves-input-wrapper{border-radius:0.2em;vertical-align:bottom}.waves-input-wrapper .waves-button-input{position:relative;top:0;left:0;z-index:1}.waves-circle{text-align:center;width:2.5em;height:2.5em;line-height:2.5em;border-radius:50%;-webkit-mask-image:none}.waves-block{display:block}.waves-effect .waves-ripple{z-index:-1}.modal{display:none;position:fixed;left:0;right:0;background-color:#fafafa;padding:0;max-height:70%;width:55%;margin:auto;overflow-y:auto;border-radius:2px;will-change:top, opacity}@media only screen and (max-width: 992px){.modal{width:80%}}.modal h1,.modal h2,.modal h3,.modal h4{margin-top:0}.modal .modal-content{padding:24px}.modal .modal-close{cursor:pointer}.modal .modal-footer{border-radius:0 0 2px 2px;background-color:#fafafa;padding:4px 6px;height:56px;width:100%;text-align:right}.modal .modal-footer .btn,.modal .modal-footer .btn-large,.modal .modal-footer .btn-small,.modal .modal-footer .btn-flat{margin:6px 0}.modal-overlay{position:fixed;z-index:999;top:-25%;left:0;bottom:0;right:0;height:125%;width:100%;background:#000;display:none;will-change:opacity}.modal.modal-fixed-footer{padding:0;height:70%}.modal.modal-fixed-footer .modal-content{position:absolute;height:calc(100% - 56px);max-height:100%;width:100%;overflow-y:auto}.modal.modal-fixed-footer .modal-footer{border-top:1px solid rgba(0,0,0,0.1);position:absolute;bottom:0}.modal.bottom-sheet{top:auto;bottom:-100%;margin:0;width:100%;max-height:45%;border-radius:0;will-change:bottom, opacity}.collapsible{border-top:1px solid #ddd;border-right:1px solid #ddd;border-left:1px solid #ddd;margin:.5rem 0 1rem 0}.collapsible-header{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;cursor:pointer;-webkit-tap-highlight-color:transparent;line-height:1.5;padding:1rem;background-color:#fff;border-bottom:1px solid #ddd}.collapsible-header:focus{outline:0}.collapsible-header i{width:2rem;font-size:1.6rem;display:inline-block;text-align:center;margin-right:1rem}.keyboard-focused .collapsible-header:focus{background-color:#eee}.collapsible-body{display:none;border-bottom:1px solid #ddd;-webkit-box-sizing:border-box;box-sizing:border-box;padding:2rem}.sidenav .collapsible,.sidenav.fixed .collapsible{border:none;-webkit-box-shadow:none;box-shadow:none}.sidenav .collapsible li,.sidenav.fixed .collapsible li{padding:0}.sidenav .collapsible-header,.sidenav.fixed .collapsible-header{background-color:transparent;border:none;line-height:inherit;height:inherit;padding:0 16px}.sidenav .collapsible-header:hover,.sidenav.fixed .collapsible-header:hover{background-color:rgba(0,0,0,0.05)}.sidenav .collapsible-header i,.sidenav.fixed .collapsible-header i{line-height:inherit}.sidenav .collapsible-body,.sidenav.fixed .collapsible-body{border:0;background-color:#fff}.sidenav .collapsible-body li a,.sidenav.fixed .collapsible-body li a{padding:0 23.5px 0 31px}.collapsible.popout{border:none;-webkit-box-shadow:none;box-shadow:none}.collapsible.popout>li{-webkit-box-shadow:0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12);box-shadow:0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12);margin:0 24px;-webkit-transition:margin 0.35s cubic-bezier(0.25, 0.46, 0.45, 0.94);transition:margin 0.35s cubic-bezier(0.25, 0.46, 0.45, 0.94)}.collapsible.popout>li.active{-webkit-box-shadow:0 5px 11px 0 rgba(0,0,0,0.18),0 4px 15px 0 rgba(0,0,0,0.15);box-shadow:0 5px 11px 0 rgba(0,0,0,0.18),0 4px 15px 0 rgba(0,0,0,0.15);margin:16px 0}.chip{display:inline-block;height:32px;font-size:13px;font-weight:500;color:rgba(0,0,0,0.6);line-height:32px;padding:0 12px;border-radius:16px;background-color:#e4e4e4;margin-bottom:5px;margin-right:5px}.chip:focus{outline:none;background-color:#26a69a;color:#fff}.chip>img{float:left;margin:0 8px 0 -12px;height:32px;width:32px;border-radius:50%}.chip .close{cursor:pointer;float:right;font-size:16px;line-height:32px;padding-left:8px}.chips{border:none;border-bottom:1px solid #9e9e9e;-webkit-box-shadow:none;box-shadow:none;margin:0 0 8px 0;min-height:45px;outline:none;-webkit-transition:all .3s;transition:all .3s}.chips.focus{border-bottom:1px solid #26a69a;-webkit-box-shadow:0 1px 0 0 #26a69a;box-shadow:0 1px 0 0 #26a69a}.chips:hover{cursor:text}.chips .input{background:none;border:0;color:rgba(0,0,0,0.6);display:inline-block;font-size:16px;height:3rem;line-height:32px;outline:0;margin:0;padding:0 !important;width:120px !important}.chips .input:focus{border:0 !important;-webkit-box-shadow:none !important;box-shadow:none !important}.chips .autocomplete-content{margin-top:0;margin-bottom:0}.prefix ~ .chips{margin-left:3rem;width:92%;width:calc(100% - 3rem)}.chips:empty ~ label{font-size:0.8rem;-webkit-transform:translateY(-140%);transform:translateY(-140%)}.materialboxed{display:block;cursor:-webkit-zoom-in;cursor:zoom-in;position:relative;-webkit-transition:opacity .4s;transition:opacity .4s;-webkit-backface-visibility:hidden}.materialboxed:hover:not(.active){opacity:.8}.materialboxed.active{cursor:-webkit-zoom-out;cursor:zoom-out}#materialbox-overlay{position:fixed;top:0;right:0;bottom:0;left:0;background-color:#292929;z-index:1000;will-change:opacity}.materialbox-caption{position:fixed;display:none;color:#fff;line-height:50px;bottom:0;left:0;width:100%;text-align:center;padding:0% 15%;height:50px;z-index:1000;-webkit-font-smoothing:antialiased}select:focus{outline:1px solid #c9f3ef}button:focus{outline:none;background-color:#2ab7a9}label{font-size:.8rem;color:#9e9e9e}::-webkit-input-placeholder{color:#d1d1d1}::-moz-placeholder{color:#d1d1d1}:-ms-input-placeholder{color:#d1d1d1}::placeholder{color:#d1d1d1}input:not([type]),input[type=text]:not(.browser-default),input[type=password]:not(.browser-default),input[type=email]:not(.browser-default),input[type=url]:not(.browser-default),input[type=time]:not(.browser-default),input[type=date]:not(.browser-default),input[type=datetime]:not(.browser-default),input[type=datetime-local]:not(.browser-default),input[type=tel]:not(.browser-default),input[type=number]:not(.browser-default),input[type=search]:not(.browser-default),textarea.materialize-textarea{background-color:transparent;border:none;border-bottom:1px solid #9e9e9e;border-radius:0;outline:none;height:3rem;width:100%;font-size:16px;margin:0 0 8px 0;padding:0;-webkit-box-shadow:none;box-shadow:none;-webkit-box-sizing:content-box;box-sizing:content-box;-webkit-transition:border .3s, -webkit-box-shadow .3s;transition:border .3s, -webkit-box-shadow .3s;transition:box-shadow .3s, border .3s;transition:box-shadow .3s, border .3s, -webkit-box-shadow .3s}input:not([type]):disabled,input:not([type])[readonly="readonly"],input[type=text]:not(.browser-default):disabled,input[type=text]:not(.browser-default)[readonly="readonly"],input[type=password]:not(.browser-default):disabled,input[type=password]:not(.browser-default)[readonly="readonly"],input[type=email]:not(.browser-default):disabled,input[type=email]:not(.browser-default)[readonly="readonly"],input[type=url]:not(.browser-default):disabled,input[type=url]:not(.browser-default)[readonly="readonly"],input[type=time]:not(.browser-default):disabled,input[type=time]:not(.browser-default)[readonly="readonly"],input[type=date]:not(.browser-default):disabled,input[type=date]:not(.browser-default)[readonly="readonly"],input[type=datetime]:not(.browser-default):disabled,input[type=datetime]:not(.browser-default)[readonly="readonly"],input[type=datetime-local]:not(.browser-default):disabled,input[type=datetime-local]:not(.browser-default)[readonly="readonly"],input[type=tel]:not(.browser-default):disabled,input[type=tel]:not(.browser-default)[readonly="readonly"],input[type=number]:not(.browser-default):disabled,input[type=number]:not(.browser-default)[readonly="readonly"],input[type=search]:not(.browser-default):disabled,input[type=search]:not(.browser-default)[readonly="readonly"],textarea.materialize-textarea:disabled,textarea.materialize-textarea[readonly="readonly"]{color:rgba(0,0,0,0.42);border-bottom:1px dotted rgba(0,0,0,0.42)}input:not([type]):disabled+label,input:not([type])[readonly="readonly"]+label,input[type=text]:not(.browser-default):disabled+label,input[type=text]:not(.browser-default)[readonly="readonly"]+label,input[type=password]:not(.browser-default):disabled+label,input[type=password]:not(.browser-default)[readonly="readonly"]+label,input[type=email]:not(.browser-default):disabled+label,input[type=email]:not(.browser-default)[readonly="readonly"]+label,input[type=url]:not(.browser-default):disabled+label,input[type=url]:not(.browser-default)[readonly="readonly"]+label,input[type=time]:not(.browser-default):disabled+label,input[type=time]:not(.browser-default)[readonly="readonly"]+label,input[type=date]:not(.browser-default):disabled+label,input[type=date]:not(.browser-default)[readonly="readonly"]+label,input[type=datetime]:not(.browser-default):disabled+label,input[type=datetime]:not(.browser-default)[readonly="readonly"]+label,input[type=datetime-local]:not(.browser-default):disabled+label,input[type=datetime-local]:not(.browser-default)[readonly="readonly"]+label,input[type=tel]:not(.browser-default):disabled+label,input[type=tel]:not(.browser-default)[readonly="readonly"]+label,input[type=number]:not(.browser-default):disabled+label,input[type=number]:not(.browser-default)[readonly="readonly"]+label,input[type=search]:not(.browser-default):disabled+label,input[type=search]:not(.browser-default)[readonly="readonly"]+label,textarea.materialize-textarea:disabled+label,textarea.materialize-textarea[readonly="readonly"]+label{color:rgba(0,0,0,0.42)}input:not([type]):focus:not([readonly]),input[type=text]:not(.browser-default):focus:not([readonly]),input[type=password]:not(.browser-default):focus:not([readonly]),input[type=email]:not(.browser-default):focus:not([readonly]),input[type=url]:not(.browser-default):focus:not([readonly]),input[type=time]:not(.browser-default):focus:not([readonly]),input[type=date]:not(.browser-default):focus:not([readonly]),input[type=datetime]:not(.browser-default):focus:not([readonly]),input[type=datetime-local]:not(.browser-default):focus:not([readonly]),input[type=tel]:not(.browser-default):focus:not([readonly]),input[type=number]:not(.browser-default):focus:not([readonly]),input[type=search]:not(.browser-default):focus:not([readonly]),textarea.materialize-textarea:focus:not([readonly]){border-bottom:1px solid #26a69a;-webkit-box-shadow:0 1px 0 0 #26a69a;box-shadow:0 1px 0 0 #26a69a}input:not([type]):focus:not([readonly])+label,input[type=text]:not(.browser-default):focus:not([readonly])+label,input[type=password]:not(.browser-default):focus:not([readonly])+label,input[type=email]:not(.browser-default):focus:not([readonly])+label,input[type=url]:not(.browser-default):focus:not([readonly])+label,input[type=time]:not(.browser-default):focus:not([readonly])+label,input[type=date]:not(.browser-default):focus:not([readonly])+label,input[type=datetime]:not(.browser-default):focus:not([readonly])+label,input[type=datetime-local]:not(.browser-default):focus:not([readonly])+label,input[type=tel]:not(.browser-default):focus:not([readonly])+label,input[type=number]:not(.browser-default):focus:not([readonly])+label,input[type=search]:not(.browser-default):focus:not([readonly])+label,textarea.materialize-textarea:focus:not([readonly])+label{color:#26a69a}input:not([type]):focus.valid ~ label,input[type=text]:not(.browser-default):focus.valid ~ label,input[type=password]:not(.browser-default):focus.valid ~ label,input[type=email]:not(.browser-default):focus.valid ~ label,input[type=url]:not(.browser-default):focus.valid ~ label,input[type=time]:not(.browser-default):focus.valid ~ label,input[type=date]:not(.browser-default):focus.valid ~ label,input[type=datetime]:not(.browser-default):focus.valid ~ label,input[type=datetime-local]:not(.browser-default):focus.valid ~ label,input[type=tel]:not(.browser-default):focus.valid ~ label,input[type=number]:not(.browser-default):focus.valid ~ label,input[type=search]:not(.browser-default):focus.valid ~ label,textarea.materialize-textarea:focus.valid ~ label{color:#4CAF50}input:not([type]):focus.invalid ~ label,input[type=text]:not(.browser-default):focus.invalid ~ label,input[type=password]:not(.browser-default):focus.invalid ~ label,input[type=email]:not(.browser-default):focus.invalid ~ label,input[type=url]:not(.browser-default):focus.invalid ~ label,input[type=time]:not(.browser-default):focus.invalid ~ label,input[type=date]:not(.browser-default):focus.invalid ~ label,input[type=datetime]:not(.browser-default):focus.invalid ~ label,input[type=datetime-local]:not(.browser-default):focus.invalid ~ label,input[type=tel]:not(.browser-default):focus.invalid ~ label,input[type=number]:not(.browser-default):focus.invalid ~ label,input[type=search]:not(.browser-default):focus.invalid ~ label,textarea.materialize-textarea:focus.invalid ~ label{color:#F44336}input:not([type]).validate+label,input[type=text]:not(.browser-default).validate+label,input[type=password]:not(.browser-default).validate+label,input[type=email]:not(.browser-default).validate+label,input[type=url]:not(.browser-default).validate+label,input[type=time]:not(.browser-default).validate+label,input[type=date]:not(.browser-default).validate+label,input[type=datetime]:not(.browser-default).validate+label,input[type=datetime-local]:not(.browser-default).validate+label,input[type=tel]:not(.browser-default).validate+label,input[type=number]:not(.browser-default).validate+label,input[type=search]:not(.browser-default).validate+label,textarea.materialize-textarea.validate+label{width:100%}input.valid:not([type]),input.valid:not([type]):focus,input[type=text].valid:not(.browser-default),input[type=text].valid:not(.browser-default):focus,input[type=password].valid:not(.browser-default),input[type=password].valid:not(.browser-default):focus,input[type=email].valid:not(.browser-default),input[type=email].valid:not(.browser-default):focus,input[type=url].valid:not(.browser-default),input[type=url].valid:not(.browser-default):focus,input[type=time].valid:not(.browser-default),input[type=time].valid:not(.browser-default):focus,input[type=date].valid:not(.browser-default),input[type=date].valid:not(.browser-default):focus,input[type=datetime].valid:not(.browser-default),input[type=datetime].valid:not(.browser-default):focus,input[type=datetime-local].valid:not(.browser-default),input[type=datetime-local].valid:not(.browser-default):focus,input[type=tel].valid:not(.browser-default),input[type=tel].valid:not(.browser-default):focus,input[type=number].valid:not(.browser-default),input[type=number].valid:not(.browser-default):focus,input[type=search].valid:not(.browser-default),input[type=search].valid:not(.browser-default):focus,textarea.materialize-textarea.valid,textarea.materialize-textarea.valid:focus,.select-wrapper.valid>input.select-dropdown{border-bottom:1px solid #4CAF50;-webkit-box-shadow:0 1px 0 0 #4CAF50;box-shadow:0 1px 0 0 #4CAF50}input.invalid:not([type]),input.invalid:not([type]):focus,input[type=text].invalid:not(.browser-default),input[type=text].invalid:not(.browser-default):focus,input[type=password].invalid:not(.browser-default),input[type=password].invalid:not(.browser-default):focus,input[type=email].invalid:not(.browser-default),input[type=email].invalid:not(.browser-default):focus,input[type=url].invalid:not(.browser-default),input[type=url].invalid:not(.browser-default):focus,input[type=time].invalid:not(.browser-default),input[type=time].invalid:not(.browser-default):focus,input[type=date].invalid:not(.browser-default),input[type=date].invalid:not(.browser-default):focus,input[type=datetime].invalid:not(.browser-default),input[type=datetime].invalid:not(.browser-default):focus,input[type=datetime-local].invalid:not(.browser-default),input[type=datetime-local].invalid:not(.browser-default):focus,input[type=tel].invalid:not(.browser-default),input[type=tel].invalid:not(.browser-default):focus,input[type=number].invalid:not(.browser-default),input[type=number].invalid:not(.browser-default):focus,input[type=search].invalid:not(.browser-default),input[type=search].invalid:not(.browser-default):focus,textarea.materialize-textarea.invalid,textarea.materialize-textarea.invalid:focus,.select-wrapper.invalid>input.select-dropdown,.select-wrapper.invalid>input.select-dropdown:focus{border-bottom:1px solid #F44336;-webkit-box-shadow:0 1px 0 0 #F44336;box-shadow:0 1px 0 0 #F44336}input:not([type]).valid ~ .helper-text[data-success],input:not([type]):focus.valid ~ .helper-text[data-success],input:not([type]).invalid ~ .helper-text[data-error],input:not([type]):focus.invalid ~ .helper-text[data-error],input[type=text]:not(.browser-default).valid ~ .helper-text[data-success],input[type=text]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=text]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=text]:not(.browser-default):focus.invalid ~ .helper-text[data-error],input[type=password]:not(.browser-default).valid ~ .helper-text[data-success],input[type=password]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=password]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=password]:not(.browser-default):focus.invalid ~ .helper-text[data-error],input[type=email]:not(.browser-default).valid ~ .helper-text[data-success],input[type=email]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=email]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=email]:not(.browser-default):focus.invalid ~ .helper-text[data-error],input[type=url]:not(.browser-default).valid ~ .helper-text[data-success],input[type=url]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=url]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=url]:not(.browser-default):focus.invalid ~ .helper-text[data-error],input[type=time]:not(.browser-default).valid ~ .helper-text[data-success],input[type=time]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=time]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=time]:not(.browser-default):focus.invalid ~ .helper-text[data-error],input[type=date]:not(.browser-default).valid ~ .helper-text[data-success],input[type=date]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=date]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=date]:not(.browser-default):focus.invalid ~ .helper-text[data-error],input[type=datetime]:not(.browser-default).valid ~ .helper-text[data-success],input[type=datetime]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=datetime]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=datetime]:not(.browser-default):focus.invalid ~ .helper-text[data-error],input[type=datetime-local]:not(.browser-default).valid ~ .helper-text[data-success],input[type=datetime-local]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=datetime-local]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=datetime-local]:not(.browser-default):focus.invalid ~ .helper-text[data-error],input[type=tel]:not(.browser-default).valid ~ .helper-text[data-success],input[type=tel]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=tel]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=tel]:not(.browser-default):focus.invalid ~ .helper-text[data-error],input[type=number]:not(.browser-default).valid ~ .helper-text[data-success],input[type=number]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=number]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=number]:not(.browser-default):focus.invalid ~ .helper-text[data-error],input[type=search]:not(.browser-default).valid ~ .helper-text[data-success],input[type=search]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=search]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=search]:not(.browser-default):focus.invalid ~ .helper-text[data-error],textarea.materialize-textarea.valid ~ .helper-text[data-success],textarea.materialize-textarea:focus.valid ~ .helper-text[data-success],textarea.materialize-textarea.invalid ~ .helper-text[data-error],textarea.materialize-textarea:focus.invalid ~ .helper-text[data-error],.select-wrapper.valid .helper-text[data-success],.select-wrapper.invalid ~ .helper-text[data-error]{color:transparent;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;pointer-events:none}input:not([type]).valid ~ .helper-text:after,input:not([type]):focus.valid ~ .helper-text:after,input[type=text]:not(.browser-default).valid ~ .helper-text:after,input[type=text]:not(.browser-default):focus.valid ~ .helper-text:after,input[type=password]:not(.browser-default).valid ~ .helper-text:after,input[type=password]:not(.browser-default):focus.valid ~ .helper-text:after,input[type=email]:not(.browser-default).valid ~ .helper-text:after,input[type=email]:not(.browser-default):focus.valid ~ .helper-text:after,input[type=url]:not(.browser-default).valid ~ .helper-text:after,input[type=url]:not(.browser-default):focus.valid ~ .helper-text:after,input[type=time]:not(.browser-default).valid ~ .helper-text:after,input[type=time]:not(.browser-default):focus.valid ~ .helper-text:after,input[type=date]:not(.browser-default).valid ~ .helper-text:after,input[type=date]:not(.browser-default):focus.valid ~ .helper-text:after,input[type=datetime]:not(.browser-default).valid ~ .helper-text:after,input[type=datetime]:not(.browser-default):focus.valid ~ .helper-text:after,input[type=datetime-local]:not(.browser-default).valid ~ .helper-text:after,input[type=datetime-local]:not(.browser-default):focus.valid ~ .helper-text:after,input[type=tel]:not(.browser-default).valid ~ .helper-text:after,input[type=tel]:not(.browser-default):focus.valid ~ .helper-text:after,input[type=number]:not(.browser-default).valid ~ .helper-text:after,input[type=number]:not(.browser-default):focus.valid ~ .helper-text:after,input[type=search]:not(.browser-default).valid ~ .helper-text:after,input[type=search]:not(.browser-default):focus.valid ~ .helper-text:after,textarea.materialize-textarea.valid ~ .helper-text:after,textarea.materialize-textarea:focus.valid ~ .helper-text:after,.select-wrapper.valid ~ .helper-text:after{content:attr(data-success);color:#4CAF50}input:not([type]).invalid ~ .helper-text:after,input:not([type]):focus.invalid ~ .helper-text:after,input[type=text]:not(.browser-default).invalid ~ .helper-text:after,input[type=text]:not(.browser-default):focus.invalid ~ .helper-text:after,input[type=password]:not(.browser-default).invalid ~ .helper-text:after,input[type=password]:not(.browser-default):focus.invalid ~ .helper-text:after,input[type=email]:not(.browser-default).invalid ~ .helper-text:after,input[type=email]:not(.browser-default):focus.invalid ~ .helper-text:after,input[type=url]:not(.browser-default).invalid ~ .helper-text:after,input[type=url]:not(.browser-default):focus.invalid ~ .helper-text:after,input[type=time]:not(.browser-default).invalid ~ .helper-text:after,input[type=time]:not(.browser-default):focus.invalid ~ .helper-text:after,input[type=date]:not(.browser-default).invalid ~ .helper-text:after,input[type=date]:not(.browser-default):focus.invalid ~ .helper-text:after,input[type=datetime]:not(.browser-default).invalid ~ .helper-text:after,input[type=datetime]:not(.browser-default):focus.invalid ~ .helper-text:after,input[type=datetime-local]:not(.browser-default).invalid ~ .helper-text:after,input[type=datetime-local]:not(.browser-default):focus.invalid ~ .helper-text:after,input[type=tel]:not(.browser-default).invalid ~ .helper-text:after,input[type=tel]:not(.browser-default):focus.invalid ~ .helper-text:after,input[type=number]:not(.browser-default).invalid ~ .helper-text:after,input[type=number]:not(.browser-default):focus.invalid ~ .helper-text:after,input[type=search]:not(.browser-default).invalid ~ .helper-text:after,input[type=search]:not(.browser-default):focus.invalid ~ .helper-text:after,textarea.materialize-textarea.invalid ~ .helper-text:after,textarea.materialize-textarea:focus.invalid ~ .helper-text:after,.select-wrapper.invalid ~ .helper-text:after{content:attr(data-error);color:#F44336}input:not([type])+label:after,input[type=text]:not(.browser-default)+label:after,input[type=password]:not(.browser-default)+label:after,input[type=email]:not(.browser-default)+label:after,input[type=url]:not(.browser-default)+label:after,input[type=time]:not(.browser-default)+label:after,input[type=date]:not(.browser-default)+label:after,input[type=datetime]:not(.browser-default)+label:after,input[type=datetime-local]:not(.browser-default)+label:after,input[type=tel]:not(.browser-default)+label:after,input[type=number]:not(.browser-default)+label:after,input[type=search]:not(.browser-default)+label:after,textarea.materialize-textarea+label:after,.select-wrapper+label:after{display:block;content:"";position:absolute;top:100%;left:0;opacity:0;-webkit-transition:.2s opacity ease-out, .2s color ease-out;transition:.2s opacity ease-out, .2s color ease-out}.input-field{position:relative;margin-top:1rem;margin-bottom:1rem}.input-field.inline{display:inline-block;vertical-align:middle;margin-left:5px}.input-field.inline input,.input-field.inline .select-dropdown{margin-bottom:1rem}.input-field.col label{left:.75rem}.input-field.col .prefix ~ label,.input-field.col .prefix ~ .validate ~ label{width:calc(100% - 3rem - 1.5rem)}.input-field>label{color:#9e9e9e;position:absolute;top:0;left:0;font-size:1rem;cursor:text;-webkit-transition:color .2s ease-out, -webkit-transform .2s ease-out;transition:color .2s ease-out, -webkit-transform .2s ease-out;transition:transform .2s ease-out, color .2s ease-out;transition:transform .2s ease-out, color .2s ease-out, -webkit-transform .2s ease-out;-webkit-transform-origin:0% 100%;transform-origin:0% 100%;text-align:initial;-webkit-transform:translateY(12px);transform:translateY(12px)}.input-field>label:not(.label-icon).active{-webkit-transform:translateY(-14px) scale(0.8);transform:translateY(-14px) scale(0.8);-webkit-transform-origin:0 0;transform-origin:0 0}.input-field>input[type]:-webkit-autofill:not(.browser-default)+label,.input-field>input[type=date]:not(.browser-default)+label,.input-field>input[type=time]:not(.browser-default)+label{-webkit-transform:translateY(-14px) scale(0.8);transform:translateY(-14px) scale(0.8);-webkit-transform-origin:0 0;transform-origin:0 0}.input-field .helper-text{position:relative;min-height:18px;display:block;font-size:12px;color:rgba(0,0,0,0.54)}.input-field .helper-text::after{opacity:1;position:absolute;top:0;left:0}.input-field .prefix{position:absolute;width:3rem;font-size:2rem;-webkit-transition:color .2s;transition:color .2s;top:.5rem}.input-field .prefix.active{color:#26a69a}.input-field .prefix ~ input,.input-field .prefix ~ textarea,.input-field .prefix ~ label,.input-field .prefix ~ .validate ~ label,.input-field .prefix ~ .helper-text,.input-field .prefix ~ .autocomplete-content{margin-left:3rem;width:92%;width:calc(100% - 3rem)}.input-field .prefix ~ label{margin-left:3rem}@media only screen and (max-width: 992px){.input-field .prefix ~ input{width:86%;width:calc(100% - 3rem)}}@media only screen and (max-width: 600px){.input-field .prefix ~ input{width:80%;width:calc(100% - 3rem)}}.input-field input[type=search]{display:block;line-height:inherit;-webkit-transition:.3s background-color;transition:.3s background-color}.nav-wrapper .input-field input[type=search]{height:inherit;padding-left:4rem;width:calc(100% - 4rem);border:0;-webkit-box-shadow:none;box-shadow:none}.input-field input[type=search]:focus:not(.browser-default){background-color:#fff;border:0;-webkit-box-shadow:none;box-shadow:none;color:#444}.input-field input[type=search]:focus:not(.browser-default)+label i,.input-field input[type=search]:focus:not(.browser-default) ~ .mdi-navigation-close,.input-field input[type=search]:focus:not(.browser-default) ~ .material-icons{color:#444}.input-field input[type=search]+.label-icon{-webkit-transform:none;transform:none;left:1rem}.input-field input[type=search] ~ .mdi-navigation-close,.input-field input[type=search] ~ .material-icons{position:absolute;top:0;right:1rem;color:transparent;cursor:pointer;font-size:2rem;-webkit-transition:.3s color;transition:.3s color}textarea{width:100%;height:3rem;background-color:transparent}textarea.materialize-textarea{line-height:normal;overflow-y:hidden;padding:.8rem 0 .8rem 0;resize:none;min-height:3rem;-webkit-box-sizing:border-box;box-sizing:border-box}.hiddendiv{visibility:hidden;white-space:pre-wrap;word-wrap:break-word;overflow-wrap:break-word;padding-top:1.2rem;position:absolute;top:0;z-index:-1}.autocomplete-content li .highlight{color:#444}.autocomplete-content li img{height:40px;width:40px;margin:5px 15px}.character-counter{min-height:18px}[type="radio"]:not(:checked),[type="radio"]:checked{position:absolute;opacity:0;pointer-events:none}[type="radio"]:not(:checked)+span,[type="radio"]:checked+span{position:relative;padding-left:35px;cursor:pointer;display:inline-block;height:25px;line-height:25px;font-size:1rem;-webkit-transition:.28s ease;transition:.28s ease;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}[type="radio"]+span:before,[type="radio"]+span:after{content:'';position:absolute;left:0;top:0;margin:4px;width:16px;height:16px;z-index:0;-webkit-transition:.28s ease;transition:.28s ease}[type="radio"]:not(:checked)+span:before,[type="radio"]:not(:checked)+span:after,[type="radio"]:checked+span:before,[type="radio"]:checked+span:after,[type="radio"].with-gap:checked+span:before,[type="radio"].with-gap:checked+span:after{border-radius:50%}[type="radio"]:not(:checked)+span:before,[type="radio"]:not(:checked)+span:after{border:2px solid #5a5a5a}[type="radio"]:not(:checked)+span:after{-webkit-transform:scale(0);transform:scale(0)}[type="radio"]:checked+span:before{border:2px solid transparent}[type="radio"]:checked+span:after,[type="radio"].with-gap:checked+span:before,[type="radio"].with-gap:checked+span:after{border:2px solid #26a69a}[type="radio"]:checked+span:after,[type="radio"].with-gap:checked+span:after{background-color:#26a69a}[type="radio"]:checked+span:after{-webkit-transform:scale(1.02);transform:scale(1.02)}[type="radio"].with-gap:checked+span:after{-webkit-transform:scale(0.5);transform:scale(0.5)}[type="radio"].tabbed:focus+span:before{-webkit-box-shadow:0 0 0 10px rgba(0,0,0,0.1);box-shadow:0 0 0 10px rgba(0,0,0,0.1)}[type="radio"].with-gap:disabled:checked+span:before{border:2px solid rgba(0,0,0,0.42)}[type="radio"].with-gap:disabled:checked+span:after{border:none;background-color:rgba(0,0,0,0.42)}[type="radio"]:disabled:not(:checked)+span:before,[type="radio"]:disabled:checked+span:before{background-color:transparent;border-color:rgba(0,0,0,0.42)}[type="radio"]:disabled+span{color:rgba(0,0,0,0.42)}[type="radio"]:disabled:not(:checked)+span:before{border-color:rgba(0,0,0,0.42)}[type="radio"]:disabled:checked+span:after{background-color:rgba(0,0,0,0.42);border-color:#949494}[type="checkbox"]:not(:checked),[type="checkbox"]:checked{position:absolute;opacity:0;pointer-events:none}[type="checkbox"]+span:not(.lever){position:relative;padding-left:35px;cursor:pointer;display:inline-block;height:25px;line-height:25px;font-size:1rem;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}[type="checkbox"]+span:not(.lever):before,[type="checkbox"]:not(.filled-in)+span:not(.lever):after{content:'';position:absolute;top:0;left:0;width:18px;height:18px;z-index:0;border:2px solid #5a5a5a;border-radius:1px;margin-top:3px;-webkit-transition:.2s;transition:.2s}[type="checkbox"]:not(.filled-in)+span:not(.lever):after{border:0;-webkit-transform:scale(0);transform:scale(0)}[type="checkbox"]:not(:checked):disabled+span:not(.lever):before{border:none;background-color:rgba(0,0,0,0.42)}[type="checkbox"].tabbed:focus+span:not(.lever):after{-webkit-transform:scale(1);transform:scale(1);border:0;border-radius:50%;-webkit-box-shadow:0 0 0 10px rgba(0,0,0,0.1);box-shadow:0 0 0 10px rgba(0,0,0,0.1);background-color:rgba(0,0,0,0.1)}[type="checkbox"]:checked+span:not(.lever):before{top:-4px;left:-5px;width:12px;height:22px;border-top:2px solid transparent;border-left:2px solid transparent;border-right:2px solid #26a69a;border-bottom:2px solid #26a69a;-webkit-transform:rotate(40deg);transform:rotate(40deg);-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-transform-origin:100% 100%;transform-origin:100% 100%}[type="checkbox"]:checked:disabled+span:before{border-right:2px solid rgba(0,0,0,0.42);border-bottom:2px solid rgba(0,0,0,0.42)}[type="checkbox"]:indeterminate+span:not(.lever):before{top:-11px;left:-12px;width:10px;height:22px;border-top:none;border-left:none;border-right:2px solid #26a69a;border-bottom:none;-webkit-transform:rotate(90deg);transform:rotate(90deg);-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-transform-origin:100% 100%;transform-origin:100% 100%}[type="checkbox"]:indeterminate:disabled+span:not(.lever):before{border-right:2px solid rgba(0,0,0,0.42);background-color:transparent}[type="checkbox"].filled-in+span:not(.lever):after{border-radius:2px}[type="checkbox"].filled-in+span:not(.lever):before,[type="checkbox"].filled-in+span:not(.lever):after{content:'';left:0;position:absolute;-webkit-transition:border .25s, background-color .25s, width .20s .1s, height .20s .1s, top .20s .1s, left .20s .1s;transition:border .25s, background-color .25s, width .20s .1s, height .20s .1s, top .20s .1s, left .20s .1s;z-index:1}[type="checkbox"].filled-in:not(:checked)+span:not(.lever):before{width:0;height:0;border:3px solid transparent;left:6px;top:10px;-webkit-transform:rotateZ(37deg);transform:rotateZ(37deg);-webkit-transform-origin:100% 100%;transform-origin:100% 100%}[type="checkbox"].filled-in:not(:checked)+span:not(.lever):after{height:20px;width:20px;background-color:transparent;border:2px solid #5a5a5a;top:0px;z-index:0}[type="checkbox"].filled-in:checked+span:not(.lever):before{top:0;left:1px;width:8px;height:13px;border-top:2px solid transparent;border-left:2px solid transparent;border-right:2px solid #fff;border-bottom:2px solid #fff;-webkit-transform:rotateZ(37deg);transform:rotateZ(37deg);-webkit-transform-origin:100% 100%;transform-origin:100% 100%}[type="checkbox"].filled-in:checked+span:not(.lever):after{top:0;width:20px;height:20px;border:2px solid #26a69a;background-color:#26a69a;z-index:0}[type="checkbox"].filled-in.tabbed:focus+span:not(.lever):after{border-radius:2px;border-color:#5a5a5a;background-color:rgba(0,0,0,0.1)}[type="checkbox"].filled-in.tabbed:checked:focus+span:not(.lever):after{border-radius:2px;background-color:#26a69a;border-color:#26a69a}[type="checkbox"].filled-in:disabled:not(:checked)+span:not(.lever):before{background-color:transparent;border:2px solid transparent}[type="checkbox"].filled-in:disabled:not(:checked)+span:not(.lever):after{border-color:transparent;background-color:#949494}[type="checkbox"].filled-in:disabled:checked+span:not(.lever):before{background-color:transparent}[type="checkbox"].filled-in:disabled:checked+span:not(.lever):after{background-color:#949494;border-color:#949494}.switch,.switch *{-webkit-tap-highlight-color:transparent;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.switch label{cursor:pointer}.switch label input[type=checkbox]{opacity:0;width:0;height:0}.switch label input[type=checkbox]:checked+.lever{background-color:#84c7c1}.switch label input[type=checkbox]:checked+.lever:before,.switch label input[type=checkbox]:checked+.lever:after{left:18px}.switch label input[type=checkbox]:checked+.lever:after{background-color:#26a69a}.switch label .lever{content:"";display:inline-block;position:relative;width:36px;height:14px;background-color:rgba(0,0,0,0.38);border-radius:15px;margin-right:10px;-webkit-transition:background 0.3s ease;transition:background 0.3s ease;vertical-align:middle;margin:0 16px}.switch label .lever:before,.switch label .lever:after{content:"";position:absolute;display:inline-block;width:20px;height:20px;border-radius:50%;left:0;top:-3px;-webkit-transition:left 0.3s ease, background .3s ease, -webkit-box-shadow 0.1s ease, -webkit-transform .1s ease;transition:left 0.3s ease, background .3s ease, -webkit-box-shadow 0.1s ease, -webkit-transform .1s ease;transition:left 0.3s ease, background .3s ease, box-shadow 0.1s ease, transform .1s ease;transition:left 0.3s ease, background .3s ease, box-shadow 0.1s ease, transform .1s ease, -webkit-box-shadow 0.1s ease, -webkit-transform .1s ease}.switch label .lever:before{background-color:rgba(38,166,154,0.15)}.switch label .lever:after{background-color:#F1F1F1;-webkit-box-shadow:0px 3px 1px -2px rgba(0,0,0,0.2),0px 2px 2px 0px rgba(0,0,0,0.14),0px 1px 5px 0px rgba(0,0,0,0.12);box-shadow:0px 3px 1px -2px rgba(0,0,0,0.2),0px 2px 2px 0px rgba(0,0,0,0.14),0px 1px 5px 0px rgba(0,0,0,0.12)}input[type=checkbox]:checked:not(:disabled) ~ .lever:active::before,input[type=checkbox]:checked:not(:disabled).tabbed:focus ~ .lever::before{-webkit-transform:scale(2.4);transform:scale(2.4);background-color:rgba(38,166,154,0.15)}input[type=checkbox]:not(:disabled) ~ .lever:active:before,input[type=checkbox]:not(:disabled).tabbed:focus ~ .lever::before{-webkit-transform:scale(2.4);transform:scale(2.4);background-color:rgba(0,0,0,0.08)}.switch input[type=checkbox][disabled]+.lever{cursor:default;background-color:rgba(0,0,0,0.12)}.switch label input[type=checkbox][disabled]+.lever:after,.switch label input[type=checkbox][disabled]:checked+.lever:after{background-color:#949494}select{display:none}select.browser-default{display:block}select{background-color:rgba(255,255,255,0.9);width:100%;padding:5px;border:1px solid #f2f2f2;border-radius:2px;height:3rem}.select-label{position:absolute}.select-wrapper{position:relative}.select-wrapper.valid+label,.select-wrapper.invalid+label{width:100%;pointer-events:none}.select-wrapper input.select-dropdown{position:relative;cursor:pointer;background-color:transparent;border:none;border-bottom:1px solid #9e9e9e;outline:none;height:3rem;line-height:3rem;width:100%;font-size:16px;margin:0 0 8px 0;padding:0;display:block;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;z-index:1}.select-wrapper input.select-dropdown:focus{border-bottom:1px solid #26a69a}.select-wrapper .caret{position:absolute;right:0;top:0;bottom:0;margin:auto 0;z-index:0;fill:rgba(0,0,0,0.87)}.select-wrapper+label{position:absolute;top:-26px;font-size:.8rem}select:disabled{color:rgba(0,0,0,0.42)}.select-wrapper.disabled+label{color:rgba(0,0,0,0.42)}.select-wrapper.disabled .caret{fill:rgba(0,0,0,0.42)}.select-wrapper input.select-dropdown:disabled{color:rgba(0,0,0,0.42);cursor:default;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.select-wrapper i{color:rgba(0,0,0,0.3)}.select-dropdown li.disabled,.select-dropdown li.disabled>span,.select-dropdown li.optgroup{color:rgba(0,0,0,0.3);background-color:transparent}body.keyboard-focused .select-dropdown.dropdown-content li:focus{background-color:rgba(0,0,0,0.08)}.select-dropdown.dropdown-content li:hover{background-color:rgba(0,0,0,0.08)}.select-dropdown.dropdown-content li.selected{background-color:rgba(0,0,0,0.03)}.prefix ~ .select-wrapper{margin-left:3rem;width:92%;width:calc(100% - 3rem)}.prefix ~ label{margin-left:3rem}.select-dropdown li img{height:40px;width:40px;margin:5px 15px;float:right}.select-dropdown li.optgroup{border-top:1px solid #eee}.select-dropdown li.optgroup.selected>span{color:rgba(0,0,0,0.7)}.select-dropdown li.optgroup>span{color:rgba(0,0,0,0.4)}.select-dropdown li.optgroup ~ li.optgroup-option{padding-left:1rem}.file-field{position:relative}.file-field .file-path-wrapper{overflow:hidden;padding-left:10px}.file-field input.file-path{width:100%}.file-field .btn,.file-field .btn-large,.file-field .btn-small{float:left;height:3rem;line-height:3rem}.file-field span{cursor:pointer}.file-field input[type=file]{position:absolute;top:0;right:0;left:0;bottom:0;width:100%;margin:0;padding:0;font-size:20px;cursor:pointer;opacity:0;filter:alpha(opacity=0)}.file-field input[type=file]::-webkit-file-upload-button{display:none}.range-field{position:relative}input[type=range],input[type=range]+.thumb{cursor:pointer}input[type=range]{position:relative;background-color:transparent;border:none;outline:none;width:100%;margin:15px 0;padding:0}input[type=range]:focus{outline:none}input[type=range]+.thumb{position:absolute;top:10px;left:0;border:none;height:0;width:0;border-radius:50%;background-color:#26a69a;margin-left:7px;-webkit-transform-origin:50% 50%;transform-origin:50% 50%;-webkit-transform:rotate(-45deg);transform:rotate(-45deg)}input[type=range]+.thumb .value{display:block;width:30px;text-align:center;color:#26a69a;font-size:0;-webkit-transform:rotate(45deg);transform:rotate(45deg)}input[type=range]+.thumb.active{border-radius:50% 50% 50% 0}input[type=range]+.thumb.active .value{color:#fff;margin-left:-1px;margin-top:8px;font-size:10px}input[type=range]{-webkit-appearance:none}input[type=range]::-webkit-slider-runnable-track{height:3px;background:#c2c0c2;border:none}input[type=range]::-webkit-slider-thumb{border:none;height:14px;width:14px;border-radius:50%;background:#26a69a;-webkit-transition:-webkit-box-shadow .3s;transition:-webkit-box-shadow .3s;transition:box-shadow .3s;transition:box-shadow .3s, -webkit-box-shadow .3s;-webkit-appearance:none;background-color:#26a69a;-webkit-transform-origin:50% 50%;transform-origin:50% 50%;margin:-5px 0 0 0}.keyboard-focused input[type=range]:focus:not(.active)::-webkit-slider-thumb{-webkit-box-shadow:0 0 0 10px rgba(38,166,154,0.26);box-shadow:0 0 0 10px rgba(38,166,154,0.26)}input[type=range]{border:1px solid white}input[type=range]::-moz-range-track{height:3px;background:#c2c0c2;border:none}input[type=range]::-moz-focus-inner{border:0}input[type=range]::-moz-range-thumb{border:none;height:14px;width:14px;border-radius:50%;background:#26a69a;-webkit-transition:-webkit-box-shadow .3s;transition:-webkit-box-shadow .3s;transition:box-shadow .3s;transition:box-shadow .3s, -webkit-box-shadow .3s;margin-top:-5px}input[type=range]:-moz-focusring{outline:1px solid #fff;outline-offset:-1px}.keyboard-focused input[type=range]:focus:not(.active)::-moz-range-thumb{box-shadow:0 0 0 10px rgba(38,166,154,0.26)}input[type=range]::-ms-track{height:3px;background:transparent;border-color:transparent;border-width:6px 0;color:transparent}input[type=range]::-ms-fill-lower{background:#777}input[type=range]::-ms-fill-upper{background:#ddd}input[type=range]::-ms-thumb{border:none;height:14px;width:14px;border-radius:50%;background:#26a69a;-webkit-transition:-webkit-box-shadow .3s;transition:-webkit-box-shadow .3s;transition:box-shadow .3s;transition:box-shadow .3s, -webkit-box-shadow .3s}.keyboard-focused input[type=range]:focus:not(.active)::-ms-thumb{box-shadow:0 0 0 10px rgba(38,166,154,0.26)}.table-of-contents.fixed{position:fixed}.table-of-contents li{padding:2px 0}.table-of-contents a{display:inline-block;font-weight:300;color:#757575;padding-left:16px;height:1.5rem;line-height:1.5rem;letter-spacing:.4;display:inline-block}.table-of-contents a:hover{color:#a8a8a8;padding-left:15px;border-left:1px solid #ee6e73}.table-of-contents a.active{font-weight:500;padding-left:14px;border-left:2px solid #ee6e73}.sidenav{position:fixed;width:300px;left:0;top:0;margin:0;-webkit-transform:translateX(-100%);transform:translateX(-100%);height:100%;height:calc(100% + 60px);height:-moz-calc(100%);padding-bottom:60px;background-color:#fff;z-index:999;overflow-y:auto;will-change:transform;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-transform:translateX(-105%);transform:translateX(-105%)}.sidenav.right-aligned{right:0;-webkit-transform:translateX(105%);transform:translateX(105%);left:auto;-webkit-transform:translateX(100%);transform:translateX(100%)}.sidenav .collapsible{margin:0}.sidenav li{float:none;line-height:48px}.sidenav li.active{background-color:rgba(0,0,0,0.05)}.sidenav li>a{color:rgba(0,0,0,0.87);display:block;font-size:14px;font-weight:500;height:48px;line-height:48px;padding:0 32px}.sidenav li>a:hover{background-color:rgba(0,0,0,0.05)}.sidenav li>a.btn,.sidenav li>a.btn-large,.sidenav li>a.btn-small,.sidenav li>a.btn-large,.sidenav li>a.btn-flat,.sidenav li>a.btn-floating{margin:10px 15px}.sidenav li>a.btn,.sidenav li>a.btn-large,.sidenav li>a.btn-small,.sidenav li>a.btn-large,.sidenav li>a.btn-floating{color:#fff}.sidenav li>a.btn-flat{color:#343434}.sidenav li>a.btn:hover,.sidenav li>a.btn-large:hover,.sidenav li>a.btn-small:hover,.sidenav li>a.btn-large:hover{background-color:#2bbbad}.sidenav li>a.btn-floating:hover{background-color:#26a69a}.sidenav li>a>i,.sidenav li>a>[class^="mdi-"],.sidenav li>a li>a>[class*="mdi-"],.sidenav li>a>i.material-icons{float:left;height:48px;line-height:48px;margin:0 32px 0 0;width:24px;color:rgba(0,0,0,0.54)}.sidenav .divider{margin:8px 0 0 0}.sidenav .subheader{cursor:initial;pointer-events:none;color:rgba(0,0,0,0.54);font-size:14px;font-weight:500;line-height:48px}.sidenav .subheader:hover{background-color:transparent}.sidenav .user-view{position:relative;padding:32px 32px 0;margin-bottom:8px}.sidenav .user-view>a{height:auto;padding:0}.sidenav .user-view>a:hover{background-color:transparent}.sidenav .user-view .background{overflow:hidden;position:absolute;top:0;right:0;bottom:0;left:0;z-index:-1}.sidenav .user-view .circle,.sidenav .user-view .name,.sidenav .user-view .email{display:block}.sidenav .user-view .circle{height:64px;width:64px}.sidenav .user-view .name,.sidenav .user-view .email{font-size:14px;line-height:24px}.sidenav .user-view .name{margin-top:16px;font-weight:500}.sidenav .user-view .email{padding-bottom:16px;font-weight:400}.drag-target{height:100%;width:10px;position:fixed;top:0;z-index:998}.drag-target.right-aligned{right:0}.sidenav.sidenav-fixed{left:0;-webkit-transform:translateX(0);transform:translateX(0);position:fixed}.sidenav.sidenav-fixed.right-aligned{right:0;left:auto}@media only screen and (max-width: 992px){.sidenav.sidenav-fixed{-webkit-transform:translateX(-105%);transform:translateX(-105%)}.sidenav.sidenav-fixed.right-aligned{-webkit-transform:translateX(105%);transform:translateX(105%)}.sidenav>a{padding:0 16px}.sidenav .user-view{padding:16px 16px 0}}.sidenav .collapsible-body>ul:not(.collapsible)>li.active,.sidenav.sidenav-fixed .collapsible-body>ul:not(.collapsible)>li.active{background-color:#ee6e73}.sidenav .collapsible-body>ul:not(.collapsible)>li.active a,.sidenav.sidenav-fixed .collapsible-body>ul:not(.collapsible)>li.active a{color:#fff}.sidenav .collapsible-body{padding:0}.sidenav-overlay{position:fixed;top:0;left:0;right:0;opacity:0;height:120vh;background-color:rgba(0,0,0,0.5);z-index:997;display:none}.preloader-wrapper{display:inline-block;position:relative;width:50px;height:50px}.preloader-wrapper.small{width:36px;height:36px}.preloader-wrapper.big{width:64px;height:64px}.preloader-wrapper.active{-webkit-animation:container-rotate 1568ms linear infinite;animation:container-rotate 1568ms linear infinite}@-webkit-keyframes container-rotate{to{-webkit-transform:rotate(360deg)}}@keyframes container-rotate{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.spinner-layer{position:absolute;width:100%;height:100%;opacity:0;border-color:#26a69a}.spinner-blue,.spinner-blue-only{border-color:#4285f4}.spinner-red,.spinner-red-only{border-color:#db4437}.spinner-yellow,.spinner-yellow-only{border-color:#f4b400}.spinner-green,.spinner-green-only{border-color:#0f9d58}.active .spinner-layer.spinner-blue{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,blue-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,blue-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .spinner-layer.spinner-red{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,red-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,red-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .spinner-layer.spinner-yellow{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,yellow-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,yellow-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .spinner-layer.spinner-green{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,green-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,green-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .spinner-layer,.active .spinner-layer.spinner-blue-only,.active .spinner-layer.spinner-red-only,.active .spinner-layer.spinner-yellow-only,.active .spinner-layer.spinner-green-only{opacity:1;-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}@-webkit-keyframes fill-unfill-rotate{12.5%{-webkit-transform:rotate(135deg)}25%{-webkit-transform:rotate(270deg)}37.5%{-webkit-transform:rotate(405deg)}50%{-webkit-transform:rotate(540deg)}62.5%{-webkit-transform:rotate(675deg)}75%{-webkit-transform:rotate(810deg)}87.5%{-webkit-transform:rotate(945deg)}to{-webkit-transform:rotate(1080deg)}}@keyframes fill-unfill-rotate{12.5%{-webkit-transform:rotate(135deg);transform:rotate(135deg)}25%{-webkit-transform:rotate(270deg);transform:rotate(270deg)}37.5%{-webkit-transform:rotate(405deg);transform:rotate(405deg)}50%{-webkit-transform:rotate(540deg);transform:rotate(540deg)}62.5%{-webkit-transform:rotate(675deg);transform:rotate(675deg)}75%{-webkit-transform:rotate(810deg);transform:rotate(810deg)}87.5%{-webkit-transform:rotate(945deg);transform:rotate(945deg)}to{-webkit-transform:rotate(1080deg);transform:rotate(1080deg)}}@-webkit-keyframes blue-fade-in-out{from{opacity:1}25%{opacity:1}26%{opacity:0}89%{opacity:0}90%{opacity:1}100%{opacity:1}}@keyframes blue-fade-in-out{from{opacity:1}25%{opacity:1}26%{opacity:0}89%{opacity:0}90%{opacity:1}100%{opacity:1}}@-webkit-keyframes red-fade-in-out{from{opacity:0}15%{opacity:0}25%{opacity:1}50%{opacity:1}51%{opacity:0}}@keyframes red-fade-in-out{from{opacity:0}15%{opacity:0}25%{opacity:1}50%{opacity:1}51%{opacity:0}}@-webkit-keyframes yellow-fade-in-out{from{opacity:0}40%{opacity:0}50%{opacity:1}75%{opacity:1}76%{opacity:0}}@keyframes yellow-fade-in-out{from{opacity:0}40%{opacity:0}50%{opacity:1}75%{opacity:1}76%{opacity:0}}@-webkit-keyframes green-fade-in-out{from{opacity:0}65%{opacity:0}75%{opacity:1}90%{opacity:1}100%{opacity:0}}@keyframes green-fade-in-out{from{opacity:0}65%{opacity:0}75%{opacity:1}90%{opacity:1}100%{opacity:0}}.gap-patch{position:absolute;top:0;left:45%;width:10%;height:100%;overflow:hidden;border-color:inherit}.gap-patch .circle{width:1000%;left:-450%}.circle-clipper{display:inline-block;position:relative;width:50%;height:100%;overflow:hidden;border-color:inherit}.circle-clipper .circle{width:200%;height:100%;border-width:3px;border-style:solid;border-color:inherit;border-bottom-color:transparent !important;border-radius:50%;-webkit-animation:none;animation:none;position:absolute;top:0;right:0;bottom:0}.circle-clipper.left .circle{left:0;border-right-color:transparent !important;-webkit-transform:rotate(129deg);transform:rotate(129deg)}.circle-clipper.right .circle{left:-100%;border-left-color:transparent !important;-webkit-transform:rotate(-129deg);transform:rotate(-129deg)}.active .circle-clipper.left .circle{-webkit-animation:left-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:left-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .circle-clipper.right .circle{-webkit-animation:right-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:right-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}@-webkit-keyframes left-spin{from{-webkit-transform:rotate(130deg)}50%{-webkit-transform:rotate(-5deg)}to{-webkit-transform:rotate(130deg)}}@keyframes left-spin{from{-webkit-transform:rotate(130deg);transform:rotate(130deg)}50%{-webkit-transform:rotate(-5deg);transform:rotate(-5deg)}to{-webkit-transform:rotate(130deg);transform:rotate(130deg)}}@-webkit-keyframes right-spin{from{-webkit-transform:rotate(-130deg)}50%{-webkit-transform:rotate(5deg)}to{-webkit-transform:rotate(-130deg)}}@keyframes right-spin{from{-webkit-transform:rotate(-130deg);transform:rotate(-130deg)}50%{-webkit-transform:rotate(5deg);transform:rotate(5deg)}to{-webkit-transform:rotate(-130deg);transform:rotate(-130deg)}}#spinnerContainer.cooldown{-webkit-animation:container-rotate 1568ms linear infinite,fade-out 400ms cubic-bezier(0.4, 0, 0.2, 1);animation:container-rotate 1568ms linear infinite,fade-out 400ms cubic-bezier(0.4, 0, 0.2, 1)}@-webkit-keyframes fade-out{from{opacity:1}to{opacity:0}}@keyframes fade-out{from{opacity:1}to{opacity:0}}.slider{position:relative;height:400px;width:100%}.slider.fullscreen{height:100%;width:100%;position:absolute;top:0;left:0;right:0;bottom:0}.slider.fullscreen ul.slides{height:100%}.slider.fullscreen ul.indicators{z-index:2;bottom:30px}.slider .slides{background-color:#9e9e9e;margin:0;height:400px}.slider .slides li{opacity:0;position:absolute;top:0;left:0;z-index:1;width:100%;height:inherit;overflow:hidden}.slider .slides li img{height:100%;width:100%;background-size:cover;background-position:center}.slider .slides li .caption{color:#fff;position:absolute;top:15%;left:15%;width:70%;opacity:0}.slider .slides li .caption p{color:#e0e0e0}.slider .slides li.active{z-index:2}.slider .indicators{position:absolute;text-align:center;left:0;right:0;bottom:0;margin:0}.slider .indicators .indicator-item{display:inline-block;position:relative;cursor:pointer;height:16px;width:16px;margin:0 12px;background-color:#e0e0e0;-webkit-transition:background-color .3s;transition:background-color .3s;border-radius:50%}.slider .indicators .indicator-item.active{background-color:#4CAF50}.carousel{overflow:hidden;position:relative;width:100%;height:400px;-webkit-perspective:500px;perspective:500px;-webkit-transform-style:preserve-3d;transform-style:preserve-3d;-webkit-transform-origin:0% 50%;transform-origin:0% 50%}.carousel.carousel-slider{top:0;left:0}.carousel.carousel-slider .carousel-fixed-item{position:absolute;left:0;right:0;bottom:20px;z-index:1}.carousel.carousel-slider .carousel-fixed-item.with-indicators{bottom:68px}.carousel.carousel-slider .carousel-item{width:100%;height:100%;min-height:400px;position:absolute;top:0;left:0}.carousel.carousel-slider .carousel-item h2{font-size:24px;font-weight:500;line-height:32px}.carousel.carousel-slider .carousel-item p{font-size:15px}.carousel .carousel-item{visibility:hidden;width:200px;height:200px;position:absolute;top:0;left:0}.carousel .carousel-item>img{width:100%}.carousel .indicators{position:absolute;text-align:center;left:0;right:0;bottom:0;margin:0}.carousel .indicators .indicator-item{display:inline-block;position:relative;cursor:pointer;height:8px;width:8px;margin:24px 4px;background-color:rgba(255,255,255,0.5);-webkit-transition:background-color .3s;transition:background-color .3s;border-radius:50%}.carousel .indicators .indicator-item.active{background-color:#fff}.carousel.scrolling .carousel-item .materialboxed,.carousel .carousel-item:not(.active) .materialboxed{pointer-events:none}.tap-target-wrapper{width:800px;height:800px;position:fixed;z-index:1000;visibility:hidden;-webkit-transition:visibility 0s .3s;transition:visibility 0s .3s}.tap-target-wrapper.open{visibility:visible;-webkit-transition:visibility 0s;transition:visibility 0s}.tap-target-wrapper.open .tap-target{-webkit-transform:scale(1);transform:scale(1);opacity:.95;-webkit-transition:opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1),-webkit-transform 0.3s cubic-bezier(0.42, 0, 0.58, 1);transition:opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1),-webkit-transform 0.3s cubic-bezier(0.42, 0, 0.58, 1);transition:transform 0.3s cubic-bezier(0.42, 0, 0.58, 1),opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1);transition:transform 0.3s cubic-bezier(0.42, 0, 0.58, 1),opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1),-webkit-transform 0.3s cubic-bezier(0.42, 0, 0.58, 1)}.tap-target-wrapper.open .tap-target-wave::before{-webkit-transform:scale(1);transform:scale(1)}.tap-target-wrapper.open .tap-target-wave::after{visibility:visible;-webkit-animation:pulse-animation 1s cubic-bezier(0.24, 0, 0.38, 1) infinite;animation:pulse-animation 1s cubic-bezier(0.24, 0, 0.38, 1) infinite;-webkit-transition:opacity .3s, visibility 0s 1s, -webkit-transform .3s;transition:opacity .3s, visibility 0s 1s, -webkit-transform .3s;transition:opacity .3s, transform .3s, visibility 0s 1s;transition:opacity .3s, transform .3s, visibility 0s 1s, -webkit-transform .3s}.tap-target{position:absolute;font-size:1rem;border-radius:50%;background-color:#ee6e73;-webkit-box-shadow:0 20px 20px 0 rgba(0,0,0,0.14),0 10px 50px 0 rgba(0,0,0,0.12),0 30px 10px -20px rgba(0,0,0,0.2);box-shadow:0 20px 20px 0 rgba(0,0,0,0.14),0 10px 50px 0 rgba(0,0,0,0.12),0 30px 10px -20px rgba(0,0,0,0.2);width:100%;height:100%;opacity:0;-webkit-transform:scale(0);transform:scale(0);-webkit-transition:opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1),-webkit-transform 0.3s cubic-bezier(0.42, 0, 0.58, 1);transition:opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1),-webkit-transform 0.3s cubic-bezier(0.42, 0, 0.58, 1);transition:transform 0.3s cubic-bezier(0.42, 0, 0.58, 1),opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1);transition:transform 0.3s cubic-bezier(0.42, 0, 0.58, 1),opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1),-webkit-transform 0.3s cubic-bezier(0.42, 0, 0.58, 1)}.tap-target-content{position:relative;display:table-cell}.tap-target-wave{position:absolute;border-radius:50%;z-index:10001}.tap-target-wave::before,.tap-target-wave::after{content:'';display:block;position:absolute;width:100%;height:100%;border-radius:50%;background-color:#ffffff}.tap-target-wave::before{-webkit-transform:scale(0);transform:scale(0);-webkit-transition:-webkit-transform .3s;transition:-webkit-transform .3s;transition:transform .3s;transition:transform .3s, -webkit-transform .3s}.tap-target-wave::after{visibility:hidden;-webkit-transition:opacity .3s, visibility 0s, -webkit-transform .3s;transition:opacity .3s, visibility 0s, -webkit-transform .3s;transition:opacity .3s, transform .3s, visibility 0s;transition:opacity .3s, transform .3s, visibility 0s, -webkit-transform .3s;z-index:-1}.tap-target-origin{top:50%;left:50%;-webkit-transform:translate(-50%, -50%);transform:translate(-50%, -50%);z-index:10002;position:absolute !important}.tap-target-origin:not(.btn):not(.btn-large):not(.btn-small),.tap-target-origin:not(.btn):not(.btn-large):not(.btn-small):hover{background:none}@media only screen and (max-width: 600px){.tap-target,.tap-target-wrapper{width:600px;height:600px}}.pulse{overflow:visible;position:relative}.pulse::before{content:'';display:block;position:absolute;width:100%;height:100%;top:0;left:0;background-color:inherit;border-radius:inherit;-webkit-transition:opacity .3s, -webkit-transform .3s;transition:opacity .3s, -webkit-transform .3s;transition:opacity .3s, transform .3s;transition:opacity .3s, transform .3s, -webkit-transform .3s;-webkit-animation:pulse-animation 1s cubic-bezier(0.24, 0, 0.38, 1) infinite;animation:pulse-animation 1s cubic-bezier(0.24, 0, 0.38, 1) infinite;z-index:-1}@-webkit-keyframes pulse-animation{0%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}50%{opacity:0;-webkit-transform:scale(1.5);transform:scale(1.5)}100%{opacity:0;-webkit-transform:scale(1.5);transform:scale(1.5)}}@keyframes pulse-animation{0%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}50%{opacity:0;-webkit-transform:scale(1.5);transform:scale(1.5)}100%{opacity:0;-webkit-transform:scale(1.5);transform:scale(1.5)}}.datepicker-modal{max-width:325px;min-width:300px;max-height:none}.datepicker-container.modal-content{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;padding:0}.datepicker-controls{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;width:280px;margin:0 auto}.datepicker-controls .selects-container{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex}.datepicker-controls .select-wrapper input{border-bottom:none;text-align:center;margin:0}.datepicker-controls .select-wrapper input:focus{border-bottom:none}.datepicker-controls .select-wrapper .caret{display:none}.datepicker-controls .select-year input{width:50px}.datepicker-controls .select-month input{width:70px}.month-prev,.month-next{margin-top:4px;cursor:pointer;background-color:transparent;border:none}.datepicker-date-display{-webkit-box-flex:1;-webkit-flex:1 auto;-ms-flex:1 auto;flex:1 auto;background-color:#26a69a;color:#fff;padding:20px 22px;font-weight:500}.datepicker-date-display .year-text{display:block;font-size:1.5rem;line-height:25px;color:rgba(255,255,255,0.7)}.datepicker-date-display .date-text{display:block;font-size:2.8rem;line-height:47px;font-weight:500}.datepicker-calendar-container{-webkit-box-flex:2.5;-webkit-flex:2.5 auto;-ms-flex:2.5 auto;flex:2.5 auto}.datepicker-table{width:280px;font-size:1rem;margin:0 auto}.datepicker-table thead{border-bottom:none}.datepicker-table th{padding:10px 5px;text-align:center}.datepicker-table tr{border:none}.datepicker-table abbr{text-decoration:none;color:#999}.datepicker-table td{border-radius:50%;padding:0}.datepicker-table td.is-today{color:#26a69a}.datepicker-table td.is-selected{background-color:#26a69a;color:#fff}.datepicker-table td.is-outside-current-month,.datepicker-table td.is-disabled{color:rgba(0,0,0,0.3);pointer-events:none}.datepicker-day-button{background-color:transparent;border:none;line-height:38px;display:block;width:100%;border-radius:50%;padding:0 5px;cursor:pointer;color:inherit}.datepicker-day-button:focus{background-color:rgba(43,161,150,0.25)}.datepicker-footer{width:280px;margin:0 auto;padding-bottom:5px;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between}.datepicker-cancel,.datepicker-clear,.datepicker-today,.datepicker-done{color:#26a69a;padding:0 1rem}.datepicker-clear{color:#F44336}@media only screen and (min-width: 601px){.datepicker-modal{max-width:625px}.datepicker-container.modal-content{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.datepicker-date-display{-webkit-box-flex:0;-webkit-flex:0 1 270px;-ms-flex:0 1 270px;flex:0 1 270px}.datepicker-controls,.datepicker-table,.datepicker-footer{width:320px}.datepicker-day-button{line-height:44px}}.timepicker-modal{max-width:325px;max-height:none}.timepicker-container.modal-content{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;padding:0}.text-primary{color:#fff}.timepicker-digital-display{-webkit-box-flex:1;-webkit-flex:1 auto;-ms-flex:1 auto;flex:1 auto;background-color:#26a69a;padding:10px;font-weight:300}.timepicker-text-container{font-size:4rem;font-weight:bold;text-align:center;color:rgba(255,255,255,0.6);font-weight:400;position:relative;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.timepicker-span-hours,.timepicker-span-minutes,.timepicker-span-am-pm div{cursor:pointer}.timepicker-span-hours{margin-right:3px}.timepicker-span-minutes{margin-left:3px}.timepicker-display-am-pm{font-size:1.3rem;position:absolute;right:1rem;bottom:1rem;font-weight:400}.timepicker-analog-display{-webkit-box-flex:2.5;-webkit-flex:2.5 auto;-ms-flex:2.5 auto;flex:2.5 auto}.timepicker-plate{background-color:#eee;border-radius:50%;width:270px;height:270px;overflow:visible;position:relative;margin:auto;margin-top:25px;margin-bottom:5px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.timepicker-canvas,.timepicker-dial{position:absolute;left:0;right:0;top:0;bottom:0}.timepicker-minutes{visibility:hidden}.timepicker-tick{border-radius:50%;color:rgba(0,0,0,0.87);line-height:40px;text-align:center;width:40px;height:40px;position:absolute;cursor:pointer;font-size:15px}.timepicker-tick.active,.timepicker-tick:hover{background-color:rgba(38,166,154,0.25)}.timepicker-dial{-webkit-transition:opacity 350ms, -webkit-transform 350ms;transition:opacity 350ms, -webkit-transform 350ms;transition:transform 350ms, opacity 350ms;transition:transform 350ms, opacity 350ms, -webkit-transform 350ms}.timepicker-dial-out{opacity:0}.timepicker-dial-out.timepicker-hours{-webkit-transform:scale(1.1, 1.1);transform:scale(1.1, 1.1)}.timepicker-dial-out.timepicker-minutes{-webkit-transform:scale(0.8, 0.8);transform:scale(0.8, 0.8)}.timepicker-canvas{-webkit-transition:opacity 175ms;transition:opacity 175ms}.timepicker-canvas line{stroke:#26a69a;stroke-width:4;stroke-linecap:round}.timepicker-canvas-out{opacity:0.25}.timepicker-canvas-bearing{stroke:none;fill:#26a69a}.timepicker-canvas-bg{stroke:none;fill:#26a69a}.timepicker-footer{margin:0 auto;padding:5px 1rem;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between}.timepicker-clear{color:#F44336}.timepicker-close{color:#26a69a}.timepicker-clear,.timepicker-close{padding:0 20px}@media only screen and (min-width: 601px){.timepicker-modal{max-width:600px}.timepicker-container.modal-content{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.timepicker-text-container{top:32%}.timepicker-display-am-pm{position:relative;right:auto;bottom:auto;text-align:center;margin-top:1.2rem}} diff --git a/hassio-google-drive-backup/backup/static/css/spinner.css b/hassio-google-drive-backup/backup/static/css/spinner.css new file mode 100644 index 0000000..f22159a --- /dev/null +++ b/hassio-google-drive-backup/backup/static/css/spinner.css @@ -0,0 +1,216 @@ +/* css-loading-spinners.css + Copyright (c) Ian A. Cook + MIT License + https://github.com/nai888/css-loading-spinners +*/ + +:root { + --cls-color: #558B6E; + --cls-sec-color: #EEEEEE; + --cls-size: 5rem; + --cls-margin: 1rem; + --cls-speed: 2s; + } + + @-webkit-keyframes spin { + 0% { + -webkit-transform: rotate(0deg); + } 100% { + -webkit-transform: rotate(360deg); + } + } + + @keyframes spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } + } + + @-webkit-keyframes flip-flop { + 0%, 100% { + -webkit-transform: perspective(calc(var(--cls-size) / 5 * 8)) rotateY(0deg); + } 50% { + -webkit-transform: perspective(calc(var(--cls-size) / 5 * 8)) rotateY(180deg); + } + } + + @keyframes flip-flop { + 0%, 100% { + -webkit-transform: perspective(calc(var(--cls-size) / 5 * 8)) rotateY(0deg); + transform: perspective(calc(var(--cls-size) / 5 * 8)) rotateY(0deg); + } 50% { + -webkit-transform: perspective(calc(var(--cls-size) / 5 * 8)) rotateY(180deg); + transform: perspective(calc(var(--cls-size) / 5 * 8)) rotateY(180deg); + } + } + + @-webkit-keyframes signal { + 0% { + -webkit-transform: scale(0); + -webkit-opacity: 0; + } 50% { + -webkit-opacity: 1; + } 100% { + -webkit-transform: scale(1); + -webkit-opacity: 0; + } + } + + @keyframes signal { + 0% { + -webkit-transform: scale(0); + transform: scale(0); + -webkit-opacity: 0; + opacity: 0; + } 50% { + -webkit-opacity: 1; + opacity: 1; + } 100% { + -webkit-transform: scale(1); + transform: scale(1); + -webkit-opacity: 0; + opacity: 0; + } + } + + @-webkit-keyframes grow-shrink { + 0%, 100% { + -webkit-transform: scale(0); + } 50% { + -webkit-transform: scale(1); + } + } + + @keyframes grow-shrink { + 0%, 100% { + -webkit-transform: scale(0); + transform: scale(0); + } 50% { + -webkit-transform: scale(1); + transform: scale(1); + } + } + + [class^="cls-"] { + box-sizing: border-box; + } + + .cls-spinner { + display: block; + position: relative; + width: var(--cls-size); + height: var(--cls-size); + margin: var(--cls-margin); + } + + .cls-flip-flop { + -webkit-animation: flip-flop calc(var(--cls-speed) / 2) ease infinite; + animation: flip-flop calc(var(--cls-speed) / 2) ease infinite; + } + + .cls-spin { + -webkit-animation: spin var(--cls-speed) linear infinite; + animation: spin var(--cls-speed) linear infinite; + } + + .cls-circle { + border-width: calc(var(--cls-size) / 5 * 0.75); + border-style: solid; + border-color: var(--cls-sec-color); + border-top-color: var(--cls-color); + border-radius: 50%; + width: 100%; + height: 100%; + } + + .cls-dual-circle { + border-width: calc(var(--cls-size) / 5 * 0.75); + border-style: solid; + border-color: var(--cls-color) transparent; + border-radius: 50%; + width: 100%; + height: 100%; + } + + .cls-bowtie, .cls-bowtie-v { + border-width: calc(var(--cls-size) / 2); + border-style: solid; + border-color: transparent var(--cls-color); + border-radius: 50%; + } + + .cls-bowtie-v { + border-color: var(--cls-color) transparent; + } + + .cls-square { + width: 100%; + height: 100%; + background-color: var(--cls-color); + } + + .cls-signal, .cls-triple-signal, .cls-triple-signal::before, .cls-triple-signal::after { + border-width: calc(var(--cls-size) / 25); + border-style: solid; + border-color: var(--cls-color); + border-radius: 50%; + width: 100%; + height: 100%; + opacity: 0; + position: absolute; + } + + .cls-triple-signal::before, .cls-triple-signal::after, .cls-rings::before, .cls-rings::after { + content: ''; + top: 50%; + left: 50%; + margin: calc(var(--cls-size) / -2) 0 0 calc(var(--cls-size) / -2); + } + + .cls-signal, .cls-triple-signal, .cls-triple-signal::before, .cls-triple-signal::after { + -webkit-animation: signal calc(var(--cls-speed) / 2) ease-out infinite; + animation: signal calc(var(--cls-speed) / 2) ease-out infinite; + } + + .cls-triple-signal::before { + -webkit-animation-delay: calc(var(--cls-speed) / 20); + animation-delay: calc(var(--cls-speed) / 20); + } + + .cls-triple-signal::after { + -webkit-animation-delay: calc(var(--cls-speed) * 3 / 20); + animation-delay: calc(var(--cls-speed) * 3 / 20); + } + + .cls-ring, .cls-rings, .cls-rings::before, .cls-rings::after { + border-width: calc(var(--cls-size) / 25); + border-style: solid; + border-color: var(--cls-color); + border-radius: 50%; + width: 100%; + height: 100%; + } + + .cls-rings, .cls-rings::before, .cls-rings::after { + position: absolute; + } + + .cls-ring, .cls-rings, .cls-rings::before, .cls-rings::after { + -webkit-animation: grow-shrink var(--cls-speed) ease-in-out infinite; + animation: grow-shrink var(--cls-speed) ease-in-out infinite; + } + + .cls-rings::before { + -webkit-animation-delay: calc(var(--cls-speed) / -4); + animation-delay: calc(var(--cls-speed) / -4); + } + + .cls-rings::after { + -webkit-animation-delay: calc(var(--cls-speed) / 4); + animation-delay: calc(var(--cls-speed) / 4); + } + \ No newline at end of file diff --git a/hassio-google-drive-backup/backup/static/css/static.css b/hassio-google-drive-backup/backup/static/css/static.css new file mode 100644 index 0000000..1413c87 --- /dev/null +++ b/hassio-google-drive-backup/backup/static/css/static.css @@ -0,0 +1,590 @@ +body { + display: flex; + min-height: 100vh; + flex-direction: column; +} + +main { + flex: 1 0 auto; +} + +html { + background-color: var(--background-primary-color); + color: var(--text-primary-color); +} + +label { + color: var(--text-primary-color); +} + +a { + color: var(--accent-link-color); +} + +input { + color: var(--text-primary-color); +} + +i.material-icons.active, input:focus + label { + color: var(--accent-focus-color) !important; +} + +i.material-icons.active, textarea:focus + label { + color: var(--accent-focus-color) !important; +} + +input:focus { + border-bottom: 1px solid var(--accent-focus-color) !important; +} + +.helper-text { + color: var(--helper-text-color) !important; +} + +.sidenav, +.card, +.dropdown-content { + background-color: var(--background-color); +} + +.dropdown-content li:hover, +.dropdown-content li.active { + background-color: var(--background-hover); +} + +nav { + background-color: var(--background-color); + border-bottom: 1px solid var(--divider-color); +} + +nav .brand-logo { + color: var(--text-secondary-color); +} + +nav ul a { + color: var(--text-secondary-color); +} + +nav a i, +nav ul a i, +.sidenav li > a > i.material-icons { + color: var(--icon-color); +} + +.modal, +.modal .modal-footer { + background-color: var(--background-modal-color); +} + +.modal { + box-shadow: 0 24px 38px 3px var(--shadow-color), 0 9px 46px 8px var(--shadow-color), 0 11px 15px -7px var(--shadow-color); +} + +.card, .dropdown-content { + box-shadow: 0 2px 2px 0 var(--shadow-color), 0 3px 1px -2px var(--shadow-color), 0 1px 5px 0 var(--shadow-color); + +} + +.dropdown-content li > a, +.dropdown-content li > span, +.sidenav li > a { + color: var(--text-secondary-color); +} + +[type="checkbox"].filled-in:checked + span:not(.lever):after { + border: 2px solid var(--accent-color); + background-color: var(--accent-color); +} + +.source-icon { + margin-right: 6px; + fill: var(--accent-color); + width: 22px; + height: 22px; + vertical-align: middle; +} + +.icon-block .material-icons { + font-size: inherit; +} + +.indented-li { + padding-left: 15px; + padding-top: 5px; + padding-bottom: 5px; +} + +.important-inline { + display: inline !important; +} + +.detail-node { + margin-top: 0px; + margin-bottom: 5px; +} + +.top-btn { + box-shadow: none; + -webkit-box-shadow: none; + margin-right: 10px; +} + +.icon-warn-delete { + color: var(--icon-warn); +} + +.input-field > label { + color: var(--input-label); +} + +.default-hidden { + display: none; +} + +.console-error { + color: #b30000; +} + +.console-warning { + color: #a55d00; +} + +.console-default { + color: #ffffff; +} + +.console-debug { + color: #3141d1; +} + +.console-trace { + color: #282828; +} + +.console-critical { + color: #810000; +} + +.colpick { + z-index: 9999; +} + +#toast-container { + top: auto !important; + left: auto !important; + bottom: 10%; + right: 7%; +} + +nav { + box-shadow: none; +} + +nav .brand-logo { + display: flex; + align-items: center; + white-space: nowrap; + height: 100%; +} + +nav .brand-logo img { + margin-left: 1rem; + margin-right: 1rem; +} + +.nav-wrapper { + height: 56px; +} + +.sidenav-header { + display: flex; + align-items: center; + flex-direction: column; + background-color: var(--background-sidenav-color); + padding-top: 1rem; + padding-bottom: 1rem; + margin-bottom: 1rem; +} + +.modal { + outline: none; +} + +.choose-folder-container { + padding: 5px 15px; + margin: 10px 0; + border-radius: 5px; +} + +.snapshot-card { + padding-top: 24px; + padding-right: 24px; + padding-left: 24px; + height: 145px; + border-radius: 0 0 2px 2px; +} + +.copyright { + height: 100%; + display: flex; + align-items: center; + padding: 0 18px; +} + +.backup-ui .archive-icon { + position: relative; + margin-right: 18px; + top: -10px; +} + +.tabs { + background-color: transparent !important; +} + +.page-footer { + padding: 10px !important; + color: var(--text-secondary-color) !important; + background-color: var(--background-color); + border-top: 1px solid var(--divider-color); +} + +.bmc-button { + display: inline-flex; + align-items: center; + height: 30px; + text-decoration: none; + color: var(--accent-text-color); + background-color: var(--accent-color); + border-radius: 5px; + border: 1px solid transparent; + padding: 5px; + font-family: Cookie, cursive; + font-size: 20px; + min-width: 128px; + white-space: nowrap; +} + +.bmc-button svg { + height: 22px; +} + +.bmc-cup { + fill: var(--accent-dark) +} + +.bmc-outline { + fill: var(--accent-color) +} +.btn-floating { + background-color: var(--accent-color); + color: var(--accent-text-color); +} + +.btn-floating:hover { + background-color: var(--accent-color); +} + +#contributors-simple a { + margin-right: 8px; + margin-bottom: 8px; +} + +#contributors-extra .contributor-img-wrapper a { + display: flex; + margin-right: 8px; +} + +#contributors a img { + width: 32px; + height: 32px; + border-radius: 9999px; +} + +#contributors-count { + font-size: 17px; + background-color: var(--divider-color); + padding: 2px 8px; + border-radius: 9999px; + font-weight: 500; +} + +.contributor-extra { + display: flex; + align-items: center; + margin-bottom: 16px; +} + +.truncate-two-lines { + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.divider { + background-color: var(--divider-color); +} + +textarea { + background-color: var(--background-primary-color); + color: var(--text-primary-color); +} + +.card .card-action a:not(.btn):not(.btn-large):not(.btn-small):not(.btn-large):not(.btn-floating) { + color: var(--accent-color); +} + +.card .card-action a:not(.btn):not(.btn-large):not(.btn-small):not(.btn-large):not(.btn-floating):hover { + color: var(--accent-hover-color); +} + +.btn-flat { + background-color: transparent; + color: var(--accent-color); + font-weight: 600; + -webkit-transition: all .3s ease; + transition: all .3s ease; + text-transform: uppercase; +} + +.btn-flat:hover { + color: var(--accent-hover-color); + background-color: var(--accent-bg-hover-color); +} + +.btn-flat:focus { + color: var(--accent-hover-color); + background-color: var(--accent-bg-hover-color); +} + +.btn-high-vis { + color: var(--accent-hover-color); + background-color: var(--accent-bg-hover-color); +} + +.btn-high-vis:hover { + color: var(--accent-hover-color); + background-color: var(--accent-bg-hover-color); +} + +.danger-btn { + color: var(--danger-text); +} + +.danger-btn:hover { + color: var(--danger-bg); + background-color: var(--danger-text); +} + +.card .card-action .danger-btn a:not(.btn):not(.btn-large):not(.btn-small):not(.btn-large):not(.btn-floating) { + color: var(--danger-text); +} + +.high-vis-danger-btn { + color: var(--danger-bg); + background-color: var(--danger-text); +} + +.high-vis-danger-btn:hover { + color: var(--danger-bg); + background-color: var(--danger-text); +} + +.danger-text { + color: var(--danger-text); +} + +.blue-icon { + color: var(--blue-icon); +} + +.card .card-action { + padding: 8px 24px; +} + +.card-title > .material-icons { + font-size: 50px; + padding-right: 10px; +} + +.card-title > span { + vertical-align: top; +} + +.inline-icon { + display: inline; + margin-right: 2px; + vertical-align: middle; + font-size: 15px; +} + +.btn-flat .material-icons { + display: inline; + margin-right: 5px; + vertical-align: middle; + font-size: 20px; + padding-bottom: 3px; +} + +.detail-badge { + margin: 8px 8px; + white-space: nowrap; + align-content: center; +} + +.detail-badge .material-icons { + display: inline; + margin-right: 2px; + vertical-align: bottom; +} + +.spinner-layer { + border-color: var(--accent-color); +} + +.preloader-wrapper.inline { + width: 16px; + height: 16px; + vertical-align: middle; +} + +.sub-detail { + font-size: 12px; +} + +.sub-detail .material-icons { + font-size: 16px; + vertical-align: text-bottom; +} + +.progress .determinate { + background-color: var(--accent-color); +} + +.progress .indeterminate { + background-color: var(--accent-color); +} + +.progress { + background: var(--progress-bg); +} + +.indent { + margin-left: 36px; +} + +blockquote { + border-left: 5px solid var(--accent-color); +} + +.content-logo { + max-height: 2rem; + margin-right: 9px; +} + +span.content-logo > i { + font-size: 2rem; +} + +.sub { + color: var(--input-label); + font-size: 12px; +} + +.content-icon { + margin-right: 9px; + fill: var(--text-primary-color); + width: 2rem; + height: 2rem; + vertical-align: middle; +} + +.detail-badge { + line-height: 1; +} + +.detail-name { + line-height: 1.2; +} + +.backup-card-icon { + vertical-align: -2px; + font-size: 1rem; +} + +.backup-help-icon { + vertical-align: 3px; + display: inline; + font-size: 0.75rem; + margin-left: 2px; +} + +input.valid:not([type]), input.valid:not([type]):focus, input[type="text"].valid:not(.browser-default), input[type="text"].valid:not(.browser-default):focus, input[type="password"].valid:not(.browser-default), input[type="password"].valid:not(.browser-default):focus, input[type="email"].valid:not(.browser-default), input[type="email"].valid:not(.browser-default):focus, input[type="url"].valid:not(.browser-default), input[type="url"].valid:not(.browser-default):focus, input[type="time"].valid:not(.browser-default), input[type="time"].valid:not(.browser-default):focus, input[type="date"].valid:not(.browser-default), input[type="date"].valid:not(.browser-default):focus, input[type="datetime"].valid:not(.browser-default), input[type="datetime"].valid:not(.browser-default):focus, input[type="datetime-local"].valid:not(.browser-default), input[type="datetime-local"].valid:not(.browser-default):focus, input[type="tel"].valid:not(.browser-default), input[type="tel"].valid:not(.browser-default):focus, input[type="number"].valid:not(.browser-default), input[type="number"].valid:not(.browser-default):focus, input[type="search"].valid:not(.browser-default), input[type="search"].valid:not(.browser-default):focus, textarea.materialize-textarea.valid, textarea.materialize-textarea.valid:focus, .select-wrapper.valid > input.select-dropdown { + border-bottom: 1px solid var(--accent-color); + -webkit-box-shadow: 0 1px 0 0 var(--accent-color); + box-shadow: 0 1px 0 0 var(--accent-color); +} + +input:not([type]):focus:not([readonly]), input[type="text"]:not(.browser-default):focus:not([readonly]), input[type="password"]:not(.browser-default):focus:not([readonly]), input[type="email"]:not(.browser-default):focus:not([readonly]), input[type="url"]:not(.browser-default):focus:not([readonly]), input[type="time"]:not(.browser-default):focus:not([readonly]), input[type="date"]:not(.browser-default):focus:not([readonly]), input[type="datetime"]:not(.browser-default):focus:not([readonly]), input[type="datetime-local"]:not(.browser-default):focus:not([readonly]), input[type="tel"]:not(.browser-default):focus:not([readonly]), input[type="number"]:not(.browser-default):focus:not([readonly]), input[type="search"]:not(.browser-default):focus:not([readonly]), textarea.materialize-textarea:focus:not([readonly]) { + border-bottom: 1px solid var(--accent-color); + -webkit-box-shadow: 0 1px 0 0 var(--accent-color); + box-shadow: 0 1px 0 0 var(--accent-color); +} + +.toast .toast-action { + color: var(--accent-color); + font-weight: 600; + margin-right: 0px; +} + +.toast .toast-action:hover { + color: var(--accent-hover-color); + background-color: var(--accent-bg-hover-color); +} + +.device-code-box { + font-size: 2em; + width: auto; + border-color: var(--accent-text-color); + color: var(--accent-text-color); + background: var(--accent-color); + border-radius: 6px; + padding: 6px; + margin: 6px; +} + +.card .card-content .card-title.mini-title { + font-size: 18px; +} + +.device-code-box:hover { + background-color: var(--accent-bg-hover-color); +} + +.qr-code { + padding: 5px; + background-color: white; + display: inline-block; +} + +.title-logo { + position: relative; + width: 40px; + height: 40px; + top: 8px; + margin-right: 10px; +} + +.flex-wrap { + display: flex; + flex-wrap: wrap; +} + +.donate-button { + margin: 7px; +} + +.donate-button img { + width: 150px; + height: 40px; +} \ No newline at end of file diff --git a/hassio-google-drive-backup/backup/static/images/bitcoin-button.svg b/hassio-google-drive-backup/backup/static/images/bitcoin-button.svg new file mode 100644 index 0000000..348bcc3 --- /dev/null +++ b/hassio-google-drive-backup/backup/static/images/bitcoin-button.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/hassio-google-drive-backup/backup/static/images/bitcoin-logo.svg b/hassio-google-drive-backup/backup/static/images/bitcoin-logo.svg new file mode 100644 index 0000000..16eb504 --- /dev/null +++ b/hassio-google-drive-backup/backup/static/images/bitcoin-logo.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/hassio-google-drive-backup/backup/static/images/bmc-button.svg b/hassio-google-drive-backup/backup/static/images/bmc-button.svg new file mode 100644 index 0000000..4754bd3 --- /dev/null +++ b/hassio-google-drive-backup/backup/static/images/bmc-button.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/hassio-google-drive-backup/backup/static/images/bmc.svg b/hassio-google-drive-backup/backup/static/images/bmc.svg new file mode 100644 index 0000000..4a1af88 --- /dev/null +++ b/hassio-google-drive-backup/backup/static/images/bmc.svg @@ -0,0 +1,26 @@ + + + + + + + + \ No newline at end of file diff --git a/hassio-google-drive-backup/backup/static/images/drive_logo.png b/hassio-google-drive-backup/backup/static/images/drive_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..f555103cc30338c4205bf1a1c1081c316fc2ae44 GIT binary patch literal 312335 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 j2AGA5H*Uzn60&9H@$9v4$hXt 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

z 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 z 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 literal 0 HcmV?d00001 diff --git a/hassio-google-drive-backup/backup/static/images/ethereum-button.svg b/hassio-google-drive-backup/backup/static/images/ethereum-button.svg new file mode 100644 index 0000000..09c4ee1 --- /dev/null +++ b/hassio-google-drive-backup/backup/static/images/ethereum-button.svg @@ -0,0 +1,37 @@ + + ethereum-eth-logo-full-horizontal + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/hassio-google-drive-backup/backup/static/images/ethereum-logo.svg b/hassio-google-drive-backup/backup/static/images/ethereum-logo.svg new file mode 100644 index 0000000..7ffd694 --- /dev/null +++ b/hassio-google-drive-backup/backup/static/images/ethereum-logo.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/hassio-google-drive-backup/backup/static/images/favicon.png b/hassio-google-drive-backup/backup/static/images/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..107f2b9e2b1e8c93a1fb9334e2c532754c78588b GIT binary patch literal 7258 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 literal 0 HcmV?d00001 diff --git a/hassio-google-drive-backup/backup/static/images/github-sponsors-button.svg b/hassio-google-drive-backup/backup/static/images/github-sponsors-button.svg new file mode 100644 index 0000000..b48d876 --- /dev/null +++ b/hassio-google-drive-backup/backup/static/images/github-sponsors-button.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/hassio-google-drive-backup/backup/static/images/hassio_logo.png b/hassio-google-drive-backup/backup/static/images/hassio_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..4b7c32bcecb926cbebf5d82d42c07160477a53c9 GIT binary patch literal 4215 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 literal 0 HcmV?d00001 diff --git a/hassio-google-drive-backup/backup/static/images/logo.png b/hassio-google-drive-backup/backup/static/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..9df76d4cb864da48741fb4ec8e651cb965418960 GIT binary patch literal 9567 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 + + \ No newline at end of file diff --git a/hassio-google-drive-backup/backup/static/images/monero-logo.svg b/hassio-google-drive-backup/backup/static/images/monero-logo.svg new file mode 100644 index 0000000..6dd7e07 --- /dev/null +++ b/hassio-google-drive-backup/backup/static/images/monero-logo.svg @@ -0,0 +1 @@ +monero \ No newline at end of file diff --git a/hassio-google-drive-backup/backup/static/images/patreon-button.svg b/hassio-google-drive-backup/backup/static/images/patreon-button.svg new file mode 100644 index 0000000..7840b9a --- /dev/null +++ b/hassio-google-drive-backup/backup/static/images/patreon-button.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/hassio-google-drive-backup/backup/static/images/patreon.png b/hassio-google-drive-backup/backup/static/images/patreon.png new file mode 100644 index 0000000000000000000000000000000000000000..c8b041d7a253e31235804f14c8036a23abf6ab98 GIT binary patch literal 3364 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 + + + +paypal-seeklogo.com + + + + + + + + + \ No newline at end of file diff --git a/hassio-google-drive-backup/backup/static/images/pp.png b/hassio-google-drive-backup/backup/static/images/pp.png new file mode 100644 index 0000000000000000000000000000000000000000..3e0dcbe6bb0a635b3f2b72bc10db3b5c1cc0b1ad GIT binary patch literal 1730 zcmV;z20i(SP)Px*d`Uz>RA_)*4J2w5&(Hu~ z1dJ#q)Oh@XkbnU(2%-=aqbNi$f;9mYG$CpaR0PCu^#E#V%eMRc;)+rH%ELb%F2)I~dK0PrMf!tn@k^+(NOd?@?{hY!y z0nY$afQ7i@0?_c>{PFm&6LzjR49vjog{M8l9m|`1C?g{a zWAY^1I{+G@za<{%xn_ydz8p!EDvFUq`VzQS`<4rgM=By!k&BV!e3eO00G0vu3eP;? zQH9YJ4@msQG0BeBTv~M})UXEeEgp?r)(T%dyTC^uR&g31bX&6JUhv7rr z-%dr}i$Bl-U6`?mBDx^225ON}3j7M}GI<=5B9@u1^_QA|KL%d1BuA>c^(H+Y=i#nL zlz5LeS1&}7X~1wmtw`&DmB1?8%CKYu=d)Cq`By!I24HWKX`w1L08C)=1)f*Qf1A?sf?AtOeCC)20lZq)Pl9?)z}IFxDz?} zj>tX3LZ!Kmf!lj~n?EnS0F1!t(L0f_x(o5z%4pAkhEh;^yf{|WBIs@`Cuz#fHO3A|>udmEF+trAndEZEQScfOzU5;4+gQhvbjdh#^fy%3ursAWHT; zgAiV?w3!`TT>u=cn2NZ!AmF~-`-C16$~?Jj>_-yiAd}8U9`@p)#G~&ci*WBm8XoDdIoi>31J5 zuidWEB{)Nuh2|Oyz^lNYalYxYS;nHSvmZDK;vw!Qi7b!&QCrtYPWK8Q!X?ILzQs<; z)wmDK3qJuj0M8+w5=IVd8L$_rumVV7@Rc?;47{n(Q6!A$$rdEEePFg@NI9JdmB3(N zI&#lR0}djE!W!T$B+oS=FV_IyU>71At%(BU7cB*PIHtdU%xJoIb}H&fRRz%5hj14k zR&Ojq?kpB&;SOIH1wzPW$ce0U_ci@w{ekl!Ps?Ln@d%vvldU+t<)VVSlqxJ}gV#l| zu3^i$7w4Q{5%%*%4RWDux^Uv08E%CvSk=JLeko*U*d07*qoM6N<$g8ptQ6#xJL literal 0 HcmV?d00001 diff --git a/hassio-google-drive-backup/backup/static/index.jinja2 b/hassio-google-drive-backup/backup/static/index.jinja2 new file mode 100644 index 0000000..1452f90 --- /dev/null +++ b/hassio-google-drive-backup/backup/static/index.jinja2 @@ -0,0 +1,390 @@ +{% extends "layouts/base.jinja2" %} + +{% block head %} + {{ super() }} +{% endblock %} + +{% block content %} +

+
+
+

+

Get Started

+
+
To begin, this add-on will need access to your Google Drive™. + On your first backup it will create a new folder at the root of your My Drive where future backups will be stored. Once + its created, you can move the folder wherever you want. + After connecting with Google Drive, you'll be prompted to select your + backup folder. +
+
+
+ +
+
+
+
+
+
+ +
+
+ + Authenticate with Google Drive + +
+
+
    +
  • + Clicking this link authenticates you with an external domain maintained by the developer of this add-on (habackup.io). +
  • +
  • You can read this to understand what this means and why it's necessary. +
  • +
  • + By doing this you must agree to the Privacy Policy and Terms of Service
+ +
  • + The addon will only have access to the files and folders it creates. It can't see any of the other files you've added to Google Drive. +
  • + + + +
    +
    + + After you get the authorization code from the link above, paste the result here: +
    + +
    + + + Save +
    +
    +
    + To generate your own private credentials with Google instead that doesn't use a third-party domain, click here. Fair warning: that way is much more complicated. +
    + + +
    +
    + Follow the instructions + here to get your own client ID and client secret from Google's Cloud Console. This is a much more complicated and time + consuming approach but doesn't require you to authenticate yourself with an external domain. +
    +
    +
    +
    + vpn_key + + +
    +
    +
    +
    + vpn_key + + +
    +
    +
    + +
    +
    +
    +
    + +
    + +

    + + + +{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/hassio-google-drive-backup/backup/static/js/URI.js b/hassio-google-drive-backup/backup/static/js/URI.js new file mode 100644 index 0000000..cbad463 --- /dev/null +++ b/hassio-google-drive-backup/backup/static/js/URI.js @@ -0,0 +1,93 @@ +/*! URI.js v1.19.2 http://medialize.github.io/URI.js/ */ +/* build contains: IPv6.js, punycode.js, SecondLevelDomains.js, URI.js, URITemplate.js */ +(function(f,n){"object"===typeof module&&module.exports?module.exports=n():"function"===typeof define&&define.amd?define(n):f.IPv6=n(f)})(this,function(f){var n=f&&f.IPv6;return{best:function(h){h=h.toLowerCase().split(":");var k=h.length,b=8;""===h[0]&&""===h[1]&&""===h[2]?(h.shift(),h.shift()):""===h[0]&&""===h[1]?h.shift():""===h[k-1]&&""===h[k-2]&&h.pop();k=h.length;-1!==h[k-1].indexOf(".")&&(b=7);var q;for(q=0;qf;f++)if("0"===k[0]&&1f&&(k=g,f=n)):"0"===h[q]&&(p=!0,g=q,n=1);n>f&&(k=g,f=n);1=f&&g>>10&1023|55296),b=56320|b&1023);return e+=t(b)}).join("")}function z(b,e){return b+22+75*(26>b)-((0!=e)<<5)}function u(b,g,h){var f=0;b=h?e(b/700):b>>1;for(b+=e(b/g);455l&&(l=0);for(c=0;c=h&&n("invalid-input");var x=b.charCodeAt(l++); +x=10>x-48?x-22:26>x-65?x-65:26>x-97?x-97:36;(36<=x||x>e((2147483647-f)/m))&&n("overflow");f+=x*m;var p=d<=a?1:d>=a+26?26:d-a;if(xe(2147483647/x)&&n("overflow");m*=x}m=g.length+1;a=u(f-c,m,0==c);e(f/m)>2147483647-k&&n("overflow");k+=e(f/m);f%=m;g.splice(f++,0,k)}return q(g)}function p(g){var h,f,k,p=[];g=b(g);var a=g.length;var c=128;var d=0;var l=72;for(k=0;km&&p.push(t(m))}for((h=f=p.length)&&p.push("-");h=c&& +me((2147483647-d)/q)&&n("overflow");d+=(x-c)*q;c=x;for(k=0;k=l+26?26:x-l;if(r= 0x80 (not a basic code point)","invalid-input":"Invalid input"},e=Math.floor,t=String.fromCharCode,y;var v={version:"1.3.2",ucs2:{decode:b,encode:q},decode:g,encode:p,toASCII:function(b){return k(b,function(b){return r.test(b)?"xn--"+p(b):b})},toUnicode:function(b){return k(b,function(b){return E.test(b)?g(b.slice(4).toLowerCase()): +b})}};if("function"==typeof define&&"object"==typeof define.amd&&define.amd)define("punycode",function(){return v});else if(D&&A)if(module.exports==D)A.exports=v;else for(y in v)v.hasOwnProperty(y)&&(D[y]=v[y]);else f.punycode=v})(this); +(function(f,n){"object"===typeof module&&module.exports?module.exports=n():"function"===typeof define&&define.amd?define(n):f.SecondLevelDomains=n(f)})(this,function(f){var n=f&&f.SecondLevelDomains,h={list:{ac:" com gov mil net org ",ae:" ac co gov mil name net org pro sch ",af:" com edu gov net org ",al:" com edu gov mil net org ",ao:" co ed gv it og pb ",ar:" com edu gob gov int mil net org tur ",at:" ac co gv or ",au:" asn com csiro edu gov id net org ",ba:" co com edu gov mil net org rs unbi unmo unsa untz unze ", +bb:" biz co com edu gov info net org store tv ",bh:" biz cc com edu gov info net org ",bn:" com edu gov net org ",bo:" com edu gob gov int mil net org tv ",br:" adm adv agr am arq art ato b bio blog bmd cim cng cnt com coop ecn edu eng esp etc eti far flog fm fnd fot fst g12 ggf gov imb ind inf jor jus lel mat med mil mus net nom not ntr odo org ppg pro psc psi qsl rec slg srv tmp trd tur tv vet vlog wiki zlg ",bs:" com edu gov net org ",bz:" du et om ov rg ",ca:" ab bc mb nb nf nl ns nt nu on pe qc sk yk ", +ck:" biz co edu gen gov info net org ",cn:" ac ah bj com cq edu fj gd gov gs gx gz ha hb he hi hl hn jl js jx ln mil net nm nx org qh sc sd sh sn sx tj tw xj xz yn zj ",co:" com edu gov mil net nom org ",cr:" ac c co ed fi go or sa ",cy:" ac biz com ekloges gov ltd name net org parliament press pro tm ","do":" art com edu gob gov mil net org sld web ",dz:" art asso com edu gov net org pol ",ec:" com edu fin gov info med mil net org pro ",eg:" com edu eun gov mil name net org sci ",er:" com edu gov ind mil net org rochest w ", +es:" com edu gob nom org ",et:" biz com edu gov info name net org ",fj:" ac biz com info mil name net org pro ",fk:" ac co gov net nom org ",fr:" asso com f gouv nom prd presse tm ",gg:" co net org ",gh:" com edu gov mil org ",gn:" ac com gov net org ",gr:" com edu gov mil net org ",gt:" com edu gob ind mil net org ",gu:" com edu gov net org ",hk:" com edu gov idv net org ",hu:" 2000 agrar bolt casino city co erotica erotika film forum games hotel info ingatlan jogasz konyvelo lakas media news org priv reklam sex shop sport suli szex tm tozsde utazas video ", +id:" ac co go mil net or sch web ",il:" ac co gov idf k12 muni net org ","in":" ac co edu ernet firm gen gov i ind mil net nic org res ",iq:" com edu gov i mil net org ",ir:" ac co dnssec gov i id net org sch ",it:" edu gov ",je:" co net org ",jo:" com edu gov mil name net org sch ",jp:" ac ad co ed go gr lg ne or ",ke:" ac co go info me mobi ne or sc ",kh:" com edu gov mil net org per ",ki:" biz com de edu gov info mob net org tel ",km:" asso com coop edu gouv k medecin mil nom notaires pharmaciens presse tm veterinaire ", +kn:" edu gov net org ",kr:" ac busan chungbuk chungnam co daegu daejeon es gangwon go gwangju gyeongbuk gyeonggi gyeongnam hs incheon jeju jeonbuk jeonnam k kg mil ms ne or pe re sc seoul ulsan ",kw:" com edu gov net org ",ky:" com edu gov net org ",kz:" com edu gov mil net org ",lb:" com edu gov net org ",lk:" assn com edu gov grp hotel int ltd net ngo org sch soc web ",lr:" com edu gov net org ",lv:" asn com conf edu gov id mil net org ",ly:" com edu gov id med net org plc sch ",ma:" ac co gov m net org press ", +mc:" asso tm ",me:" ac co edu gov its net org priv ",mg:" com edu gov mil nom org prd tm ",mk:" com edu gov inf name net org pro ",ml:" com edu gov net org presse ",mn:" edu gov org ",mo:" com edu gov net org ",mt:" com edu gov net org ",mv:" aero biz com coop edu gov info int mil museum name net org pro ",mw:" ac co com coop edu gov int museum net org ",mx:" com edu gob net org ",my:" com edu gov mil name net org sch ",nf:" arts com firm info net other per rec store web ",ng:" biz com edu gov mil mobi name net org sch ", +ni:" ac co com edu gob mil net nom org ",np:" com edu gov mil net org ",nr:" biz com edu gov info net org ",om:" ac biz co com edu gov med mil museum net org pro sch ",pe:" com edu gob mil net nom org sld ",ph:" com edu gov i mil net ngo org ",pk:" biz com edu fam gob gok gon gop gos gov net org web ",pl:" art bialystok biz com edu gda gdansk gorzow gov info katowice krakow lodz lublin mil net ngo olsztyn org poznan pwr radom slupsk szczecin torun warszawa waw wroc wroclaw zgora ",pr:" ac biz com edu est gov info isla name net org pro prof ", +ps:" com edu gov net org plo sec ",pw:" belau co ed go ne or ",ro:" arts com firm info nom nt org rec store tm www ",rs:" ac co edu gov in org ",sb:" com edu gov net org ",sc:" com edu gov net org ",sh:" co com edu gov net nom org ",sl:" com edu gov net org ",st:" co com consulado edu embaixada gov mil net org principe saotome store ",sv:" com edu gob org red ",sz:" ac co org ",tr:" av bbs bel biz com dr edu gen gov info k12 name net org pol tel tsk tv web ",tt:" aero biz cat co com coop edu gov info int jobs mil mobi museum name net org pro tel travel ", +tw:" club com ebiz edu game gov idv mil net org ",mu:" ac co com gov net or org ",mz:" ac co edu gov org ",na:" co com ",nz:" ac co cri geek gen govt health iwi maori mil net org parliament school ",pa:" abo ac com edu gob ing med net nom org sld ",pt:" com edu gov int net nome org publ ",py:" com edu gov mil net org ",qa:" com edu gov mil net org ",re:" asso com nom ",ru:" ac adygeya altai amur arkhangelsk astrakhan bashkiria belgorod bir bryansk buryatia cbg chel chelyabinsk chita chukotka chuvashia com dagestan e-burg edu gov grozny int irkutsk ivanovo izhevsk jar joshkar-ola kalmykia kaluga kamchatka karelia kazan kchr kemerovo khabarovsk khakassia khv kirov koenig komi kostroma kranoyarsk kuban kurgan kursk lipetsk magadan mari mari-el marine mil mordovia mosreg msk murmansk nalchik net nnov nov novosibirsk nsk omsk orenburg org oryol penza perm pp pskov ptz rnd ryazan sakhalin samara saratov simbirsk smolensk spb stavropol stv surgut tambov tatarstan tom tomsk tsaritsyn tsk tula tuva tver tyumen udm udmurtia ulan-ude vladikavkaz vladimir vladivostok volgograd vologda voronezh vrn vyatka yakutia yamal yekaterinburg yuzhno-sakhalinsk ", +rw:" ac co com edu gouv gov int mil net ",sa:" com edu gov med net org pub sch ",sd:" com edu gov info med net org tv ",se:" a ac b bd c d e f g h i k l m n o org p parti pp press r s t tm u w x y z ",sg:" com edu gov idn net org per ",sn:" art com edu gouv org perso univ ",sy:" com edu gov mil net news org ",th:" ac co go in mi net or ",tj:" ac biz co com edu go gov info int mil name net nic org test web ",tn:" agrinet com defense edunet ens fin gov ind info intl mincom nat net org perso rnrt rns rnu tourism ", +tz:" ac co go ne or ",ua:" biz cherkassy chernigov chernovtsy ck cn co com crimea cv dn dnepropetrovsk donetsk dp edu gov if in ivano-frankivsk kh kharkov kherson khmelnitskiy kiev kirovograd km kr ks kv lg lugansk lutsk lviv me mk net nikolaev od odessa org pl poltava pp rovno rv sebastopol sumy te ternopil uzhgorod vinnica vn zaporizhzhe zhitomir zp zt ",ug:" ac co go ne or org sc ",uk:" ac bl british-library co cym gov govt icnet jet lea ltd me mil mod national-library-scotland nel net nhs nic nls org orgn parliament plc police sch scot soc ", +us:" dni fed isa kids nsn ",uy:" com edu gub mil net org ",ve:" co com edu gob info mil net org web ",vi:" co com k12 net org ",vn:" ac biz com edu gov health info int name net org pro ",ye:" co com gov ltd me net org plc ",yu:" ac co edu gov org ",za:" ac agric alt bourse city co cybernet db edu gov grondar iaccess imt inca landesign law mil net ngo nis nom olivetti org pix school tm web ",zm:" ac co com edu gov net org sch ",com:"ar br cn de eu gb gr hu jpn kr no qc ru sa se uk us uy za ",net:"gb jp se uk ", +org:"ae",de:"com "},has:function(f){var b=f.lastIndexOf(".");if(0>=b||b>=f.length-1)return!1;var k=f.lastIndexOf(".",b-1);if(0>=k||k>=b-1)return!1;var n=h.list[f.slice(b+1)];return n?0<=n.indexOf(" "+f.slice(k+1,b)+" "):!1},is:function(f){var b=f.lastIndexOf(".");if(0>=b||b>=f.length-1||0<=f.lastIndexOf(".",b-1))return!1;var k=h.list[f.slice(b+1)];return k?0<=k.indexOf(" "+f.slice(0,b)+" "):!1},get:function(f){var b=f.lastIndexOf(".");if(0>=b||b>=f.length-1)return null;var k=f.lastIndexOf(".",b-1); +if(0>=k||k>=b-1)return null;var n=h.list[f.slice(b+1)];return!n||0>n.indexOf(" "+f.slice(k+1,b)+" ")?null:f.slice(k+1)},noConflict:function(){f.SecondLevelDomains===this&&(f.SecondLevelDomains=n);return this}};return h}); +(function(f,n){"object"===typeof module&&module.exports?module.exports=n(require("./punycode"),require("./IPv6"),require("./SecondLevelDomains")):"function"===typeof define&&define.amd?define(["./punycode","./IPv6","./SecondLevelDomains"],n):f.URI=n(f.punycode,f.IPv6,f.SecondLevelDomains,f)})(this,function(f,n,h,k){function b(a,c){var d=1<=arguments.length,l=2<=arguments.length;if(!(this instanceof b))return d?l?new b(a,c):new b(a):new b;if(void 0===a){if(d)throw new TypeError("undefined is not a valid argument for URI"); +a="undefined"!==typeof location?location.href+"":""}if(null===a&&d)throw new TypeError("null is not a valid argument for URI");this.href(a);return void 0!==c?this.absoluteTo(c):this}function q(a){return a.replace(/([.*+?^=!:${}()|[\]\/\\])/g,"\\$1")}function z(a){return void 0===a?"Undefined":String(Object.prototype.toString.call(a)).slice(8,-1)}function u(a){return"Array"===z(a)}function g(a,c){var d={},b;if("RegExp"===z(c))d=null;else if(u(c)){var m=0;for(b=c.length;m]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?\u00ab\u00bb\u201c\u201d\u2018\u2019]))/ig;b.findUri={start:/\b(?:([a-z][a-z0-9.+-]*:\/\/)|www\.)/gi,end:/[\s\r\n]|$/,trim:/[`!()\[\]{};:'".,<>?\u00ab\u00bb\u201c\u201d\u201e\u2018\u2019]+$/,parens:/(\([^\)]*\)|\[[^\]]*\]|\{[^}]*\}|<[^>]*>)/g};b.defaultPorts={http:"80",https:"443",ftp:"21", +gopher:"70",ws:"80",wss:"443"};b.hostProtocols=["http","https"];b.invalid_hostname_characters=/[^a-zA-Z0-9\.\-:_]/;b.domAttributes={a:"href",blockquote:"cite",link:"href",base:"href",script:"src",form:"action",img:"src",area:"href",iframe:"src",embed:"src",source:"src",track:"src",input:"src",audio:"src",video:"src"};b.getDomAttribute=function(a){if(a&&a.nodeName){var c=a.nodeName.toLowerCase();if("input"!==c||"image"===a.type)return b.domAttributes[c]}};b.encode=E;b.decode=decodeURIComponent;b.iso8859= +function(){b.encode=escape;b.decode=unescape};b.unicode=function(){b.encode=E;b.decode=decodeURIComponent};b.characters={pathname:{encode:{expression:/%(24|26|2B|2C|3B|3D|3A|40)/ig,map:{"%24":"$","%26":"&","%2B":"+","%2C":",","%3B":";","%3D":"=","%3A":":","%40":"@"}},decode:{expression:/[\/\?#]/g,map:{"/":"%2F","?":"%3F","#":"%23"}}},reserved:{encode:{expression:/%(21|23|24|26|27|28|29|2A|2B|2C|2F|3A|3B|3D|3F|40|5B|5D)/ig,map:{"%3A":":","%2F":"/","%3F":"?","%23":"#","%5B":"[","%5D":"]","%40":"@", +"%21":"!","%24":"$","%26":"&","%27":"'","%28":"(","%29":")","%2A":"*","%2B":"+","%2C":",","%3B":";","%3D":"="}}},urnpath:{encode:{expression:/%(21|24|27|28|29|2A|2B|2C|3B|3D|40)/ig,map:{"%21":"!","%24":"$","%27":"'","%28":"(","%29":")","%2A":"*","%2B":"+","%2C":",","%3B":";","%3D":"=","%40":"@"}},decode:{expression:/[\/\?#:]/g,map:{"/":"%2F","?":"%3F","#":"%23",":":"%3A"}}}};b.encodeQuery=function(a,c){var d=b.encode(a+"");void 0===c&&(c=b.escapeQuerySpace);return c?d.replace(/%20/g,"+"):d};b.decodeQuery= +function(a,c){a+="";void 0===c&&(c=b.escapeQuerySpace);try{return b.decode(c?a.replace(/\+/g,"%20"):a)}catch(d){return a}};var y={encode:"encode",decode:"decode"},v,F=function(a,c){return function(d){try{return b[c](d+"").replace(b.characters[a][c].expression,function(d){return b.characters[a][c].map[d]})}catch(l){return d}}};for(v in y)b[v+"PathSegment"]=F("pathname",y[v]),b[v+"UrnPathSegment"]=F("urnpath",y[v]);y=function(a,c,d){return function(l){var m=d?function(a){return b[c](b[d](a))}:b[c]; +l=(l+"").split(a);for(var e=0,g=l.length;eb)return a.charAt(0)===c.charAt(0)&&"/"===a.charAt(0)?"/":"";if("/"!==a.charAt(b)||"/"!==c.charAt(b))b=a.substring(0,b).lastIndexOf("/");return a.substring(0,b+1)};b.withinString=function(a,c,d){d||(d={});var l=d.start||b.findUri.start,e=d.end||b.findUri.end,g=d.trim||b.findUri.trim,f=d.parens||b.findUri.parens,h=/[a-z0-9-]=["']?$/i; +for(l.lastIndex=0;;){var k=l.exec(a);if(!k)break;var p=k.index;if(d.ignoreHtml){var n=a.slice(Math.max(p-3,0),p);if(n&&h.test(n))continue}var r=p+a.slice(p).search(e);n=a.slice(p,r);for(r=-1;;){var q=f.exec(n);if(!q)break;r=Math.max(r,q.index+q[0].length)}n=-1c))throw new TypeError('Port "'+a+'" is not a valid port');}};b.noConflict=function(a){if(a)return a={URI:this.noConflict()},k.URITemplate&&"function"===typeof k.URITemplate.noConflict&&(a.URITemplate=k.URITemplate.noConflict()),k.IPv6&&"function"===typeof k.IPv6.noConflict&&(a.IPv6=k.IPv6.noConflict()),k.SecondLevelDomains&&"function"===typeof k.SecondLevelDomains.noConflict&&(a.SecondLevelDomains=k.SecondLevelDomains.noConflict()),a;k.URI===this&&(k.URI= +w);return this};e.build=function(a){if(!0===a)this._deferred_build=!0;else if(void 0===a||this._deferred_build)this._string=b.build(this._parts),this._deferred_build=!1;return this};e.clone=function(){return new b(this)};e.valueOf=e.toString=function(){return this.build(!1)._string};e.protocol=r("protocol");e.username=r("username");e.password=r("password");e.hostname=r("hostname");e.port=r("port");e.query=C("query","?");e.fragment=C("fragment","#");e.search=function(a,c){var b=this.query(a,c);return"string"=== +typeof b&&b.length?"?"+b:b};e.hash=function(a,c){var b=this.fragment(a,c);return"string"===typeof b&&b.length?"#"+b:b};e.pathname=function(a,c){if(void 0===a||!0===a){var d=this._parts.path||(this._parts.hostname?"/":"");return a?(this._parts.urn?b.decodeUrnPath:b.decodePath)(d):d}this._parts.path=this._parts.urn?a?b.recodeUrnPath(a):"":a?b.recodePath(a):"/";this.build(!c);return this};e.path=e.pathname;e.href=function(a,c){var d;if(void 0===a)return this.toString();this._string="";this._parts=b._parts(); +var l=a instanceof b,e="object"===typeof a&&(a.hostname||a.path||a.pathname);a.nodeName&&(e=b.getDomAttribute(a),a=a[e]||"",e=!1);!l&&e&&void 0!==a.pathname&&(a=a.toString());if("string"===typeof a||a instanceof String)this._parts=b.parse(String(a),this._parts);else if(l||e){l=l?a._parts:a;for(d in l)"query"!==d&&t.call(this._parts,d)&&(this._parts[d]=l[d]);l.query&&this.query(l.query,!1)}else throw new TypeError("invalid input");this.build(!c);return this};e.is=function(a){var c=!1,d=!1,e=!1,m=!1, +g=!1,f=!1,k=!1,p=!this._parts.urn;this._parts.hostname&&(p=!1,d=b.ip4_expression.test(this._parts.hostname),e=b.ip6_expression.test(this._parts.hostname),c=d||e,g=(m=!c)&&h&&h.has(this._parts.hostname),f=m&&b.idn_expression.test(this._parts.hostname),k=m&&b.punycode_expression.test(this._parts.hostname));switch(a.toLowerCase()){case "relative":return p;case "absolute":return!p;case "domain":case "name":return m;case "sld":return g;case "ip":return c;case "ip4":case "ipv4":case "inet4":return d;case "ip6":case "ipv6":case "inet6":return e; +case "idn":return f;case "url":return!this._parts.urn;case "urn":return!!this._parts.urn;case "punycode":return k}return null};var G=e.protocol,H=e.port,I=e.hostname;e.protocol=function(a,c){if(a&&(a=a.replace(/:(\/\/)?$/,""),!a.match(b.protocol_expression)))throw new TypeError('Protocol "'+a+"\" contains characters other than [A-Z0-9.+-] or doesn't start with [A-Z]");return G.call(this,a,c)};e.scheme=e.protocol;e.port=function(a,c){if(this._parts.urn)return void 0===a?"":this;void 0!==a&&(0===a&& +(a=null),a&&(a+="",":"===a.charAt(0)&&(a=a.substring(1)),b.ensureValidPort(a)));return H.call(this,a,c)};e.hostname=function(a,c){if(this._parts.urn)return void 0===a?"":this;if(void 0!==a){var d={preventInvalidHostname:this._parts.preventInvalidHostname};if("/"!==b.parseHost(a,d))throw new TypeError('Hostname "'+a+'" contains characters other than [A-Z0-9.-]');a=d.hostname;this._parts.preventInvalidHostname&&b.ensureValidHostname(a,this._parts.protocol)}return I.call(this,a,c)};e.origin=function(a, +c){if(this._parts.urn)return void 0===a?"":this;if(void 0===a){var d=this.protocol();return this.authority()?(d?d+"://":"")+this.authority():""}d=b(a);this.protocol(d.protocol()).authority(d.authority()).build(!c);return this};e.host=function(a,c){if(this._parts.urn)return void 0===a?"":this;if(void 0===a)return this._parts.hostname?b.buildHost(this._parts):"";if("/"!==b.parseHost(a,this._parts))throw new TypeError('Hostname "'+a+'" contains characters other than [A-Z0-9.-]');this.build(!c);return this}; +e.authority=function(a,c){if(this._parts.urn)return void 0===a?"":this;if(void 0===a)return this._parts.hostname?b.buildAuthority(this._parts):"";if("/"!==b.parseAuthority(a,this._parts))throw new TypeError('Hostname "'+a+'" contains characters other than [A-Z0-9.-]');this.build(!c);return this};e.userinfo=function(a,c){if(this._parts.urn)return void 0===a?"":this;if(void 0===a){var d=b.buildUserinfo(this._parts);return d?d.substring(0,d.length-1):d}"@"!==a[a.length-1]&&(a+="@");b.parseUserinfo(a, +this._parts);this.build(!c);return this};e.resource=function(a,c){if(void 0===a)return this.path()+this.search()+this.hash();var d=b.parse(a);this._parts.path=d.path;this._parts.query=d.query;this._parts.fragment=d.fragment;this.build(!c);return this};e.subdomain=function(a,c){if(this._parts.urn)return void 0===a?"":this;if(void 0===a){if(!this._parts.hostname||this.is("IP"))return"";var d=this._parts.hostname.length-this.domain().length-1;return this._parts.hostname.substring(0,d)||""}d=this._parts.hostname.length- +this.domain().length;d=this._parts.hostname.substring(0,d);d=new RegExp("^"+q(d));a&&"."!==a.charAt(a.length-1)&&(a+=".");if(-1!==a.indexOf(":"))throw new TypeError("Domains cannot contain colons");a&&b.ensureValidHostname(a,this._parts.protocol);this._parts.hostname=this._parts.hostname.replace(d,a);this.build(!c);return this};e.domain=function(a,c){if(this._parts.urn)return void 0===a?"":this;"boolean"===typeof a&&(c=a,a=void 0);if(void 0===a){if(!this._parts.hostname||this.is("IP"))return"";var d= +this._parts.hostname.match(/\./g);if(d&&2>d.length)return this._parts.hostname;d=this._parts.hostname.length-this.tld(c).length-1;d=this._parts.hostname.lastIndexOf(".",d-1)+1;return this._parts.hostname.substring(d)||""}if(!a)throw new TypeError("cannot set domain empty");if(-1!==a.indexOf(":"))throw new TypeError("Domains cannot contain colons");b.ensureValidHostname(a,this._parts.protocol);!this._parts.hostname||this.is("IP")?this._parts.hostname=a:(d=new RegExp(q(this.domain())+"$"),this._parts.hostname= +this._parts.hostname.replace(d,a));this.build(!c);return this};e.tld=function(a,c){if(this._parts.urn)return void 0===a?"":this;"boolean"===typeof a&&(c=a,a=void 0);if(void 0===a){if(!this._parts.hostname||this.is("IP"))return"";var b=this._parts.hostname.lastIndexOf(".");b=this._parts.hostname.substring(b+1);return!0!==c&&h&&h.list[b.toLowerCase()]?h.get(this._parts.hostname)||b:b}if(a)if(a.match(/[^a-zA-Z0-9-]/))if(h&&h.is(a))b=new RegExp(q(this.tld())+"$"),this._parts.hostname=this._parts.hostname.replace(b, +a);else throw new TypeError('TLD "'+a+'" contains characters other than [A-Z0-9]');else{if(!this._parts.hostname||this.is("IP"))throw new ReferenceError("cannot set TLD on non-domain host");b=new RegExp(q(this.tld())+"$");this._parts.hostname=this._parts.hostname.replace(b,a)}else throw new TypeError("cannot set TLD empty");this.build(!c);return this};e.directory=function(a,c){if(this._parts.urn)return void 0===a?"":this;if(void 0===a||!0===a){if(!this._parts.path&&!this._parts.hostname)return""; +if("/"===this._parts.path)return"/";var d=this._parts.path.length-this.filename().length-1;d=this._parts.path.substring(0,d)||(this._parts.hostname?"/":"");return a?b.decodePath(d):d}d=this._parts.path.length-this.filename().length;d=this._parts.path.substring(0,d);d=new RegExp("^"+q(d));this.is("relative")||(a||(a="/"),"/"!==a.charAt(0)&&(a="/"+a));a&&"/"!==a.charAt(a.length-1)&&(a+="/");a=b.recodePath(a);this._parts.path=this._parts.path.replace(d,a);this.build(!c);return this};e.filename=function(a, +c){if(this._parts.urn)return void 0===a?"":this;if("string"!==typeof a){if(!this._parts.path||"/"===this._parts.path)return"";var d=this._parts.path.lastIndexOf("/");d=this._parts.path.substring(d+1);return a?b.decodePathSegment(d):d}d=!1;"/"===a.charAt(0)&&(a=a.substring(1));a.match(/\.?\//)&&(d=!0);var e=new RegExp(q(this.filename())+"$");a=b.recodePath(a);this._parts.path=this._parts.path.replace(e,a);d?this.normalizePath(c):this.build(!c);return this};e.suffix=function(a,c){if(this._parts.urn)return void 0=== +a?"":this;if(void 0===a||!0===a){if(!this._parts.path||"/"===this._parts.path)return"";var d=this.filename(),e=d.lastIndexOf(".");if(-1===e)return"";d=d.substring(e+1);d=/^[a-z0-9%]+$/i.test(d)?d:"";return a?b.decodePathSegment(d):d}"."===a.charAt(0)&&(a=a.substring(1));if(d=this.suffix())e=a?new RegExp(q(d)+"$"):new RegExp(q("."+d)+"$");else{if(!a)return this;this._parts.path+="."+b.recodePath(a)}e&&(a=b.recodePath(a),this._parts.path=this._parts.path.replace(e,a));this.build(!c);return this};e.segment= +function(a,c,b){var d=this._parts.urn?":":"/",e=this.path(),g="/"===e.substring(0,1);e=e.split(d);void 0!==a&&"number"!==typeof a&&(b=c,c=a,a=void 0);if(void 0!==a&&"number"!==typeof a)throw Error('Bad segment "'+a+'", must be 0-based integer');g&&e.shift();0>a&&(a=Math.max(e.length+a,0));if(void 0===c)return void 0===a?e:e[a];if(null===a||void 0===e[a])if(u(c)){e=[];a=0;for(var f=c.length;a{}"`^| \\]/;h.expand=function(b,f,k){var g=u[b.operator],p=g.named?"Named":"Unnamed";b=b.variables;var n=[],r,q;for(q=0;r=b[q];q++){var w=f.get(r.name);if(0===w.type&&k&&k.strict)throw Error('Missing expansion value for variable "'+ +r.name+'"');if(w.val.length){if(1 { + const img = new Image(); + img.src = url; + img.onload = () => resolve(img); + }); +} + +const extraContributorTemplate = ` +
    +
    +
    + +
    {details}
    +
    +
    +`; + +async function loadContributor(contributor) { + const img = await preloadImage(contributor.avatar_url); + img.className = "contributor-img"; + img.title = `@${contributor.login}`; + img.alt = img.title; + + const a = document.createElement("a"); + a.href = contributor.html_url; + a.target = "_blank"; + a.appendChild(img); + + if (contributor.login in EXTRA_CONTRIBUTORS) { + const div = createElementFromHTML( + extraContributorTemplate + .replace("{hrefUsername}", contributor.html_url) + .replace("{username}", `@${contributor.login}`) + .replace("{hrefContributions}", CONTRIBUTOR_COMMIT_URL.replace("{AUTHOR}", contributor.login)) + .replace("{contributions}", `${contributor.contributions} contributions`) + .replace("{details}", EXTRA_CONTRIBUTORS[contributor.login]), + ); + div.querySelector(".contributor-img-wrapper").appendChild(a); + + return { element: div, isExtra: true }; + } else { + return { element: a, isExtra: false }; + } +} + +async function fetchContributors() { + if (contributorsLoaded) return; + + const data = await fetch(CONTRIBUTORS_API_URL); + showElement("contributors-loading"); + hideElement("contributors-count"); + hideElement("contributors"); + + if (data.ok) { + const contributors = await data.json(); + const containerSimple = document.getElementById("contributors-simple"); + const containerExtra = document.getElementById("contributors-extra"); + containerSimple.innerHTML = ""; + containerExtra.innerHTML = ""; + + const contributorsToShow = contributors.slice(0, CONTRIBUTORS_COUNT_TO_SHOW); + const contributorItems = await Promise.all(contributorsToShow.map((contributor) => loadContributor(contributor))); + + contributorItems.forEach((item) => { + (item.isExtra ? containerExtra : containerSimple).appendChild(item.element); + }); + + document.getElementById("contributors-more-link-count").innerText = contributors.length - CONTRIBUTORS_COUNT_TO_SHOW; + document.getElementById("contributors-count").innerText = contributors.length; + showElement("contributors-count"); + showElement("contributors"); + contributorsLoaded = true; + } + + hideElement("contributors-loading"); +} + +$(document).ready(function () { + M.Modal.init(document.querySelector("#about_modal"), { + onOpenStart: () => fetchContributors(), + }); +}); diff --git a/hassio-google-drive-backup/backup/static/js/colpick.js b/hassio-google-drive-backup/backup/static/js/colpick.js new file mode 100644 index 0000000..900a0f5 --- /dev/null +++ b/hassio-google-drive-backup/backup/static/js/colpick.js @@ -0,0 +1,605 @@ +/*! + * colpick Color Picker + * https://github.com/mrgrain/colpick + * + * Copyright 2013, 2015 Moritz Kornher, Jose Vargas, Stefan Petre + * Released under the MIT license and GPLv2 license + * https://github.com/mrgrain/colpick/blob/master/LICENSE + */ +(function (factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(['jquery'], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS + factory(require('jquery')); + } else { + // Browser globals + factory(jQuery); + } +}(function ($) { + var colpick = function () { + var + tpl = '
    #
    R
    G
    B
    H
    S
    B
    ', + defaults = { + showEvent: 'click', + onShow: function () { + }, + onBeforeShow: function () { + }, + onHide: function () { + }, + onChange: function () { + }, + onSubmit: function () { + }, + colorScheme: 'light', + color: 'auto', + livePreview: true, + flat: false, + layout: 'full', + submit: 1, + submitText: 'OK', + height: 156, + polyfill: false, + styles: false + }, + //Fill the inputs of the plugin + fillRGBFields = function (hsb, cal) { + var rgb = hsbToRgb(hsb); + $(cal).data('colpick').fields + .eq(1).val(rgb.r).end() + .eq(2).val(rgb.g).end() + .eq(3).val(rgb.b).end(); + }, + fillHSBFields = function (hsb, cal) { + $(cal).data('colpick').fields + .eq(4).val(Math.round(hsb.h)).end() + .eq(5).val(Math.round(hsb.s)).end() + .eq(6).val(Math.round(hsb.b)).end(); + }, + fillHexFields = function (hsb, cal) { + $(cal).data('colpick').fields.eq(0).val(hsbToHex(hsb)); + }, + //Set the round selector position + setSelector = function (hsb, cal) { + $(cal).data('colpick').selector.css('backgroundColor', '#' + hsbToHex({h: hsb.h, s: 100, b: 100})); + $(cal).data('colpick').selectorIndic.css({ + left: parseInt($(cal).data('colpick').height * hsb.s / 100, 10), + top: parseInt($(cal).data('colpick').height * (100 - hsb.b) / 100, 10) + }); + }, + //Set the hue selector position + setHue = function (hsb, cal) { + $(cal).data('colpick').hue.css('top', parseInt($(cal).data('colpick').height - $(cal).data('colpick').height * hsb.h / 360, 10)); + }, + //Set current and new colors + setCurrentColor = function (hsb, cal) { + $(cal).data('colpick').currentColor.css('backgroundColor', '#' + hsbToHex(hsb)); + }, + setNewColor = function (hsb, cal) { + $(cal).data('colpick').newColor.css('backgroundColor', '#' + hsbToHex(hsb)); + }, + //Called when the new color is changed + change = function () { + var cal = $(this).parent().parent(), col; + if (this.parentNode.className.indexOf('_hex') > 0) { + cal.data('colpick').color = col = hexToHsb(fixHex(this.value)); + fillRGBFields(col, cal.get(0)); + fillHSBFields(col, cal.get(0)); + } else if (this.parentNode.className.indexOf('_hsb') > 0) { + cal.data('colpick').color = col = fixHSB({ + h: parseInt(cal.data('colpick').fields.eq(4).val(), 10), + s: parseInt(cal.data('colpick').fields.eq(5).val(), 10), + b: parseInt(cal.data('colpick').fields.eq(6).val(), 10) + }); + fillRGBFields(col, cal.get(0)); + fillHexFields(col, cal.get(0)); + } else { + cal.data('colpick').color = col = rgbToHsb(fixRGB({ + r: parseInt(cal.data('colpick').fields.eq(1).val(), 10), + g: parseInt(cal.data('colpick').fields.eq(2).val(), 10), + b: parseInt(cal.data('colpick').fields.eq(3).val(), 10) + })); + fillHexFields(col, cal.get(0)); + fillHSBFields(col, cal.get(0)); + } + setSelector(col, cal.get(0)); + setHue(col, cal.get(0)); + setNewColor(col, cal.get(0)); + cal.data('colpick').onChange.apply(cal.parent(), [col, hsbToHex(col), hsbToRgb(col), cal.data('colpick').el, 0]); + }, + //Change style on blur and on focus of inputs + blur = function () { + $(this).parent().removeClass('colpick_focus'); + }, + focus = function () { + $(this).parent().parent().data('colpick').fields.parent().removeClass('colpick_focus'); + $(this).parent().addClass('colpick_focus'); + }, + //Increment/decrement arrows functions + downIncrement = function (ev) { + ev.preventDefault ? ev.preventDefault() : ev.returnValue = false; + var field = $(this).parent().find('input').focus(); + var current = { + el: $(this).parent().addClass('colpick_slider'), + max: this.parentNode.className.indexOf('_hsb_h') > 0 ? 360 : (this.parentNode.className.indexOf('_hsb') > 0 ? 100 : 255), + y: ev.pageY, + field: field, + val: parseInt(field.val(), 10), + preview: $(this).parent().parent().data('colpick').livePreview + }; + $(document).mouseup(current, upIncrement); + $(document).mousemove(current, moveIncrement); + }, + moveIncrement = function (ev) { + ev.data.field.val(Math.max(0, Math.min(ev.data.max, parseInt(ev.data.val - ev.pageY + ev.data.y, 10)))); + if (ev.data.preview) { + change.apply(ev.data.field.get(0), [true]); + } + return false; + }, + upIncrement = function (ev) { + change.apply(ev.data.field.get(0), [true]); + ev.data.el.removeClass('colpick_slider').find('input').focus(); + $(document).off('mouseup', upIncrement); + $(document).off('mousemove', moveIncrement); + return false; + }, + //Hue slider functions + downHue = function (ev) { + ev.preventDefault ? ev.preventDefault() : ev.returnValue = false; + var current = { + cal: $(this).parent(), + y: $(this).offset().top + }; + $(document).on('mouseup touchend', current, upHue); + $(document).on('mousemove touchmove', current, moveHue); + + var pageY = ((ev.type == 'touchstart') ? ev.originalEvent.changedTouches[0].pageY : ev.pageY ); + change.apply( + current.cal.data('colpick') + .fields.eq(4).val(parseInt(360 * (current.cal.data('colpick').height - (pageY - current.y)) / current.cal.data('colpick').height, 10)) + .get(0), + [current.cal.data('colpick').livePreview] + ); + return false; + }, + moveHue = function (ev) { + var pageY = ((ev.type == 'touchmove') ? ev.originalEvent.changedTouches[0].pageY : ev.pageY ); + change.apply( + ev.data.cal.data('colpick') + .fields.eq(4).val(parseInt(360 * (ev.data.cal.data('colpick').height - Math.max(0, Math.min(ev.data.cal.data('colpick').height, (pageY - ev.data.y)))) / ev.data.cal.data('colpick').height, 10)) + .get(0), + [ev.data.preview] + ); + return false; + }, + upHue = function (ev) { + fillRGBFields(ev.data.cal.data('colpick').color, ev.data.cal.get(0)); + fillHexFields(ev.data.cal.data('colpick').color, ev.data.cal.get(0)); + $(document).off('mouseup touchend', upHue); + $(document).off('mousemove touchmove', moveHue); + return false; + }, + //Color selector functions + downSelector = function (ev) { + ev.preventDefault ? ev.preventDefault() : ev.returnValue = false; + var current = { + cal: $(this).parent(), + pos: $(this).offset() + }; + current.preview = current.cal.data('colpick').livePreview; + + $(document).on('mouseup touchend', current, upSelector); + $(document).on('mousemove touchmove', current, moveSelector); + + var pageX, pageY; + if (ev.type == 'touchstart') { + pageX = ev.originalEvent.changedTouches[0].pageX; + pageY = ev.originalEvent.changedTouches[0].pageY; + } else { + pageX = ev.pageX; + pageY = ev.pageY; + } + + change.apply( + current.cal.data('colpick').fields + .eq(6).val(parseInt(100 * (current.cal.data('colpick').height - (pageY - current.pos.top)) / current.cal.data('colpick').height, 10)).end() + .eq(5).val(parseInt(100 * (pageX - current.pos.left) / current.cal.data('colpick').height, 10)) + .get(0), + [current.preview] + ); + return false; + }, + moveSelector = function (ev) { + var pageX, pageY; + if (ev.type == 'touchmove') { + pageX = ev.originalEvent.changedTouches[0].pageX; + pageY = ev.originalEvent.changedTouches[0].pageY; + } else { + pageX = ev.pageX; + pageY = ev.pageY; + } + + change.apply( + ev.data.cal.data('colpick').fields + .eq(6).val(parseInt(100 * (ev.data.cal.data('colpick').height - Math.max(0, Math.min(ev.data.cal.data('colpick').height, (pageY - ev.data.pos.top)))) / ev.data.cal.data('colpick').height, 10)).end() + .eq(5).val(parseInt(100 * (Math.max(0, Math.min(ev.data.cal.data('colpick').height, (pageX - ev.data.pos.left)))) / ev.data.cal.data('colpick').height, 10)) + .get(0), + [ev.data.preview] + ); + return false; + }, + upSelector = function (ev) { + fillRGBFields(ev.data.cal.data('colpick').color, ev.data.cal.get(0)); + fillHexFields(ev.data.cal.data('colpick').color, ev.data.cal.get(0)); + $(document).off('mouseup touchend', upSelector); + $(document).off('mousemove touchmove', moveSelector); + return false; + }, + //Submit button + clickSubmit = function () { + var cal = $(this).parent(); + var col = cal.data('colpick').color; + cal.data('colpick').origColor = col; + setCurrentColor(col, cal.get(0)); + cal.data('colpick').onSubmit(col, hsbToHex(col), hsbToRgb(col), cal.data('colpick').el); + }, + //Show/hide the color picker + show = function (ev) { + if (ev) { + // Prevent the trigger of any direct parent + ev.stopPropagation(); + } + var cal = $('#' + $(this).data('colpickId')); + if (ev && !cal.data('colpick').polyfill) { + ev.preventDefault(); + } + cal.data('colpick').onBeforeShow.apply(this, [cal.get(0)]); + var pos = $(this).offset(); + var top = pos.top + this.offsetHeight; + var left = pos.left; + var viewPort = getViewport(); + var calW = cal.width(); + if (left + calW > viewPort.l + viewPort.w) { + left -= calW; + } + cal.css({left: left + 'px', top: top + 'px'}); + if (cal.data('colpick').onShow.apply(this, [cal.get(0)]) != false) { + cal.show(); + } + //Hide when user clicks outside + $('html').mousedown({cal: cal}, hide); + cal.mousedown(function (ev) { + ev.stopPropagation(); + }) + }, + hide = function (ev) { + var cal = $('#' + $(this).data('colpickId')); + if (ev) { + cal = ev.data.cal; + } + if (cal.data('colpick').onHide.apply(this, [cal.get(0)]) != false) { + cal.hide(); + } + $('html').off('mousedown', hide); + }, + getViewport = function () { + var m = document.compatMode == 'CSS1Compat'; + return { + l: window.pageXOffset || (m ? document.documentElement.scrollLeft : document.body.scrollLeft), + w: window.innerWidth || (m ? document.documentElement.clientWidth : document.body.clientWidth) + }; + }, + //Fix the values if the user enters a negative or high value + fixHSB = function (hsb) { + return { + h: Math.min(360, Math.max(0, hsb.h)), + s: Math.min(100, Math.max(0, hsb.s)), + b: Math.min(100, Math.max(0, hsb.b)) + }; + }, + fixRGB = function (rgb) { + return { + r: Math.min(255, Math.max(0, rgb.r)), + g: Math.min(255, Math.max(0, rgb.g)), + b: Math.min(255, Math.max(0, rgb.b)) + }; + }, + fixHex = function (hex) { + var len = 6 - hex.length; + if (len == 3) { + var e = []; + for (var j = 0; j < len; j++) { + e.push(hex[j]); + e.push(hex[j]); + } + hex = e.join(''); + } else { + if (len > 0) { + var o = []; + for (var i = 0; i < len; i++) { + o.push('0'); + } + o.push(hex); + hex = o.join(''); + } + } + return hex; + }, + restoreOriginal = function () { + var cal = $(this).parent(); + var col = cal.data('colpick').origColor; + cal.data('colpick').color = col; + fillRGBFields(col, cal.get(0)); + fillHexFields(col, cal.get(0)); + fillHSBFields(col, cal.get(0)); + setSelector(col, cal.get(0)); + setHue(col, cal.get(0)); + setNewColor(col, cal.get(0)); + }; + return { + init: function (opt) { + opt = $.extend({}, defaults, opt || {}); + //Set color + if (opt.color === 'auto') { + } else if (typeof opt.color == 'string') { + opt.color = hexToHsb(opt.color); + } else if (opt.color.r != undefined && opt.color.g != undefined && opt.color.b != undefined) { + opt.color = rgbToHsb(opt.color); + } else if (opt.color.h != undefined && opt.color.s != undefined && opt.color.b != undefined) { + opt.color = fixHSB(opt.color); + } else { + return this; + } + + //For each selected DOM element + return this.each(function () { + //If the element does not have an ID + if (!$(this).data('colpickId')) { + var options = $.extend({}, opt); + //Color + if (opt.color === 'auto') { + options.color = $(this).val() ? hexToHsb($(this).val()) : {h: 0, s: 0, b: 0}; + } + options.origColor = options.color; + + //Polyfill + if (typeof opt.polyfill == 'function') { + options.polyfill = opt.polyfill(this); + } + if (options.polyfill && $(this).is('input') && this.type === "color") { + return; + } + + //Generate and assign a random ID + var id = 'collorpicker_' + parseInt(Math.random() * 1000); + $(this).data('colpickId', id); + //Set the tpl's ID and get the HTML + var cal = $(tpl).attr('id', id); + //Add class according to layout + cal.addClass('colpick_' + options.layout + (options.submit ? '' : ' colpick_' + options.layout + '_ns')); + //Add class if the color scheme is not default + if (options.colorScheme != 'light') { + cal.addClass('colpick_' + options.colorScheme); + } + //Setup submit button + cal.find('div.colpick_submit').html(options.submitText).click(clickSubmit); + //Setup input fields + options.fields = cal.find('input').change(change).blur(blur).focus(focus); + cal.find('div.colpick_field_arrs').mousedown(downIncrement).end().find('div.colpick_current_color').click(restoreOriginal); + //Setup hue selector + options.selector = cal.find('div.colpick_color').on('mousedown touchstart', downSelector); + options.selectorIndic = options.selector.find('div.colpick_selector_outer'); + //Store parts of the plugin + options.el = this; + options.hue = cal.find('div.colpick_hue_arrs'); + var huebar = options.hue.parent(); + //Paint the hue bar + var UA = navigator.userAgent.toLowerCase(); + var isIE = navigator.appName === 'Microsoft Internet Explorer'; + var IEver = isIE ? parseFloat(UA.match(/msie ([0-9]*[\.0-9]+)/)[1]) : 0; + var ngIE = ( isIE && IEver < 10 ); + var stops = ['#ff0000', '#ff0080', '#ff00ff', '#8000ff', '#0000ff', '#0080ff', '#00ffff', '#00ff80', '#00ff00', '#80ff00', '#ffff00', '#ff8000', '#ff0000']; + if (ngIE) { + var i, div; + for (i = 0; i <= 11; i++) { + div = $('
    ').attr('style', 'height:8.333333%; filter:progid:DXImageTransform.Microsoft.gradient(GradientType=0,startColorstr=' + stops[i] + ', endColorstr=' + stops[i + 1] + '); -ms-filter: "progid:DXImageTransform.Microsoft.gradient(GradientType=0,startColorstr=' + stops[i] + ', endColorstr=' + stops[i + 1] + ')";'); + huebar.append(div); + } + } else { + var stopList = stops.join(','); + huebar.attr('style', 'background:-webkit-linear-gradient(top,' + stopList + '); background: -o-linear-gradient(top,' + stopList + '); background: -ms-linear-gradient(top,' + stopList + '); background:-moz-linear-gradient(top,' + stopList + '); -webkit-linear-gradient(top,' + stopList + '); background:linear-gradient(to bottom,' + stopList + '); '); + } + cal.find('div.colpick_hue').on('mousedown touchstart', downHue); + options.newColor = cal.find('div.colpick_new_color'); + options.currentColor = cal.find('div.colpick_current_color'); + //Store options and fill with default color + cal.data('colpick', options); + fillRGBFields(options.color, cal.get(0)); + fillHSBFields(options.color, cal.get(0)); + fillHexFields(options.color, cal.get(0)); + setHue(options.color, cal.get(0)); + setSelector(options.color, cal.get(0)); + setCurrentColor(options.color, cal.get(0)); + setNewColor(options.color, cal.get(0)); + //Append to body if flat=false, else show in place + if (options.flat) { + cal.appendTo(options.appendTo || this).show(); + cal.css(options.styles || { + position: 'relative', + display: 'block' + }); + } else { + cal.appendTo(options.appendTo || document.body); + $(this).on(options.showEvent, show); + cal.css(options.styles || { + position: 'absolute' + }); + } + } + }); + }, + //Shows the picker + showPicker: function () { + return this.each(function () { + if ($(this).data('colpickId')) { + show.apply(this); + } + }); + }, + //Hides the picker + hidePicker: function () { + return this.each(function () { + if ($(this).data('colpickId')) { + hide.apply(this); + } + }); + }, + //Sets a color as new and current (default) + setColor: function (col, setCurrent) { + setCurrent = (typeof setCurrent === "undefined") ? 1 : setCurrent; + if (typeof col == 'string') { + col = hexToHsb(col); + } else if (col.r != undefined && col.g != undefined && col.b != undefined) { + col = rgbToHsb(col); + } else if (col.h != undefined && col.s != undefined && col.b != undefined) { + col = fixHSB(col); + } else { + return this; + } + return this.each(function () { + if ($(this).data('colpickId')) { + var cal = $('#' + $(this).data('colpickId')); + cal.data('colpick').color = col; + cal.data('colpick').origColor = col; + fillRGBFields(col, cal.get(0)); + fillHSBFields(col, cal.get(0)); + fillHexFields(col, cal.get(0)); + setHue(col, cal.get(0)); + setSelector(col, cal.get(0)); + + setNewColor(col, cal.get(0)); + cal.data('colpick').onChange.apply(cal.parent(), [col, hsbToHex(col), hsbToRgb(col), cal.data('colpick').el, 1]); + if (setCurrent) { + setCurrentColor(col, cal.get(0)); + } + } + }); + }, + destroy: function () { + $('#' + $(this).data('colpickId')).remove(); + } + }; + }(); + //Color space conversions + var hexToRgb = function (hex) { + hex = parseInt(((hex.indexOf('#') > -1) ? hex.substring(1) : hex), 16); + return {r: hex >> 16, g: (hex & 0x00FF00) >> 8, b: (hex & 0x0000FF)}; + }; + var hexToHsb = function (hex) { + return rgbToHsb(hexToRgb(hex)); + }; + var rgbToHsb = function (rgb) { + var hsb = {h: 0, s: 0, b: 0}; + var min = Math.min(rgb.r, rgb.g, rgb.b); + var max = Math.max(rgb.r, rgb.g, rgb.b); + var delta = max - min; + hsb.b = max; + hsb.s = max != 0 ? 255 * delta / max : 0; + if (hsb.s != 0) { + if (rgb.r == max) hsb.h = (rgb.g - rgb.b) / delta; + else if (rgb.g == max) hsb.h = 2 + (rgb.b - rgb.r) / delta; + else hsb.h = 4 + (rgb.r - rgb.g) / delta; + } else hsb.h = -1; + hsb.h *= 60; + if (hsb.h < 0) hsb.h += 360; + hsb.s *= 100 / 255; + hsb.b *= 100 / 255; + return hsb; + }; + var hsbToRgb = function (hsb) { + var rgb = {}; + var h = hsb.h; + var s = hsb.s * 255 / 100; + var v = hsb.b * 255 / 100; + if (s == 0) { + rgb.r = rgb.g = rgb.b = v; + } else { + var t1 = v; + var t2 = (255 - s) * v / 255; + var t3 = (t1 - t2) * (h % 60) / 60; + if (h == 360) h = 0; + if (h < 60) { + rgb.r = t1; + rgb.b = t2; + rgb.g = t2 + t3 + } + else if (h < 120) { + rgb.g = t1; + rgb.b = t2; + rgb.r = t1 - t3 + } + else if (h < 180) { + rgb.g = t1; + rgb.r = t2; + rgb.b = t2 + t3 + } + else if (h < 240) { + rgb.b = t1; + rgb.r = t2; + rgb.g = t1 - t3 + } + else if (h < 300) { + rgb.b = t1; + rgb.g = t2; + rgb.r = t2 + t3 + } + else if (h < 360) { + rgb.r = t1; + rgb.g = t2; + rgb.b = t1 - t3 + } + else { + rgb.r = 0; + rgb.g = 0; + rgb.b = 0 + } + } + return {r: Math.round(rgb.r), g: Math.round(rgb.g), b: Math.round(rgb.b)}; + }; + var rgbToHex = function (rgb) { + var hex = [ + rgb.r.toString(16), + rgb.g.toString(16), + rgb.b.toString(16) + ]; + $.each(hex, function (nr, val) { + if (val.length == 1) { + hex[nr] = '0' + val; + } + }); + return hex.join(''); + }; + var hsbToHex = function (hsb) { + return rgbToHex(hsbToRgb(hsb)); + }; + $.fn.extend({ + colpick: colpick.init, + colpickHide: colpick.hidePicker, + colpickShow: colpick.showPicker, + colpickSetColor: colpick.setColor, + colpickDestroy: colpick.destroy + }); + $.extend({ + colpick: { + rgbToHex: rgbToHex, + rgbToHsb: rgbToHsb, + hsbToHex: hsbToHex, + hsbToRgb: hsbToRgb, + hexToHsb: hexToHsb, + hexToRgb: hexToRgb + } + }); +})); \ No newline at end of file diff --git a/hassio-google-drive-backup/backup/static/js/iconfont/MaterialIcons-Regular.eot b/hassio-google-drive-backup/backup/static/js/iconfont/MaterialIcons-Regular.eot new file mode 100644 index 0000000000000000000000000000000000000000..70508ebabc9992e64f1314f866b2d7ab90438c58 GIT binary patch literal 143258 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 Hzx)3OGG_Ot literal 0 HcmV?d00001 diff --git a/hassio-google-drive-backup/backup/static/js/iconfont/MaterialIcons-Regular.ijmap b/hassio-google-drive-backup/backup/static/js/iconfont/MaterialIcons-Regular.ijmap new file mode 100644 index 0000000..d9f1d25 --- /dev/null +++ b/hassio-google-drive-backup/backup/static/js/iconfont/MaterialIcons-Regular.ijmap @@ -0,0 +1 @@ +{"icons":{"e84d":{"name":"3d Rotation"},"eb3b":{"name":"Ac Unit"},"e190":{"name":"Access Alarm"},"e191":{"name":"Access Alarms"},"e192":{"name":"Access Time"},"e84e":{"name":"Accessibility"},"e914":{"name":"Accessible"},"e84f":{"name":"Account Balance"},"e850":{"name":"Account Balance Wallet"},"e851":{"name":"Account Box"},"e853":{"name":"Account Circle"},"e60e":{"name":"Adb"},"e145":{"name":"Add"},"e439":{"name":"Add A Photo"},"e193":{"name":"Add Alarm"},"e003":{"name":"Add Alert"},"e146":{"name":"Add Box"},"e147":{"name":"Add Circle"},"e148":{"name":"Add Circle Outline"},"e567":{"name":"Add Location"},"e854":{"name":"Add Shopping Cart"},"e39d":{"name":"Add To Photos"},"e05c":{"name":"Add To Queue"},"e39e":{"name":"Adjust"},"e630":{"name":"Airline Seat Flat"},"e631":{"name":"Airline Seat Flat Angled"},"e632":{"name":"Airline Seat Individual Suite"},"e633":{"name":"Airline Seat Legroom Extra"},"e634":{"name":"Airline Seat Legroom Normal"},"e635":{"name":"Airline Seat Legroom Reduced"},"e636":{"name":"Airline Seat Recline Extra"},"e637":{"name":"Airline Seat Recline Normal"},"e195":{"name":"Airplanemode Active"},"e194":{"name":"Airplanemode Inactive"},"e055":{"name":"Airplay"},"eb3c":{"name":"Airport Shuttle"},"e855":{"name":"Alarm"},"e856":{"name":"Alarm Add"},"e857":{"name":"Alarm Off"},"e858":{"name":"Alarm On"},"e019":{"name":"Album"},"eb3d":{"name":"All Inclusive"},"e90b":{"name":"All Out"},"e859":{"name":"Android"},"e85a":{"name":"Announcement"},"e5c3":{"name":"Apps"},"e149":{"name":"Archive"},"e5c4":{"name":"Arrow Back"},"e5db":{"name":"Arrow Downward"},"e5c5":{"name":"Arrow Drop Down"},"e5c6":{"name":"Arrow Drop Down Circle"},"e5c7":{"name":"Arrow Drop Up"},"e5c8":{"name":"Arrow Forward"},"e5d8":{"name":"Arrow Upward"},"e060":{"name":"Art Track"},"e85b":{"name":"Aspect Ratio"},"e85c":{"name":"Assessment"},"e85d":{"name":"Assignment"},"e85e":{"name":"Assignment Ind"},"e85f":{"name":"Assignment Late"},"e860":{"name":"Assignment Return"},"e861":{"name":"Assignment Returned"},"e862":{"name":"Assignment Turned In"},"e39f":{"name":"Assistant"},"e3a0":{"name":"Assistant Photo"},"e226":{"name":"Attach File"},"e227":{"name":"Attach Money"},"e2bc":{"name":"Attachment"},"e3a1":{"name":"Audiotrack"},"e863":{"name":"Autorenew"},"e01b":{"name":"Av Timer"},"e14a":{"name":"Backspace"},"e864":{"name":"Backup"},"e19c":{"name":"Battery Alert"},"e1a3":{"name":"Battery Charging Full"},"e1a4":{"name":"Battery Full"},"e1a5":{"name":"Battery Std"},"e1a6":{"name":"Battery Unknown"},"eb3e":{"name":"Beach Access"},"e52d":{"name":"Beenhere"},"e14b":{"name":"Block"},"e1a7":{"name":"Bluetooth"},"e60f":{"name":"Bluetooth Audio"},"e1a8":{"name":"Bluetooth Connected"},"e1a9":{"name":"Bluetooth Disabled"},"e1aa":{"name":"Bluetooth Searching"},"e3a2":{"name":"Blur Circular"},"e3a3":{"name":"Blur Linear"},"e3a4":{"name":"Blur Off"},"e3a5":{"name":"Blur On"},"e865":{"name":"Book"},"e866":{"name":"Bookmark"},"e867":{"name":"Bookmark Border"},"e228":{"name":"Border All"},"e229":{"name":"Border Bottom"},"e22a":{"name":"Border Clear"},"e22b":{"name":"Border Color"},"e22c":{"name":"Border Horizontal"},"e22d":{"name":"Border Inner"},"e22e":{"name":"Border Left"},"e22f":{"name":"Border Outer"},"e230":{"name":"Border Right"},"e231":{"name":"Border Style"},"e232":{"name":"Border Top"},"e233":{"name":"Border Vertical"},"e06b":{"name":"Branding Watermark"},"e3a6":{"name":"Brightness 1"},"e3a7":{"name":"Brightness 2"},"e3a8":{"name":"Brightness 3"},"e3a9":{"name":"Brightness 4"},"e3aa":{"name":"Brightness 5"},"e3ab":{"name":"Brightness 6"},"e3ac":{"name":"Brightness 7"},"e1ab":{"name":"Brightness Auto"},"e1ac":{"name":"Brightness High"},"e1ad":{"name":"Brightness Low"},"e1ae":{"name":"Brightness Medium"},"e3ad":{"name":"Broken Image"},"e3ae":{"name":"Brush"},"e6dd":{"name":"Bubble Chart"},"e868":{"name":"Bug Report"},"e869":{"name":"Build"},"e43c":{"name":"Burst Mode"},"e0af":{"name":"Business"},"eb3f":{"name":"Business Center"},"e86a":{"name":"Cached"},"e7e9":{"name":"Cake"},"e0b0":{"name":"Call"},"e0b1":{"name":"Call End"},"e0b2":{"name":"Call Made"},"e0b3":{"name":"Call Merge"},"e0b4":{"name":"Call Missed"},"e0e4":{"name":"Call Missed Outgoing"},"e0b5":{"name":"Call Received"},"e0b6":{"name":"Call Split"},"e06c":{"name":"Call To Action"},"e3af":{"name":"Camera"},"e3b0":{"name":"Camera Alt"},"e8fc":{"name":"Camera Enhance"},"e3b1":{"name":"Camera Front"},"e3b2":{"name":"Camera Rear"},"e3b3":{"name":"Camera Roll"},"e5c9":{"name":"Cancel"},"e8f6":{"name":"Card Giftcard"},"e8f7":{"name":"Card Membership"},"e8f8":{"name":"Card Travel"},"eb40":{"name":"Casino"},"e307":{"name":"Cast"},"e308":{"name":"Cast Connected"},"e3b4":{"name":"Center Focus Strong"},"e3b5":{"name":"Center Focus Weak"},"e86b":{"name":"Change History"},"e0b7":{"name":"Chat"},"e0ca":{"name":"Chat Bubble"},"e0cb":{"name":"Chat Bubble Outline"},"e5ca":{"name":"Check"},"e834":{"name":"Check Box"},"e835":{"name":"Check Box Outline Blank"},"e86c":{"name":"Check Circle"},"e5cb":{"name":"Chevron Left"},"e5cc":{"name":"Chevron Right"},"eb41":{"name":"Child Care"},"eb42":{"name":"Child Friendly"},"e86d":{"name":"Chrome Reader Mode"},"e86e":{"name":"Class"},"e14c":{"name":"Clear"},"e0b8":{"name":"Clear All"},"e5cd":{"name":"Close"},"e01c":{"name":"Closed Caption"},"e2bd":{"name":"Cloud"},"e2be":{"name":"Cloud Circle"},"e2bf":{"name":"Cloud Done"},"e2c0":{"name":"Cloud Download"},"e2c1":{"name":"Cloud Off"},"e2c2":{"name":"Cloud Queue"},"e2c3":{"name":"Cloud Upload"},"e86f":{"name":"Code"},"e3b6":{"name":"Collections"},"e431":{"name":"Collections Bookmark"},"e3b7":{"name":"Color Lens"},"e3b8":{"name":"Colorize"},"e0b9":{"name":"Comment"},"e3b9":{"name":"Compare"},"e915":{"name":"Compare Arrows"},"e30a":{"name":"Computer"},"e638":{"name":"Confirmation Number"},"e0d0":{"name":"Contact Mail"},"e0cf":{"name":"Contact Phone"},"e0ba":{"name":"Contacts"},"e14d":{"name":"Content Copy"},"e14e":{"name":"Content Cut"},"e14f":{"name":"Content Paste"},"e3ba":{"name":"Control Point"},"e3bb":{"name":"Control Point Duplicate"},"e90c":{"name":"Copyright"},"e150":{"name":"Create"},"e2cc":{"name":"Create New Folder"},"e870":{"name":"Credit Card"},"e3be":{"name":"Crop"},"e3bc":{"name":"Crop 16 9"},"e3bd":{"name":"Crop 3 2"},"e3bf":{"name":"Crop 5 4"},"e3c0":{"name":"Crop 7 5"},"e3c1":{"name":"Crop Din"},"e3c2":{"name":"Crop Free"},"e3c3":{"name":"Crop Landscape"},"e3c4":{"name":"Crop Original"},"e3c5":{"name":"Crop Portrait"},"e437":{"name":"Crop Rotate"},"e3c6":{"name":"Crop Square"},"e871":{"name":"Dashboard"},"e1af":{"name":"Data Usage"},"e916":{"name":"Date Range"},"e3c7":{"name":"Dehaze"},"e872":{"name":"Delete"},"e92b":{"name":"Delete Forever"},"e16c":{"name":"Delete Sweep"},"e873":{"name":"Description"},"e30b":{"name":"Desktop Mac"},"e30c":{"name":"Desktop Windows"},"e3c8":{"name":"Details"},"e30d":{"name":"Developer Board"},"e1b0":{"name":"Developer Mode"},"e335":{"name":"Device Hub"},"e1b1":{"name":"Devices"},"e337":{"name":"Devices Other"},"e0bb":{"name":"Dialer Sip"},"e0bc":{"name":"Dialpad"},"e52e":{"name":"Directions"},"e52f":{"name":"Directions Bike"},"e532":{"name":"Directions Boat"},"e530":{"name":"Directions Bus"},"e531":{"name":"Directions Car"},"e534":{"name":"Directions Railway"},"e566":{"name":"Directions Run"},"e533":{"name":"Directions Subway"},"e535":{"name":"Directions Transit"},"e536":{"name":"Directions Walk"},"e610":{"name":"Disc Full"},"e875":{"name":"Dns"},"e612":{"name":"Do Not Disturb"},"e611":{"name":"Do Not Disturb Alt"},"e643":{"name":"Do Not Disturb Off"},"e644":{"name":"Do Not Disturb On"},"e30e":{"name":"Dock"},"e7ee":{"name":"Domain"},"e876":{"name":"Done"},"e877":{"name":"Done All"},"e917":{"name":"Donut Large"},"e918":{"name":"Donut Small"},"e151":{"name":"Drafts"},"e25d":{"name":"Drag Handle"},"e613":{"name":"Drive Eta"},"e1b2":{"name":"Dvr"},"e3c9":{"name":"Edit"},"e568":{"name":"Edit Location"},"e8fb":{"name":"Eject"},"e0be":{"name":"Email"},"e63f":{"name":"Enhanced Encryption"},"e01d":{"name":"Equalizer"},"e000":{"name":"Error"},"e001":{"name":"Error Outline"},"e926":{"name":"Euro Symbol"},"e56d":{"name":"Ev Station"},"e878":{"name":"Event"},"e614":{"name":"Event Available"},"e615":{"name":"Event Busy"},"e616":{"name":"Event Note"},"e903":{"name":"Event Seat"},"e879":{"name":"Exit To App"},"e5ce":{"name":"Expand Less"},"e5cf":{"name":"Expand More"},"e01e":{"name":"Explicit"},"e87a":{"name":"Explore"},"e3ca":{"name":"Exposure"},"e3cb":{"name":"Exposure Neg 1"},"e3cc":{"name":"Exposure Neg 2"},"e3cd":{"name":"Exposure Plus 1"},"e3ce":{"name":"Exposure Plus 2"},"e3cf":{"name":"Exposure Zero"},"e87b":{"name":"Extension"},"e87c":{"name":"Face"},"e01f":{"name":"Fast Forward"},"e020":{"name":"Fast Rewind"},"e87d":{"name":"Favorite"},"e87e":{"name":"Favorite Border"},"e06d":{"name":"Featured Play List"},"e06e":{"name":"Featured Video"},"e87f":{"name":"Feedback"},"e05d":{"name":"Fiber Dvr"},"e061":{"name":"Fiber Manual Record"},"e05e":{"name":"Fiber New"},"e06a":{"name":"Fiber Pin"},"e062":{"name":"Fiber Smart Record"},"e2c4":{"name":"File Download"},"e2c6":{"name":"File Upload"},"e3d3":{"name":"Filter"},"e3d0":{"name":"Filter 1"},"e3d1":{"name":"Filter 2"},"e3d2":{"name":"Filter 3"},"e3d4":{"name":"Filter 4"},"e3d5":{"name":"Filter 5"},"e3d6":{"name":"Filter 6"},"e3d7":{"name":"Filter 7"},"e3d8":{"name":"Filter 8"},"e3d9":{"name":"Filter 9"},"e3da":{"name":"Filter 9 Plus"},"e3db":{"name":"Filter B And W"},"e3dc":{"name":"Filter Center Focus"},"e3dd":{"name":"Filter Drama"},"e3de":{"name":"Filter Frames"},"e3df":{"name":"Filter Hdr"},"e152":{"name":"Filter List"},"e3e0":{"name":"Filter None"},"e3e2":{"name":"Filter Tilt Shift"},"e3e3":{"name":"Filter Vintage"},"e880":{"name":"Find In Page"},"e881":{"name":"Find Replace"},"e90d":{"name":"Fingerprint"},"e5dc":{"name":"First Page"},"eb43":{"name":"Fitness Center"},"e153":{"name":"Flag"},"e3e4":{"name":"Flare"},"e3e5":{"name":"Flash Auto"},"e3e6":{"name":"Flash Off"},"e3e7":{"name":"Flash On"},"e539":{"name":"Flight"},"e904":{"name":"Flight Land"},"e905":{"name":"Flight Takeoff"},"e3e8":{"name":"Flip"},"e882":{"name":"Flip To Back"},"e883":{"name":"Flip To Front"},"e2c7":{"name":"Folder"},"e2c8":{"name":"Folder Open"},"e2c9":{"name":"Folder Shared"},"e617":{"name":"Folder Special"},"e167":{"name":"Font Download"},"e234":{"name":"Format Align Center"},"e235":{"name":"Format Align Justify"},"e236":{"name":"Format Align Left"},"e237":{"name":"Format Align Right"},"e238":{"name":"Format Bold"},"e239":{"name":"Format Clear"},"e23a":{"name":"Format Color Fill"},"e23b":{"name":"Format Color Reset"},"e23c":{"name":"Format Color Text"},"e23d":{"name":"Format Indent Decrease"},"e23e":{"name":"Format Indent Increase"},"e23f":{"name":"Format Italic"},"e240":{"name":"Format Line Spacing"},"e241":{"name":"Format List Bulleted"},"e242":{"name":"Format List Numbered"},"e243":{"name":"Format Paint"},"e244":{"name":"Format Quote"},"e25e":{"name":"Format Shapes"},"e245":{"name":"Format Size"},"e246":{"name":"Format Strikethrough"},"e247":{"name":"Format Textdirection L To R"},"e248":{"name":"Format Textdirection R To L"},"e249":{"name":"Format Underlined"},"e0bf":{"name":"Forum"},"e154":{"name":"Forward"},"e056":{"name":"Forward 10"},"e057":{"name":"Forward 30"},"e058":{"name":"Forward 5"},"eb44":{"name":"Free Breakfast"},"e5d0":{"name":"Fullscreen"},"e5d1":{"name":"Fullscreen Exit"},"e24a":{"name":"Functions"},"e927":{"name":"G Translate"},"e30f":{"name":"Gamepad"},"e021":{"name":"Games"},"e90e":{"name":"Gavel"},"e155":{"name":"Gesture"},"e884":{"name":"Get App"},"e908":{"name":"Gif"},"eb45":{"name":"Golf Course"},"e1b3":{"name":"Gps Fixed"},"e1b4":{"name":"Gps Not Fixed"},"e1b5":{"name":"Gps Off"},"e885":{"name":"Grade"},"e3e9":{"name":"Gradient"},"e3ea":{"name":"Grain"},"e1b8":{"name":"Graphic Eq"},"e3eb":{"name":"Grid Off"},"e3ec":{"name":"Grid On"},"e7ef":{"name":"Group"},"e7f0":{"name":"Group Add"},"e886":{"name":"Group Work"},"e052":{"name":"Hd"},"e3ed":{"name":"Hdr Off"},"e3ee":{"name":"Hdr On"},"e3f1":{"name":"Hdr Strong"},"e3f2":{"name":"Hdr Weak"},"e310":{"name":"Headset"},"e311":{"name":"Headset Mic"},"e3f3":{"name":"Healing"},"e023":{"name":"Hearing"},"e887":{"name":"Help"},"e8fd":{"name":"Help Outline"},"e024":{"name":"High Quality"},"e25f":{"name":"Highlight"},"e888":{"name":"Highlight Off"},"e889":{"name":"History"},"e88a":{"name":"Home"},"eb46":{"name":"Hot Tub"},"e53a":{"name":"Hotel"},"e88b":{"name":"Hourglass Empty"},"e88c":{"name":"Hourglass Full"},"e902":{"name":"Http"},"e88d":{"name":"Https"},"e3f4":{"name":"Image"},"e3f5":{"name":"Image Aspect Ratio"},"e0e0":{"name":"Import Contacts"},"e0c3":{"name":"Import Export"},"e912":{"name":"Important Devices"},"e156":{"name":"Inbox"},"e909":{"name":"Indeterminate Check Box"},"e88e":{"name":"Info"},"e88f":{"name":"Info Outline"},"e890":{"name":"Input"},"e24b":{"name":"Insert Chart"},"e24c":{"name":"Insert Comment"},"e24d":{"name":"Insert Drive File"},"e24e":{"name":"Insert Emoticon"},"e24f":{"name":"Insert Invitation"},"e250":{"name":"Insert Link"},"e251":{"name":"Insert Photo"},"e891":{"name":"Invert Colors"},"e0c4":{"name":"Invert Colors Off"},"e3f6":{"name":"Iso"},"e312":{"name":"Keyboard"},"e313":{"name":"Keyboard Arrow Down"},"e314":{"name":"Keyboard Arrow Left"},"e315":{"name":"Keyboard Arrow Right"},"e316":{"name":"Keyboard Arrow Up"},"e317":{"name":"Keyboard Backspace"},"e318":{"name":"Keyboard Capslock"},"e31a":{"name":"Keyboard Hide"},"e31b":{"name":"Keyboard Return"},"e31c":{"name":"Keyboard Tab"},"e31d":{"name":"Keyboard Voice"},"eb47":{"name":"Kitchen"},"e892":{"name":"Label"},"e893":{"name":"Label Outline"},"e3f7":{"name":"Landscape"},"e894":{"name":"Language"},"e31e":{"name":"Laptop"},"e31f":{"name":"Laptop Chromebook"},"e320":{"name":"Laptop Mac"},"e321":{"name":"Laptop Windows"},"e5dd":{"name":"Last Page"},"e895":{"name":"Launch"},"e53b":{"name":"Layers"},"e53c":{"name":"Layers Clear"},"e3f8":{"name":"Leak Add"},"e3f9":{"name":"Leak Remove"},"e3fa":{"name":"Lens"},"e02e":{"name":"Library Add"},"e02f":{"name":"Library Books"},"e030":{"name":"Library Music"},"e90f":{"name":"Lightbulb Outline"},"e919":{"name":"Line Style"},"e91a":{"name":"Line Weight"},"e260":{"name":"Linear Scale"},"e157":{"name":"Link"},"e438":{"name":"Linked Camera"},"e896":{"name":"List"},"e0c6":{"name":"Live Help"},"e639":{"name":"Live Tv"},"e53f":{"name":"Local Activity"},"e53d":{"name":"Local Airport"},"e53e":{"name":"Local Atm"},"e540":{"name":"Local Bar"},"e541":{"name":"Local Cafe"},"e542":{"name":"Local Car Wash"},"e543":{"name":"Local Convenience Store"},"e556":{"name":"Local Dining"},"e544":{"name":"Local Drink"},"e545":{"name":"Local Florist"},"e546":{"name":"Local Gas Station"},"e547":{"name":"Local Grocery Store"},"e548":{"name":"Local Hospital"},"e549":{"name":"Local Hotel"},"e54a":{"name":"Local Laundry Service"},"e54b":{"name":"Local Library"},"e54c":{"name":"Local Mall"},"e54d":{"name":"Local Movies"},"e54e":{"name":"Local Offer"},"e54f":{"name":"Local Parking"},"e550":{"name":"Local Pharmacy"},"e551":{"name":"Local Phone"},"e552":{"name":"Local Pizza"},"e553":{"name":"Local Play"},"e554":{"name":"Local Post Office"},"e555":{"name":"Local Printshop"},"e557":{"name":"Local See"},"e558":{"name":"Local Shipping"},"e559":{"name":"Local Taxi"},"e7f1":{"name":"Location City"},"e1b6":{"name":"Location Disabled"},"e0c7":{"name":"Location Off"},"e0c8":{"name":"Location On"},"e1b7":{"name":"Location Searching"},"e897":{"name":"Lock"},"e898":{"name":"Lock Open"},"e899":{"name":"Lock Outline"},"e3fc":{"name":"Looks"},"e3fb":{"name":"Looks 3"},"e3fd":{"name":"Looks 4"},"e3fe":{"name":"Looks 5"},"e3ff":{"name":"Looks 6"},"e400":{"name":"Looks One"},"e401":{"name":"Looks Two"},"e028":{"name":"Loop"},"e402":{"name":"Loupe"},"e16d":{"name":"Low Priority"},"e89a":{"name":"Loyalty"},"e158":{"name":"Mail"},"e0e1":{"name":"Mail Outline"},"e55b":{"name":"Map"},"e159":{"name":"Markunread"},"e89b":{"name":"Markunread Mailbox"},"e322":{"name":"Memory"},"e5d2":{"name":"Menu"},"e252":{"name":"Merge Type"},"e0c9":{"name":"Message"},"e029":{"name":"Mic"},"e02a":{"name":"Mic None"},"e02b":{"name":"Mic Off"},"e618":{"name":"Mms"},"e253":{"name":"Mode Comment"},"e254":{"name":"Mode Edit"},"e263":{"name":"Monetization On"},"e25c":{"name":"Money Off"},"e403":{"name":"Monochrome Photos"},"e7f2":{"name":"Mood"},"e7f3":{"name":"Mood Bad"},"e619":{"name":"More"},"e5d3":{"name":"More Horiz"},"e5d4":{"name":"More Vert"},"e91b":{"name":"Motorcycle"},"e323":{"name":"Mouse"},"e168":{"name":"Move To Inbox"},"e02c":{"name":"Movie"},"e404":{"name":"Movie Creation"},"e43a":{"name":"Movie Filter"},"e6df":{"name":"Multiline Chart"},"e405":{"name":"Music Note"},"e063":{"name":"Music Video"},"e55c":{"name":"My Location"},"e406":{"name":"Nature"},"e407":{"name":"Nature People"},"e408":{"name":"Navigate Before"},"e409":{"name":"Navigate Next"},"e55d":{"name":"Navigation"},"e569":{"name":"Near Me"},"e1b9":{"name":"Network Cell"},"e640":{"name":"Network Check"},"e61a":{"name":"Network Locked"},"e1ba":{"name":"Network Wifi"},"e031":{"name":"New Releases"},"e16a":{"name":"Next Week"},"e1bb":{"name":"Nfc"},"e641":{"name":"No Encryption"},"e0cc":{"name":"No Sim"},"e033":{"name":"Not Interested"},"e06f":{"name":"Note"},"e89c":{"name":"Note Add"},"e7f4":{"name":"Notifications"},"e7f7":{"name":"Notifications Active"},"e7f5":{"name":"Notifications None"},"e7f6":{"name":"Notifications Off"},"e7f8":{"name":"Notifications Paused"},"e90a":{"name":"Offline Pin"},"e63a":{"name":"Ondemand Video"},"e91c":{"name":"Opacity"},"e89d":{"name":"Open In Browser"},"e89e":{"name":"Open In New"},"e89f":{"name":"Open With"},"e7f9":{"name":"Pages"},"e8a0":{"name":"Pageview"},"e40a":{"name":"Palette"},"e925":{"name":"Pan Tool"},"e40b":{"name":"Panorama"},"e40c":{"name":"Panorama Fish Eye"},"e40d":{"name":"Panorama Horizontal"},"e40e":{"name":"Panorama Vertical"},"e40f":{"name":"Panorama Wide Angle"},"e7fa":{"name":"Party Mode"},"e034":{"name":"Pause"},"e035":{"name":"Pause Circle Filled"},"e036":{"name":"Pause Circle Outline"},"e8a1":{"name":"Payment"},"e7fb":{"name":"People"},"e7fc":{"name":"People Outline"},"e8a2":{"name":"Perm Camera Mic"},"e8a3":{"name":"Perm Contact Calendar"},"e8a4":{"name":"Perm Data Setting"},"e8a5":{"name":"Perm Device Information"},"e8a6":{"name":"Perm Identity"},"e8a7":{"name":"Perm Media"},"e8a8":{"name":"Perm Phone Msg"},"e8a9":{"name":"Perm Scan Wifi"},"e7fd":{"name":"Person"},"e7fe":{"name":"Person Add"},"e7ff":{"name":"Person Outline"},"e55a":{"name":"Person Pin"},"e56a":{"name":"Person Pin Circle"},"e63b":{"name":"Personal Video"},"e91d":{"name":"Pets"},"e0cd":{"name":"Phone"},"e324":{"name":"Phone Android"},"e61b":{"name":"Phone Bluetooth Speaker"},"e61c":{"name":"Phone Forwarded"},"e61d":{"name":"Phone In Talk"},"e325":{"name":"Phone Iphone"},"e61e":{"name":"Phone Locked"},"e61f":{"name":"Phone Missed"},"e620":{"name":"Phone Paused"},"e326":{"name":"Phonelink"},"e0db":{"name":"Phonelink Erase"},"e0dc":{"name":"Phonelink Lock"},"e327":{"name":"Phonelink Off"},"e0dd":{"name":"Phonelink Ring"},"e0de":{"name":"Phonelink Setup"},"e410":{"name":"Photo"},"e411":{"name":"Photo Album"},"e412":{"name":"Photo Camera"},"e43b":{"name":"Photo Filter"},"e413":{"name":"Photo Library"},"e432":{"name":"Photo Size Select Actual"},"e433":{"name":"Photo Size Select Large"},"e434":{"name":"Photo Size Select Small"},"e415":{"name":"Picture As Pdf"},"e8aa":{"name":"Picture In Picture"},"e911":{"name":"Picture In Picture Alt"},"e6c4":{"name":"Pie Chart"},"e6c5":{"name":"Pie Chart Outlined"},"e55e":{"name":"Pin Drop"},"e55f":{"name":"Place"},"e037":{"name":"Play Arrow"},"e038":{"name":"Play Circle Filled"},"e039":{"name":"Play Circle Outline"},"e906":{"name":"Play For Work"},"e03b":{"name":"Playlist Add"},"e065":{"name":"Playlist Add Check"},"e05f":{"name":"Playlist Play"},"e800":{"name":"Plus One"},"e801":{"name":"Poll"},"e8ab":{"name":"Polymer"},"eb48":{"name":"Pool"},"e0ce":{"name":"Portable Wifi Off"},"e416":{"name":"Portrait"},"e63c":{"name":"Power"},"e336":{"name":"Power Input"},"e8ac":{"name":"Power Settings New"},"e91e":{"name":"Pregnant Woman"},"e0df":{"name":"Present To All"},"e8ad":{"name":"Print"},"e645":{"name":"Priority High"},"e80b":{"name":"Public"},"e255":{"name":"Publish"},"e8ae":{"name":"Query Builder"},"e8af":{"name":"Question Answer"},"e03c":{"name":"Queue"},"e03d":{"name":"Queue Music"},"e066":{"name":"Queue Play Next"},"e03e":{"name":"Radio"},"e837":{"name":"Radio Button Checked"},"e836":{"name":"Radio Button Unchecked"},"e560":{"name":"Rate Review"},"e8b0":{"name":"Receipt"},"e03f":{"name":"Recent Actors"},"e91f":{"name":"Record Voice Over"},"e8b1":{"name":"Redeem"},"e15a":{"name":"Redo"},"e5d5":{"name":"Refresh"},"e15b":{"name":"Remove"},"e15c":{"name":"Remove Circle"},"e15d":{"name":"Remove Circle Outline"},"e067":{"name":"Remove From Queue"},"e417":{"name":"Remove Red Eye"},"e928":{"name":"Remove Shopping Cart"},"e8fe":{"name":"Reorder"},"e040":{"name":"Repeat"},"e041":{"name":"Repeat One"},"e042":{"name":"Replay"},"e059":{"name":"Replay 10"},"e05a":{"name":"Replay 30"},"e05b":{"name":"Replay 5"},"e15e":{"name":"Reply"},"e15f":{"name":"Reply All"},"e160":{"name":"Report"},"e8b2":{"name":"Report Problem"},"e56c":{"name":"Restaurant"},"e561":{"name":"Restaurant Menu"},"e8b3":{"name":"Restore"},"e929":{"name":"Restore Page"},"e0d1":{"name":"Ring Volume"},"e8b4":{"name":"Room"},"eb49":{"name":"Room Service"},"e418":{"name":"Rotate 90 Degrees Ccw"},"e419":{"name":"Rotate Left"},"e41a":{"name":"Rotate Right"},"e920":{"name":"Rounded Corner"},"e328":{"name":"Router"},"e921":{"name":"Rowing"},"e0e5":{"name":"Rss Feed"},"e642":{"name":"Rv Hookup"},"e562":{"name":"Satellite"},"e161":{"name":"Save"},"e329":{"name":"Scanner"},"e8b5":{"name":"Schedule"},"e80c":{"name":"School"},"e1be":{"name":"Screen Lock Landscape"},"e1bf":{"name":"Screen Lock Portrait"},"e1c0":{"name":"Screen Lock Rotation"},"e1c1":{"name":"Screen Rotation"},"e0e2":{"name":"Screen Share"},"e623":{"name":"Sd Card"},"e1c2":{"name":"Sd Storage"},"e8b6":{"name":"Search"},"e32a":{"name":"Security"},"e162":{"name":"Select All"},"e163":{"name":"Send"},"e811":{"name":"Sentiment Dissatisfied"},"e812":{"name":"Sentiment Neutral"},"e813":{"name":"Sentiment Satisfied"},"e814":{"name":"Sentiment Very Dissatisfied"},"e815":{"name":"Sentiment Very Satisfied"},"e8b8":{"name":"Settings"},"e8b9":{"name":"Settings Applications"},"e8ba":{"name":"Settings Backup Restore"},"e8bb":{"name":"Settings Bluetooth"},"e8bd":{"name":"Settings Brightness"},"e8bc":{"name":"Settings Cell"},"e8be":{"name":"Settings Ethernet"},"e8bf":{"name":"Settings Input Antenna"},"e8c0":{"name":"Settings Input Component"},"e8c1":{"name":"Settings Input Composite"},"e8c2":{"name":"Settings Input Hdmi"},"e8c3":{"name":"Settings Input Svideo"},"e8c4":{"name":"Settings Overscan"},"e8c5":{"name":"Settings Phone"},"e8c6":{"name":"Settings Power"},"e8c7":{"name":"Settings Remote"},"e1c3":{"name":"Settings System Daydream"},"e8c8":{"name":"Settings Voice"},"e80d":{"name":"Share"},"e8c9":{"name":"Shop"},"e8ca":{"name":"Shop Two"},"e8cb":{"name":"Shopping Basket"},"e8cc":{"name":"Shopping Cart"},"e261":{"name":"Short Text"},"e6e1":{"name":"Show Chart"},"e043":{"name":"Shuffle"},"e1c8":{"name":"Signal Cellular 4 Bar"},"e1cd":{"name":"Signal Cellular Connected No Internet 4 Bar"},"e1ce":{"name":"Signal Cellular No Sim"},"e1cf":{"name":"Signal Cellular Null"},"e1d0":{"name":"Signal Cellular Off"},"e1d8":{"name":"Signal Wifi 4 Bar"},"e1d9":{"name":"Signal Wifi 4 Bar Lock"},"e1da":{"name":"Signal Wifi Off"},"e32b":{"name":"Sim Card"},"e624":{"name":"Sim Card Alert"},"e044":{"name":"Skip Next"},"e045":{"name":"Skip Previous"},"e41b":{"name":"Slideshow"},"e068":{"name":"Slow Motion Video"},"e32c":{"name":"Smartphone"},"eb4a":{"name":"Smoke Free"},"eb4b":{"name":"Smoking Rooms"},"e625":{"name":"Sms"},"e626":{"name":"Sms Failed"},"e046":{"name":"Snooze"},"e164":{"name":"Sort"},"e053":{"name":"Sort By Alpha"},"eb4c":{"name":"Spa"},"e256":{"name":"Space Bar"},"e32d":{"name":"Speaker"},"e32e":{"name":"Speaker Group"},"e8cd":{"name":"Speaker Notes"},"e92a":{"name":"Speaker Notes Off"},"e0d2":{"name":"Speaker Phone"},"e8ce":{"name":"Spellcheck"},"e838":{"name":"Star"},"e83a":{"name":"Star Border"},"e839":{"name":"Star Half"},"e8d0":{"name":"Stars"},"e0d3":{"name":"Stay Current Landscape"},"e0d4":{"name":"Stay Current Portrait"},"e0d5":{"name":"Stay Primary Landscape"},"e0d6":{"name":"Stay Primary Portrait"},"e047":{"name":"Stop"},"e0e3":{"name":"Stop Screen Share"},"e1db":{"name":"Storage"},"e8d1":{"name":"Store"},"e563":{"name":"Store Mall Directory"},"e41c":{"name":"Straighten"},"e56e":{"name":"Streetview"},"e257":{"name":"Strikethrough S"},"e41d":{"name":"Style"},"e5d9":{"name":"Subdirectory Arrow Left"},"e5da":{"name":"Subdirectory Arrow Right"},"e8d2":{"name":"Subject"},"e064":{"name":"Subscriptions"},"e048":{"name":"Subtitles"},"e56f":{"name":"Subway"},"e8d3":{"name":"Supervisor Account"},"e049":{"name":"Surround Sound"},"e0d7":{"name":"Swap Calls"},"e8d4":{"name":"Swap Horiz"},"e8d5":{"name":"Swap Vert"},"e8d6":{"name":"Swap Vertical Circle"},"e41e":{"name":"Switch Camera"},"e41f":{"name":"Switch Video"},"e627":{"name":"Sync"},"e628":{"name":"Sync Disabled"},"e629":{"name":"Sync Problem"},"e62a":{"name":"System Update"},"e8d7":{"name":"System Update Alt"},"e8d8":{"name":"Tab"},"e8d9":{"name":"Tab Unselected"},"e32f":{"name":"Tablet"},"e330":{"name":"Tablet Android"},"e331":{"name":"Tablet Mac"},"e420":{"name":"Tag Faces"},"e62b":{"name":"Tap And Play"},"e564":{"name":"Terrain"},"e262":{"name":"Text Fields"},"e165":{"name":"Text Format"},"e0d8":{"name":"Textsms"},"e421":{"name":"Texture"},"e8da":{"name":"Theaters"},"e8db":{"name":"Thumb Down"},"e8dc":{"name":"Thumb Up"},"e8dd":{"name":"Thumbs Up Down"},"e62c":{"name":"Time To Leave"},"e422":{"name":"Timelapse"},"e922":{"name":"Timeline"},"e425":{"name":"Timer"},"e423":{"name":"Timer 10"},"e424":{"name":"Timer 3"},"e426":{"name":"Timer Off"},"e264":{"name":"Title"},"e8de":{"name":"Toc"},"e8df":{"name":"Today"},"e8e0":{"name":"Toll"},"e427":{"name":"Tonality"},"e913":{"name":"Touch App"},"e332":{"name":"Toys"},"e8e1":{"name":"Track Changes"},"e565":{"name":"Traffic"},"e570":{"name":"Train"},"e571":{"name":"Tram"},"e572":{"name":"Transfer Within A Station"},"e428":{"name":"Transform"},"e8e2":{"name":"Translate"},"e8e3":{"name":"Trending Down"},"e8e4":{"name":"Trending Flat"},"e8e5":{"name":"Trending Up"},"e429":{"name":"Tune"},"e8e6":{"name":"Turned In"},"e8e7":{"name":"Turned In Not"},"e333":{"name":"Tv"},"e169":{"name":"Unarchive"},"e166":{"name":"Undo"},"e5d6":{"name":"Unfold Less"},"e5d7":{"name":"Unfold More"},"e923":{"name":"Update"},"e1e0":{"name":"Usb"},"e8e8":{"name":"Verified User"},"e258":{"name":"Vertical Align Bottom"},"e259":{"name":"Vertical Align Center"},"e25a":{"name":"Vertical Align Top"},"e62d":{"name":"Vibration"},"e070":{"name":"Video Call"},"e071":{"name":"Video Label"},"e04a":{"name":"Video Library"},"e04b":{"name":"Videocam"},"e04c":{"name":"Videocam Off"},"e338":{"name":"Videogame Asset"},"e8e9":{"name":"View Agenda"},"e8ea":{"name":"View Array"},"e8eb":{"name":"View Carousel"},"e8ec":{"name":"View Column"},"e42a":{"name":"View Comfy"},"e42b":{"name":"View Compact"},"e8ed":{"name":"View Day"},"e8ee":{"name":"View Headline"},"e8ef":{"name":"View List"},"e8f0":{"name":"View Module"},"e8f1":{"name":"View Quilt"},"e8f2":{"name":"View Stream"},"e8f3":{"name":"View Week"},"e435":{"name":"Vignette"},"e8f4":{"name":"Visibility"},"e8f5":{"name":"Visibility Off"},"e62e":{"name":"Voice Chat"},"e0d9":{"name":"Voicemail"},"e04d":{"name":"Volume Down"},"e04e":{"name":"Volume Mute"},"e04f":{"name":"Volume Off"},"e050":{"name":"Volume Up"},"e0da":{"name":"Vpn Key"},"e62f":{"name":"Vpn Lock"},"e1bc":{"name":"Wallpaper"},"e002":{"name":"Warning"},"e334":{"name":"Watch"},"e924":{"name":"Watch Later"},"e42c":{"name":"Wb Auto"},"e42d":{"name":"Wb Cloudy"},"e42e":{"name":"Wb Incandescent"},"e436":{"name":"Wb Iridescent"},"e430":{"name":"Wb Sunny"},"e63d":{"name":"Wc"},"e051":{"name":"Web"},"e069":{"name":"Web Asset"},"e16b":{"name":"Weekend"},"e80e":{"name":"Whatshot"},"e1bd":{"name":"Widgets"},"e63e":{"name":"Wifi"},"e1e1":{"name":"Wifi Lock"},"e1e2":{"name":"Wifi Tethering"},"e8f9":{"name":"Work"},"e25b":{"name":"Wrap Text"},"e8fa":{"name":"Youtube Searched For"},"e8ff":{"name":"Zoom In"},"e900":{"name":"Zoom Out"},"e56b":{"name":"Zoom Out Map"}}} \ No newline at end of file diff --git a/hassio-google-drive-backup/backup/static/js/iconfont/MaterialIcons-Regular.svg b/hassio-google-drive-backup/backup/static/js/iconfont/MaterialIcons-Regular.svg new file mode 100644 index 0000000..a449327 --- /dev/null +++ b/hassio-google-drive-backup/backup/static/js/iconfont/MaterialIcons-Regular.svg @@ -0,0 +1,2373 @@ + + + + + +Created by FontForge 20151118 at Mon Feb 8 11:58:02 2016 + By shyndman +Copyright 2015 Google, Inc. All Rights Reserved. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/hassio-google-drive-backup/backup/static/js/iconfont/MaterialIcons-Regular.ttf b/hassio-google-drive-backup/backup/static/js/iconfont/MaterialIcons-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..7015564ad166a3e9d88c82f17829f0cc01ebe29a GIT binary patch literal 128180 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 ohyj3!`#ee%L;DTjx@8!5k5~VH0QmdE^#A|> literal 0 HcmV?d00001 diff --git a/hassio-google-drive-backup/backup/static/js/iconfont/MaterialIcons-Regular.woff b/hassio-google-drive-backup/backup/static/js/iconfont/MaterialIcons-Regular.woff new file mode 100644 index 0000000000000000000000000000000000000000..b648a3eea2d16b6ce783906d6b7d5f251b9eb56c GIT binary patch literal 57620 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

    wF7DI|Kx|V1sBQ9FYs>m5C)C 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 BJ$(QG literal 0 HcmV?d00001 diff --git a/hassio-google-drive-backup/backup/static/js/iconfont/MaterialIcons-Regular.woff2 b/hassio-google-drive-backup/backup/static/js/iconfont/MaterialIcons-Regular.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..9fa211252080046a23b2449dbdced6abc2b0bb34 GIT binary patch literal 44300 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 QW8}wPUeo^@jR61v08@RD2LJ#7 literal 0 HcmV?d00001 diff --git a/hassio-google-drive-backup/backup/static/js/iconfont/README.md b/hassio-google-drive-backup/backup/static/js/iconfont/README.md new file mode 100644 index 0000000..ce4141e --- /dev/null +++ b/hassio-google-drive-backup/backup/static/js/iconfont/README.md @@ -0,0 +1,9 @@ +The recommended way to use the Material Icons font is by linking to the web font hosted on Google Fonts: + +```html + +``` + +Read more in our full usage guide: +http://google.github.io/material-design-icons/#icon-font-for-the-web diff --git a/hassio-google-drive-backup/backup/static/js/iconfont/codepoints b/hassio-google-drive-backup/backup/static/js/iconfont/codepoints new file mode 100644 index 0000000..3c8b075 --- /dev/null +++ b/hassio-google-drive-backup/backup/static/js/iconfont/codepoints @@ -0,0 +1,932 @@ +3d_rotation e84d +ac_unit eb3b +access_alarm e190 +access_alarms e191 +access_time e192 +accessibility e84e +accessible e914 +account_balance e84f +account_balance_wallet e850 +account_box e851 +account_circle e853 +adb e60e +add e145 +add_a_photo e439 +add_alarm e193 +add_alert e003 +add_box e146 +add_circle e147 +add_circle_outline e148 +add_location e567 +add_shopping_cart e854 +add_to_photos e39d +add_to_queue e05c +adjust e39e +airline_seat_flat e630 +airline_seat_flat_angled e631 +airline_seat_individual_suite e632 +airline_seat_legroom_extra e633 +airline_seat_legroom_normal e634 +airline_seat_legroom_reduced e635 +airline_seat_recline_extra e636 +airline_seat_recline_normal e637 +airplanemode_active e195 +airplanemode_inactive e194 +airplay e055 +airport_shuttle eb3c +alarm e855 +alarm_add e856 +alarm_off e857 +alarm_on e858 +album e019 +all_inclusive eb3d +all_out e90b +android e859 +announcement e85a +apps e5c3 +archive e149 +arrow_back e5c4 +arrow_downward e5db +arrow_drop_down e5c5 +arrow_drop_down_circle e5c6 +arrow_drop_up e5c7 +arrow_forward e5c8 +arrow_upward e5d8 +art_track e060 +aspect_ratio e85b +assessment e85c +assignment e85d +assignment_ind e85e +assignment_late e85f +assignment_return e860 +assignment_returned e861 +assignment_turned_in e862 +assistant e39f +assistant_photo e3a0 +attach_file e226 +attach_money e227 +attachment e2bc +audiotrack e3a1 +autorenew e863 +av_timer e01b +backspace e14a +backup e864 +battery_alert e19c +battery_charging_full e1a3 +battery_full e1a4 +battery_std e1a5 +battery_unknown e1a6 +beach_access eb3e +beenhere e52d +block e14b +bluetooth e1a7 +bluetooth_audio e60f +bluetooth_connected e1a8 +bluetooth_disabled e1a9 +bluetooth_searching e1aa +blur_circular e3a2 +blur_linear e3a3 +blur_off e3a4 +blur_on e3a5 +book e865 +bookmark e866 +bookmark_border e867 +border_all e228 +border_bottom e229 +border_clear e22a +border_color e22b +border_horizontal e22c +border_inner e22d +border_left e22e +border_outer e22f +border_right e230 +border_style e231 +border_top e232 +border_vertical e233 +branding_watermark e06b +brightness_1 e3a6 +brightness_2 e3a7 +brightness_3 e3a8 +brightness_4 e3a9 +brightness_5 e3aa +brightness_6 e3ab +brightness_7 e3ac +brightness_auto e1ab +brightness_high e1ac +brightness_low e1ad +brightness_medium e1ae +broken_image e3ad +brush e3ae +bubble_chart e6dd +bug_report e868 +build e869 +burst_mode e43c +business e0af +business_center eb3f +cached e86a +cake e7e9 +call e0b0 +call_end e0b1 +call_made e0b2 +call_merge e0b3 +call_missed e0b4 +call_missed_outgoing e0e4 +call_received e0b5 +call_split e0b6 +call_to_action e06c +camera e3af +camera_alt e3b0 +camera_enhance e8fc +camera_front e3b1 +camera_rear e3b2 +camera_roll e3b3 +cancel e5c9 +card_giftcard e8f6 +card_membership e8f7 +card_travel e8f8 +casino eb40 +cast e307 +cast_connected e308 +center_focus_strong e3b4 +center_focus_weak e3b5 +change_history e86b +chat e0b7 +chat_bubble e0ca +chat_bubble_outline e0cb +check e5ca +check_box e834 +check_box_outline_blank e835 +check_circle e86c +chevron_left e5cb +chevron_right e5cc +child_care eb41 +child_friendly eb42 +chrome_reader_mode e86d +class e86e +clear e14c +clear_all e0b8 +close e5cd +closed_caption e01c +cloud e2bd +cloud_circle e2be +cloud_done e2bf +cloud_download e2c0 +cloud_off e2c1 +cloud_queue e2c2 +cloud_upload e2c3 +code e86f +collections e3b6 +collections_bookmark e431 +color_lens e3b7 +colorize e3b8 +comment e0b9 +compare e3b9 +compare_arrows e915 +computer e30a +confirmation_number e638 +contact_mail e0d0 +contact_phone e0cf +contacts e0ba +content_copy e14d +content_cut e14e +content_paste e14f +control_point e3ba +control_point_duplicate e3bb +copyright e90c +create e150 +create_new_folder e2cc +credit_card e870 +crop e3be +crop_16_9 e3bc +crop_3_2 e3bd +crop_5_4 e3bf +crop_7_5 e3c0 +crop_din e3c1 +crop_free e3c2 +crop_landscape e3c3 +crop_original e3c4 +crop_portrait e3c5 +crop_rotate e437 +crop_square e3c6 +dashboard e871 +data_usage e1af +date_range e916 +dehaze e3c7 +delete e872 +delete_forever e92b +delete_sweep e16c +description e873 +desktop_mac e30b +desktop_windows e30c +details e3c8 +developer_board e30d +developer_mode e1b0 +device_hub e335 +devices e1b1 +devices_other e337 +dialer_sip e0bb +dialpad e0bc +directions e52e +directions_bike e52f +directions_boat e532 +directions_bus e530 +directions_car e531 +directions_railway e534 +directions_run e566 +directions_subway e533 +directions_transit e535 +directions_walk e536 +disc_full e610 +dns e875 +do_not_disturb e612 +do_not_disturb_alt e611 +do_not_disturb_off e643 +do_not_disturb_on e644 +dock e30e +domain e7ee +done e876 +done_all e877 +donut_large e917 +donut_small e918 +drafts e151 +drag_handle e25d +drive_eta e613 +dvr e1b2 +edit e3c9 +edit_location e568 +eject e8fb +email e0be +enhanced_encryption e63f +equalizer e01d +error e000 +error_outline e001 +euro_symbol e926 +ev_station e56d +event e878 +event_available e614 +event_busy e615 +event_note e616 +event_seat e903 +exit_to_app e879 +expand_less e5ce +expand_more e5cf +explicit e01e +explore e87a +exposure e3ca +exposure_neg_1 e3cb +exposure_neg_2 e3cc +exposure_plus_1 e3cd +exposure_plus_2 e3ce +exposure_zero e3cf +extension e87b +face e87c +fast_forward e01f +fast_rewind e020 +favorite e87d +favorite_border e87e +featured_play_list e06d +featured_video e06e +feedback e87f +fiber_dvr e05d +fiber_manual_record e061 +fiber_new e05e +fiber_pin e06a +fiber_smart_record e062 +file_download e2c4 +file_upload e2c6 +filter e3d3 +filter_1 e3d0 +filter_2 e3d1 +filter_3 e3d2 +filter_4 e3d4 +filter_5 e3d5 +filter_6 e3d6 +filter_7 e3d7 +filter_8 e3d8 +filter_9 e3d9 +filter_9_plus e3da +filter_b_and_w e3db +filter_center_focus e3dc +filter_drama e3dd +filter_frames e3de +filter_hdr e3df +filter_list e152 +filter_none e3e0 +filter_tilt_shift e3e2 +filter_vintage e3e3 +find_in_page e880 +find_replace e881 +fingerprint e90d +first_page e5dc +fitness_center eb43 +flag e153 +flare e3e4 +flash_auto e3e5 +flash_off e3e6 +flash_on e3e7 +flight e539 +flight_land e904 +flight_takeoff e905 +flip e3e8 +flip_to_back e882 +flip_to_front e883 +folder e2c7 +folder_open e2c8 +folder_shared e2c9 +folder_special e617 +font_download e167 +format_align_center e234 +format_align_justify e235 +format_align_left e236 +format_align_right e237 +format_bold e238 +format_clear e239 +format_color_fill e23a +format_color_reset e23b +format_color_text e23c +format_indent_decrease e23d +format_indent_increase e23e +format_italic e23f +format_line_spacing e240 +format_list_bulleted e241 +format_list_numbered e242 +format_paint e243 +format_quote e244 +format_shapes e25e +format_size e245 +format_strikethrough e246 +format_textdirection_l_to_r e247 +format_textdirection_r_to_l e248 +format_underlined e249 +forum e0bf +forward e154 +forward_10 e056 +forward_30 e057 +forward_5 e058 +free_breakfast eb44 +fullscreen e5d0 +fullscreen_exit e5d1 +functions e24a +g_translate e927 +gamepad e30f +games e021 +gavel e90e +gesture e155 +get_app e884 +gif e908 +golf_course eb45 +gps_fixed e1b3 +gps_not_fixed e1b4 +gps_off e1b5 +grade e885 +gradient e3e9 +grain e3ea +graphic_eq e1b8 +grid_off e3eb +grid_on e3ec +group e7ef +group_add e7f0 +group_work e886 +hd e052 +hdr_off e3ed +hdr_on e3ee +hdr_strong e3f1 +hdr_weak e3f2 +headset e310 +headset_mic e311 +healing e3f3 +hearing e023 +help e887 +help_outline e8fd +high_quality e024 +highlight e25f +highlight_off e888 +history e889 +home e88a +hot_tub eb46 +hotel e53a +hourglass_empty e88b +hourglass_full e88c +http e902 +https e88d +image e3f4 +image_aspect_ratio e3f5 +import_contacts e0e0 +import_export e0c3 +important_devices e912 +inbox e156 +indeterminate_check_box e909 +info e88e +info_outline e88f +input e890 +insert_chart e24b +insert_comment e24c +insert_drive_file e24d +insert_emoticon e24e +insert_invitation e24f +insert_link e250 +insert_photo e251 +invert_colors e891 +invert_colors_off e0c4 +iso e3f6 +keyboard e312 +keyboard_arrow_down e313 +keyboard_arrow_left e314 +keyboard_arrow_right e315 +keyboard_arrow_up e316 +keyboard_backspace e317 +keyboard_capslock e318 +keyboard_hide e31a +keyboard_return e31b +keyboard_tab e31c +keyboard_voice e31d +kitchen eb47 +label e892 +label_outline e893 +landscape e3f7 +language e894 +laptop e31e +laptop_chromebook e31f +laptop_mac e320 +laptop_windows e321 +last_page e5dd +launch e895 +layers e53b +layers_clear e53c +leak_add e3f8 +leak_remove e3f9 +lens e3fa +library_add e02e +library_books e02f +library_music e030 +lightbulb_outline e90f +line_style e919 +line_weight e91a +linear_scale e260 +link e157 +linked_camera e438 +list e896 +live_help e0c6 +live_tv e639 +local_activity e53f +local_airport e53d +local_atm e53e +local_bar e540 +local_cafe e541 +local_car_wash e542 +local_convenience_store e543 +local_dining e556 +local_drink e544 +local_florist e545 +local_gas_station e546 +local_grocery_store e547 +local_hospital e548 +local_hotel e549 +local_laundry_service e54a +local_library e54b +local_mall e54c +local_movies e54d +local_offer e54e +local_parking e54f +local_pharmacy e550 +local_phone e551 +local_pizza e552 +local_play e553 +local_post_office e554 +local_printshop e555 +local_see e557 +local_shipping e558 +local_taxi e559 +location_city e7f1 +location_disabled e1b6 +location_off e0c7 +location_on e0c8 +location_searching e1b7 +lock e897 +lock_open e898 +lock_outline e899 +looks e3fc +looks_3 e3fb +looks_4 e3fd +looks_5 e3fe +looks_6 e3ff +looks_one e400 +looks_two e401 +loop e028 +loupe e402 +low_priority e16d +loyalty e89a +mail e158 +mail_outline e0e1 +map e55b +markunread e159 +markunread_mailbox e89b +memory e322 +menu e5d2 +merge_type e252 +message e0c9 +mic e029 +mic_none e02a +mic_off e02b +mms e618 +mode_comment e253 +mode_edit e254 +monetization_on e263 +money_off e25c +monochrome_photos e403 +mood e7f2 +mood_bad e7f3 +more e619 +more_horiz e5d3 +more_vert e5d4 +motorcycle e91b +mouse e323 +move_to_inbox e168 +movie e02c +movie_creation e404 +movie_filter e43a +multiline_chart e6df +music_note e405 +music_video e063 +my_location e55c +nature e406 +nature_people e407 +navigate_before e408 +navigate_next e409 +navigation e55d +near_me e569 +network_cell e1b9 +network_check e640 +network_locked e61a +network_wifi e1ba +new_releases e031 +next_week e16a +nfc e1bb +no_encryption e641 +no_sim e0cc +not_interested e033 +note e06f +note_add e89c +notifications e7f4 +notifications_active e7f7 +notifications_none e7f5 +notifications_off e7f6 +notifications_paused e7f8 +offline_pin e90a +ondemand_video e63a +opacity e91c +open_in_browser e89d +open_in_new e89e +open_with e89f +pages e7f9 +pageview e8a0 +palette e40a +pan_tool e925 +panorama e40b +panorama_fish_eye e40c +panorama_horizontal e40d +panorama_vertical e40e +panorama_wide_angle e40f +party_mode e7fa +pause e034 +pause_circle_filled e035 +pause_circle_outline e036 +payment e8a1 +people e7fb +people_outline e7fc +perm_camera_mic e8a2 +perm_contact_calendar e8a3 +perm_data_setting e8a4 +perm_device_information e8a5 +perm_identity e8a6 +perm_media e8a7 +perm_phone_msg e8a8 +perm_scan_wifi e8a9 +person e7fd +person_add e7fe +person_outline e7ff +person_pin e55a +person_pin_circle e56a +personal_video e63b +pets e91d +phone e0cd +phone_android e324 +phone_bluetooth_speaker e61b +phone_forwarded e61c +phone_in_talk e61d +phone_iphone e325 +phone_locked e61e +phone_missed e61f +phone_paused e620 +phonelink e326 +phonelink_erase e0db +phonelink_lock e0dc +phonelink_off e327 +phonelink_ring e0dd +phonelink_setup e0de +photo e410 +photo_album e411 +photo_camera e412 +photo_filter e43b +photo_library e413 +photo_size_select_actual e432 +photo_size_select_large e433 +photo_size_select_small e434 +picture_as_pdf e415 +picture_in_picture e8aa +picture_in_picture_alt e911 +pie_chart e6c4 +pie_chart_outlined e6c5 +pin_drop e55e +place e55f +play_arrow e037 +play_circle_filled e038 +play_circle_outline e039 +play_for_work e906 +playlist_add e03b +playlist_add_check e065 +playlist_play e05f +plus_one e800 +poll e801 +polymer e8ab +pool eb48 +portable_wifi_off e0ce +portrait e416 +power e63c +power_input e336 +power_settings_new e8ac +pregnant_woman e91e +present_to_all e0df +print e8ad +priority_high e645 +public e80b +publish e255 +query_builder e8ae +question_answer e8af +queue e03c +queue_music e03d +queue_play_next e066 +radio e03e +radio_button_checked e837 +radio_button_unchecked e836 +rate_review e560 +receipt e8b0 +recent_actors e03f +record_voice_over e91f +redeem e8b1 +redo e15a +refresh e5d5 +remove e15b +remove_circle e15c +remove_circle_outline e15d +remove_from_queue e067 +remove_red_eye e417 +remove_shopping_cart e928 +reorder e8fe +repeat e040 +repeat_one e041 +replay e042 +replay_10 e059 +replay_30 e05a +replay_5 e05b +reply e15e +reply_all e15f +report e160 +report_problem e8b2 +restaurant e56c +restaurant_menu e561 +restore e8b3 +restore_page e929 +ring_volume e0d1 +room e8b4 +room_service eb49 +rotate_90_degrees_ccw e418 +rotate_left e419 +rotate_right e41a +rounded_corner e920 +router e328 +rowing e921 +rss_feed e0e5 +rv_hookup e642 +satellite e562 +save e161 +scanner e329 +schedule e8b5 +school e80c +screen_lock_landscape e1be +screen_lock_portrait e1bf +screen_lock_rotation e1c0 +screen_rotation e1c1 +screen_share e0e2 +sd_card e623 +sd_storage e1c2 +search e8b6 +security e32a +select_all e162 +send e163 +sentiment_dissatisfied e811 +sentiment_neutral e812 +sentiment_satisfied e813 +sentiment_very_dissatisfied e814 +sentiment_very_satisfied e815 +settings e8b8 +settings_applications e8b9 +settings_backup_restore e8ba +settings_bluetooth e8bb +settings_brightness e8bd +settings_cell e8bc +settings_ethernet e8be +settings_input_antenna e8bf +settings_input_component e8c0 +settings_input_composite e8c1 +settings_input_hdmi e8c2 +settings_input_svideo e8c3 +settings_overscan e8c4 +settings_phone e8c5 +settings_power e8c6 +settings_remote e8c7 +settings_system_daydream e1c3 +settings_voice e8c8 +share e80d +shop e8c9 +shop_two e8ca +shopping_basket e8cb +shopping_cart e8cc +short_text e261 +show_chart e6e1 +shuffle e043 +signal_cellular_4_bar e1c8 +signal_cellular_connected_no_internet_4_bar e1cd +signal_cellular_no_sim e1ce +signal_cellular_null e1cf +signal_cellular_off e1d0 +signal_wifi_4_bar e1d8 +signal_wifi_4_bar_lock e1d9 +signal_wifi_off e1da +sim_card e32b +sim_card_alert e624 +skip_next e044 +skip_previous e045 +slideshow e41b +slow_motion_video e068 +smartphone e32c +smoke_free eb4a +smoking_rooms eb4b +sms e625 +sms_failed e626 +snooze e046 +sort e164 +sort_by_alpha e053 +spa eb4c +space_bar e256 +speaker e32d +speaker_group e32e +speaker_notes e8cd +speaker_notes_off e92a +speaker_phone e0d2 +spellcheck e8ce +star e838 +star_border e83a +star_half e839 +stars e8d0 +stay_current_landscape e0d3 +stay_current_portrait e0d4 +stay_primary_landscape e0d5 +stay_primary_portrait e0d6 +stop e047 +stop_screen_share e0e3 +storage e1db +store e8d1 +store_mall_directory e563 +straighten e41c +streetview e56e +strikethrough_s e257 +style e41d +subdirectory_arrow_left e5d9 +subdirectory_arrow_right e5da +subject e8d2 +subscriptions e064 +subtitles e048 +subway e56f +supervisor_account e8d3 +surround_sound e049 +swap_calls e0d7 +swap_horiz e8d4 +swap_vert e8d5 +swap_vertical_circle e8d6 +switch_camera e41e +switch_video e41f +sync e627 +sync_disabled e628 +sync_problem e629 +system_update e62a +system_update_alt e8d7 +tab e8d8 +tab_unselected e8d9 +tablet e32f +tablet_android e330 +tablet_mac e331 +tag_faces e420 +tap_and_play e62b +terrain e564 +text_fields e262 +text_format e165 +textsms e0d8 +texture e421 +theaters e8da +thumb_down e8db +thumb_up e8dc +thumbs_up_down e8dd +time_to_leave e62c +timelapse e422 +timeline e922 +timer e425 +timer_10 e423 +timer_3 e424 +timer_off e426 +title e264 +toc e8de +today e8df +toll e8e0 +tonality e427 +touch_app e913 +toys e332 +track_changes e8e1 +traffic e565 +train e570 +tram e571 +transfer_within_a_station e572 +transform e428 +translate e8e2 +trending_down e8e3 +trending_flat e8e4 +trending_up e8e5 +tune e429 +turned_in e8e6 +turned_in_not e8e7 +tv e333 +unarchive e169 +undo e166 +unfold_less e5d6 +unfold_more e5d7 +update e923 +usb e1e0 +verified_user e8e8 +vertical_align_bottom e258 +vertical_align_center e259 +vertical_align_top e25a +vibration e62d +video_call e070 +video_label e071 +video_library e04a +videocam e04b +videocam_off e04c +videogame_asset e338 +view_agenda e8e9 +view_array e8ea +view_carousel e8eb +view_column e8ec +view_comfy e42a +view_compact e42b +view_day e8ed +view_headline e8ee +view_list e8ef +view_module e8f0 +view_quilt e8f1 +view_stream e8f2 +view_week e8f3 +vignette e435 +visibility e8f4 +visibility_off e8f5 +voice_chat e62e +voicemail e0d9 +volume_down e04d +volume_mute e04e +volume_off e04f +volume_up e050 +vpn_key e0da +vpn_lock e62f +wallpaper e1bc +warning e002 +watch e334 +watch_later e924 +wb_auto e42c +wb_cloudy e42d +wb_incandescent e42e +wb_iridescent e436 +wb_sunny e430 +wc e63d +web e051 +web_asset e069 +weekend e16b +whatshot e80e +widgets e1bd +wifi e63e +wifi_lock e1e1 +wifi_tethering e1e2 +work e8f9 +wrap_text e25b +youtube_searched_for e8fa +zoom_in e8ff +zoom_out e900 +zoom_out_map e56b diff --git a/hassio-google-drive-backup/backup/static/js/iconfont/material-icons.css b/hassio-google-drive-backup/backup/static/js/iconfont/material-icons.css new file mode 100644 index 0000000..2270c09 --- /dev/null +++ b/hassio-google-drive-backup/backup/static/js/iconfont/material-icons.css @@ -0,0 +1,36 @@ +@font-face { + font-family: 'Material Icons'; + font-style: normal; + font-weight: 400; + src: url(MaterialIcons-Regular.eot); /* For IE6-8 */ + src: local('Material Icons'), + local('MaterialIcons-Regular'), + url(MaterialIcons-Regular.woff2) format('woff2'), + url(MaterialIcons-Regular.woff) format('woff'), + url(MaterialIcons-Regular.ttf) format('truetype'); +} + +.material-icons { + font-family: 'Material Icons'; + font-weight: normal; + font-style: normal; + font-size: 24px; /* Preferred icon size */ + display: inline-block; + line-height: 1; + text-transform: none; + letter-spacing: normal; + word-wrap: normal; + white-space: nowrap; + direction: ltr; + + /* Support for all WebKit browsers. */ + -webkit-font-smoothing: antialiased; + /* Support for Safari and Chrome. */ + text-rendering: optimizeLegibility; + + /* Support for Firefox. */ + -moz-osx-font-smoothing: grayscale; + + /* Support for IE. */ + font-feature-settings: 'liga'; +} diff --git a/hassio-google-drive-backup/backup/static/js/icons.css b/hassio-google-drive-backup/backup/static/js/icons.css new file mode 100644 index 0000000..d6ba092 --- /dev/null +++ b/hassio-google-drive-backup/backup/static/js/icons.css @@ -0,0 +1,36 @@ +@font-face { + font-family: 'Material Icons'; + font-style: normal; + font-weight: 400; + src: url(iconfont/MaterialIcons-Regular.eot); /* For IE6-8 */ + src: local('Material Icons'), + local('MaterialIcons-Regular'), + url(iconfont/MaterialIcons-Regular.woff2) format('woff2'), + url(iconfont/MaterialIcons-Regular.woff) format('woff'), + url(iconfont/MaterialIcons-Regular.ttf) format('truetype'); +} + +.material-icons { + font-family: 'Material Icons'; + font-weight: normal; + font-style: normal; + font-size: 24px; /* Preferred icon size */ + display: inline-block; + line-height: 1; + text-transform: none; + letter-spacing: normal; + word-wrap: normal; + white-space: nowrap; + direction: ltr; + + /* Support for all WebKit browsers. */ + -webkit-font-smoothing: antialiased; + /* Support for Safari and Chrome. */ + text-rendering: optimizeLegibility; + + /* Support for Firefox. */ + -moz-osx-font-smoothing: grayscale; + + /* Support for IE. */ + font-feature-settings: 'liga'; +} \ No newline at end of file diff --git a/hassio-google-drive-backup/backup/static/js/init.js b/hassio-google-drive-backup/backup/static/js/init.js new file mode 100644 index 0000000..1112054 --- /dev/null +++ b/hassio-google-drive-backup/backup/static/js/init.js @@ -0,0 +1,7 @@ +(function($){ + $(function(){ + + $('.sidenav').sidenav(); + + }); // end of document ready +})(jQuery); // end of jQuery name space diff --git a/hassio-google-drive-backup/backup/static/js/jquery.js b/hassio-google-drive-backup/backup/static/js/jquery.js new file mode 100644 index 0000000..49d1fcf --- /dev/null +++ b/hassio-google-drive-backup/backup/static/js/jquery.js @@ -0,0 +1,2 @@ +/*! jQuery v3.3.1 | (c) JS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(e,t){"use strict";var n=[],r=e.document,i=Object.getPrototypeOf,o=n.slice,a=n.concat,s=n.push,u=n.indexOf,l={},c=l.toString,f=l.hasOwnProperty,p=f.toString,d=p.call(Object),h={},g=function e(t){return"function"==typeof t&&"number"!=typeof t.nodeType},y=function e(t){return null!=t&&t===t.window},v={type:!0,src:!0,noModule:!0};function m(e,t,n){var i,o=(t=t||r).createElement("script");if(o.text=e,n)for(i in v)n[i]&&(o[i]=n[i]);t.head.appendChild(o).parentNode.removeChild(o)}function x(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?l[c.call(e)]||"object":typeof e}var b="3.3.1",w=function(e,t){return new w.fn.init(e,t)},T=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;w.fn=w.prototype={jquery:"3.3.1",constructor:w,length:0,toArray:function(){return o.call(this)},get:function(e){return null==e?o.call(this):e<0?this[e+this.length]:this[e]},pushStack:function(e){var t=w.merge(this.constructor(),e);return t.prevObject=this,t},each:function(e){return w.each(this,e)},map:function(e){return this.pushStack(w.map(this,function(t,n){return e.call(t,n,t)}))},slice:function(){return this.pushStack(o.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(e<0?t:0);return this.pushStack(n>=0&&n0&&t-1 in e)}var E=function(e){var t,n,r,i,o,a,s,u,l,c,f,p,d,h,g,y,v,m,x,b="sizzle"+1*new Date,w=e.document,T=0,C=0,E=ae(),k=ae(),S=ae(),D=function(e,t){return e===t&&(f=!0),0},N={}.hasOwnProperty,A=[],j=A.pop,q=A.push,L=A.push,H=A.slice,O=function(e,t){for(var n=0,r=e.length;n+~]|"+M+")"+M+"*"),z=new RegExp("="+M+"*([^\\]'\"]*?)"+M+"*\\]","g"),X=new RegExp(W),U=new RegExp("^"+R+"$"),V={ID:new RegExp("^#("+R+")"),CLASS:new RegExp("^\\.("+R+")"),TAG:new RegExp("^("+R+"|[*])"),ATTR:new RegExp("^"+I),PSEUDO:new RegExp("^"+W),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+P+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},G=/^(?:input|select|textarea|button)$/i,Y=/^h\d$/i,Q=/^[^{]+\{\s*\[native \w/,J=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,K=/[+~]/,Z=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),ee=function(e,t,n){var r="0x"+t-65536;return r!==r||n?t:r<0?String.fromCharCode(r+65536):String.fromCharCode(r>>10|55296,1023&r|56320)},te=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ne=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},re=function(){p()},ie=me(function(e){return!0===e.disabled&&("form"in e||"label"in e)},{dir:"parentNode",next:"legend"});try{L.apply(A=H.call(w.childNodes),w.childNodes),A[w.childNodes.length].nodeType}catch(e){L={apply:A.length?function(e,t){q.apply(e,H.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function oe(e,t,r,i){var o,s,l,c,f,h,v,m=t&&t.ownerDocument,T=t?t.nodeType:9;if(r=r||[],"string"!=typeof e||!e||1!==T&&9!==T&&11!==T)return r;if(!i&&((t?t.ownerDocument||t:w)!==d&&p(t),t=t||d,g)){if(11!==T&&(f=J.exec(e)))if(o=f[1]){if(9===T){if(!(l=t.getElementById(o)))return r;if(l.id===o)return r.push(l),r}else if(m&&(l=m.getElementById(o))&&x(t,l)&&l.id===o)return r.push(l),r}else{if(f[2])return L.apply(r,t.getElementsByTagName(e)),r;if((o=f[3])&&n.getElementsByClassName&&t.getElementsByClassName)return L.apply(r,t.getElementsByClassName(o)),r}if(n.qsa&&!S[e+" "]&&(!y||!y.test(e))){if(1!==T)m=t,v=e;else if("object"!==t.nodeName.toLowerCase()){(c=t.getAttribute("id"))?c=c.replace(te,ne):t.setAttribute("id",c=b),s=(h=a(e)).length;while(s--)h[s]="#"+c+" "+ve(h[s]);v=h.join(","),m=K.test(e)&&ge(t.parentNode)||t}if(v)try{return L.apply(r,m.querySelectorAll(v)),r}catch(e){}finally{c===b&&t.removeAttribute("id")}}}return u(e.replace(B,"$1"),t,r,i)}function ae(){var e=[];function t(n,i){return e.push(n+" ")>r.cacheLength&&delete t[e.shift()],t[n+" "]=i}return t}function se(e){return e[b]=!0,e}function ue(e){var t=d.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function le(e,t){var n=e.split("|"),i=n.length;while(i--)r.attrHandle[n[i]]=t}function ce(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function fe(e){return function(t){return"input"===t.nodeName.toLowerCase()&&t.type===e}}function pe(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function de(e){return function(t){return"form"in t?t.parentNode&&!1===t.disabled?"label"in t?"label"in t.parentNode?t.parentNode.disabled===e:t.disabled===e:t.isDisabled===e||t.isDisabled!==!e&&ie(t)===e:t.disabled===e:"label"in t&&t.disabled===e}}function he(e){return se(function(t){return t=+t,se(function(n,r){var i,o=e([],n.length,t),a=o.length;while(a--)n[i=o[a]]&&(n[i]=!(r[i]=n[i]))})})}function ge(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}n=oe.support={},o=oe.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return!!t&&"HTML"!==t.nodeName},p=oe.setDocument=function(e){var t,i,a=e?e.ownerDocument||e:w;return a!==d&&9===a.nodeType&&a.documentElement?(d=a,h=d.documentElement,g=!o(d),w!==d&&(i=d.defaultView)&&i.top!==i&&(i.addEventListener?i.addEventListener("unload",re,!1):i.attachEvent&&i.attachEvent("onunload",re)),n.attributes=ue(function(e){return e.className="i",!e.getAttribute("className")}),n.getElementsByTagName=ue(function(e){return e.appendChild(d.createComment("")),!e.getElementsByTagName("*").length}),n.getElementsByClassName=Q.test(d.getElementsByClassName),n.getById=ue(function(e){return h.appendChild(e).id=b,!d.getElementsByName||!d.getElementsByName(b).length}),n.getById?(r.filter.ID=function(e){var t=e.replace(Z,ee);return function(e){return e.getAttribute("id")===t}},r.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&g){var n=t.getElementById(e);return n?[n]:[]}}):(r.filter.ID=function(e){var t=e.replace(Z,ee);return function(e){var n="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return n&&n.value===t}},r.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&g){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),r.find.TAG=n.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):n.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},r.find.CLASS=n.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&g)return t.getElementsByClassName(e)},v=[],y=[],(n.qsa=Q.test(d.querySelectorAll))&&(ue(function(e){h.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&y.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||y.push("\\["+M+"*(?:value|"+P+")"),e.querySelectorAll("[id~="+b+"-]").length||y.push("~="),e.querySelectorAll(":checked").length||y.push(":checked"),e.querySelectorAll("a#"+b+"+*").length||y.push(".#.+[+~]")}),ue(function(e){e.innerHTML="";var t=d.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&y.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&y.push(":enabled",":disabled"),h.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&y.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),y.push(",.*:")})),(n.matchesSelector=Q.test(m=h.matches||h.webkitMatchesSelector||h.mozMatchesSelector||h.oMatchesSelector||h.msMatchesSelector))&&ue(function(e){n.disconnectedMatch=m.call(e,"*"),m.call(e,"[s!='']:x"),v.push("!=",W)}),y=y.length&&new RegExp(y.join("|")),v=v.length&&new RegExp(v.join("|")),t=Q.test(h.compareDocumentPosition),x=t||Q.test(h.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},D=t?function(e,t){if(e===t)return f=!0,0;var r=!e.compareDocumentPosition-!t.compareDocumentPosition;return r||(1&(r=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!n.sortDetached&&t.compareDocumentPosition(e)===r?e===d||e.ownerDocument===w&&x(w,e)?-1:t===d||t.ownerDocument===w&&x(w,t)?1:c?O(c,e)-O(c,t):0:4&r?-1:1)}:function(e,t){if(e===t)return f=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e===d?-1:t===d?1:i?-1:o?1:c?O(c,e)-O(c,t):0;if(i===o)return ce(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?ce(a[r],s[r]):a[r]===w?-1:s[r]===w?1:0},d):d},oe.matches=function(e,t){return oe(e,null,null,t)},oe.matchesSelector=function(e,t){if((e.ownerDocument||e)!==d&&p(e),t=t.replace(z,"='$1']"),n.matchesSelector&&g&&!S[t+" "]&&(!v||!v.test(t))&&(!y||!y.test(t)))try{var r=m.call(e,t);if(r||n.disconnectedMatch||e.document&&11!==e.document.nodeType)return r}catch(e){}return oe(t,d,null,[e]).length>0},oe.contains=function(e,t){return(e.ownerDocument||e)!==d&&p(e),x(e,t)},oe.attr=function(e,t){(e.ownerDocument||e)!==d&&p(e);var i=r.attrHandle[t.toLowerCase()],o=i&&N.call(r.attrHandle,t.toLowerCase())?i(e,t,!g):void 0;return void 0!==o?o:n.attributes||!g?e.getAttribute(t):(o=e.getAttributeNode(t))&&o.specified?o.value:null},oe.escape=function(e){return(e+"").replace(te,ne)},oe.error=function(e){throw new Error("Syntax error, unrecognized expression: "+e)},oe.uniqueSort=function(e){var t,r=[],i=0,o=0;if(f=!n.detectDuplicates,c=!n.sortStable&&e.slice(0),e.sort(D),f){while(t=e[o++])t===e[o]&&(i=r.push(o));while(i--)e.splice(r[i],1)}return c=null,e},i=oe.getText=function(e){var t,n="",r=0,o=e.nodeType;if(o){if(1===o||9===o||11===o){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=i(e)}else if(3===o||4===o)return e.nodeValue}else while(t=e[r++])n+=i(t);return n},(r=oe.selectors={cacheLength:50,createPseudo:se,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(Z,ee),e[3]=(e[3]||e[4]||e[5]||"").replace(Z,ee),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||oe.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&oe.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return V.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=a(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(Z,ee).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=E[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&E(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r){var i=oe.attr(r,e);return null==i?"!="===t:!t||(i+="","="===t?i===n:"!="===t?i!==n:"^="===t?n&&0===i.indexOf(n):"*="===t?n&&i.indexOf(n)>-1:"$="===t?n&&i.slice(-n.length)===n:"~="===t?(" "+i.replace($," ")+" ").indexOf(n)>-1:"|="===t&&(i===n||i.slice(0,n.length+1)===n+"-"))}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),a="last"!==e.slice(-4),s="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,u){var l,c,f,p,d,h,g=o!==a?"nextSibling":"previousSibling",y=t.parentNode,v=s&&t.nodeName.toLowerCase(),m=!u&&!s,x=!1;if(y){if(o){while(g){p=t;while(p=p[g])if(s?p.nodeName.toLowerCase()===v:1===p.nodeType)return!1;h=g="only"===e&&!h&&"nextSibling"}return!0}if(h=[a?y.firstChild:y.lastChild],a&&m){x=(d=(l=(c=(f=(p=y)[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]||[])[0]===T&&l[1])&&l[2],p=d&&y.childNodes[d];while(p=++d&&p&&p[g]||(x=d=0)||h.pop())if(1===p.nodeType&&++x&&p===t){c[e]=[T,d,x];break}}else if(m&&(x=d=(l=(c=(f=(p=t)[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]||[])[0]===T&&l[1]),!1===x)while(p=++d&&p&&p[g]||(x=d=0)||h.pop())if((s?p.nodeName.toLowerCase()===v:1===p.nodeType)&&++x&&(m&&((c=(f=p[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]=[T,x]),p===t))break;return(x-=i)===r||x%r==0&&x/r>=0}}},PSEUDO:function(e,t){var n,i=r.pseudos[e]||r.setFilters[e.toLowerCase()]||oe.error("unsupported pseudo: "+e);return i[b]?i(t):i.length>1?(n=[e,e,"",t],r.setFilters.hasOwnProperty(e.toLowerCase())?se(function(e,n){var r,o=i(e,t),a=o.length;while(a--)e[r=O(e,o[a])]=!(n[r]=o[a])}):function(e){return i(e,0,n)}):i}},pseudos:{not:se(function(e){var t=[],n=[],r=s(e.replace(B,"$1"));return r[b]?se(function(e,t,n,i){var o,a=r(e,null,i,[]),s=e.length;while(s--)(o=a[s])&&(e[s]=!(t[s]=o))}):function(e,i,o){return t[0]=e,r(t,null,o,n),t[0]=null,!n.pop()}}),has:se(function(e){return function(t){return oe(e,t).length>0}}),contains:se(function(e){return e=e.replace(Z,ee),function(t){return(t.textContent||t.innerText||i(t)).indexOf(e)>-1}}),lang:se(function(e){return U.test(e||"")||oe.error("unsupported lang: "+e),e=e.replace(Z,ee).toLowerCase(),function(t){var n;do{if(n=g?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return(n=n.toLowerCase())===e||0===n.indexOf(e+"-")}while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===h},focus:function(e){return e===d.activeElement&&(!d.hasFocus||d.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:de(!1),disabled:de(!0),checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,!0===e.selected},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeType<6)return!1;return!0},parent:function(e){return!r.pseudos.empty(e)},header:function(e){return Y.test(e.nodeName)},input:function(e){return G.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||"text"===t.toLowerCase())},first:he(function(){return[0]}),last:he(function(e,t){return[t-1]}),eq:he(function(e,t,n){return[n<0?n+t:n]}),even:he(function(e,t){for(var n=0;n=0;)e.push(r);return e}),gt:he(function(e,t,n){for(var r=n<0?n+t:n;++r1?function(t,n,r){var i=e.length;while(i--)if(!e[i](t,n,r))return!1;return!0}:e[0]}function be(e,t,n){for(var r=0,i=t.length;r-1&&(o[l]=!(a[l]=f))}}else v=we(v===a?v.splice(h,v.length):v),i?i(null,a,v,u):L.apply(a,v)})}function Ce(e){for(var t,n,i,o=e.length,a=r.relative[e[0].type],s=a||r.relative[" "],u=a?1:0,c=me(function(e){return e===t},s,!0),f=me(function(e){return O(t,e)>-1},s,!0),p=[function(e,n,r){var i=!a&&(r||n!==l)||((t=n).nodeType?c(e,n,r):f(e,n,r));return t=null,i}];u1&&xe(p),u>1&&ve(e.slice(0,u-1).concat({value:" "===e[u-2].type?"*":""})).replace(B,"$1"),n,u0,i=e.length>0,o=function(o,a,s,u,c){var f,h,y,v=0,m="0",x=o&&[],b=[],w=l,C=o||i&&r.find.TAG("*",c),E=T+=null==w?1:Math.random()||.1,k=C.length;for(c&&(l=a===d||a||c);m!==k&&null!=(f=C[m]);m++){if(i&&f){h=0,a||f.ownerDocument===d||(p(f),s=!g);while(y=e[h++])if(y(f,a||d,s)){u.push(f);break}c&&(T=E)}n&&((f=!y&&f)&&v--,o&&x.push(f))}if(v+=m,n&&m!==v){h=0;while(y=t[h++])y(x,b,a,s);if(o){if(v>0)while(m--)x[m]||b[m]||(b[m]=j.call(u));b=we(b)}L.apply(u,b),c&&!o&&b.length>0&&v+t.length>1&&oe.uniqueSort(u)}return c&&(T=E,l=w),x};return n?se(o):o}return s=oe.compile=function(e,t){var n,r=[],i=[],o=S[e+" "];if(!o){t||(t=a(e)),n=t.length;while(n--)(o=Ce(t[n]))[b]?r.push(o):i.push(o);(o=S(e,Ee(i,r))).selector=e}return o},u=oe.select=function(e,t,n,i){var o,u,l,c,f,p="function"==typeof e&&e,d=!i&&a(e=p.selector||e);if(n=n||[],1===d.length){if((u=d[0]=d[0].slice(0)).length>2&&"ID"===(l=u[0]).type&&9===t.nodeType&&g&&r.relative[u[1].type]){if(!(t=(r.find.ID(l.matches[0].replace(Z,ee),t)||[])[0]))return n;p&&(t=t.parentNode),e=e.slice(u.shift().value.length)}o=V.needsContext.test(e)?0:u.length;while(o--){if(l=u[o],r.relative[c=l.type])break;if((f=r.find[c])&&(i=f(l.matches[0].replace(Z,ee),K.test(u[0].type)&&ge(t.parentNode)||t))){if(u.splice(o,1),!(e=i.length&&ve(u)))return L.apply(n,i),n;break}}}return(p||s(e,d))(i,t,!g,n,!t||K.test(e)&&ge(t.parentNode)||t),n},n.sortStable=b.split("").sort(D).join("")===b,n.detectDuplicates=!!f,p(),n.sortDetached=ue(function(e){return 1&e.compareDocumentPosition(d.createElement("fieldset"))}),ue(function(e){return e.innerHTML="","#"===e.firstChild.getAttribute("href")})||le("type|href|height|width",function(e,t,n){if(!n)return e.getAttribute(t,"type"===t.toLowerCase()?1:2)}),n.attributes&&ue(function(e){return e.innerHTML="",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||le("value",function(e,t,n){if(!n&&"input"===e.nodeName.toLowerCase())return e.defaultValue}),ue(function(e){return null==e.getAttribute("disabled")})||le(P,function(e,t,n){var r;if(!n)return!0===e[t]?t.toLowerCase():(r=e.getAttributeNode(t))&&r.specified?r.value:null}),oe}(e);w.find=E,w.expr=E.selectors,w.expr[":"]=w.expr.pseudos,w.uniqueSort=w.unique=E.uniqueSort,w.text=E.getText,w.isXMLDoc=E.isXML,w.contains=E.contains,w.escapeSelector=E.escape;var k=function(e,t,n){var r=[],i=void 0!==n;while((e=e[t])&&9!==e.nodeType)if(1===e.nodeType){if(i&&w(e).is(n))break;r.push(e)}return r},S=function(e,t){for(var n=[];e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n},D=w.expr.match.needsContext;function N(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()}var A=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,t,n){return g(t)?w.grep(e,function(e,r){return!!t.call(e,r,e)!==n}):t.nodeType?w.grep(e,function(e){return e===t!==n}):"string"!=typeof t?w.grep(e,function(e){return u.call(t,e)>-1!==n}):w.filter(t,e,n)}w.filter=function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?w.find.matchesSelector(r,e)?[r]:[]:w.find.matches(e,w.grep(t,function(e){return 1===e.nodeType}))},w.fn.extend({find:function(e){var t,n,r=this.length,i=this;if("string"!=typeof e)return this.pushStack(w(e).filter(function(){for(t=0;t1?w.uniqueSort(n):n},filter:function(e){return this.pushStack(j(this,e||[],!1))},not:function(e){return this.pushStack(j(this,e||[],!0))},is:function(e){return!!j(this,"string"==typeof e&&D.test(e)?w(e):e||[],!1).length}});var q,L=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/;(w.fn.init=function(e,t,n){var i,o;if(!e)return this;if(n=n||q,"string"==typeof e){if(!(i="<"===e[0]&&">"===e[e.length-1]&&e.length>=3?[null,e,null]:L.exec(e))||!i[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(i[1]){if(t=t instanceof w?t[0]:t,w.merge(this,w.parseHTML(i[1],t&&t.nodeType?t.ownerDocument||t:r,!0)),A.test(i[1])&&w.isPlainObject(t))for(i in t)g(this[i])?this[i](t[i]):this.attr(i,t[i]);return this}return(o=r.getElementById(i[2]))&&(this[0]=o,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):g(e)?void 0!==n.ready?n.ready(e):e(w):w.makeArray(e,this)}).prototype=w.fn,q=w(r);var H=/^(?:parents|prev(?:Until|All))/,O={children:!0,contents:!0,next:!0,prev:!0};w.fn.extend({has:function(e){var t=w(e,this),n=t.length;return this.filter(function(){for(var e=0;e-1:1===n.nodeType&&w.find.matchesSelector(n,e))){o.push(n);break}return this.pushStack(o.length>1?w.uniqueSort(o):o)},index:function(e){return e?"string"==typeof e?u.call(w(e),this[0]):u.call(this,e.jquery?e[0]:e):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){return this.pushStack(w.uniqueSort(w.merge(this.get(),w(e,t))))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}});function P(e,t){while((e=e[t])&&1!==e.nodeType);return e}w.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return k(e,"parentNode")},parentsUntil:function(e,t,n){return k(e,"parentNode",n)},next:function(e){return P(e,"nextSibling")},prev:function(e){return P(e,"previousSibling")},nextAll:function(e){return k(e,"nextSibling")},prevAll:function(e){return k(e,"previousSibling")},nextUntil:function(e,t,n){return k(e,"nextSibling",n)},prevUntil:function(e,t,n){return k(e,"previousSibling",n)},siblings:function(e){return S((e.parentNode||{}).firstChild,e)},children:function(e){return S(e.firstChild)},contents:function(e){return N(e,"iframe")?e.contentDocument:(N(e,"template")&&(e=e.content||e),w.merge([],e.childNodes))}},function(e,t){w.fn[e]=function(n,r){var i=w.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=w.filter(r,i)),this.length>1&&(O[e]||w.uniqueSort(i),H.test(e)&&i.reverse()),this.pushStack(i)}});var M=/[^\x20\t\r\n\f]+/g;function R(e){var t={};return w.each(e.match(M)||[],function(e,n){t[n]=!0}),t}w.Callbacks=function(e){e="string"==typeof e?R(e):w.extend({},e);var t,n,r,i,o=[],a=[],s=-1,u=function(){for(i=i||e.once,r=t=!0;a.length;s=-1){n=a.shift();while(++s-1)o.splice(n,1),n<=s&&s--}),this},has:function(e){return e?w.inArray(e,o)>-1:o.length>0},empty:function(){return o&&(o=[]),this},disable:function(){return i=a=[],o=n="",this},disabled:function(){return!o},lock:function(){return i=a=[],n||t||(o=n=""),this},locked:function(){return!!i},fireWith:function(e,n){return i||(n=[e,(n=n||[]).slice?n.slice():n],a.push(n),t||u()),this},fire:function(){return l.fireWith(this,arguments),this},fired:function(){return!!r}};return l};function I(e){return e}function W(e){throw e}function $(e,t,n,r){var i;try{e&&g(i=e.promise)?i.call(e).done(t).fail(n):e&&g(i=e.then)?i.call(e,t,n):t.apply(void 0,[e].slice(r))}catch(e){n.apply(void 0,[e])}}w.extend({Deferred:function(t){var n=[["notify","progress",w.Callbacks("memory"),w.Callbacks("memory"),2],["resolve","done",w.Callbacks("once memory"),w.Callbacks("once memory"),0,"resolved"],["reject","fail",w.Callbacks("once memory"),w.Callbacks("once memory"),1,"rejected"]],r="pending",i={state:function(){return r},always:function(){return o.done(arguments).fail(arguments),this},"catch":function(e){return i.then(null,e)},pipe:function(){var e=arguments;return w.Deferred(function(t){w.each(n,function(n,r){var i=g(e[r[4]])&&e[r[4]];o[r[1]](function(){var e=i&&i.apply(this,arguments);e&&g(e.promise)?e.promise().progress(t.notify).done(t.resolve).fail(t.reject):t[r[0]+"With"](this,i?[e]:arguments)})}),e=null}).promise()},then:function(t,r,i){var o=0;function a(t,n,r,i){return function(){var s=this,u=arguments,l=function(){var e,l;if(!(t=o&&(r!==W&&(s=void 0,u=[e]),n.rejectWith(s,u))}};t?c():(w.Deferred.getStackHook&&(c.stackTrace=w.Deferred.getStackHook()),e.setTimeout(c))}}return w.Deferred(function(e){n[0][3].add(a(0,e,g(i)?i:I,e.notifyWith)),n[1][3].add(a(0,e,g(t)?t:I)),n[2][3].add(a(0,e,g(r)?r:W))}).promise()},promise:function(e){return null!=e?w.extend(e,i):i}},o={};return w.each(n,function(e,t){var a=t[2],s=t[5];i[t[1]]=a.add,s&&a.add(function(){r=s},n[3-e][2].disable,n[3-e][3].disable,n[0][2].lock,n[0][3].lock),a.add(t[3].fire),o[t[0]]=function(){return o[t[0]+"With"](this===o?void 0:this,arguments),this},o[t[0]+"With"]=a.fireWith}),i.promise(o),t&&t.call(o,o),o},when:function(e){var t=arguments.length,n=t,r=Array(n),i=o.call(arguments),a=w.Deferred(),s=function(e){return function(n){r[e]=this,i[e]=arguments.length>1?o.call(arguments):n,--t||a.resolveWith(r,i)}};if(t<=1&&($(e,a.done(s(n)).resolve,a.reject,!t),"pending"===a.state()||g(i[n]&&i[n].then)))return a.then();while(n--)$(i[n],s(n),a.reject);return a.promise()}});var B=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;w.Deferred.exceptionHook=function(t,n){e.console&&e.console.warn&&t&&B.test(t.name)&&e.console.warn("jQuery.Deferred exception: "+t.message,t.stack,n)},w.readyException=function(t){e.setTimeout(function(){throw t})};var F=w.Deferred();w.fn.ready=function(e){return F.then(e)["catch"](function(e){w.readyException(e)}),this},w.extend({isReady:!1,readyWait:1,ready:function(e){(!0===e?--w.readyWait:w.isReady)||(w.isReady=!0,!0!==e&&--w.readyWait>0||F.resolveWith(r,[w]))}}),w.ready.then=F.then;function _(){r.removeEventListener("DOMContentLoaded",_),e.removeEventListener("load",_),w.ready()}"complete"===r.readyState||"loading"!==r.readyState&&!r.documentElement.doScroll?e.setTimeout(w.ready):(r.addEventListener("DOMContentLoaded",_),e.addEventListener("load",_));var z=function(e,t,n,r,i,o,a){var s=0,u=e.length,l=null==n;if("object"===x(n)){i=!0;for(s in n)z(e,t,s,n[s],!0,o,a)}else if(void 0!==r&&(i=!0,g(r)||(a=!0),l&&(a?(t.call(e,r),t=null):(l=t,t=function(e,t,n){return l.call(w(e),n)})),t))for(;s1,null,!0)},removeData:function(e){return this.each(function(){K.remove(this,e)})}}),w.extend({queue:function(e,t,n){var r;if(e)return t=(t||"fx")+"queue",r=J.get(e,t),n&&(!r||Array.isArray(n)?r=J.access(e,t,w.makeArray(n)):r.push(n)),r||[]},dequeue:function(e,t){t=t||"fx";var n=w.queue(e,t),r=n.length,i=n.shift(),o=w._queueHooks(e,t),a=function(){w.dequeue(e,t)};"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,a,o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return J.get(e,n)||J.access(e,n,{empty:w.Callbacks("once memory").add(function(){J.remove(e,[t+"queue",n])})})}}),w.fn.extend({queue:function(e,t){var n=2;return"string"!=typeof e&&(t=e,e="fx",n--),arguments.length\x20\t\r\n\f]+)/i,he=/^$|^module$|\/(?:java|ecma)script/i,ge={option:[1,""],thead:[1,"","
    "],col:[2,"","
    "],tr:[2,"","
    "],td:[3,"","
    "],_default:[0,"",""]};ge.optgroup=ge.option,ge.tbody=ge.tfoot=ge.colgroup=ge.caption=ge.thead,ge.th=ge.td;function ye(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&N(e,t)?w.merge([e],n):n}function ve(e,t){for(var n=0,r=e.length;n-1)i&&i.push(o);else if(l=w.contains(o.ownerDocument,o),a=ye(f.appendChild(o),"script"),l&&ve(a),n){c=0;while(o=a[c++])he.test(o.type||"")&&n.push(o)}return f}!function(){var e=r.createDocumentFragment().appendChild(r.createElement("div")),t=r.createElement("input");t.setAttribute("type","radio"),t.setAttribute("checked","checked"),t.setAttribute("name","t"),e.appendChild(t),h.checkClone=e.cloneNode(!0).cloneNode(!0).lastChild.checked,e.innerHTML="",h.noCloneChecked=!!e.cloneNode(!0).lastChild.defaultValue}();var be=r.documentElement,we=/^key/,Te=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,Ce=/^([^.]*)(?:\.(.+)|)/;function Ee(){return!0}function ke(){return!1}function Se(){try{return r.activeElement}catch(e){}}function De(e,t,n,r,i,o){var a,s;if("object"==typeof t){"string"!=typeof n&&(r=r||n,n=void 0);for(s in t)De(e,s,n,r,t[s],o);return e}if(null==r&&null==i?(i=n,r=n=void 0):null==i&&("string"==typeof n?(i=r,r=void 0):(i=r,r=n,n=void 0)),!1===i)i=ke;else if(!i)return e;return 1===o&&(a=i,(i=function(e){return w().off(e),a.apply(this,arguments)}).guid=a.guid||(a.guid=w.guid++)),e.each(function(){w.event.add(this,t,i,r,n)})}w.event={global:{},add:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,y=J.get(e);if(y){n.handler&&(n=(o=n).handler,i=o.selector),i&&w.find.matchesSelector(be,i),n.guid||(n.guid=w.guid++),(u=y.events)||(u=y.events={}),(a=y.handle)||(a=y.handle=function(t){return"undefined"!=typeof w&&w.event.triggered!==t.type?w.event.dispatch.apply(e,arguments):void 0}),l=(t=(t||"").match(M)||[""]).length;while(l--)d=g=(s=Ce.exec(t[l])||[])[1],h=(s[2]||"").split(".").sort(),d&&(f=w.event.special[d]||{},d=(i?f.delegateType:f.bindType)||d,f=w.event.special[d]||{},c=w.extend({type:d,origType:g,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&w.expr.match.needsContext.test(i),namespace:h.join(".")},o),(p=u[d])||((p=u[d]=[]).delegateCount=0,f.setup&&!1!==f.setup.call(e,r,h,a)||e.addEventListener&&e.addEventListener(d,a)),f.add&&(f.add.call(e,c),c.handler.guid||(c.handler.guid=n.guid)),i?p.splice(p.delegateCount++,0,c):p.push(c),w.event.global[d]=!0)}},remove:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,y=J.hasData(e)&&J.get(e);if(y&&(u=y.events)){l=(t=(t||"").match(M)||[""]).length;while(l--)if(s=Ce.exec(t[l])||[],d=g=s[1],h=(s[2]||"").split(".").sort(),d){f=w.event.special[d]||{},p=u[d=(r?f.delegateType:f.bindType)||d]||[],s=s[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),a=o=p.length;while(o--)c=p[o],!i&&g!==c.origType||n&&n.guid!==c.guid||s&&!s.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(p.splice(o,1),c.selector&&p.delegateCount--,f.remove&&f.remove.call(e,c));a&&!p.length&&(f.teardown&&!1!==f.teardown.call(e,h,y.handle)||w.removeEvent(e,d,y.handle),delete u[d])}else for(d in u)w.event.remove(e,d+t[l],n,r,!0);w.isEmptyObject(u)&&J.remove(e,"handle events")}},dispatch:function(e){var t=w.event.fix(e),n,r,i,o,a,s,u=new Array(arguments.length),l=(J.get(this,"events")||{})[t.type]||[],c=w.event.special[t.type]||{};for(u[0]=t,n=1;n=1))for(;l!==this;l=l.parentNode||this)if(1===l.nodeType&&("click"!==e.type||!0!==l.disabled)){for(o=[],a={},n=0;n-1:w.find(i,this,null,[l]).length),a[i]&&o.push(r);o.length&&s.push({elem:l,handlers:o})}return l=this,u\x20\t\r\n\f]*)[^>]*)\/>/gi,Ae=/\s*$/g;function Le(e,t){return N(e,"table")&&N(11!==t.nodeType?t:t.firstChild,"tr")?w(e).children("tbody")[0]||e:e}function He(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function Oe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Pe(e,t){var n,r,i,o,a,s,u,l;if(1===t.nodeType){if(J.hasData(e)&&(o=J.access(e),a=J.set(t,o),l=o.events)){delete a.handle,a.events={};for(i in l)for(n=0,r=l[i].length;n1&&"string"==typeof y&&!h.checkClone&&je.test(y))return e.each(function(i){var o=e.eq(i);v&&(t[0]=y.call(this,i,o.html())),Re(o,t,n,r)});if(p&&(i=xe(t,e[0].ownerDocument,!1,e,r),o=i.firstChild,1===i.childNodes.length&&(i=o),o||r)){for(u=(s=w.map(ye(i,"script"),He)).length;f")},clone:function(e,t,n){var r,i,o,a,s=e.cloneNode(!0),u=w.contains(e.ownerDocument,e);if(!(h.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||w.isXMLDoc(e)))for(a=ye(s),r=0,i=(o=ye(e)).length;r0&&ve(a,!u&&ye(e,"script")),s},cleanData:function(e){for(var t,n,r,i=w.event.special,o=0;void 0!==(n=e[o]);o++)if(Y(n)){if(t=n[J.expando]){if(t.events)for(r in t.events)i[r]?w.event.remove(n,r):w.removeEvent(n,r,t.handle);n[J.expando]=void 0}n[K.expando]&&(n[K.expando]=void 0)}}}),w.fn.extend({detach:function(e){return Ie(this,e,!0)},remove:function(e){return Ie(this,e)},text:function(e){return z(this,function(e){return void 0===e?w.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=e)})},null,e,arguments.length)},append:function(){return Re(this,arguments,function(e){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||Le(this,e).appendChild(e)})},prepend:function(){return Re(this,arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=Le(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return Re(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return Re(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},empty:function(){for(var e,t=0;null!=(e=this[t]);t++)1===e.nodeType&&(w.cleanData(ye(e,!1)),e.textContent="");return this},clone:function(e,t){return e=null!=e&&e,t=null==t?e:t,this.map(function(){return w.clone(this,e,t)})},html:function(e){return z(this,function(e){var t=this[0]||{},n=0,r=this.length;if(void 0===e&&1===t.nodeType)return t.innerHTML;if("string"==typeof e&&!Ae.test(e)&&!ge[(de.exec(e)||["",""])[1].toLowerCase()]){e=w.htmlPrefilter(e);try{for(;n=0&&(u+=Math.max(0,Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-o-u-s-.5))),u}function et(e,t,n){var r=$e(e),i=Fe(e,t,r),o="border-box"===w.css(e,"boxSizing",!1,r),a=o;if(We.test(i)){if(!n)return i;i="auto"}return a=a&&(h.boxSizingReliable()||i===e.style[t]),("auto"===i||!parseFloat(i)&&"inline"===w.css(e,"display",!1,r))&&(i=e["offset"+t[0].toUpperCase()+t.slice(1)],a=!0),(i=parseFloat(i)||0)+Ze(e,t,n||(o?"border":"content"),a,r,i)+"px"}w.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=Fe(e,"opacity");return""===n?"1":n}}}},cssNumber:{animationIterationCount:!0,columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{},style:function(e,t,n,r){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var i,o,a,s=G(t),u=Xe.test(t),l=e.style;if(u||(t=Je(s)),a=w.cssHooks[t]||w.cssHooks[s],void 0===n)return a&&"get"in a&&void 0!==(i=a.get(e,!1,r))?i:l[t];"string"==(o=typeof n)&&(i=ie.exec(n))&&i[1]&&(n=ue(e,t,i),o="number"),null!=n&&n===n&&("number"===o&&(n+=i&&i[3]||(w.cssNumber[s]?"":"px")),h.clearCloneStyle||""!==n||0!==t.indexOf("background")||(l[t]="inherit"),a&&"set"in a&&void 0===(n=a.set(e,n,r))||(u?l.setProperty(t,n):l[t]=n))}},css:function(e,t,n,r){var i,o,a,s=G(t);return Xe.test(t)||(t=Je(s)),(a=w.cssHooks[t]||w.cssHooks[s])&&"get"in a&&(i=a.get(e,!0,n)),void 0===i&&(i=Fe(e,t,r)),"normal"===i&&t in Ve&&(i=Ve[t]),""===n||n?(o=parseFloat(i),!0===n||isFinite(o)?o||0:i):i}}),w.each(["height","width"],function(e,t){w.cssHooks[t]={get:function(e,n,r){if(n)return!ze.test(w.css(e,"display"))||e.getClientRects().length&&e.getBoundingClientRect().width?et(e,t,r):se(e,Ue,function(){return et(e,t,r)})},set:function(e,n,r){var i,o=$e(e),a="border-box"===w.css(e,"boxSizing",!1,o),s=r&&Ze(e,t,r,a,o);return a&&h.scrollboxSize()===o.position&&(s-=Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-parseFloat(o[t])-Ze(e,t,"border",!1,o)-.5)),s&&(i=ie.exec(n))&&"px"!==(i[3]||"px")&&(e.style[t]=n,n=w.css(e,t)),Ke(e,n,s)}}}),w.cssHooks.marginLeft=_e(h.reliableMarginLeft,function(e,t){if(t)return(parseFloat(Fe(e,"marginLeft"))||e.getBoundingClientRect().left-se(e,{marginLeft:0},function(){return e.getBoundingClientRect().left}))+"px"}),w.each({margin:"",padding:"",border:"Width"},function(e,t){w.cssHooks[e+t]={expand:function(n){for(var r=0,i={},o="string"==typeof n?n.split(" "):[n];r<4;r++)i[e+oe[r]+t]=o[r]||o[r-2]||o[0];return i}},"margin"!==e&&(w.cssHooks[e+t].set=Ke)}),w.fn.extend({css:function(e,t){return z(this,function(e,t,n){var r,i,o={},a=0;if(Array.isArray(t)){for(r=$e(e),i=t.length;a1)}});function tt(e,t,n,r,i){return new tt.prototype.init(e,t,n,r,i)}w.Tween=tt,tt.prototype={constructor:tt,init:function(e,t,n,r,i,o){this.elem=e,this.prop=n,this.easing=i||w.easing._default,this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=o||(w.cssNumber[n]?"":"px")},cur:function(){var e=tt.propHooks[this.prop];return e&&e.get?e.get(this):tt.propHooks._default.get(this)},run:function(e){var t,n=tt.propHooks[this.prop];return this.options.duration?this.pos=t=w.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):this.pos=t=e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):tt.propHooks._default.set(this),this}},tt.prototype.init.prototype=tt.prototype,tt.propHooks={_default:{get:function(e){var t;return 1!==e.elem.nodeType||null!=e.elem[e.prop]&&null==e.elem.style[e.prop]?e.elem[e.prop]:(t=w.css(e.elem,e.prop,""))&&"auto"!==t?t:0},set:function(e){w.fx.step[e.prop]?w.fx.step[e.prop](e):1!==e.elem.nodeType||null==e.elem.style[w.cssProps[e.prop]]&&!w.cssHooks[e.prop]?e.elem[e.prop]=e.now:w.style(e.elem,e.prop,e.now+e.unit)}}},tt.propHooks.scrollTop=tt.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},w.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2},_default:"swing"},w.fx=tt.prototype.init,w.fx.step={};var nt,rt,it=/^(?:toggle|show|hide)$/,ot=/queueHooks$/;function at(){rt&&(!1===r.hidden&&e.requestAnimationFrame?e.requestAnimationFrame(at):e.setTimeout(at,w.fx.interval),w.fx.tick())}function st(){return e.setTimeout(function(){nt=void 0}),nt=Date.now()}function ut(e,t){var n,r=0,i={height:e};for(t=t?1:0;r<4;r+=2-t)i["margin"+(n=oe[r])]=i["padding"+n]=e;return t&&(i.opacity=i.width=e),i}function lt(e,t,n){for(var r,i=(pt.tweeners[t]||[]).concat(pt.tweeners["*"]),o=0,a=i.length;o1)},removeAttr:function(e){return this.each(function(){w.removeAttr(this,e)})}}),w.extend({attr:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return"undefined"==typeof e.getAttribute?w.prop(e,t,n):(1===o&&w.isXMLDoc(e)||(i=w.attrHooks[t.toLowerCase()]||(w.expr.match.bool.test(t)?dt:void 0)),void 0!==n?null===n?void w.removeAttr(e,t):i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:(e.setAttribute(t,n+""),n):i&&"get"in i&&null!==(r=i.get(e,t))?r:null==(r=w.find.attr(e,t))?void 0:r)},attrHooks:{type:{set:function(e,t){if(!h.radioValue&&"radio"===t&&N(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},removeAttr:function(e,t){var n,r=0,i=t&&t.match(M);if(i&&1===e.nodeType)while(n=i[r++])e.removeAttribute(n)}}),dt={set:function(e,t,n){return!1===t?w.removeAttr(e,n):e.setAttribute(n,n),n}},w.each(w.expr.match.bool.source.match(/\w+/g),function(e,t){var n=ht[t]||w.find.attr;ht[t]=function(e,t,r){var i,o,a=t.toLowerCase();return r||(o=ht[a],ht[a]=i,i=null!=n(e,t,r)?a:null,ht[a]=o),i}});var gt=/^(?:input|select|textarea|button)$/i,yt=/^(?:a|area)$/i;w.fn.extend({prop:function(e,t){return z(this,w.prop,e,t,arguments.length>1)},removeProp:function(e){return this.each(function(){delete this[w.propFix[e]||e]})}}),w.extend({prop:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return 1===o&&w.isXMLDoc(e)||(t=w.propFix[t]||t,i=w.propHooks[t]),void 0!==n?i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:e[t]=n:i&&"get"in i&&null!==(r=i.get(e,t))?r:e[t]},propHooks:{tabIndex:{get:function(e){var t=w.find.attr(e,"tabindex");return t?parseInt(t,10):gt.test(e.nodeName)||yt.test(e.nodeName)&&e.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),h.optSelected||(w.propHooks.selected={get:function(e){var t=e.parentNode;return t&&t.parentNode&&t.parentNode.selectedIndex,null},set:function(e){var t=e.parentNode;t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex)}}),w.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){w.propFix[this.toLowerCase()]=this});function vt(e){return(e.match(M)||[]).join(" ")}function mt(e){return e.getAttribute&&e.getAttribute("class")||""}function xt(e){return Array.isArray(e)?e:"string"==typeof e?e.match(M)||[]:[]}w.fn.extend({addClass:function(e){var t,n,r,i,o,a,s,u=0;if(g(e))return this.each(function(t){w(this).addClass(e.call(this,t,mt(this)))});if((t=xt(e)).length)while(n=this[u++])if(i=mt(n),r=1===n.nodeType&&" "+vt(i)+" "){a=0;while(o=t[a++])r.indexOf(" "+o+" ")<0&&(r+=o+" ");i!==(s=vt(r))&&n.setAttribute("class",s)}return this},removeClass:function(e){var t,n,r,i,o,a,s,u=0;if(g(e))return this.each(function(t){w(this).removeClass(e.call(this,t,mt(this)))});if(!arguments.length)return this.attr("class","");if((t=xt(e)).length)while(n=this[u++])if(i=mt(n),r=1===n.nodeType&&" "+vt(i)+" "){a=0;while(o=t[a++])while(r.indexOf(" "+o+" ")>-1)r=r.replace(" "+o+" "," ");i!==(s=vt(r))&&n.setAttribute("class",s)}return this},toggleClass:function(e,t){var n=typeof e,r="string"===n||Array.isArray(e);return"boolean"==typeof t&&r?t?this.addClass(e):this.removeClass(e):g(e)?this.each(function(n){w(this).toggleClass(e.call(this,n,mt(this),t),t)}):this.each(function(){var t,i,o,a;if(r){i=0,o=w(this),a=xt(e);while(t=a[i++])o.hasClass(t)?o.removeClass(t):o.addClass(t)}else void 0!==e&&"boolean"!==n||((t=mt(this))&&J.set(this,"__className__",t),this.setAttribute&&this.setAttribute("class",t||!1===e?"":J.get(this,"__className__")||""))})},hasClass:function(e){var t,n,r=0;t=" "+e+" ";while(n=this[r++])if(1===n.nodeType&&(" "+vt(mt(n))+" ").indexOf(t)>-1)return!0;return!1}});var bt=/\r/g;w.fn.extend({val:function(e){var t,n,r,i=this[0];{if(arguments.length)return r=g(e),this.each(function(n){var i;1===this.nodeType&&(null==(i=r?e.call(this,n,w(this).val()):e)?i="":"number"==typeof i?i+="":Array.isArray(i)&&(i=w.map(i,function(e){return null==e?"":e+""})),(t=w.valHooks[this.type]||w.valHooks[this.nodeName.toLowerCase()])&&"set"in t&&void 0!==t.set(this,i,"value")||(this.value=i))});if(i)return(t=w.valHooks[i.type]||w.valHooks[i.nodeName.toLowerCase()])&&"get"in t&&void 0!==(n=t.get(i,"value"))?n:"string"==typeof(n=i.value)?n.replace(bt,""):null==n?"":n}}}),w.extend({valHooks:{option:{get:function(e){var t=w.find.attr(e,"value");return null!=t?t:vt(w.text(e))}},select:{get:function(e){var t,n,r,i=e.options,o=e.selectedIndex,a="select-one"===e.type,s=a?null:[],u=a?o+1:i.length;for(r=o<0?u:a?o:0;r-1)&&(n=!0);return n||(e.selectedIndex=-1),o}}}}),w.each(["radio","checkbox"],function(){w.valHooks[this]={set:function(e,t){if(Array.isArray(t))return e.checked=w.inArray(w(e).val(),t)>-1}},h.checkOn||(w.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})}),h.focusin="onfocusin"in e;var wt=/^(?:focusinfocus|focusoutblur)$/,Tt=function(e){e.stopPropagation()};w.extend(w.event,{trigger:function(t,n,i,o){var a,s,u,l,c,p,d,h,v=[i||r],m=f.call(t,"type")?t.type:t,x=f.call(t,"namespace")?t.namespace.split("."):[];if(s=h=u=i=i||r,3!==i.nodeType&&8!==i.nodeType&&!wt.test(m+w.event.triggered)&&(m.indexOf(".")>-1&&(m=(x=m.split(".")).shift(),x.sort()),c=m.indexOf(":")<0&&"on"+m,t=t[w.expando]?t:new w.Event(m,"object"==typeof t&&t),t.isTrigger=o?2:3,t.namespace=x.join("."),t.rnamespace=t.namespace?new RegExp("(^|\\.)"+x.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,t.result=void 0,t.target||(t.target=i),n=null==n?[t]:w.makeArray(n,[t]),d=w.event.special[m]||{},o||!d.trigger||!1!==d.trigger.apply(i,n))){if(!o&&!d.noBubble&&!y(i)){for(l=d.delegateType||m,wt.test(l+m)||(s=s.parentNode);s;s=s.parentNode)v.push(s),u=s;u===(i.ownerDocument||r)&&v.push(u.defaultView||u.parentWindow||e)}a=0;while((s=v[a++])&&!t.isPropagationStopped())h=s,t.type=a>1?l:d.bindType||m,(p=(J.get(s,"events")||{})[t.type]&&J.get(s,"handle"))&&p.apply(s,n),(p=c&&s[c])&&p.apply&&Y(s)&&(t.result=p.apply(s,n),!1===t.result&&t.preventDefault());return t.type=m,o||t.isDefaultPrevented()||d._default&&!1!==d._default.apply(v.pop(),n)||!Y(i)||c&&g(i[m])&&!y(i)&&((u=i[c])&&(i[c]=null),w.event.triggered=m,t.isPropagationStopped()&&h.addEventListener(m,Tt),i[m](),t.isPropagationStopped()&&h.removeEventListener(m,Tt),w.event.triggered=void 0,u&&(i[c]=u)),t.result}},simulate:function(e,t,n){var r=w.extend(new w.Event,n,{type:e,isSimulated:!0});w.event.trigger(r,null,t)}}),w.fn.extend({trigger:function(e,t){return this.each(function(){w.event.trigger(e,t,this)})},triggerHandler:function(e,t){var n=this[0];if(n)return w.event.trigger(e,t,n,!0)}}),h.focusin||w.each({focus:"focusin",blur:"focusout"},function(e,t){var n=function(e){w.event.simulate(t,e.target,w.event.fix(e))};w.event.special[t]={setup:function(){var r=this.ownerDocument||this,i=J.access(r,t);i||r.addEventListener(e,n,!0),J.access(r,t,(i||0)+1)},teardown:function(){var r=this.ownerDocument||this,i=J.access(r,t)-1;i?J.access(r,t,i):(r.removeEventListener(e,n,!0),J.remove(r,t))}}});var Ct=e.location,Et=Date.now(),kt=/\?/;w.parseXML=function(t){var n;if(!t||"string"!=typeof t)return null;try{n=(new e.DOMParser).parseFromString(t,"text/xml")}catch(e){n=void 0}return n&&!n.getElementsByTagName("parsererror").length||w.error("Invalid XML: "+t),n};var St=/\[\]$/,Dt=/\r?\n/g,Nt=/^(?:submit|button|image|reset|file)$/i,At=/^(?:input|select|textarea|keygen)/i;function jt(e,t,n,r){var i;if(Array.isArray(t))w.each(t,function(t,i){n||St.test(e)?r(e,i):jt(e+"["+("object"==typeof i&&null!=i?t:"")+"]",i,n,r)});else if(n||"object"!==x(t))r(e,t);else for(i in t)jt(e+"["+i+"]",t[i],n,r)}w.param=function(e,t){var n,r=[],i=function(e,t){var n=g(t)?t():t;r[r.length]=encodeURIComponent(e)+"="+encodeURIComponent(null==n?"":n)};if(Array.isArray(e)||e.jquery&&!w.isPlainObject(e))w.each(e,function(){i(this.name,this.value)});else for(n in e)jt(n,e[n],t,i);return r.join("&")},w.fn.extend({serialize:function(){return w.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var e=w.prop(this,"elements");return e?w.makeArray(e):this}).filter(function(){var e=this.type;return this.name&&!w(this).is(":disabled")&&At.test(this.nodeName)&&!Nt.test(e)&&(this.checked||!pe.test(e))}).map(function(e,t){var n=w(this).val();return null==n?null:Array.isArray(n)?w.map(n,function(e){return{name:t.name,value:e.replace(Dt,"\r\n")}}):{name:t.name,value:n.replace(Dt,"\r\n")}}).get()}});var qt=/%20/g,Lt=/#.*$/,Ht=/([?&])_=[^&]*/,Ot=/^(.*?):[ \t]*([^\r\n]*)$/gm,Pt=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Mt=/^(?:GET|HEAD)$/,Rt=/^\/\//,It={},Wt={},$t="*/".concat("*"),Bt=r.createElement("a");Bt.href=Ct.href;function Ft(e){return function(t,n){"string"!=typeof t&&(n=t,t="*");var r,i=0,o=t.toLowerCase().match(M)||[];if(g(n))while(r=o[i++])"+"===r[0]?(r=r.slice(1)||"*",(e[r]=e[r]||[]).unshift(n)):(e[r]=e[r]||[]).push(n)}}function _t(e,t,n,r){var i={},o=e===Wt;function a(s){var u;return i[s]=!0,w.each(e[s]||[],function(e,s){var l=s(t,n,r);return"string"!=typeof l||o||i[l]?o?!(u=l):void 0:(t.dataTypes.unshift(l),a(l),!1)}),u}return a(t.dataTypes[0])||!i["*"]&&a("*")}function zt(e,t){var n,r,i=w.ajaxSettings.flatOptions||{};for(n in t)void 0!==t[n]&&((i[n]?e:r||(r={}))[n]=t[n]);return r&&w.extend(!0,e,r),e}function Xt(e,t,n){var r,i,o,a,s=e.contents,u=e.dataTypes;while("*"===u[0])u.shift(),void 0===r&&(r=e.mimeType||t.getResponseHeader("Content-Type"));if(r)for(i in s)if(s[i]&&s[i].test(r)){u.unshift(i);break}if(u[0]in n)o=u[0];else{for(i in n){if(!u[0]||e.converters[i+" "+u[0]]){o=i;break}a||(a=i)}o=o||a}if(o)return o!==u[0]&&u.unshift(o),n[o]}function Ut(e,t,n,r){var i,o,a,s,u,l={},c=e.dataTypes.slice();if(c[1])for(a in e.converters)l[a.toLowerCase()]=e.converters[a];o=c.shift();while(o)if(e.responseFields[o]&&(n[e.responseFields[o]]=t),!u&&r&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),u=o,o=c.shift())if("*"===o)o=u;else if("*"!==u&&u!==o){if(!(a=l[u+" "+o]||l["* "+o]))for(i in l)if((s=i.split(" "))[1]===o&&(a=l[u+" "+s[0]]||l["* "+s[0]])){!0===a?a=l[i]:!0!==l[i]&&(o=s[0],c.unshift(s[1]));break}if(!0!==a)if(a&&e["throws"])t=a(t);else try{t=a(t)}catch(e){return{state:"parsererror",error:a?e:"No conversion from "+u+" to "+o}}}return{state:"success",data:t}}w.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:Ct.href,type:"GET",isLocal:Pt.test(Ct.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":$t,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":w.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?zt(zt(e,w.ajaxSettings),t):zt(w.ajaxSettings,e)},ajaxPrefilter:Ft(It),ajaxTransport:Ft(Wt),ajax:function(t,n){"object"==typeof t&&(n=t,t=void 0),n=n||{};var i,o,a,s,u,l,c,f,p,d,h=w.ajaxSetup({},n),g=h.context||h,y=h.context&&(g.nodeType||g.jquery)?w(g):w.event,v=w.Deferred(),m=w.Callbacks("once memory"),x=h.statusCode||{},b={},T={},C="canceled",E={readyState:0,getResponseHeader:function(e){var t;if(c){if(!s){s={};while(t=Ot.exec(a))s[t[1].toLowerCase()]=t[2]}t=s[e.toLowerCase()]}return null==t?null:t},getAllResponseHeaders:function(){return c?a:null},setRequestHeader:function(e,t){return null==c&&(e=T[e.toLowerCase()]=T[e.toLowerCase()]||e,b[e]=t),this},overrideMimeType:function(e){return null==c&&(h.mimeType=e),this},statusCode:function(e){var t;if(e)if(c)E.always(e[E.status]);else for(t in e)x[t]=[x[t],e[t]];return this},abort:function(e){var t=e||C;return i&&i.abort(t),k(0,t),this}};if(v.promise(E),h.url=((t||h.url||Ct.href)+"").replace(Rt,Ct.protocol+"//"),h.type=n.method||n.type||h.method||h.type,h.dataTypes=(h.dataType||"*").toLowerCase().match(M)||[""],null==h.crossDomain){l=r.createElement("a");try{l.href=h.url,l.href=l.href,h.crossDomain=Bt.protocol+"//"+Bt.host!=l.protocol+"//"+l.host}catch(e){h.crossDomain=!0}}if(h.data&&h.processData&&"string"!=typeof h.data&&(h.data=w.param(h.data,h.traditional)),_t(It,h,n,E),c)return E;(f=w.event&&h.global)&&0==w.active++&&w.event.trigger("ajaxStart"),h.type=h.type.toUpperCase(),h.hasContent=!Mt.test(h.type),o=h.url.replace(Lt,""),h.hasContent?h.data&&h.processData&&0===(h.contentType||"").indexOf("application/x-www-form-urlencoded")&&(h.data=h.data.replace(qt,"+")):(d=h.url.slice(o.length),h.data&&(h.processData||"string"==typeof h.data)&&(o+=(kt.test(o)?"&":"?")+h.data,delete h.data),!1===h.cache&&(o=o.replace(Ht,"$1"),d=(kt.test(o)?"&":"?")+"_="+Et+++d),h.url=o+d),h.ifModified&&(w.lastModified[o]&&E.setRequestHeader("If-Modified-Since",w.lastModified[o]),w.etag[o]&&E.setRequestHeader("If-None-Match",w.etag[o])),(h.data&&h.hasContent&&!1!==h.contentType||n.contentType)&&E.setRequestHeader("Content-Type",h.contentType),E.setRequestHeader("Accept",h.dataTypes[0]&&h.accepts[h.dataTypes[0]]?h.accepts[h.dataTypes[0]]+("*"!==h.dataTypes[0]?", "+$t+"; q=0.01":""):h.accepts["*"]);for(p in h.headers)E.setRequestHeader(p,h.headers[p]);if(h.beforeSend&&(!1===h.beforeSend.call(g,E,h)||c))return E.abort();if(C="abort",m.add(h.complete),E.done(h.success),E.fail(h.error),i=_t(Wt,h,n,E)){if(E.readyState=1,f&&y.trigger("ajaxSend",[E,h]),c)return E;h.async&&h.timeout>0&&(u=e.setTimeout(function(){E.abort("timeout")},h.timeout));try{c=!1,i.send(b,k)}catch(e){if(c)throw e;k(-1,e)}}else k(-1,"No Transport");function k(t,n,r,s){var l,p,d,b,T,C=n;c||(c=!0,u&&e.clearTimeout(u),i=void 0,a=s||"",E.readyState=t>0?4:0,l=t>=200&&t<300||304===t,r&&(b=Xt(h,E,r)),b=Ut(h,b,E,l),l?(h.ifModified&&((T=E.getResponseHeader("Last-Modified"))&&(w.lastModified[o]=T),(T=E.getResponseHeader("etag"))&&(w.etag[o]=T)),204===t||"HEAD"===h.type?C="nocontent":304===t?C="notmodified":(C=b.state,p=b.data,l=!(d=b.error))):(d=C,!t&&C||(C="error",t<0&&(t=0))),E.status=t,E.statusText=(n||C)+"",l?v.resolveWith(g,[p,C,E]):v.rejectWith(g,[E,C,d]),E.statusCode(x),x=void 0,f&&y.trigger(l?"ajaxSuccess":"ajaxError",[E,h,l?p:d]),m.fireWith(g,[E,C]),f&&(y.trigger("ajaxComplete",[E,h]),--w.active||w.event.trigger("ajaxStop")))}return E},getJSON:function(e,t,n){return w.get(e,t,n,"json")},getScript:function(e,t){return w.get(e,void 0,t,"script")}}),w.each(["get","post"],function(e,t){w[t]=function(e,n,r,i){return g(n)&&(i=i||r,r=n,n=void 0),w.ajax(w.extend({url:e,type:t,dataType:i,data:n,success:r},w.isPlainObject(e)&&e))}}),w._evalUrl=function(e){return w.ajax({url:e,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},w.fn.extend({wrapAll:function(e){var t;return this[0]&&(g(e)&&(e=e.call(this[0])),t=w(e,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){var e=this;while(e.firstElementChild)e=e.firstElementChild;return e}).append(this)),this},wrapInner:function(e){return g(e)?this.each(function(t){w(this).wrapInner(e.call(this,t))}):this.each(function(){var t=w(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=g(e);return this.each(function(n){w(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(e){return this.parent(e).not("body").each(function(){w(this).replaceWith(this.childNodes)}),this}}),w.expr.pseudos.hidden=function(e){return!w.expr.pseudos.visible(e)},w.expr.pseudos.visible=function(e){return!!(e.offsetWidth||e.offsetHeight||e.getClientRects().length)},w.ajaxSettings.xhr=function(){try{return new e.XMLHttpRequest}catch(e){}};var Vt={0:200,1223:204},Gt=w.ajaxSettings.xhr();h.cors=!!Gt&&"withCredentials"in Gt,h.ajax=Gt=!!Gt,w.ajaxTransport(function(t){var n,r;if(h.cors||Gt&&!t.crossDomain)return{send:function(i,o){var a,s=t.xhr();if(s.open(t.type,t.url,t.async,t.username,t.password),t.xhrFields)for(a in t.xhrFields)s[a]=t.xhrFields[a];t.mimeType&&s.overrideMimeType&&s.overrideMimeType(t.mimeType),t.crossDomain||i["X-Requested-With"]||(i["X-Requested-With"]="XMLHttpRequest");for(a in i)s.setRequestHeader(a,i[a]);n=function(e){return function(){n&&(n=r=s.onload=s.onerror=s.onabort=s.ontimeout=s.onreadystatechange=null,"abort"===e?s.abort():"error"===e?"number"!=typeof s.status?o(0,"error"):o(s.status,s.statusText):o(Vt[s.status]||s.status,s.statusText,"text"!==(s.responseType||"text")||"string"!=typeof s.responseText?{binary:s.response}:{text:s.responseText},s.getAllResponseHeaders()))}},s.onload=n(),r=s.onerror=s.ontimeout=n("error"),void 0!==s.onabort?s.onabort=r:s.onreadystatechange=function(){4===s.readyState&&e.setTimeout(function(){n&&r()})},n=n("abort");try{s.send(t.hasContent&&t.data||null)}catch(e){if(n)throw e}},abort:function(){n&&n()}}}),w.ajaxPrefilter(function(e){e.crossDomain&&(e.contents.script=!1)}),w.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(e){return w.globalEval(e),e}}}),w.ajaxPrefilter("script",function(e){void 0===e.cache&&(e.cache=!1),e.crossDomain&&(e.type="GET")}),w.ajaxTransport("script",function(e){if(e.crossDomain){var t,n;return{send:function(i,o){t=w(" + + + + + + + + + + + {% endblock %} + + + + {% block content %}{% endblock %} + + {% include 'layouts/partials/footer.jinja2' %} + + {% block scripts %}{% endblock %} + + + \ No newline at end of file diff --git a/hassio-google-drive-backup/backup/static/layouts/base.jinja2 b/hassio-google-drive-backup/backup/static/layouts/base.jinja2 new file mode 100644 index 0000000..39be220 --- /dev/null +++ b/hassio-google-drive-backup/backup/static/layouts/base.jinja2 @@ -0,0 +1,51 @@ + + + + + + + {% block head %} + + Home Assistant Google Drive Backup + + + + + + + + + + + + + + + + + + + + + + + + + {% endblock %} + + + + {% block navbar %} + {% include 'layouts/partials/navbar.jinja2' %} + {% endblock %} + + {% block content %}{% endblock %} + + {% include 'layouts/partials/footer.jinja2' %} + + {% block scripts %}{% endblock %} + + + \ No newline at end of file diff --git a/hassio-google-drive-backup/backup/static/layouts/macros.jinja2 b/hassio-google-drive-backup/backup/static/layouts/macros.jinja2 new file mode 100644 index 0000000..c13597c --- /dev/null +++ b/hassio-google-drive-backup/backup/static/layouts/macros.jinja2 @@ -0,0 +1,36 @@ +{% macro header(version, title) -%} +

    + +
    +{%- endmacro %} + +{% macro errorMessage(title, id, icon, actions) -%} +
    +
    + + {{ icon }} + {{ title }} + + {{ caller() }} +
    + {% if actions %} +
    + {{actions}} +
    + {% endif %} +
    +{%- endmacro %} diff --git a/hassio-google-drive-backup/backup/static/layouts/partials/error-messages.jinja2 b/hassio-google-drive-backup/backup/static/layouts/partials/error-messages.jinja2 new file mode 100644 index 0000000..8980f7b --- /dev/null +++ b/hassio-google-drive-backup/backup/static/layouts/partials/error-messages.jinja2 @@ -0,0 +1,693 @@ +{% import "layouts/macros.jinja2" as macros %} + +{# Reusable macros #} + +{% macro chooseFolder(id) %} +
    +
    + folder_open + + + The Google Drive ID of the folder you'd like to upload backups into. You + can get this id by navigating to the folder in + Google Drive + and copying it out of the URL. It should be a long sequence of characters at the end of the URL. For + example if the URL for my folder is + https://drive.google.com/drive/folders/1el4ZWcE0xdkIu_Yt_2kxk36qE7K5O188, + then the Drive ID is 1el4ZWcE0xdkIu_Yt_2kxk36qE7K5O188 + Use the "Choose Folder" button to open a new window where you can select the + folder you'd like to use. Doing this gives the addon permission to store backups in that folder. + + open_in_new + Choose Folder + + + save + Save + +
    +
    +{% endmacro %} + + +{# Error messages #} + +{% set error_card_actions %} + searchFind similar issues + bug_reportFile this issue on Github + file_downloadDownload logs +{% endset %} +{% call macros.errorMessage("Error while backing up", "error_card", "sync_problem", error_card_actions) %} +

    + The add-on got the error below while trying to sync your backups. Some transient errors are expected + (e.g. an upload could fail if the internet is down) but if this persist for more than a few minutes your + backups won't get uploaded and the information you see on this page may be stale. If you file an + issue on GitHub with the link below, it could be helpful to provide the logs downloaded from the + "Download Logs" link below +

    +
    +

    + +{% endcall %} + +{% set low_space_actions %} + refreshTry Syncing Again + addCreate backup anyway +{% endset %} +{% call macros.errorMessage("Low Disk Space", "low_space", "disc_full", low_space_actions) %} +

    + It looks like you're low on disk space. The addon won't attempt to create a backup until you take + action or more space becomes available. It looks like your backup folder in Home Assistant is + full and only has left so creating + another backup runs the risk of exhausting the space available. When Home Assistant runs out of + disk space, it can stop responding and become very difficult to fix. This addon can only estimate + how big the next backup will be based on your most recent one, it can't know how big the backup + will be until its actually created. +

    +

    To resolve this you can:

    +
      +
    • + Free up space in Home Assistant. Addons like the Configurator can help you see what files might be taking up space. +
    • +
    • + Make your backups smaller (see this tip). + You can also click the little info + next to backups below to see which parts of them are taking up the most space. +
    • +
    • + Modify your settings add-on settings + to store fewer backups in Home Assistant. +
    • +
    • + Delete existing backups from Home Assistant by clicking "Action" -> "Delete" on backups below. Make sure either you don't need them anymore or they're uploaded to Google Drive. +
    • +
    • + Modify your settings add-on settings + and disable "Stop on low disk space". This will make the addon stop checking disk usage, do so at your own peril. +
    • +
    • + Click "Create backup anyway" to bypass this warning just this one time. This is useful if you're confident that the backup actually will fit in the available space. Again, do so at your own peril. +
    • +
    +{% endcall %} + +{% set drive_full_actions %} + refreshTry Syncing Again +{% endset %} +{% call macros.errorMessage("Google Drive is Full", "drive_full", "disc_full", drive_full_actions) %} +

    + Google Drive is full, backups can't continue until space is available. You can go to Google Drive's + quota page + to see what's using up space in your Google Drive, but because that space is shared between several Google products (e.g. Gmail, Google Photos) + one of those products might be using the space instead. You can see your usage across all those products on + Google's storage settings page. +

    +

    To resolve this you can:

    +
      +
    • + Free up space in Google Drive, Gmail, or Google Photos by deleting things. +
    • +
    • + Modify your settings add-on settings + to store fewer backups in Google Drive +
    • +
    • + Buy more storage from Google +
    • +
    • + Make your backups smaller (see this tip) +
    • +{% endcall %} + +{% call macros.errorMessage("Couldn't Log in with Google", "creds_bad", "error") %} +

      Google Drive rejected your credentials. Your backups will not be uploaded until this is resolved.

      +

      This can happen if:

      +
        +
      • You revoked the add-on's access in your Google account settings.
      • +
      • You recently changed your Google account password.
      • +
      • You haven't used your Google account for over 6 months.
      • +
      • The add-on's credentials changed, or Google changed them.
      • +
      +

      To resolve this, you'll need to authenticate this add-on with Google again. If that still doesn't help, you may need to update the add-on

      +{% endcall %} + +{% set delete_error_actions %} + refreshTry Syncing Again +{% endset %} +{% call macros.errorMessage("Couldn't delete old backups in Home Assistant", "delete_error", "delete_forever", delete_error_actions) %} +

      + The add-on ran into an error (HTTP 400) trying to delete your oldest backup. This can happen + sometimes (especially if you just installed the add-on) when Home Assistant's cache of your existing backups + is out of sync. The add-on can't continue until this is resolved. +

      +

      To resolve this, you can try:

      +
        +
      • + Refreshing your backups by going to the "Backups" page in Home Assistant and clicking the refresh icon in the top right. +
      • +
      • Restarting the machine Home Assistant runs on.
      • +
      +

      + The add-on will retry periodically to check if the issue is resolved. To retry now, click "Try + Syncing Again" below. If the above doesn't help, please file an issue on GitHub + to get more help. +

      +{% endcall %} + +{% set google_dns_actions %} + refreshTry Syncing Again +{% endset %} +{% call macros.errorMessage("Couldn't Reach Google Drive (DNS error)", "google_dns", "cloud_off", google_dns_actions) %} +

      + The add-on couldn't resolve Google Drive's server address. Backups can't be uploaded + without being able to reach Google Drive. +

      +

      Please ensure:

      +
        +
      • The internet is working.
      • +
      • + The server www.googleapis.com is reachable from your Home Assistant + machine. Some ad-blocking and firewall software (e.g. Ad Guard, Pi-hole, etc) can block this + addresses. +
      • +
      +
      + arrow_drop_down Show DNS info +
      +

      + Below is the list of names and IP Addresses Google's API servers resolve to. Next to each is + the result from an attempt the add-on has made to ping the address. This may help in + determining if something in your network is blocking access to one of Google's API servers, + or if Google's API servers are having problems. +

      +
      +
      +
      +
      +

      You can also try restarting this add-on, the supervisor, or Home Assistant.

      +{% endcall %} + +{% set google_timeout_actions %} + refreshTry Syncing Again +{% endset %} +{% call macros.errorMessage("Google Drive Connection Timeout", "google_timeout", "cloud_off", google_timeout_actions) %} +

      + The connection timed out or was closed unexpectedly while trying to communicate with Google Drive. + Backups can't be uploaded without being able to reach Google Drive reliably. This can happen transiently from time to + time and the addon will continue to attempt to sync your backups. This problem almost always resolves itself after some time. +

      +

      Please ensure:

      +
        +
      • The internet is working.
      • +
      • + The server www.googleapis.com are reachable from your Home Assistant + machine. Some ad-blocking and + firewall software (e.g. Ad Guard, Pi-hole, etc) can block or throttle traffic to this address. +
      • +
      +
      + + arrow_drop_down + Show DNS info + +
      +

      + Below is the list of names and IP Addresses Google's API servers resolve to. Next to each is + the result from an attempt the add-on has made to ping the address. This may help in + determining if something in your network is blocking access to one of Google's API servers, + or if Google's API servers are having problems. +

      +
      +
      +
      +
      +

      You can also try syncing again or restarting this add-on, the supervisor, or Home Assistant.

      +{% endcall %} + +{% set google_cant_connect_actions %} + refreshTry Syncing Again +{% endset %} +{% call macros.errorMessage("Can't Connect to Google Drive", "google_cant_connect", "cloud_off", google_cant_connect_actions) %} +

      + Google Drive refused an attempt to connect with it. Backups can't be uploaded without being able + to reach Google Drive. +

      +

      Please ensure:

      +
        +
      • The internet is working.
      • +
      • + The server www.googleapis.com are reachable from your Home Assistant + machine. Some ad-blocking and + firewall software (eg Ad Guard, pi-hole, etc) can block or throttle traffic to this address. +
      • +
      +
      + + arrow_drop_down + Show DNS info + +
      +

      + Below is the list of names and IP Addresses Google's API servers resolve to. Next to each is + the result from an attempt the add-on has made to ping the address. This may help in + determining if something in your network is blocking access to one of Google's API servers, + or if Google's API servers are having problems. +

      +
      +
      +
      +
      +

      You can also try syncing again or restarting this add-on, the supervisor, or Home Assistant.

      +{% endcall %} + +{% set password_key_invalid_actions %} + refreshTry Syncing Again +{% endset %} +{% call macros.errorMessage("Invalid Backup Password", "password_key_invalid", "vpn_key", password_key_invalid_actions) %} +

      + The add-on couldn't find your backup password in your secrets.yaml file. Backups can't be created + until you either configure a valid password or remove it. +

      +

      This can happen if:

      +
        +
      • Your secrets.yaml file isn't present in your Home Assistant configuration directory
      • +
      • The password key isn't present in your secrets.yaml file.
      • +
      +

      + Please review your settings add-on settings + to ensure you've set the backup password you intended, then sync again. +

      +{% endcall %} + +{% set backup_in_progress_actions %} + refreshTry Syncing Again +{% endset %} +{% call macros.errorMessage("Backup Already in Progress", "backup_in_progress", "settings_backup_restore", backup_in_progress_actions) %} +

      The add-on couldn't create a backup on schedule because one is already in progress. Home Assistant can + only create one backup at a time. This can happen if:

      +
        +
      • The add-on was started while a backup was already in progress
      • +
      • A backup was started outside of the add-on, eg manually from the supervisor Backups page.
      • +
      +

      Typically you don't need to do anything to resolve this, as the add-on will pick up again once the + backup in progress finishes. + If you are certain no backup is in progress already or this error sticks around, you can try + restarting the addon or Home Assistant's host machine.

      +{% endcall %} + +{% set multiple_deletes_actions %} + deleteAllow Deletion + delete_foreverNever Ask again +{% endset %} +{% call macros.errorMessage("Attempting to Delete Multiple Backups", "multiple_deletes", "delete_forever", multiple_deletes_actions) %} +

      The add-on attempted to delete (?) backup(s) from Google + Drive and (?) backup(s) from Home Assistant. + Under normal circumstance, the add-on will only delete one backup at a time (the oldest). + This warning is produced to prevent the add-on from accidentally deleting your history of backups. + This is normal if you recently lowered the number of backups to keep in Google Drive or Home + Assistant.

      +

      To resolve this you can:

      +
        +
      • + Review the settings add-on settings + and increase the number of backups you keep. +
      • +
      • Click "Allow Deletion" below to permit these deletions to happen this one time.
      • +
      • Click "Never ask again" below to permit the deletes this time and in the future (not recommended) +
      • +
      +{% endcall %} + +{% set google_server_error_actions %} + refreshTry Syncing Again +{% endset %} +{% call macros.errorMessage("Google Server Error", "google_server_error", "cloud_off", google_server_error_actions) %} +

      Google returned a server error (HTTP code 500) while trying to backup your backups.

      +

      This happens sometimes for reasons beyond anyone's control and almost always the problem goes away on + its own. The add-on will attempt uploading again soon, but you can also click "Try Syncing Again" + below to make it retry now.

      +

      If this error keeps sticking around, you can try:

      +
        +
      • Making sure the internet is working fine.
      • +
      • Making sure you're able to log into your Google Drive + account and upload files without problems.
      • +
      • Re-authenticating the add-on with Google Drive (click "ACTIONS" -> "Reauthorize Google Drive" up top) +
      • +
      • Restarting the add-on, Home Assistant, or the supervisor.
      • +
      • Reinstalling the add-on.
      • +
      +{% endcall %} + +{% set google_session_expired_actions %} + refreshTry Syncing Again +{% endset %} +{% call macros.errorMessage("Google Session Expired", "google_session_expired", "cloud_off", google_session_expired_actions) %} +

      Google returned a server error (HTTP code 404) while trying to upload your backups.

      +

      In most cases, this indicates that your upload took too long to upload. According to Google's + documentation if an upload takes longer than a week then it will be forcibly cancelled and return this + error. This can happen if your internet is very slow or your backups are very large, however the + developer of this addon suspects this can happen in more common circumstances. If the below options don't + help, please file an issue on GitHub with as much + detail as you can stomach to help him look into it. +

      +

      If this error keeps sticking around, you can try:

      +
        +
      • Syncing again (if it hasn't already started) by clicking "Try Syncing Again" below.
      • +
      • Making sure you're able to log into your Google + Drive + account and upload files without problems.
      • +
      • Ensuring your internet connection is reasonably fast by doing a speed test
      • +
      • Making your backups smaller. Usually backups are large because of a huge sensor database. You + can keep a smaller history by setting up the recorder component purge settings in + Home Assistant
      • +
      • Restarting the add-on, Home Assistant, or the supervisor.
      • +
      • Reinstalling the add-on.
      • +
      +{% endcall %} + +{% set existing_backup_folder_actions %} + folderUse the folder + open_in_newChoose a different folder + create_new_folderCreate a new folder +{% endset %} +{% call macros.errorMessage("Existing Backup Folder Found", "existing_backup_folder", "folder_special", existing_backup_folder_actions) %} +

      The add-on found a backup folder named "" in Google + Drive that might already have backups + in it. This is normal if you've used this addon before, but you'll need to decide where backups + should go before this will start backing up again. Click + here to see the folder contents in Google Drive.

      +

      To resolve this you can:

      +
        +
      • "Use the folder" and merge in backups from this Home Assistant instance. If this + instance has newer backups, then some of the backups in Google Drive might get deleted to make + room for these newer ones (this depends on your settings). You probably want this option if you just + reinstalled the addon.
      • +
      • "Choose a different folder" to store backups in, ignoring the existing folder. You probably want + his option if you have multiple Home Assistant instances that should all back up to their + own folder.
      • +
      • Or "create a new folder" to automatically create a folder and ignore the existing one. The new folder will be created + at the root of your My Drive.
      • +
      +

      Note: If you run multiple instance of Home Assistant with this addon using the same Google + Drive account, + its recommended that you use a different folder for each instance. Otherwise they'll delete + each other's + backups!

      +{% endcall %} + +{% set backup_folder_missing_actions %} + settingsOpen settings +{% endset %} +{% call macros.errorMessage("Please choose a backup folder", "backup_folder_missing", "folder_special", backup_folder_missing_actions) %} +

      You've chosen to upload your backups to a folder of your choosing. + Click the link below to open a new window and select the folder in Google Drive you'd like to store + your backups in.

      +

      You've chosen to upload your backups to a folder of your choosing. + Please navigate to the addon settings to specify a folder to store backups in.

      + {{ chooseFolder(id="choose_folder_folder_missing") }} +
        +
      • +

        If you'd rather not specify the folder for uploads, please return to your settings add-on + settings and change your preference.

        +
      • +
      • + Your backups in Home Assistant will be re-uploaded to the new folder. If you already have them in + an existing folder in Google Drive and you'd like to avoid this, please move them in manually before + selecting the folder. +
      • +
      • +

        If you run multiple instances of + Home Assistant with this addon using the same Google Drive account, its recommended that you use a + different folder + for each instance. Otherwise they'll delete each other's backups!

        +
      • +
      +

      Note: Google's authentication mechanisms require you to + select your folder through an external domain, which is where the "Choose Folder" link will take you. + See here for details if you'd like to know more about why.

      +{% endcall %} + +{% set backup_folder_inaccessible_actions %} + refreshTry syncing again +{% endset %} +{% call macros.errorMessage("Couldn't use your backup folder", "backup_folder_inaccessible", "sync_problem", backup_folder_inaccessible_actions) %} +

      The addon couldn't use the folder you chose for storing backups. The addon can't backup your + backups until you resolve this. This can happen if: +

        +
      • The folder was deleted
      • +
      • You lost permissions on the folder
      • +
      • You gave the addon an invalid folder ID
      • +
      • You signed in with drive using different credentials
      • +
      • Something else made the folder unreadable
      • +
      +

      +

      + To resolve this, you can: +

      +
        +
      • +

        Use the link below to choose a new folder to store backups in. + {{ chooseFolder(id="choose_folder_folder_inaccessible") }} +

        +
      • +
      • +

        Open your settings add-on + settings and, enter a valid "Backup Folder Id".

        +
      • +
      • +

        Open your settings add-on + settings and deselect "Manually specify the backup folder". The addon will automatically + create a new folder for you.

        +
      • +
      • +

        If the folder still exists, you can restore access to it by the addon by ensuring that your + google account has access to it. The folder should be accessible from this link. After doing so, click "sync now" below to try syncing again.

        +
      • +
      +

      Note: Google's authentication mechanisms require you to select + your folder through an + external domain, which is where the "choose folder" link below will take you. + See here for details if you'd like to know more about why.

      +{% endcall %} + +{% set cancelled_actions %} + play_arrowResume +{% endset %} +{% call macros.errorMessage("Backup is Paused", "cancelled", "pause", cancelled_actions) %} +

      Sync was cancelled because you requested it. The addon will not resume syncing for another hour unless you take action.

      +
      +

      + Please note: +

      +
        +
      • +

        A pending backup can't be stopped once its started, because its creation is managed by Home Assistant. Being "Paused" only stops the addon from creating new backups and syncing them with Google Drive.

        +
      • +
      • +

        You can resume by clicking the link below or just waiting.

        +
      • +
      +{% endcall %} + +{% set token_refresh_google_error_actions %} + refreshtry syncing again +{% endset %} +{% call macros.errorMessage("Couldn't Refresh Google Drive Credentials", "token_refresh_google_error", "error", token_refresh_google_error_actions) %} +

      + The addon was unable to refresh your credentials with Google Drive. When it tried, Google said: +

      +
      +    (?)
      +  
      +

      This can happen if:

      +
        +
      • The domain https://habackup.io, which grants your credentials from Google Drive (click here to see why), is having trouble communicating with Google Drive.
      • +
      • Google Drive is experiencing a temporary outage.
      • +
      • Something else has gone wrong with your Google Drive credentials.
      • +
      +

      Usually this problem will resolve on its own, but you can try:

      +
        +
      • Syncing again with the button below. The addon will retry on its own periodically.
      • +
      • Re-authenticating this add-on with Google + again. Its unlikely this will resolve the problem, but it also won't cause you to lose any of your data.
      • +
      • If the problem persists, file an issue on GitHub to let the developer of this addon know about it.
      • +
      +{% endcall %} + +{% set token_refresh_my_error_actions %} + refreshtry syncing again +{% endset %} +{% call macros.errorMessage("Couldn't Refresh Google Drive Credentials", "token_refresh_my_error", "error", token_refresh_my_error_actions) %} +

      + The addon was unable to refresh your credentials with Google Drive. When it tried, https://habackup.io said: +

      +
      +    (?)
      +  
      +

      This can happen if:

      +
        +
      • The domain https://habackup.io, which grants your credentials from Google Drive (click here to see why), is having some unexpected problems.
      • +
      • Something else has gone wrong with your Google Drive credentials.
      • +
      +

      Usually this problem will resolve on its own, but you can try:

      +
        +
      • Syncing again with the button below. The addon will retry on its own periodically.
      • +
      • Re-authenticating this add-on with Google + again. Its unlikely this will resolve the problem, but it also won't cause you to lose any of your data.
      • +
      • If the problem persists, file an issue on GitHub to let the developer of this addon know about it.
      • +
      +{% endcall %} + +{% set supervisor_connection_actions %} + refreshtry syncing again +{% endset %} +{% call macros.errorMessage("Unable to Reach Home Assistant Supervisor", "supervisor_connection", "error", supervisor_connection_actions) %} +

      + The addon was unable to connect to the Home Assistant Supervisor. This addon requires the supervisor to be running to create + backups, load configuration, etc. Without the supervisor the addon will not be able to do anything useful. Typically this + should never happen, and indicates that something is wrong with your Home Assistant installation. +

      +

      To resolve this you can try:

      +
        +
      • Syncing again with the button below. The addon will retry on its own periodically.
      • +
      • Restarting the machine that Home Assistant runs on. Often this resolves transient problems the Home Assistant Supervisor might have.
      • +
      • If the problem persists, file an issue on GitHub to let the developer of this addon know about it.
      • +
      +{% endcall %} + +{% set supervisor_permission_actions %} + refreshtry syncing again +{% endset %} +{% call macros.errorMessage("Unable to Talk to the Home Assistant Supervisor", "supervisor_permission", "error", supervisor_permission_actions) %} +

      + The addon was unable to communicate with the Supervisor because it said the addon is unauthorized. This addon + must communicate with the supervisor to create backups, load configuration, etc. Without the supervisor the + addon will not be able to do anything useful. Typically this should never happen, and indicates that something + is wrong with your Home Assistant installation. +

      +
        +
      • Syncing again with the button below. The addon will also retry on its own periodically.
      • +
      • Restarting the addon, the supervisor, or the whole machine Home Assistant is running on. Sometimes the supervisor + can get into a bad state and this works around it.
      • +
      • Checking the Supervisor's logs ("Supervisor" > "System" > "Logs") to see if any errors might indicate what the problem is.
      • +
      • If the problem persists, file an issue on GitHub to let the developer of this addon know about it. He might be able to help.
      • +
      +{% endcall %} + +{% set supervisor_timeout_actions %} + refreshtry syncing again +{% endset %} +{% call macros.errorMessage("A Request to the Supervisor Timed Out", "supervisor_timeout", "error", supervisor_timeout_actions) %} +

      + The addon was unable to communicate with the Supervisor because it timed out. This addon + must communicate with the supervisor to create backups, load configuration, etc. Without the supervisor the + addon will not be able to do anything useful. This usually resolves itself after the addon retries, but if + you see this error stick around for a while you can try +

      +
        +
      • Syncing again with the button below. The addon will also retry on its own periodically.
      • +
      • Restarting the addon, the supervisor, or the whole machine Home Assistant is running on. Sometimes the supervisor + can get into a bad state and this works around it.
      • +
      • If the problem persists, file an issue on GitHub to let the developer of this addon know about it. He might be able to help.
      • +
      +{% endcall %} + +{% set supervisor_unexpected_actions %} + refreshtry syncing again +{% endset %} +{% call macros.errorMessage("The Supervisor Returned an Unexpected Response", "supervisor_unexpected", "error", supervisor_unexpected_actions) %} +

      + The addon was unable to communicate with the Supervisor because it gave back an unreadable response. + This addon must communicate with the supervisor to create backups, load configuration, etc. Without + the supervisor the addon will not be able to do anything useful. This usually + resolves itself after the addon retries, but if you see this error stick around for a while you can try: +

      +
        +
      • Syncing again with the button below. The addon will also retry on its own periodically.
      • +
      • Restarting the addon, the supervisor, or the whole machine Home Assistant is running on. Sometimes the supervisor + can get into a bad state and this works around it.
      • +
      • If the problem persists, file an issue on GitHub to let the developer of this addon know about it. He might be able to help.
      • +
      +{% endcall %} + +{% set supervisor_fs_error_actions %} + refreshtry syncing again +{% endset %} +{% call macros.errorMessage("Unable to write to the file system", "supervisor_fs_error", "error", supervisor_fs_error_actions) %} +

      + The addon was unable to write to the file system on this machine. Normally this indicates there is a problem + with the Home Assistant supervisor, which is responsible for creating the file system this addon needs to function. Here are some reasons this might happen: +

      +
        +
      • You recently upgraded Home Assistant. Sometimes during an upgrade, the docker environment Home Assistant creates for the addon can get messed up. + If this is the problem, then restarting the host machine where Home assistant is running will usually resolve it.
      • +
      • Your machine may have run out of space. Home Assistant will sometimes make the file system read-only to protect itself if it is close to running out of space, causing this error. You + should be able to see an estimate of how much space is remaining down below on this page or by going to "Supervisor" > "System" in the Home Assistant UI.
      • +
      • If none of the above helps and the problem persists, file an issue on GitHub to let the developer of this addon know about it. He might be able to help.
      • +
      +{% endcall %} + +{% set google_unexpected_actions %} + refreshtry syncing again +{% endset %} +{% call macros.errorMessage("Google drive Returned an Unexpected Response", "google_unexpected", "error", google_unexpected_actions) %} +

      + The addon was unable to communicate with Google Drive because it gave back an unreadable response. + Without being able to communicate with Google Drive, the addon will not be able to transfer backups to the cloud. + This usually resolves itself after the addon retries, but if you see this error stick around for a while you can try: +

      +
        +
      • Syncing again with the button below. The addon will also retry on its own periodically.
      • +
      • Restarting the addon, the supervisor, or the whole machine Home Assistant is running on.
      • +
      • If the problem persists, file an issue on GitHub to let the developer of this addon know about it. He might be able to help.
      • +
      +{% endcall %} + +{% set log_in_to_drive_actions %} + refreshtry syncing again +{% endset %} +{% call macros.errorMessage("Please log in to Google Drive Manually", "log_in_to_drive", "account_circle", log_in_to_drive_actions) %} +

      The addon couldn't backup your backups because it looks like it you've never logged in + to the Google Drive website before. Doing this at + least once is required for applications like this to use Google Drive. +

      +

      + To resolve this, you can: +

      +
        +
      • +

        Visit https://drive.google.com and log-in with + the account you used to connect this addon to Google Drive, then click "SYNC NOW" below to start syncing.

        +
      • +
      • +

        Reauthorize the addon with Google Drive using a different account.

        +
      • +
      +{% endcall %} + +{% set error_details_card_actions %} + closeClose +{% endset %} +{% call macros.errorMessage("Error details", "error_details_card", "error", error_details_card_actions) %} +

      +{% endcall %} diff --git a/hassio-google-drive-backup/backup/static/layouts/partials/footer.jinja2 b/hassio-google-drive-backup/backup/static/layouts/partials/footer.jinja2 new file mode 100644 index 0000000..3889e57 --- /dev/null +++ b/hassio-google-drive-backup/backup/static/layouts/partials/footer.jinja2 @@ -0,0 +1,40 @@ + + \ No newline at end of file diff --git a/hassio-google-drive-backup/backup/static/layouts/partials/icons.jinja2 b/hassio-google-drive-backup/backup/static/layouts/partials/icons.jinja2 new file mode 100644 index 0000000..52f81a4 --- /dev/null +++ b/hassio-google-drive-backup/backup/static/layouts/partials/icons.jinja2 @@ -0,0 +1,80 @@ +
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      \ No newline at end of file diff --git a/hassio-google-drive-backup/backup/static/layouts/partials/modals/about.jinja2 b/hassio-google-drive-backup/backup/static/layouts/partials/modals/about.jinja2 new file mode 100644 index 0000000..7a1f3d7 --- /dev/null +++ b/hassio-google-drive-backup/backup/static/layouts/partials/modals/about.jinja2 @@ -0,0 +1,76 @@ + diff --git a/hassio-google-drive-backup/backup/static/layouts/partials/modals/backup/delete.jinja2 b/hassio-google-drive-backup/backup/static/layouts/partials/modals/backup/delete.jinja2 new file mode 100644 index 0000000..36c96f0 --- /dev/null +++ b/hassio-google-drive-backup/backup/static/layouts/partials/modals/backup/delete.jinja2 @@ -0,0 +1,106 @@ + diff --git a/hassio-google-drive-backup/backup/static/layouts/partials/modals/backup/details.jinja2 b/hassio-google-drive-backup/backup/static/layouts/partials/modals/backup/details.jinja2 new file mode 100644 index 0000000..f66d349 --- /dev/null +++ b/hassio-google-drive-backup/backup/static/layouts/partials/modals/backup/details.jinja2 @@ -0,0 +1,610 @@ + diff --git a/hassio-google-drive-backup/backup/static/layouts/partials/modals/backup/new.jinja2 b/hassio-google-drive-backup/backup/static/layouts/partials/modals/backup/new.jinja2 new file mode 100644 index 0000000..583ec7f --- /dev/null +++ b/hassio-google-drive-backup/backup/static/layouts/partials/modals/backup/new.jinja2 @@ -0,0 +1,62 @@ + diff --git a/hassio-google-drive-backup/backup/static/layouts/partials/modals/backup/upload.jinja2 b/hassio-google-drive-backup/backup/static/layouts/partials/modals/backup/upload.jinja2 new file mode 100644 index 0000000..83da034 --- /dev/null +++ b/hassio-google-drive-backup/backup/static/layouts/partials/modals/backup/upload.jinja2 @@ -0,0 +1,16 @@ + diff --git a/hassio-google-drive-backup/backup/static/layouts/partials/modals/bug.jinja2 b/hassio-google-drive-backup/backup/static/layouts/partials/modals/bug.jinja2 new file mode 100644 index 0000000..bb623e5 --- /dev/null +++ b/hassio-google-drive-backup/backup/static/layouts/partials/modals/bug.jinja2 @@ -0,0 +1,51 @@ + diff --git a/hassio-google-drive-backup/backup/static/layouts/partials/modals/crypto_donate.jinja2 b/hassio-google-drive-backup/backup/static/layouts/partials/modals/crypto_donate.jinja2 new file mode 100644 index 0000000..4556619 --- /dev/null +++ b/hassio-google-drive-backup/backup/static/layouts/partials/modals/crypto_donate.jinja2 @@ -0,0 +1,138 @@ + + + + + + diff --git a/hassio-google-drive-backup/backup/static/layouts/partials/modals/debug.jinja2 b/hassio-google-drive-backup/backup/static/layouts/partials/modals/debug.jinja2 new file mode 100644 index 0000000..d8071bc --- /dev/null +++ b/hassio-google-drive-backup/backup/static/layouts/partials/modals/debug.jinja2 @@ -0,0 +1,26 @@ + diff --git a/hassio-google-drive-backup/backup/static/layouts/partials/modals/help.jinja2 b/hassio-google-drive-backup/backup/static/layouts/partials/modals/help.jinja2 new file mode 100644 index 0000000..9d45f72 --- /dev/null +++ b/hassio-google-drive-backup/backup/static/layouts/partials/modals/help.jinja2 @@ -0,0 +1,78 @@ + diff --git a/hassio-google-drive-backup/backup/static/layouts/partials/modals/settings.jinja2 b/hassio-google-drive-backup/backup/static/layouts/partials/modals/settings.jinja2 new file mode 100644 index 0000000..081473c --- /dev/null +++ b/hassio-google-drive-backup/backup/static/layouts/partials/modals/settings.jinja2 @@ -0,0 +1,632 @@ + diff --git a/hassio-google-drive-backup/backup/static/layouts/partials/navbar.jinja2 b/hassio-google-drive-backup/backup/static/layouts/partials/navbar.jinja2 new file mode 100644 index 0000000..7cace82 --- /dev/null +++ b/hassio-google-drive-backup/backup/static/layouts/partials/navbar.jinja2 @@ -0,0 +1,129 @@ +{% import "layouts/macros.jinja2" as macros %} + +{% if coordEnabled %} +{% include 'layouts/partials/modals/backup/delete.jinja2' %} +{% include 'layouts/partials/modals/backup/new.jinja2' %} +{% include 'layouts/partials/modals/backup/upload.jinja2' %} +{% include 'layouts/partials/modals/backup/details.jinja2' %} +{% endif %} + +{% include 'layouts/partials/modals/settings.jinja2' %} +{% include 'layouts/partials/modals/help.jinja2' %} +{% include 'layouts/partials/modals/bug.jinja2' %} +{% include 'layouts/partials/modals/about.jinja2' %} +{% include 'layouts/partials/modals/about.jinja2' %} +{% include 'layouts/partials/modals/crypto_donate.jinja2' %} + +{% if coordEnabled %} + +{% endif %} + + + + +{% macro actionMenu(id) %} + +{% endmacro %} + +{% call macros.header(version, navBarTitle) %} + + menu + + + +{% endcall %} + + + +{% if coordEnabled %} + +{% endif %} diff --git a/hassio-google-drive-backup/backup/static/logs.jinja2 b/hassio-google-drive-backup/backup/static/logs.jinja2 new file mode 100644 index 0000000..04fa142 --- /dev/null +++ b/hassio-google-drive-backup/backup/static/logs.jinja2 @@ -0,0 +1,171 @@ +{% extends "layouts/base.jinja2" %} + +{% block head %} + {{ super() }} + + +{% endblock %} + +{% block content %} +
      +
      +
      +
      +
      +
      + A Message from the Developer +

      Thanks for taking a look at the logs! I did my best to make this work well, but if you're + here then maybe you've run into a problem. I'm only human after all. You can file an + issue on the project GitHub issue + tracker. I take bugs pretty seriously and I want this add-on to work well for + you. You can download a copy of these logs to attach to an issue by clicking here.

      +
      +

      — Stephen <3

      +
      + +
      +
      +
      +
      +
      + file_downloadDownload logs + +
      +
      +
      +
      +
      +                Loading logs...
      +            
      +
      +
      +{% endblock %} diff --git a/hassio-google-drive-backup/backup/static/picker.jinja2 b/hassio-google-drive-backup/backup/static/picker.jinja2 new file mode 100644 index 0000000..c8cc174 --- /dev/null +++ b/hassio-google-drive-backup/backup/static/picker.jinja2 @@ -0,0 +1,178 @@ +{% import "layouts/macros.jinja2" as macros %} +{% extends "layouts/base-server.jinja2" %} + +{% block head %} + {{ super() }} + + + +{% endblock %} +{% block content %} + {% call macros.header(version) %}{% endcall %} +
      +
      +
      +
      +
      +
      +
      + Backup Folder Picker +

      Use the button below to log into your google account and select a folder from your Google Drive. +

        +
      • The button opens a pop-up that creates a session with Google's services and then redirects back here. If your browser has extensions that prevent that kind of thing (eg aggressive pop-up blockers, cross-site cookie restriction, Firefox containers, etc) then you might need to disable them to make the authorization work. This is just how Google does authorization.
      • +
      • If the folder you want to use doesn't exist yet, navigate to Google Drive and create it there first. The folder picker doesn't let you create new folders.
      • +
      • The folder you select is where the addon will store backups from now on. This should be what you want if you're on this page.
      • +
      • If you'd like to know why this page has to be hosted on an external domain, click here to learn more.
      • +
      +

      + +
      +
      + +
      +
      +
      +
      +
      +{% endblock %} + diff --git a/hassio-google-drive-backup/backup/static/privacy_policy.jinja2 b/hassio-google-drive-backup/backup/static/privacy_policy.jinja2 new file mode 100644 index 0000000..bc52dc3 --- /dev/null +++ b/hassio-google-drive-backup/backup/static/privacy_policy.jinja2 @@ -0,0 +1,240 @@ +{% extends "layouts/base.jinja2" %} + +{% block content %} +
      +
      +

      Privacy Policy

      + +

      Effective date: April 23, 2019

      + +

      + Home Assistant Google Drive Backup ("us", "we", or "our") operates the + https://habackup.io/ website (hereinafter referred to as the "Service"). +

      + +

      + This page informs you of our policies regarding the collection, use, and + disclosure of personal data when you use our Service and the choices you + have associated with that data. +

      + +

      + We use your data to provide and improve the Service. By using the Service, + you agree to the collection and use of information in accordance with this + policy. Unless otherwise defined in this Privacy Policy, the terms used in + this Privacy Policy have the same meanings as in our Terms and Conditions, + accessible from https://habackup.io/ +

      + +

      Definitions

      +
        +
      • +

        Service

        +

        + Service is the https://habackup.io/ website operated by Home Assistant + Google Drive Backup +

        +
      • +
      • +

        Personal Data

        +

        + Personal Data means data about a living individual who can be + identified from those data (or from those and other information either + in our possession or likely to come into our possession). +

        +
      • +
      • +

        Usage Data

        +

        + Usage Data is data collected automatically either generated by the use + of the Service or from the Service infrastructure itself (for example, + by authenticating the add-on with Google Drive). +

        +
      • +
      • +

        The Add-on

        +

        + The add-on is the component of this software that runs on your home + Assistant instance. It is responsible for creating new backups and + uploading them to your personal Google Drive account. +

        +
      • +
      + +

      Information Collection and Use

      +

      + We collect several different types of information for the purposes of + providing the Service to you. +

      + +

      Types of Data Collected

      + +

      Personal Data

      +

      + When using the service, you may connect your Google Drive account. When + doing so you may be required to provide your username, password, and other + authentication information to Google's authentication servers. Google + returns an opaque token to the service that can be used to access files + created in Google Drive. This token is passed back to the add-on and not + maintained by the Service, but it is briefly in the Service's possession. +

      + +

      Usage Data

      +

      + You may opt in to send error reports generated by the add-on to the + Service. This is the full text of error messages generated by the add-on + and version identifiers for the add-on +

      +

      + Traffic to and from Google Drive generated by the add-on is aggregated + among all users. Information about the total number of requests and + information identifying the source of the requests (such as your IP + Address) is maintained by Google and made available to the Service. +

      + +

      Use of Data

      + +

      + Home Assistant Google Drive Backup uses the collected data for various + purposes: +

      +
        +
      • To provide and maintain the Service
      • +
      • To provide customer care and support
      • +
      • + To provide analysis or valuable information so that we can improve the + Service +
      • +
      • To monitor the usage of the Service
      • +
      • To detect, prevent and address technical issues
      • +
      + +

      Transfer of Data

      +

      + Your information, including Personal Data, may be transferred to - and + maintained on - computers located outside of your state, province, country + or other governmental jurisdiction where the data protection laws may + differ than those from your jurisdiction. +

      +

      + If you are located outside United States and choose to provide information + to us, please note that we transfer the data, including Personal Data, to + United States and process it there. +

      +

      + Your consent to this Privacy Policy followed by your submission of such + information represents your agreement to that transfer. +

      +

      + Home Assistant Google Drive Backup will take all steps reasonably + necessary to ensure that your data is treated securely and in accordance + with this Privacy Policy and no transfer of your Personal Data will take + place to an organization or a country unless there are adequate controls + in place including the security of your data and other personal + information. +

      + +

      Disclosure of Data

      + +

      Legal Requirements

      +

      + Home Assistant Google Drive Backup may disclose your Personal Data in the + good faith belief that such action is necessary to: +

      +
        +
      • To comply with a legal obligation
      • +
      • + To protect and defend the rights or property of Home Assistant Google + Drive Backup +
      • +
      • + To prevent or investigate possible wrongdoing in connection with the + Service +
      • +
      • + To protect the personal safety of users of the Service or the public +
      • +
      • To protect against legal liability
      • +
      + +

      + As a European citizen, under GDPR, you have certain individual rights. + You can learn more about these guides in the + GDPR Guide. +

      + +

      Security of Data

      +

      + The security of your data is important to us but remember that no method + of transmission over the Internet or method of electronic storage is 100% + secure. While we strive to use commercially acceptable means to protect + your Personal Data, we cannot guarantee its absolute security. +

      + +

      Service Providers

      +

      + We may employ third-party companies and individuals to facilitate our + Service ("Service Providers"), to provide the Service on our behalf, to + perform Service-related services or to assist us in analyzing how our + Service is used. +

      +

      + These third parties have access to your Personal Data only to perform + these tasks on our behalf and are obligated not to disclose or use it for + any other purpose. +

      + +

      Links to Other Sites

      +

      + Our Service may contain links to other sites that are not operated by us. + If you click a third-party link, you will be directed to that third + party's site. We strongly advise you to review the Privacy Policy of every + site you visit. +

      +

      + We have no control over and assume no responsibility for the content, + privacy policies or practices of any third-party sites or services. +

      + +

      Children's Privacy

      +

      Our Service does not address anyone under the age of 18 ("Children").

      +

      + We do not knowingly collect personally identifiable information from + anyone under the age of 18. If you are a parent or guardian and you are + aware that your Child has provided us with Personal Data, please contact + us. If we become aware that we have collected Personal Data from children + without verification of parental consent, we take steps to remove that + information from our servers. +

      + +

      Changes to This Privacy Policy

      +

      + We may update our Privacy Policy from time to time. We will notify you of + any changes by posting the new Privacy Policy on this page. +

      +

      + We will let you know via email and/or a prominent notice on our Service, + prior to the change becoming effective and update the "effective date" at + the top of this Privacy Policy. +

      +

      + You are advised to review this Privacy Policy periodically for any + changes. Changes to this Privacy Policy are effective when they are posted + on this page. +

      + +

      Contact Us

      +

      + If you have any questions about this Privacy Policy, please contact us: +

      +
        +
      • By email: stephen@beechens.com
      • +
      +
      +
      +{% endblock %} diff --git a/hassio-google-drive-backup/backup/static/redirect.jinja2 b/hassio-google-drive-backup/backup/static/redirect.jinja2 new file mode 100644 index 0000000..830e5ab --- /dev/null +++ b/hassio-google-drive-backup/backup/static/redirect.jinja2 @@ -0,0 +1,49 @@ +{% extends "layouts/base.jinja2" %} + +{% block head %} + {{ super() }} + +{% endblock %} + +{% block navbar %} + +{% endblock %} + +{% block content %} +
      +
      +
      +
      +
      + Redirecting... +

      You should be redirected to {{ url }} in a moment. Click below to be redirected now. +

      +
      + go now +
      +
      +
      +
      +
      +
      +{% endblock %} \ No newline at end of file diff --git a/hassio-google-drive-backup/backup/static/server-index.jinja2 b/hassio-google-drive-backup/backup/static/server-index.jinja2 new file mode 100644 index 0000000..66eb989 --- /dev/null +++ b/hassio-google-drive-backup/backup/static/server-index.jinja2 @@ -0,0 +1,52 @@ +{% import "layouts/macros.jinja2" as macros %} +{% extends "layouts/base-server.jinja2" %} + +{% block head %} + {{ super() }} +{% endblock %} + +{% block content %} + {% call macros.header(version) %}{% endcall %} +
      +
      +
      +

      +

      Simple Backup for Home Assistant

      +
      +
      A simple way to upload Home Assistant backups to + Google Drive™. Keep your backups secured in Google's + highly redundant and super reliable infrastructure. Stay safe! You don't want a random hard drive failure to keep you from turning on your lights. +
      +
      +
      +
      +
        +
      • Super easy setup and install
      • +
      • UI targeted towards new users or anyone sick of writing YAML files
      • +
      • Privacy-centric design philosophy
      • +
      • Highly configurable
      • +
      • Thorough documentation
      • +
      +
      +
      +
      + +
      + Follow the instructions + here to get your own client ID and client secret from Google. This is a much more complicated and time + consuming approach, but doesn't require you to authenticate yourself with an external domain. +
      +
      +
      +

      +
      +
      +
      +{% endblock %} \ No newline at end of file diff --git a/hassio-google-drive-backup/backup/static/server-redirect.jinja2 b/hassio-google-drive-backup/backup/static/server-redirect.jinja2 new file mode 100644 index 0000000..9cec62f --- /dev/null +++ b/hassio-google-drive-backup/backup/static/server-redirect.jinja2 @@ -0,0 +1,45 @@ +{% import "layouts/macros.jinja2" as macros %} +{% extends "layouts/base-server.jinja2" %} + +{% block head %} + {{ super() }} + +{% endblock %} + +{% block content %} +{% call macros.header(version) %}{% endcall %} +
      +
      +
      +

      +
      That should be it.
      +
      +
      + Redirecting you back to your Home Assistant server at {{ redirect_text }} +
      +
      +

      +
      +
      +
      +{% endblock %} \ No newline at end of file diff --git a/hassio-google-drive-backup/backup/static/terms_of_service.jinja2 b/hassio-google-drive-backup/backup/static/terms_of_service.jinja2 new file mode 100644 index 0000000..f3da534 --- /dev/null +++ b/hassio-google-drive-backup/backup/static/terms_of_service.jinja2 @@ -0,0 +1,86 @@ +{% extends "layouts/base.jinja2" %} + +{% block content %} +
      +
      +

      Terms and Conditions

      + +

      Last updated: April 23, 2019

      + +

      + Please read these Terms and Conditions ("Terms", "Terms and Conditions") + carefully before using the https://habackup.io/ website (the "Service") + operated by Home Assistant Google Drive Backup ("us", "we", or "our"). +

      + +

      + Your access to and use of the Service is conditioned on your acceptance of + and compliance with these Terms. These Terms apply to all visitors, users + and others who access or use the Service. +

      + +

      + By accessing or using the Service you agree to be bound by these Terms. If + you disagree with any part of the terms then you may not access the + Service. +

      + +

      Links To Other Web Sites

      + +

      + Our Service may contain links to third-party web sites or services that + are not owned or controlled by Home Assistant Google Drive Backup. +

      + +

      + Home Assistant Google Drive Backup has no control over, and assumes no + responsibility for, the content, privacy policies, or practices of any + third party web sites or services. You further acknowledge and agree that + Home Assistant Google Drive Backup shall not be responsible or liable, + directly or indirectly, for any damage or loss caused or alleged to be + caused by or in connection with use of or reliance on any such content, + goods or services available on or through any such web sites or services. +

      + +

      + We strongly advise you to read the terms and conditions and privacy + policies of any third-party web sites or services that you visit. +

      + +

      Governing Law

      + +

      + These Terms shall be governed and construed in accordance with the laws of + Colorado, United States, without regard to its conflict of law provisions. +

      + +

      + Our failure to enforce any right or provision of these Terms will not be + considered a waiver of those rights. If any provision of these Terms is + held to be invalid or unenforceable by a court, the remaining provisions + of these Terms will remain in effect. These Terms constitute the entire + agreement between us regarding our Service, and supersede and replace any + prior agreements we might have between us regarding the Service. +

      + +

      Changes

      + +

      + We reserve the right, at our sole discretion, to modify or replace these + Terms at any time. If a revision is material we will try to provide at + least 30 days notice prior to any new terms taking effect. What + constitutes a material change will be determined at our sole discretion. +

      + +

      + By continuing to access or use our Service after those revisions become + effective, you agree to be bound by the revised terms. If you do not agree + to the new terms, please stop using the Service. +

      + +

      Contact Us

      + +

      If you have any questions about these Terms, please contact us.

      +
      +
      +{% endblock %} diff --git a/hassio-google-drive-backup/backup/static/working.jinja2 b/hassio-google-drive-backup/backup/static/working.jinja2 new file mode 100644 index 0000000..eb1515f --- /dev/null +++ b/hassio-google-drive-backup/backup/static/working.jinja2 @@ -0,0 +1,332 @@ +{% extends "layouts/base.jinja2" %} + +{% block head %} +{% include 'layouts/partials/icons.jinja2' %} +{{ super() }} + +{% endblock %} + +{% block content %} + + + + + +
      +
      +
      +
      +
      Backup Statistics
      +
      +
      +
      +
      +

      + timer + + Last Backup + +

      +

      + update + + Next Backup in + +

      +
      +
      + +
      + {% include 'layouts/partials/error-messages.jinja2' %} + +
      +
      + + info_outline + Share Error Reports? + +

      If you select yes, the text of unexpected exceptions this add-on produces will be sent to a server + maintained by the developer. This is completely optional, but helps give him a heads up for problems + with new releases and aids in providing better help messages when things go wrong. If you're unsure, + just select "No". You can always change this in the settings menu up on the top right of the page.

      +
      + +
      + +
      +
      + + info_outline + 'Snapshots' have been renamed to 'Backups' + +

      In August 2021 the Home Assistant + team announced that 'snapshots' will be called 'backups' moving forward. This addon exposes a + binary sensor to indicate if snapshots are stale and another sensor that publishes details about backups. Both of the sensors + used 'snapshot' in their names and values, so they had to be changed to match the new language. In the spirit of "not breaking + your stuff" the addon will only start using the new names and values if you tell it to, just in case you have automations or UI + using the old sensor names/values. +

        +
      • If you don't have any automations based on these sensors, you can just click 'Use the new names and values'.
      • +
      • If you do, you can either dismiss this message or update your automations/UI to use the new names and values.
      • +
      • You can look at this document to see exactly how the sensors have changed.
      • +
      • You can change this later in the settingsadd-on + settings if you choose. Look for it under "Uncommon Options"
      • +
      +

      +
      + +
      + +
      +
      + + info_outline + Ignoring upgrade backups is recommended + +

      With your current settings, this addon assumes any backup it finds should be uploaded to Google Drive. This can create undesirable behavior when you upgrade + Home Assistant or an addon and an automatic partial backup is created by Home Assistant for the upgrade (this is Home Assistant's default behavior). +

      +
      + For Example + If you save 4 full backups in Google Drive and then upgrade 4 add-ons, the 4 partial backups Home Assistant automatically creates would overwrite ALL of the full backups this add-on made previously leaving you with no + full backups to restore from until the next one is made 24 hours or more later. Most people don't want that to happen, as it leaves a possible gap in your backup strategy. +
      +
      +

      +

      + To prevent this most people enable the "Ignore upgrade backups" setting in this addon, which will cause it to completely ignore these partial upgrade backups. + Moving forward this will be enabled by default for new installations, and it is recommended for everyone. Because you're upgrading from an + older version of the addon that had different behavior, you have to choose. You can also enable some other features in the settingsadd-on settings that might be useful for this, such as: +

        +
      • The setting 'Delete ignored backups after: X' can be used to automatically delete upgrade backups after some time period, which helps reclaim the space they might use.
      • +
      • The 'Ignore Other Backups' setting makes the addon ignore any backup it didn't create, including 'upgrade' backups.
      • +
      + If none of this makes any sense to you, just click "Start ignoring upgrade backups" below to make this notification go away. +

      +
      + +
      + +
      +
      + + info_outline + Your credentials with Google may expire + +

      Google recently announced changes they're making + to how their OAuth credentials are generated. You previously followed + these instructions to set up credentials + the add-on can use to upload to Google Drive. If you did that before March 16 2022, Google is going to make those credentials invalid in October of 2022. + If you are affected this means that in October 2022 the add-on will be unable to authorize with Google Drive and your backups will not get uploaded. +

      +

      + While annoying, this change is also somewhat expected. Using Google Cloud credentials for a local installation like this is not something Google 'intended' for us to do, it isn't an officially supported way of using their services. We can still make do though. +

      +

      + If you are affected, you likely received an email from Google titled 'Migrate your OAuth out-of-band flow'. To make your credentials work, please follow the steps linked here to + make your credentials and reauthorize the addon. + You don't need to follow all the steps exactly; you just need to modify what you have to match up with the instructions. In particular please ensure that: +

        +
      • You can skip steps 0, 1, and 2 because you already did them
      • +
      • In step 3, ensure your "OAuth Consent Screen" has a Publishing status of "In production". You don't need to create a new consent screen.
      • +
      • In step 4, ensure your credentials use application type "TVs and Limited Input Devices". If not, you will need to create new credentials, which will create a new client ID and client secret you can use to reauthorize the addon.
      • +
      + + Once you do, please
      reauthorize the add-on with Google Drive. You can also wait until your credentials stop working and regenerate them then. There is no rush. +

      +
      + +
      + +
      +
      + Use Ingress instead? +

      The latest version of this add-on supports ingress. Ingress allows + this web interface to be embedded within Home Assistant, which is usually more secure because it + delegates authentication to the same mechanisms you've configured in Home Assistant (e.g. SSL and + username/password). Currently this interface is being served on an additional port outside of + Home Assistant which is less secure.

      +
      +

      Select YES to only use ingress to access this interface. This is recommended, as ingress is + almost always more secure. + In the future you can access this interface by following the "WEB UI" link from the add-on + installation page or embedding it in + your Home Assistant sidebar from the same place.

      +
      +

      Select NO to continue exposing a web interface on an open port with the SSL and authentication + settings you choose. The port, which defaults to 1627, can be changed from the add-on page in Home + Assistant.

      +
      +

      You can change this option any time from the settings menu. No matter which you choose, the interface + will be available through ingress.

      +
      +
      + +
      +
      +
      + Restore a Backup +

      Backups must be restored through the supervisor Backups page. You can restore a backup by + navigating to + "Settings" > "System" > "Backups" from the Home Assistant Web-UI, + or by clicking the button below to navigate there by a direct link. Depending on how you access the Home + Assistant Web-UI the direct link may not work, so you'll just have to navigate there.

      +
      + +
      +
      +
      + + info_outline + Double check your ignored backups + +

      Because this addon gets a limited view of the world, it might not be able to accurately determine if all of your older backups were created by the addon. + Its recommended that you check below to ensure that all of the backups the addon is ignoring now are the ones you intend to have ignored. + If not, you can click on an ignored backups to see its details and click "Don't ignore" to have the addon include them in future backup, clean-up, etc.

      +
      + +
      +
      +
      + + timer + Taking a backup soon + +

      Since the addon just started up a few minutes ago, it's going to wait a few minutes before taking a backup to make sure that's what you want. Look below to see exactly how long before the next backup. + You can wait for that to happen, change your settingsadd-on + settings to something different, or just click the button below to let the automatic backup happen immediately.

      +
      + +
      +
      +
      +
      +
      + info_outline +
      +
      +

      You don't have any backups in Google Drive or Home Assistant yet.

      +
      +
      +
      +
      +
      +
      +
      +
      + refresh +
      +
      +

      Waiting for backups to load. If you don't see this go away soon, make sure the add-on is still + running. Info on this page may be stale until then.

      +
      +
      +
      +
      +
      + + +
      +
      +
      +
      +{% endblock %} diff --git a/hassio-google-drive-backup/backup/time.py b/hassio-google-drive-backup/backup/time.py new file mode 100644 index 0000000..7d2ee53 --- /dev/null +++ b/hassio-google-drive-backup/backup/time.py @@ -0,0 +1,175 @@ +import asyncio +from datetime import datetime, timedelta + + +from injector import inject, singleton +from dateutil.relativedelta import relativedelta +from dateutil.parser import parse +from .logger import getLogger +import pytz +import os +from pytz import timezone, utc +from tzlocal import get_localzone_name, get_localzone +from dateutil.tz import tzlocal + + +logger = getLogger(__name__) + + +def get_local_tz(): + methods = [ + _infer_timezone_from_env, + _infer_timezone_from_system, + _infer_timezone_from_name, + _infer_timezone_from_offset + ] + for method in methods: + try: + tz = method() + if tz is not None: + return tz + except Exception: + pass + return utc + + +def _infer_timezone_from_offset(): + now = datetime.now() + desired_offset = tzlocal().utcoffset(now) + for tz_name in pytz.all_timezones: + tz = pytz.timezone(tz_name) + if desired_offset == tz.utcoffset(now): + return tz + return None + + +def _infer_timezone_from_name(): + name = get_localzone_name() + if name is not None: + return timezone(name) + return None + + +def _infer_timezone_from_system(): + tz = get_localzone() + if tz is not None: + return tz + return None + + +def _infer_timezone_from_env(): + if "TZ" in os.environ: + tz = timezone(os.environ["TZ"]) + if tz is not None: + return tz + return None + +@singleton +class Time(object): + @inject + def __init__(self, local_tz=get_local_tz()): + self.local_tz = local_tz + self._offset = timedelta(seconds=0) + + def now(self) -> datetime: + return datetime.now(pytz.utc) + self._offset + + def nowLocal(self) -> datetime: + return datetime.now(self.local_tz) + self._offset + + @property + def offset(self): + return self._offset + + @offset.setter + def offset(self, delta: timedelta): + self._offset = delta + + @classmethod + def parse(cls, text: str) -> datetime: + ret = parse(text) + if ret.tzinfo is None: + ret = ret.replace(tzinfo=utc) + return ret + + def toLocal(self, dt: datetime) -> datetime: + return dt.astimezone(self.local_tz) + + def localize(self, dt: datetime) -> datetime: + return self.local_tz.localize(dt) + + def toUtc(self, dt: datetime) -> datetime: + return dt.astimezone(utc) + + async def sleepAsync(self, seconds: float, early_exit: asyncio.Event = None) -> None: + if early_exit is None: + await asyncio.sleep(seconds) + else: + try: + await asyncio.wait_for(early_exit.wait(), seconds) + except asyncio.TimeoutError: + pass + + def local(self, year, month, day, hour=0, minute=0, second=0, ms=0): + return self.local_tz.localize(datetime(year, month, day, hour, minute, second, ms)) + + def formatDelta(self, time: datetime, now=None) -> str: + if not now: + now = self.now() + + delta: relativedelta = None + flavor = "" + if time < now: + delta = relativedelta(now, time) + flavor = " ago" + else: + delta = relativedelta(time, now) + flavor = "" + if delta.years > 0: + plural = "s" if delta.years > 1 else "" + return "{0} year{2}{1}".format(delta.years, flavor, plural) + if (delta.months != 0): + plural = "s" if delta.months > 1 else "" + if delta.days > 15: + return "{0} month{2}{1}".format(delta.months + 1, flavor, plural) + return "{0} month{2}{1}".format(delta.months, flavor, plural) + if (delta.days != 0): + plural = "s" if delta.days > 1 else "" + if delta.hours >= 12: + return "{0} day{2}{1}".format(delta.days + 1, flavor, plural) + return "{0} day{2}{1}".format(delta.days, flavor, plural) + if (delta.hours != 0): + plural = "s" if delta.hours > 1 else "" + if delta.minutes >= 30: + return "{0} hour{2}{1}".format(delta.hours + 1, flavor, plural) + return "{0} hour{2}{1}".format(delta.hours, flavor, plural) + if (delta.minutes != 0): + plural = "s" if delta.minutes > 1 else "" + if delta.minutes >= 30: + return "{0} minute{2}{1}".format(delta.minutes + 1, flavor, plural) + return "{0} minute{2}{1}".format(delta.minutes, flavor, plural) + if (delta.seconds != 0): + plural = "s" if delta.seconds > 1 else "" + return "{0} second{2}{1}".format(delta.seconds, flavor, plural) + return "right now" + + def asRfc3339String(self, time: datetime) -> str: + if time is None: + time = self.now() + return time.strftime("%Y-%m-%dT%H:%M:%SZ") + + +class AcceleratedTime(Time): + def __init__(self, dialation=1.0): + super().__init__() + self.start = datetime.now(utc) + self.dialation = dialation + + def now(self): + return self.start + timedelta(seconds=(datetime.now(utc) - self.start).total_seconds() * self.dialation) + + def nowLocal(self) -> datetime: + return self.localize(self.now()) + + async def sleepAsync(self, seconds: float) -> None: + await asyncio.sleep(seconds / self.dialation) diff --git a/hassio-google-drive-backup/backup/tracing_session.py b/hassio-google-drive-backup/backup/tracing_session.py new file mode 100644 index 0000000..1c2dd56 --- /dev/null +++ b/hassio-google-drive-backup/backup/tracing_session.py @@ -0,0 +1,72 @@ + +import aiohttp.client as aiohttp +import aiohttp.tracing as tracing +from backup.config import Config, Setting +from .logger import getLogger + +logger = getLogger(__name__) + + +class TracingSession(aiohttp.ClientSession): + def __init__(self, config: Config, **kwargs): + self.config: Config = config + self.trace_config = aiohttp.TraceConfig() + if self.config.get(Setting.TRACE_REQUESTS): + self.trace_config.on_request_start.append(self.trace_request_start) + self.trace_config.on_request_end.append(self.trace_request_end) + self.trace_config.on_request_exception.append(self.trace_request_exception) + self.trace_config.on_response_chunk_received.append(self.trace_chunk_recv) + self.trace_config.on_request_chunk_sent.append(self.trace_chunk_sent) + self._record = False + self._records = [] + super().__init__(**kwargs, trace_configs=[self.trace_config]) + + @property + def record(self) -> bool: + return self._record + + @record.setter + def record(self, value: bool): + self._record = value + + def clearRecord(self): + self._records = [] + + async def _request(self, method, url, **kwargs): + if self.record: + self._records.append({'method': method, 'url': url, 'other_args': kwargs}) + if not self.config.get(Setting.TRACE_REQUESTS): + return await super()._request(method, url, **kwargs, trace_request_ctx="{0} {1}".format(method, url)) + resp = await super()._request(method, url, **kwargs, trace_request_ctx="{0} {1}".format(method, url)) + logger.trace("Initial response data: %s %s", resp.method, resp.url) + logger.trace(" headers:") + for header in resp.headers: + logger.trace(" %s: %s", header, resp.headers[header]) + return resp + + def print_headers(self, headers): + for header in headers: + logger.trace(" %s: %s", header, 'redacted' if ('Authorization' in header) else headers[header]) + + async def trace_request_start(self, session, trace_config_ctx, params: tracing.TraceRequestStartParams): + logger.trace("Request started: %s", trace_config_ctx.trace_request_ctx) + logger.trace(" sent headers:") + self.print_headers(params.headers) + + async def trace_request_exception(self, session, trace_config_ctx, params: tracing.TraceRequestExceptionParams): + logger.trace("Request exception: %s", trace_config_ctx.trace_request_ctx) + logger.trace(logger.formatException(params.exception)) + + async def trace_request_end(self, session, trace_config_ctx, params: tracing.TraceRequestEndParams): + logger.trace("Request ending: %s", trace_config_ctx.trace_request_ctx) + logger.trace(" status: %s", params.response.status) + logger.trace(" recieved headers:") + self.print_headers(params.response.headers) + + async def trace_chunk_recv(self, session, trace_config_ctx, params: tracing.TraceResponseChunkReceivedParams): + logger.trace("Recieved chunk: %s", trace_config_ctx.trace_request_ctx) + logger.trace(" length: %s", len(params.chunk)) + + async def trace_chunk_sent(self, session, trace_config_ctx, params: tracing.TraceRequestChunkSentParams): + logger.trace("Sent chunk: %s", trace_config_ctx.trace_request_ctx) + logger.trace(" length: %s", len(params.chunk)) diff --git a/hassio-google-drive-backup/backup/ui/__init__.py b/hassio-google-drive-backup/backup/ui/__init__.py new file mode 100644 index 0000000..ade725c --- /dev/null +++ b/hassio-google-drive-backup/backup/ui/__init__.py @@ -0,0 +1,3 @@ +# flake8: noqa +from .uiserver import UiServer +from .restarter import Restarter diff --git a/hassio-google-drive-backup/backup/ui/debug.py b/hassio-google-drive-backup/backup/ui/debug.py new file mode 100644 index 0000000..f98f661 --- /dev/null +++ b/hassio-google-drive-backup/backup/ui/debug.py @@ -0,0 +1,75 @@ +import asyncio +from aiohttp import web +from aiohttp.web import Request +from injector import inject, singleton +from backup.time import Time +from backup.model import Model, Coordinator +from backup.logger import getLogger +from datetime import timedelta + +logger = getLogger(__name__) + + +@singleton +class Debug(): + @inject + def __init__(self, model: Model, coord: Coordinator, time: Time): + self._model = model + self._coord = coord + self._time = time + + async def getTasks(self, request): + resp = [] + for task in asyncio.all_tasks(): + data = { + "name": task.get_name(), + "state": str(task._state), + "coroutine": str(task.get_coro()) + } + + # Get exception + try: + ex = task.exception() + if ex is None: + data['exception'] = "None" + else: + data['exception'] = logger.formatException(ex) + except asyncio.CancelledError: + data['exception'] = "CancelledError" + except asyncio.InvalidStateError: + data['exception'] = "Unfinished" + except Exception: + pass + + # Get result + try: + ret = task.result() + if ex is None: + data['result'] = "None" + else: + data['result'] = str(ret) + except asyncio.CancelledError: + data['result'] = "CancelledError" + except asyncio.InvalidStateError: + data['result'] = "Unfinished" + except Exception: + pass + data['stack'] = [] + for frame in task.get_stack(): + data['stack'].append(str(frame)) + resp.append(data) + return web.json_response(resp) + + async def simerror(self, request: Request): + error = request.query.get("error", "") + if len(error) == 0: + self._model.simulate_error = None + else: + self._model.simulate_error = error + self._coord.trigger() + return web.json_response({}) + + async def timeoffset(self, request: Request): + delta = int(request.query.get("offset", "")) + self._time.offset = timedelta(seconds=delta) + return web.json_response({}) diff --git a/hassio-google-drive-backup/backup/ui/restarter.py b/hassio-google-drive-backup/backup/ui/restarter.py new file mode 100644 index 0000000..9e614cb --- /dev/null +++ b/hassio-google-drive-backup/backup/ui/restarter.py @@ -0,0 +1,40 @@ +from .uiserver import UiServer +from ..config import Config, Startable +from asyncio import create_task, Event +from injector import inject, singleton +from ..logger import getLogger + +logger = getLogger(__name__) + + +@singleton +class Restarter(Startable): + @inject + def __init__(self, server: UiServer, config: Config): + self._server = server + self._config = config + self._old_options = config.getServerOptions() + self._restarted = Event() + + async def start(self): + self._config.subscribe(self.trigger) + + async def check(self): + if self._old_options == self._config.getServerOptions(): + # no restart is necessary because the server didn't change + return + self._old_options = self._config.getServerOptions() + try: + # Restart the server + logger.info("Restarting Web-UI server") + await self._server.run() + self._restarted.set() + except Exception as e: + logger.error("Problem while restarting the Web-UI server " + logger.formatException(e)) + + def trigger(self): + create_task(self.check(), name="Web-UI Restarter") + + async def waitForRestart(self): + await self._restarted.wait() + self._restarted.clear() diff --git a/hassio-google-drive-backup/backup/ui/uiserver.py b/hassio-google-drive-backup/backup/ui/uiserver.py new file mode 100644 index 0000000..617efc6 --- /dev/null +++ b/hassio-google-drive-backup/backup/ui/uiserver.py @@ -0,0 +1,908 @@ +import asyncio +import ssl +import json +import aiohttp_jinja2 +import jinja2 +import base64 +from datetime import timedelta +from os.path import abspath, join +from typing import Any, Dict + +from aiohttp import BasicAuth, hdrs, web, ClientSession, ClientResponseError +from aiohttp.web import HTTPException, Request, HTTPSeeOther, HTTPNotFound +from injector import ClassAssistedBuilder, ProviderOf, inject, singleton + +from backup.config import Config, Setting, CreateOptions, BoolValidator, Startable, Version, VERSION +from backup.const import SOURCE_GOOGLE_DRIVE, SOURCE_HA, GITHUB_BUG_TEMPLATE +from backup.model import Coordinator, Backup, AbstractBackup +from backup.exceptions import KnownError, GoogleCredGenerateError, ensureKey +from backup.util import GlobalInfo, Estimator, DataCache, UpgradeFlags +from backup.file import File +from backup.ha import HaSource, PendingBackup, BACKUP_NAME_KEYS, HaRequests, HaUpdater +from backup.ha import Password +from backup.time import Time +from backup.worker import Trigger +from backup.logger import getLogger, getHistory, TraceLogger +from backup.creds import Exchanger, Creds +from backup.debugworker import DebugWorker +from backup.drive import FolderFinder, AuthCodeQuery +from backup.const import FOLDERS +from .debug import Debug +from yarl import URL + +logger = getLogger(__name__) + +# Used to Google's oauth verification +SCOPE: str = 'https://www.googleapis.com/auth/drive.file' + +MIME_TEXT_HTML = "text/html" +MIME_JSON = "application/json" +VERSION_CREATION_TRACKING = Version(0, 104, 0) + + +@singleton +class UiServer(Trigger, Startable): + @inject + def __init__(self, debug: Debug, coord: Coordinator, ha_source: HaSource, harequests: HaRequests, + time: Time, config: Config, global_info: GlobalInfo, estimator: Estimator, + session: ClientSession, exchanger_builder: ClassAssistedBuilder[Exchanger], + debug_worker: DebugWorker, folder_finder: FolderFinder, data_cache: DataCache, + haupdater: HaUpdater, custom_auth_provider: ProviderOf[AuthCodeQuery]): + super().__init__() + # Currently running server tasks + self.runners = [] + self.exchanger_builder = exchanger_builder + self._coord = coord + self._time = time + self.custom_auth_provider = custom_auth_provider + self.manual_exchanger: Exchanger = None + self.config: Config = config + self.auth_cache: Dict[str, Any] = {} + self.last_log_index = 0 + self.host_server = None + self.ingress_server = None + self.running = False + self._harequests = harequests + self._global_info = global_info + self._ha_source = ha_source + self._starts = 0 + self._estimator = estimator + self._debug = debug + self.session = session + self.debug_worker = debug_worker + self.folder_finder = folder_finder + self.ignore_other_turned_on = False + self._data_cache = data_cache + self._haupdater = haupdater + self._check_creds_loop: asyncio.Task = None + self._check_creds_error: Exception = None + self._device_code_authorizer: AuthCodeQuery = None + self._upload_event = asyncio.Event() + + def name(self): + return "UI Server" + + def base_context(self): + return { + 'version': VERSION, + 'backgroundColor': self.config.get(Setting.BACKGROUND_COLOR), + 'accentColor': self.config.get(Setting.ACCENT_COLOR), + 'coordEnabled': self._coord.enabled(), + 'save_drive_creds_path': self.config.get(Setting.SAVE_DRIVE_CREDS_PATH), + 'bmc_logo_path': "static/" + VERSION + "/images/bmc.svg" + } + + async def getstatus(self, request) -> Dict[Any, Any]: + return web.json_response(await self.buildStatusInfo()) + + async def buildStatusInfo(self): + status: Dict[Any, Any] = {} + status['folder_id'] = self.folder_finder.getCachedFolder() + status['backups'] = [] + backups = self._coord.backups() + for backup in backups: + status['backups'].append(self.getBackupDetails(backup)) + status['ha_url_base'] = self._ha_source.getHomeAssistantUrl() + status['restore_backup_path'] = "hassio/backups" + status['ask_error_reports'] = not self.config.isExplicit( + Setting.SEND_ERROR_REPORTS) + status['warn_ingress_upgrade'] = False + status['cred_version'] = self._global_info.credVersion + next = self._coord.nextBackupTime() + if next is None: + status['next_backup_text'] = "Disabled" + status['next_backup_machine'] = "" + status['next_backup_detail'] = "Disabled" + elif (next < self._time.now()): + status['next_backup_text'] = self._time.formatDelta( + self._time.now()) + status['next_backup_machine'] = self._time.asRfc3339String( + self._time.now()) + status['next_backup_detail'] = self._time.toLocal( + self._time.now()).strftime("%c") + else: + status['next_backup_text'] = self._time.formatDelta(next) + status['next_backup_machine'] = self._time.asRfc3339String(next) + status['next_backup_detail'] = self._time.toLocal( + next).strftime("%c") + not_ignored = list(filter(lambda s: not s.ignore(), self._coord.backups())) + if len(not_ignored) > 0: + latest = not_ignored[len(not_ignored) - 1].date() + status['last_backup_text'] = self._time.formatDelta(latest) + status['last_backup_machine'] = self._time.asRfc3339String( + latest) + status['last_backup_detail'] = self._time.toLocal( + latest).strftime("%c") + else: + status['last_backup_text'] = "Never" + status['last_backup_machine'] = "" + status['last_backup_detail'] = "Never" + + status['last_error'] = None + if self._global_info._last_error is not None and self._global_info.isErrorSuppressed(): + status['last_error'] = self.processError( + self._global_info._last_error) + status["last_error_count"] = self._global_info.failureCount() + status["ignore_errors_for_now"] = self._global_info.ignoreErrorsForNow() + status["syncing"] = self._coord.isSyncing() + status["ignore_sync_error"] = self._coord.isWorkingThroughUpload() + status["firstSync"] = self._global_info._first_sync + status["backup_name_template"] = self.config.get( + Setting.BACKUP_NAME) + status['sources'] = self._coord.buildBackupMetrics() + status['authenticate_url'] = str(URL(self.config.get(Setting.AUTHORIZATION_HOST)).with_path("/drive/authorize")) + choose_url = str(URL(self.config.get(Setting.AUTHORIZATION_HOST)).with_path('/drive/picker').with_query({ + "bg": self.config.get(Setting.BACKGROUND_COLOR), + "ac": self.config.get(Setting.ACCENT_COLOR), + "version": VERSION + })) + status['choose_folder_url'] = str(choose_url) + status['dns_info'] = self._global_info.getDnsInfo() + status['enable_drive_upload'] = self.config.get( + Setting.ENABLE_DRIVE_UPLOAD) + status['is_custom_creds'] = self._coord._model.dest.isCustomCreds() + status['is_specify_folder'] = self.config.get( + Setting.SPECIFY_BACKUP_FOLDER) + status['backup_cooldown_active'] = self._coord.isWaitingForStartup() + name_keys = {} + for key in BACKUP_NAME_KEYS: + name_keys[key] = BACKUP_NAME_KEYS[key]( + "Full", self._time.now(), self._ha_source.getHostInfo()) + status['backup_name_keys'] = name_keys + + # Indicate the user should be notified for a specific situation where: + # - They recently turned on "IGNORE_OTHER_BACKUPS" + # - They have ignored backups created before upgrading to v0.104.0 or higher. + upgrade_date = self._data_cache.getUpgradeTime(VERSION_CREATION_TRACKING) + ignored = len(list(filter(lambda s: s.date() < upgrade_date, filter(Backup.ignore, self._coord.backups())))) + status["notify_check_ignored"] = ignored > 0 and self.ignore_other_turned_on + status["warn_backup_upgrade"] = self.config.get(Setting.CALL_BACKUP_SNAPSHOT) and not self._data_cache.checkFlag(UpgradeFlags.NOTIFIED_ABOUT_BACKUP_RENAME) + status["warn_upgrade_backups"] = self._data_cache.notifyForIgnoreUpgrades + status["warn_oob_oauth"] = not self._data_cache.checkFlag(UpgradeFlags.NOTIFIED_ABOUT_OOB_FLOW) and self._coord._model.dest.might_be_oob_creds + return status + + async def bootstrap(self, request) -> Dict[Any, Any]: + return web.Response(body="bootstrap_update_data = {0};".format(json.dumps(await self.buildStatusInfo(), indent=4)), content_type="text/javascript") + + def getBackupDetails(self, backup: Backup): + ha = backup.getSource(SOURCE_HA) + sources = [] + for source_key in backup.sources: + source: AbstractBackup = backup.sources[source_key] + sources.append({ + 'name': source.name(), + 'key': source_key, + 'size': source.size(), + 'retained': source.retained(), + 'delete_next': backup.getPurges().get(source_key) or False, + 'slug': backup.slug(), + 'ignored': source.ignore(), + }) + + return { + 'name': backup.name(), + 'slug': backup.slug(), + 'size': backup.sizeString(), + 'status': backup.status(), + 'date': self._time.toLocal(backup.date()).strftime("%c"), + 'createdAt': self._time.formatDelta(backup.date()), + 'isPending': ha is not None and type(ha) is PendingBackup, + 'protected': backup.protected(), + 'type': backup.backupType(), + 'folders': backup.details().get("folders", []), + 'addons': self.formatAddons(backup.details()), + 'sources': sources, + 'haVersion': False if backup.version() == None else backup.version(), + 'uploadable': backup.getSource(SOURCE_HA) is None and len(backup.sources) > 0, + 'restorable': backup.getSource(SOURCE_HA) is not None, + 'status_detail': backup.getStatusDetail(), + 'upload_info': backup.getUploadInfo(self._time), + 'ignored': backup.ignore(), + 'timestamp': backup.date().timestamp(), + 'note': backup.note() + } + + def formatAddons(self, backup_data): + addons = [] + for addon in backup_data.get("addons", []): + addons.append({ + 'name': addon.get('name', "Unknown"), + 'slug': addon.get("slug", "unknown"), + 'version': addon.get("version", ""), + # The supervisor stores backup size in MB + 'size': self._estimator.asSizeString(float(addon.get("size", 0)) * 1024 * 1024), + }) + return addons + + async def manualCredCheckLoop(self, auth: AuthCodeQuery): + try: + creds = await auth.waitForPermission() + self._coord.saveCreds(creds) + self._data_cache.addFlag(UpgradeFlags.NOTIFIED_ABOUT_OOB_FLOW) + except asyncio.CancelledError: + # Cancelled, thats fine + pass + except Exception as e: + self._check_creds_error = e + + async def checkManualAuth(self, request: Request): + if self._check_creds_error is not None: + raise self._check_creds_error + elif self._device_code_authorizer is not None: + return web.json_response({ + 'message': "Waiting for you to authorize the add-on.", + 'auth_url': self._device_code_authorizer.verification_url, + 'code': self._device_code_authorizer.user_code, + 'expires': self._time.formatDelta(self._device_code_authorizer.expiration) + }) + else: + return web.json_response({ + 'message': "No request for authorization is in progress." + }) + + async def manualauth(self, request: Request) -> None: + client_id = request.query.get("client_id", "") + client_secret = request.query.get("client_secret", "") + if client_id == "" or client_secret == "": + raise GoogleCredGenerateError("Invalid information provided") + + if self._check_creds_loop is not None and not self._check_creds_loop.done(): + self._check_creds_loop.cancel() + await self._check_creds_loop + self._device_code_authorizer = self.custom_auth_provider.get() + await self._device_code_authorizer.requestCredentials(client_id, client_secret) + self._check_creds_error = None + self._check_creds_loop = asyncio.create_task(self.manualCredCheckLoop(self._device_code_authorizer)) + return web.json_response({ + 'auth_url': self._device_code_authorizer.verification_url, + 'code': self._device_code_authorizer.user_code, + 'expires': self._time.formatDelta(self._device_code_authorizer.expiration), + }) + + async def backup(self, request: Request) -> Any: + custom_name = request.query.get("custom_name", None) + retain_drive = BoolValidator.strToBool( + request.query.get("retain_drive", False)) + retain_ha = BoolValidator.strToBool( + request.query.get("retain_ha", False)) + note = request.query.get("note", None) + options = CreateOptions(self._time.now(), custom_name, { + SOURCE_GOOGLE_DRIVE: retain_drive, + SOURCE_HA: retain_ha + }, note=note) + backup = await self._coord.startBackup(options) + return web.json_response({"message": "Requested backup '{0}'".format(backup.name())}) + + async def deleteSnapshot(self, request: Request): + data = await request.json() + # Check to make sure the slug is valid. + self._coord.getBackup(data['slug']) + await self._coord.delete(data['sources'], data['slug']) + return web.json_response({"message": "Deleted from {0} place(s)".format(len(data['sources']))}) + + async def ignore(self, request: Request): + data = await request.json() + # Check to make sure the slug is valid. + backup = self._coord.getBackup(data['slug']) + await self._coord.ignore(data['slug'], data['ignore']) + await self.startSync(request) + if data['ignore']: + return web.json_response({"message": "'{0}' will be ignored.".format(backup.name())}) + else: + return web.json_response({"message": "'{0}' will be included.".format(backup.name())}) + + async def retain(self, request: Request): + data = await request.json() + slug = data['slug'] + + self._coord.getBackup(slug) + await self._coord.retain(data['sources'], slug) + return web.json_response({'message': "Updated the backup's settings"}) + + async def note(self, request: Request): + data = await request.json() + slug = data['slug'] + + self._coord.getBackup(slug) + await self._coord.note(data.get("note", None), slug) + return web.json_response({'message': "Updated the backup's settings"}) + + async def resolvefolder(self, request: Request): + use_existing = BoolValidator.strToBool( + request.query.get("use_existing", False)) + self.folder_finder.resolveExisting(use_existing) + self._global_info.suppressError() + self._global_info.setIngoreErrorsForNow(True) + await self.sync() + return web.json_response({'message': 'Done'}) + + async def skipspacecheck(self, request: Request): + self._global_info.setSkipSpaceCheckOnce(True) + self._global_info.setIngoreErrorsForNow(True) + await self.startSync(request) + return web.json_response({'message': 'Done'}) + + async def confirmdelete(self, request: Request): + always = BoolValidator.strToBool(request.query.get("always", False)) + self._global_info.allowMultipleDeletes() + self._global_info.setIngoreErrorsForNow(True) + if always: + validated = self.config.validateUpdate( + {"confirm_multiple_deletes": False}) + await self._updateConfiguration(validated) + await self.sync() + return web.json_response({'message': 'Configuration updated, I\'ll never ask again'}) + else: + await self.sync() + return web.json_response({'message': 'Backups deleted this one time'}) + + async def log(self, request: Request) -> Any: + format = request.query.get("format", "download") + catchup = BoolValidator.strToBool( + request.query.get("catchup", "False")) + if not catchup: + self.last_log_index = 0 + if format == "view": + context = self.base_context() + return aiohttp_jinja2.render_template("logs.jinja2", + request, + context) + resp = web.StreamResponse() + if format == "html": + resp.content_type = 'text/html' + else: + resp.content_type = 'text/plain' + resp.headers['Content-Disposition'] = 'attachment; filename="home-assistant-google-drive-backup.log"' + + await resp.prepare(request) + + def content(): + html = format == "colored" + if format == "html": + yield "Home Assistant Google Drive Backup Log
      \n"
      +            for line in getHistory(self.last_log_index, html):
      +                self.last_log_index = line[0]
      +                if line:
      +                    yield line[1].replace("\n", "   \n") + "\n"
      +            if format == "html":
      +                yield "
      \n" + + for line in content(): + await resp.write(line.encode()) + await resp.write_eof() + + async def token(self, request: Request) -> None: + self._global_info.setIngoreErrorsForNow(True) + creds_deserialized = json.loads(str(base64.b64decode(request.query.get('creds').strip().encode("utf-8")), 'utf-8')) + creds = Creds.load(self._time, creds_deserialized) + self._coord.saveCreds(creds) + + # Build the redirect url + if 'host' in request.query: + redirect = request.query.get('host') + else: + redirect = self._ha_source.getAddonUrl() + if MIME_JSON in request.headers[hdrs.ACCEPT]: + return web.json_response({'redirect': str(redirect)}) + else: + raise HTTPSeeOther(redirect) + + async def changefolder(self, request: Request) -> None: + # update config to specify backup folder + await self._updateConfiguration(self.config.validateUpdate({Setting.SPECIFY_BACKUP_FOLDER: True})) + + id = request.query.get("id", None) + await self.folder_finder.save(id) + self._global_info.setIngoreErrorsForNow(True) + self.trigger() + return web.json_response({}) + + async def sync(self, request: Request = None) -> Any: + self._coord.clearCaches() + await self._coord.sync() + return await self.getstatus(request) + + async def startSync(self, request) -> Any: + self._coord.clearCaches() + asyncio.create_task(self._coord.sync(), name="Sync from web request") + await self._coord._sync_start.wait() + return await self.getstatus(request) + + async def cancelSync(self, request: Request): + await self._coord.cancel() + return await self.getstatus(request) + + async def getconfig(self, request: Request): + await self._ha_source.refresh() + current_config = {} + for setting in Setting: + current_config[setting.key()] = self.config.getForUi(setting) + default_config = {} + for setting in Setting: + default_config[setting.key()] = setting.default() + return web.json_response({ + 'config': current_config, + 'addons': self._global_info.addons, + 'folders': FOLDERS, + 'defaults': default_config, + 'backup_folder': self.folder_finder.getCachedFolder(), + 'is_custom_creds': self._coord._model.dest.isCustomCreds() + }) + + async def errorreports(self, request: Request): + send = BoolValidator.strToBool(request.query.get("send", False)) + + update = { + "send_error_reports": send + } + validated = self.config.validateUpdate(update) + await self._updateConfiguration(validated) + return web.json_response({'message': 'Configuration updated'}) + + async def callbackupsnapshot(self, request: Request): + switch = BoolValidator.strToBool(request.query.get("switch", False)) + + if switch: + validated = self.config.validateUpdate({Setting.CALL_BACKUP_SNAPSHOT: False}) + await self._updateConfiguration(validated) + self._data_cache.addFlag(UpgradeFlags.NOTIFIED_ABOUT_BACKUP_RENAME) + self._data_cache.addFlag(UpgradeFlags.NOTIFIED_ABOUT_BACKUP_RENAME) + self._data_cache.saveIfDirty() + return web.json_response({'message': 'Configuration updated'}) + + async def ignoredbackupswitch(self, request: Request): + switch = BoolValidator.strToBool(request.query.get("switch", False)) + + validated = self.config.validateUpdate({Setting.IGNORE_UPGRADE_BACKUPS: switch}) + await self._updateConfiguration(validated) + self._data_cache.addFlag(UpgradeFlags.NOTIFIED_ABOUT_IGNORED_BACKUPS) + self._data_cache.saveIfDirty() + if switch: + return web.json_response({'message': 'Configuration updated'}) + else: + return web.json_response({'message': 'Acknowledged'}) + + async def ignorestartupcooldown(self, request: Request): + self._coord.ignoreStartupDelay() + return await self.sync(request) + + async def exposeserver(self, request: Request): + expose = BoolValidator.strToBool(request.query.get("expose", False)) + if expose: + update = { + Setting.EXPOSE_EXTRA_SERVER: True + } + else: + update = { + Setting.EXPOSE_EXTRA_SERVER: False, + Setting.USE_SSL: False, + Setting.REQUIRE_LOGIN: False + } + validated = self.config.validateUpdate(update) + await self._updateConfiguration(validated) + + File.touch(self.config.get(Setting.INGRESS_TOKEN_FILE_PATH)) + await self._ha_source.init() + + redirect = "" + try: + if request.url.port != self.config.get(Setting.INGRESS_PORT): + redirect = self._ha_source.getFullAddonUrl() + except: # noqa: E722 + # eat the error + pass + return web.json_response({ + 'message': 'Configuration updated', + 'redirect': redirect + }) + + async def makeanissue(self, request: Request): + if self._global_info._last_error is not None: + error = logger.formatException(self._global_info._last_error) + else: + error = "No error could be identified automatically." + data = await self.debug_worker.buildBugReportData(error) + body = GITHUB_BUG_TEMPLATE + for key in data: + if isinstance(data[key], dict): + body = body.replace( + "{" + key + "}", json.dumps(data[key], indent=4)) + else: + body = body.replace("{" + key + "}", str(data[key])) + return web.json_response({'markdown': body}) + + async def saveconfig(self, request: Request) -> Any: + data = await request.json() + update = ensureKey("config", data, "the configuration update request") + + # validate the backup password + Password(self.config.getConfigFor(update)).resolve() + + validated, needUpdate = self.config.validate(update) + message = await self._updateConfiguration(validated, ensureKey("backup_folder", data, "the configuration update request"), trigger=False) + try: + await self.cancelSync(request) + await self.startSync(request) + except: # noqa: E722 + # eat the error, just cancel optimistically + pass + return web.json_response(message) + + async def ackignorecheck(self, request: Request): + self.ignore_other_turned_on = False + return web.json_response({'message': "Acknowledged."}) + + async def aknowledgeooboauth(self, request: Request): + self._data_cache.addFlag(UpgradeFlags.NOTIFIED_ABOUT_OOB_FLOW) + self._data_cache.saveIfDirty() + return web.json_response({'message': "Acknowledged"}) + + async def _updateConfiguration(self, new_config, backup_folder_id=None, trigger=True): + update = {} + for key in new_config: + update[key.key()] = new_config[key] + old_drive_option = self.config.get(Setting.ENABLE_DRIVE_UPLOAD) + old_ignore_others_option = self.config.get(Setting.IGNORE_OTHER_BACKUPS) + await self._harequests.updateConfig(update) + + self.config.update(new_config) + + if not old_ignore_others_option and self.config.get(Setting.IGNORE_OTHER_BACKUPS): + self.ignore_other_turned_on = True + self._haupdater.triggerRefresh() + if self.config.get(Setting.SPECIFY_BACKUP_FOLDER) and backup_folder_id is not None and len(backup_folder_id): + await self.folder_finder.save(backup_folder_id) + if trigger: + self.trigger() + return { + 'message': 'Settings saved', + 'reload_page': self.config.get(Setting.ENABLE_DRIVE_UPLOAD) != old_drive_option + } + + async def waitForUpload(self): + await self._upload_event.wait() + + async def _doUpload(self, slug): + await self._coord.uploadBackups(slug) + self._upload_event.set() + + async def upload(self, request: Request): + slug = request.query.get("slug", "") + asyncio.create_task(self._doUpload(slug)) + return web.json_response({'message': "Uploading backup in the background"}) + + async def redirect(self, request, url): + context = { + **self.base_context(), + 'url': url + } + return aiohttp_jinja2.render_template("redirect.jinja2", + request, + context) + + async def addonLogo(self, request: Request): + slug = request.match_info.get('slug') + if not self._ha_source.addonHasLogo(slug): + raise HTTPNotFound() + try: + (content_type, data) = await self._harequests.getAddonLogo(slug) + return web.Response(headers={hdrs.CONTENT_TYPE: content_type}, body=data) + except ClientResponseError as e: + return web.Response(status=e.status) + + async def download(self, request: Request): + slug = request.query.get("slug", "") + backup = self._coord.getBackup(slug) + stream = await self._coord.download(slug) + await stream.setup() + resp = web.StreamResponse() + resp.content_type = 'application/tar' + resp.headers['Content-Disposition'] = 'attachment; filename="{}.tar"'.format( + backup.name()) + resp.headers['Content-Length'] = str(stream.size()) + + await resp.prepare(request) + + # SOMEDAY: consider re-streaming a decrypted tar file for the sake of convenience + + async for chunk in stream.generator(self.config.get(Setting.DEFAULT_CHUNK_SIZE)): + await resp.write(chunk) + + await resp.write_eof() + + async def run(self) -> None: + await self.stop() + + # Create the ingress server + app = web.Application(middlewares=[self.error_middleware]) + aiohttp_jinja2.setup(app, loader=jinja2.FileSystemLoader(self.filePath())) + self._addRoutes(app) + + # The ingress port is considered secured by Home Assistant, so it doesn't get SSL or basic HTTP auth + logger.info("Starting server on port {}".format( + self.config.get(Setting.INGRESS_PORT))) + await self._start_site(app, self.config.get(Setting.INGRESS_PORT)) + + try: + if self.config.get(Setting.EXPOSE_EXTRA_SERVER): + ssl_context = None + if self.config.get(Setting.USE_SSL): + ssl_context = ssl.create_default_context( + ssl.Purpose.CLIENT_AUTH) + ssl_context.load_cert_chain(self.config.get( + Setting.CERTFILE), self.config.get(Setting.KEYFILE)) + middleware = [self.error_middleware] + if self.config.get(Setting.REQUIRE_LOGIN): + middleware.append(HomeAssistantLoginAuth( + self._time, self._harequests)) + + extra_app = web.Application(middlewares=middleware) + aiohttp_jinja2.setup(extra_app, loader=jinja2.FileSystemLoader(self.filePath())) + self._addRoutes(extra_app) + logger.info("Starting server on port {}".format( + self.config.get(Setting.PORT))) + await self._start_site(extra_app, self.config.get(Setting.PORT), ssl_context=ssl_context) + except FileNotFoundError: + logger.error("The configured SSL key or certificate files couldn't be found and so \nan SSL server couldn't be started, please check your settings. \nThe addon web-ui is still available through ingress.") + except ssl.SSLError: + logger.error("Your SSL certificate or key couldn't be loaded and so an SSL server couldn't be started. Please verify that your SSL settings are correctly configured. The addon web-ui is still available through ingress.") + logger.info("Server started") + self.running = True + self._starts += 1 + + def _addRoutes(self, app): + app.add_routes( + [web.static('/static/' + VERSION, abspath(join(__file__, "..", "..", "static")), append_version=True)]) + app.add_routes([web.get('/', self.index)]) + app.add_routes([web.get('/index.html', self.index)]) + app.add_routes([web.get('/index', self.index)]) + app.add_routes([web.get('/favicon.ico', self.favicon)]) + app.add_routes([web.get('/logo/{slug}', self.addonLogo)]) + self._addRoute(app, self.reauthenticate) + self._addRoute(app, self.bootstrap) + self._addRoute(app, self.tos) + self._addRoute(app, self.pp) + + self._addRoute(app, self.getstatus) + self._addRoute(app, self.backup) + self._addRoute(app, self.manualauth) + self._addRoute(app, self.token) + + self._addRoute(app, self.log) + + self._addRoute(app, self.sync) + self._addRoute(app, self.startSync) + self._addRoute(app, self.cancelSync) + + self._addRoute(app, self.getconfig) + self._addRoute(app, self.errorreports) + self._addRoute(app, self.exposeserver) + self._addRoute(app, self.saveconfig) + self._addRoute(app, self.changefolder) + self._addRoute(app, self.confirmdelete) + self._addRoute(app, self.resolvefolder) + self._addRoute(app, self.skipspacecheck) + + self._addRoute(app, self.upload) + self._addRoute(app, self.download) + self._addRoute(app, self.deleteSnapshot) + self._addRoute(app, self.retain) + self._addRoute(app, self.note) + + self._addRoute(app, self._debug.simerror) + self._addRoute(app, self._debug.getTasks) + self._addRoute(app, self._debug.timeoffset) + self._addRoute(app, self.makeanissue) + self._addRoute(app, self.ignorestartupcooldown) + self._addRoute(app, self.callbackupsnapshot) + self._addRoute(app, self.ignoredbackupswitch) + self._addRoute(app, self.ignore) + self._addRoute(app, self.ackignorecheck) + self._addRoute(app, self.aknowledgeooboauth) + self._addRoute(app, self.checkManualAuth) + + def _addRoute(self, app, method): + app.add_routes([ + web.get("/" + method.__name__, method), + web.post("/" + method.__name__, method) + ]) + + async def start(self): + await self.run() + + async def _start_site(self, app, port, ssl_context=None): + aiohttp_logger = TraceLogger("aiohttp.access") + if self.config.get(Setting.TRACE_REQUESTS): + runner = web.AppRunner(app, logger=aiohttp_logger, access_log=aiohttp_logger, + access_log_format='%a %t "%r" %s %b "%{Referer}i" "%{User-Agent}i (%Tfs)"') + else: + runner = web.AppRunner(app) + self.runners.append(runner) + await runner.setup() + # maybe host should be 0.0.0.0 + site = web.TCPSite(runner, "0.0.0.0", port, ssl_context=ssl_context) + await site.start() + + async def stop(self): + # Stop pending requests for all available servers + for runner in self.runners: + try: + await runner.shutdown() + except Exception as e: + logger.error( + "Error while trying to shut down server: " + str(e)) + try: + await runner.cleanup() + except Exception as e: + logger.error( + "Error while trying to shut down server: " + str(e)) + self.runners = [] + + async def shutdown(self): + await self.stop() + + @web.middleware + async def error_middleware(self, request: Request, handler): + try: + log_trace = self.config.get(Setting.TRACE_REQUESTS) + if log_trace: + logger.trace("Serving %s %s to %s", request.method, + request.url, request.remote) + handled = await handler(request) + if log_trace: + logger.trace("Completed %s %s", request.method, request.url) + return handled + except Exception as ex: + if isinstance(ex, HTTPException): + raise + logger.error("Error serving %s %s", request.method, request.url) + logger.error(logger.formatException(ex)) + data = self.processError(ex) + return web.json_response(data, status=data['http_status']) + + def processError(self, e): + if isinstance(e, KnownError): + known: KnownError = e + return { + 'http_status': known.httpStatus(), + 'error_type': known.code(), + 'message': known.message(), + 'details': logger.formatException(e), + 'data': known.data() + } + else: + return { + 'http_status': 500, + 'error_type': "generic_error", + 'message': "An unexpected error occurred: " + str(e), + 'details': logger.formatException(e) + } + + def filePath(self, name=None): + if name is None: + return abspath(join(__file__, "..", "..", "static")) + else: + return abspath(join(__file__, "..", "..", "static", name)) + + def cssElement(self, selector, keys): + ret = selector + ret += " {\n" + for key in keys: + ret += "\t" + key + ": " + keys[key] + ";\n" + ret += "}\n\n" + return ret + + async def index(self, request: Request): + if not self._coord.enabled(): + template = "index.jinja2" + context = { + **self.base_context(), + 'showOpenDriveLink': True + } + else: + template = "working.jinja2" + context = { + **self.base_context(), + 'showOpenDriveLink': True, + 'navBarTitle': 'Backups' + } + response = aiohttp_jinja2.render_template(template, + request, + context) + response.headers['cache-control'] = 'no-store' + return response + + @aiohttp_jinja2.template('privacy_policy.jinja2') + async def pp(self, request: Request): + return self.base_context() + + @aiohttp_jinja2.template('terms_of_service.jinja2') + async def tos(self, request: Request): + return self.base_context() + + async def favicon(self, request: Request): + return web.FileResponse(abspath(join(__file__, "..", "..", "static", "images", "favicon.png"))) + + @aiohttp_jinja2.template('index.jinja2') + async def reauthenticate(self, request: Request) -> Any: + return { + **self.base_context(), + 'showOpenDriveLink': True + } + + +@web.middleware +class HomeAssistantLoginAuth(): + def __init__(self, time, harequests): + self._time = time + self._harequests = harequests + self.auth_cache: Dict[str, Any] = {} + self.realm = "Home Assistant Login" + + def parse_auth_header(self, request): + auth_header = request.headers.get(hdrs.AUTHORIZATION) + if not auth_header: + return None + try: + auth = BasicAuth.decode(auth_header=auth_header) + except ValueError: + auth = None + return auth + + async def authenticate(self, request): + auth = self.parse_auth_header(request) + return (auth is not None and await self.check_credentials(auth.login, auth.password)) + + async def check_credentials(self, username, password): + if username is None: + raise ValueError('username is None') # pragma: no cover + + if password is None: + raise ValueError('password is None') # pragma: no cover + + if username in self.auth_cache and self.auth_cache[username]['password'] == password and self.auth_cache[username]['timeout'] > self._time.now(): + return True + try: + await self._harequests.auth(username, password) + self.auth_cache[username] = {'password': password, 'timeout': ( + self._time.now() + timedelta(minutes=10))} + return True + except Exception as e: + logger.printException(e) + return False + + def challenge(self): + return web.Response( + body=b'', status=401, reason='UNAUTHORIZED', + headers={ + hdrs.WWW_AUTHENTICATE: 'Basic realm="%s"' % self.realm, + hdrs.CONTENT_TYPE: 'text/html; charset=utf-8', + hdrs.CONNECTION: 'keep-alive' + } + ) + + async def __call__(self, request, handler): + if await self.authenticate(request): + return await handler(request) + else: + return self.challenge() diff --git a/hassio-google-drive-backup/backup/util/__init__.py b/hassio-google-drive-backup/backup/util/__init__.py new file mode 100644 index 0000000..091088e --- /dev/null +++ b/hassio-google-drive-backup/backup/util/__init__.py @@ -0,0 +1,8 @@ +# flake8: noqa +from .asynchttpgetter import AsyncHttpGetter +from .backoff import Backoff +from .estimator import Estimator +from .globalinfo import GlobalInfo +from .resolver import Resolver +from .rangelookup import RangeLookup +from .data_cache import DataCache, KEY_CREATED, KEY_I_MADE_THIS, KEY_PENDING, KEY_NOTE, KEY_IGNORE, KEY_LAST_SEEN, KEY_NAME, CACHE_EXPIRATION_DAYS, UpgradeFlags diff --git a/hassio-google-drive-backup/backup/util/asynchttpgetter.py b/hassio-google-drive-backup/backup/util/asynchttpgetter.py new file mode 100644 index 0000000..15827b7 --- /dev/null +++ b/hassio-google-drive-backup/backup/util/asynchttpgetter.py @@ -0,0 +1,241 @@ +from datetime import timedelta +import io +from typing import Dict + +from aiohttp import ClientSession +from aiohttp.client import ClientResponse, ClientPayloadError, ClientOSError +from asyncio.exceptions import TimeoutError +from collections import deque + +from ..exceptions import LogicError, ensureKey +from ..logger import getLogger +from ..time import Time + +logger = getLogger(__name__) + +CONTENT_LENGTH_HEADER = "content-length" +CONTENT_LENGTH_ERROR = "Content size must be provided if the webserver doesn't provide it" +SERVER_CONTENT_LENGTH_ERROR = "Server returned a content length that didn't match the requested size" +POSITION_ERROR_MESSAGE = "AsyncHttpGetter must also be set up at position 0" +DEFAULT_CHUNK_SIZE = 1024 * 1024 + + +# This class is dumb but it gets around a dumb problem +class Stupid(io.BytesIO): + def __len__(self): + return len(self.getvalue()) + + +class AsyncHttpGetter: + def __init__(self, url, headers: Dict[str, str], session, size: int = None, timeout=None, timeoutFactory=None, otherErrorFactory=None, time: Time = None): + self._url: str = url + + # Current position of the stream + self._position: int = 0 + + # Total size of the stream + self._size: int = size + + # Headers that should get sent with every request + self._headers: Dict[str, str] = headers + + # Session used to make http requests. + self._session: ClientSession = session + + # Most recent session request + self._response: ClientResponse = None + + # Where the resposne currently starts + self._responseStart = 0 + + self._history = deque() + self._time = time + self._startTime = self._time.now() + self.timeoutFactory = timeoutFactory + self.otherErrorFactory = otherErrorFactory + self.timeout = timeout + + async def setup(self): + if not self._position == 0: + raise LogicError(POSITION_ERROR_MESSAGE) + await self._startReadRemoteAt(0) + if CONTENT_LENGTH_HEADER in self._response.headers: + self._size = int(ensureKey( + CONTENT_LENGTH_HEADER, self._response.headers, "web server get request's headers")) + elif self._size is None: + raise LogicError( + CONTENT_LENGTH_ERROR) + + self._history.append([self._time.now(), 0]) + return self._size + + def _ensureSetup(self): + if self._size is None: + raise LogicError("AsyncHttpGetter.setup() must be called first") + + def size(self) -> int: + self._ensureSetup() + return self._size + + def __len__(self): + return self.size() + + def position(self, pos=None): + if pos is not None: + self._position = pos + return self._position + + async def generator(self, chunk_size): + while True: + chunk = await self.read(chunk_size) + if len(chunk.getbuffer()) == 0: + break + yield chunk.getbuffer() + + def progress(self): + if self._size == 0: + return 0 + self._ensureSetup() + if self._size == 0: + return 0 + return 100 * float(self.position()) / float(self._size) + + # return the estimated speed of the tranfser in bytes/second + def speed(self, period: timedelta = timedelta(seconds=10)): + if len(self._history) < 2: + return None + now = self._time.now() + intervals = [] + current = self._history[0] + last_speed = 0 + for x in range(1, len(self._history)): + next = self._history[x] + seconds = (next[0] - current[0]).total_seconds() + data = next[1] - current[1] + if seconds == 0: + speed = 0 # avoid div by 0 + else: + speed = data / seconds + intervals.append([current[0], next[0], speed]) + current = next + last_speed = speed + + time_since_last = now - current[0] + if time_since_last > period: + # linear decay the "estimated" rate over the period + diff = time_since_last - period + if diff > period: + return 0 + else: + return last_speed * (1 - diff.total_seconds() / period.total_seconds()) + + # behave as though we continued with the pseed form the last interval + intervals.append([current[0], now, last_speed]) + + # calculate the time-averaged speed over the given period + stop = now + start = now - period + total = 0 + minimum_time = now + for interval in intervals: + if start > interval[1]: + continue + if stop < interval[0]: + continue + overlap_start = max(start, interval[0]) + overlap_stop = min(stop, interval[1]) + if overlap_stop > overlap_start: + total += (overlap_stop - overlap_start).total_seconds() * interval[2] + if overlap_start < minimum_time: + minimum_time = overlap_start + return total / (now - minimum_time).total_seconds() + + def startTime(self): + return self._startTime + + def __format__(self, format_spec: str) -> str: + return str(int(self.progress())) + + async def _startReadRemoteAt(self, where: int): + headers = self._headers.copy() + # request a byte range + if where != 0: + headers['range'] = "bytes=%s-%s" % (self._position, self._size - 1) + if self._response is not None: + self._response.release() + try: + resp = await self._session.get(self._url, headers=headers, timeout=self.timeout) + except TimeoutError: + if self.timeoutFactory is not None: + raise self.timeoutFactory() + raise + except ClientPayloadError: + if self.otherErrorFactory is not None: + raise self.otherErrorFactory() + raise + except ClientOSError: + if self.otherErrorFactory is not None: + raise self.otherErrorFactory() + raise + resp.raise_for_status() + if where == 0 and self._size is not None and CONTENT_LENGTH_HEADER in resp.headers and int(resp.headers[CONTENT_LENGTH_HEADER]) != self._size: + resp.release() + raise LogicError(SERVER_CONTENT_LENGTH_ERROR) + self._response = resp + self._responseStart = where + + async def read(self, count=DEFAULT_CHUNK_SIZE): + self._ensureSetup() + ret = Stupid() + if self._size is not None and self._position >= self._size: + return ret + + # See if we need to move the stream elsewhere + if self._responseStart != self._position: + # Reset the stream's position + await self._startReadRemoteAt(self._position) + + # Limit by how much we can get from the stream + needed = min(count, self.size() - self._position) + + # And then get it + try: + data = await self._response.content.readexactly(needed) + except TimeoutError: + if self.timeoutFactory is not None: + raise self.timeoutFactory() + raise + except ClientPayloadError: + if self.otherErrorFactory is not None: + raise self.otherErrorFactory() + raise + except ClientOSError: + if self.otherErrorFactory is not None: + raise self.otherErrorFactory() + raise + ret.write(data) + # Keep track of where we are in the stream + self._responseStart += len(data) + self._position += len(data) + self._history.append([self._time.now(), self._position]) + if len(self._history) > 50: + self._history.popleft() + + ret.seek(0) + return ret + + async def __aenter__(self): + await self.setup() + + async def __aexit__(self, type, value, traceback): + if self._response is not None: + self._response.release() + + def __aiter__(self): + return self + + async def __anext__(self): + val = await self.read() + if len(val) == 0: + raise StopAsyncIteration + return val.getbuffer() diff --git a/hassio-google-drive-backup/backup/util/backoff.py b/hassio-google-drive-backup/backup/util/backoff.py new file mode 100644 index 0000000..0bf7933 --- /dev/null +++ b/hassio-google-drive-backup/backup/util/backoff.py @@ -0,0 +1,42 @@ +from ..logger import getLogger + +MAX_FACTOR = 20 # avoid weirdness with python's arbitary integer precision +MAX_WAIT = 60 * 60 # 1 hour + +logger = getLogger(__name__) + + +class Backoff(): + def __init__(self, initial=None, base=2, exp=2, max=MAX_WAIT, attempts=None): + self._attempts = attempts + self._initial = initial + self._start = base + self._max = max + self._exp = exp + self._counter = 0 + + def reset(self): + self._counter = 0 + + def peek(self): + exp = self._counter - 1 + if self._counter == 1 and self._initial is not None: + return self._initial + elif self._initial is not None: + exp -= 1 + + exp = min(exp, MAX_FACTOR) + computed = self._start * pow(self._exp, exp) + + if self._max: + computed = min(self._max, computed) + return computed + + def backoff(self, error): + if self._attempts and self._counter >= self._attempts: + raise error + self._counter += 1 + return self.peek() + + def maxOut(self): + self._counter = 100 diff --git a/hassio-google-drive-backup/backup/util/data_cache.py b/hassio-google-drive-backup/backup/util/data_cache.py new file mode 100644 index 0000000..d87ffa0 --- /dev/null +++ b/hassio-google-drive-backup/backup/util/data_cache.py @@ -0,0 +1,149 @@ +from datetime import timedelta +from enum import Enum, unique +from backup.config import Config, Setting, VERSION, Version +from backup.file import JsonFileSaver +from backup.const import NECESSARY_OLD_BACKUP_PLURAL_NAME +from injector import inject, singleton +from ..time import Time +from typing import Dict +import json +import os + +KEY_I_MADE_THIS = "i_made_this" +KEY_PENDING = "pending" +KEY_CREATED = "created" +KEY_IGNORE = "ignore" +KEY_LAST_SEEN = "last_seen" +KEY_NAME = "name" +KEY_LAST_VERSION = "last_verison" +KEY_UPGRADES = "upgrades" +KEY_FLAGS = "flags" +KEY_NOTE = "note" + +CACHE_EXPIRATION_DAYS = 30 + +VERSION_DEFUALT_IGNORE_UPGRADES = Version.parse("0.108.2") + + +@unique +class UpgradeFlags(Enum): + NOTIFIED_ABOUT_BACKUP_RENAME = "notified_backup_rename" + NOTIFIED_ABOUT_IGNORED_BACKUPS = "notified_ignored_backups" + NOTIFIED_ABOUT_OOB_FLOW = "notified_about_oob_flow" + TESTING_FLAG = "testing_flag" + + +@singleton +class DataCache: + @inject + def __init__(self, config: Config, time: Time): + self._config = config + self._data = {} + self._dirty = {} + self._time = time + self._last_version = Version.default() + self._first_version = Version.default() + self._flags = set() + self._load() + + def _load(self): + path = self._config.get(Setting.DATA_CACHE_FILE_PATH) + if not JsonFileSaver.exists(path): + self._data = {NECESSARY_OLD_BACKUP_PLURAL_NAME: {}} + else: + self._data = JsonFileSaver.read(path) + + # Check for an upgrade. + if KEY_LAST_VERSION in self._data: + self._last_version = Version.parse(self._data[KEY_LAST_VERSION]) + if self.previousVersion != self.currentVersion: + # add an upgrade marker + if KEY_UPGRADES not in self._data: + self._data[KEY_UPGRADES] = [] + self._data[KEY_UPGRADES].append({ + 'prev_version': str(self.previousVersion), + 'new_version': str(self.currentVersion), + 'date': self._time.now().isoformat() + }) + self._data[KEY_LAST_VERSION] = str(self.currentVersion) + self.makeDirty() + if KEY_UPGRADES not in self._data or len(self._data[KEY_UPGRADES]) == 0: + self._first_version = self.currentVersion + else: + self._first_version = Version.parse(self._data[KEY_UPGRADES][0]['new_version']) + + if self._config.isExplicit(Setting.IGNORE_OTHER_BACKUPS) or self._config.isExplicit(Setting.IGNORE_UPGRADE_BACKUPS): + self.addFlag(UpgradeFlags.NOTIFIED_ABOUT_IGNORED_BACKUPS) + + if self.notifyForIgnoreUpgrades: + self._config.useLegacyIgnoredBehavior(True) + + self.saveIfDirty() + + def save(self, data=None): + if data is None: + data = self._data + path = self._config.get(Setting.DATA_CACHE_FILE_PATH) + JsonFileSaver.write(path, data) + self._dirty = False + + def makeDirty(self): + self._dirty = True + + @property + def dirty(self) -> bool: + return self._dirty + + @property + def backups(self) -> Dict[str, Dict[str, str]]: + if NECESSARY_OLD_BACKUP_PLURAL_NAME not in self._data: + self._data[NECESSARY_OLD_BACKUP_PLURAL_NAME] = {} + return self._data[NECESSARY_OLD_BACKUP_PLURAL_NAME] + + def backup(self, slug) -> Dict[str, str]: + if slug not in self.backups: + self.backups[slug] = {} + return self.backups[slug] + + def saveIfDirty(self): + if self._dirty: + # See if we need to remove any old entries + for slug in list(self.backups.keys()): + data = self.backups[slug].get(KEY_LAST_SEEN) + if data is not None and self._time.now() > self._time.parse(data) + timedelta(days=CACHE_EXPIRATION_DAYS): + del self.backups[slug] + self.save() + + @property + def previousVersion(self): + return self._last_version + + @property + def firstVersion(self): + return self._first_version + + @property + def currentVersion(self): + return Version.parse(VERSION) + + @property + def notifyForIgnoreUpgrades(self): + return self.firstVersion < VERSION_DEFUALT_IGNORE_UPGRADES and not self.checkFlag(UpgradeFlags.NOTIFIED_ABOUT_IGNORED_BACKUPS) and not self._config.isExplicit(Setting.IGNORE_OTHER_BACKUPS) and not self._config.isExplicit(Setting.IGNORE_UPGRADE_BACKUPS) + + def checkFlag(self, flag: UpgradeFlags): + return flag.value in self._data.get(KEY_FLAGS, []) + + def TESTS_ONLY_clearFlags(self): + self._data[KEY_FLAGS] = [] + + def addFlag(self, flag: UpgradeFlags): + all_flags = set(self._data.get(KEY_FLAGS, [])) + all_flags.add(flag.value) + self._data[KEY_FLAGS] = list(all_flags) + self.makeDirty() + + def getUpgradeTime(self, version: Version): + for upgrade in self._data[KEY_UPGRADES]: + if Version.parse(upgrade['new_version']) >= version: + return self._time.parse(upgrade['date']) + return self._time.now() diff --git a/hassio-google-drive-backup/backup/util/error_analyzer.py b/hassio-google-drive-backup/backup/util/error_analyzer.py new file mode 100644 index 0000000..1cd5496 --- /dev/null +++ b/hassio-google-drive-backup/backup/util/error_analyzer.py @@ -0,0 +1,97 @@ +import asyncio +import firebase_admin +from firebase_admin import credentials +from firebase_admin import firestore +from pathlib import Path + +KNWON_ERRORS = ['existing_backup_folder', 'google_dns', 'cancelled', 'google_timeout', 'low_space', 'multiple_deletes'] + + +async def main(): + cred = credentials.Certificate(str(Path.home().joinpath("Documents/secrets/server-firestore-creds.json"))) + firebase_admin.initialize_app(cred) + + db = firestore.client() + reports = db.collection(u'error_reports') + + while(True): + command = input() + if command == "clear all": + print("Deleteing records...") + batch = db.batch() + batch_size = 0 + for item in reports.stream(): + batch.delete(reports.document(item.id)) + batch_size += 1 + batch.commit() + print("Done") + if command == "": + # Do regular analysis + await stats(db, reports) + if command.startswith("get "): + query = reports.where("report.client", "==", command[4:]) + for report in query.get(): + print(report.to_dict()) + print("done") + elif command == "exit" or command == "quit" or command == "q": + return + + # stream = reports.stream() + for report in query.get(): + print(report.to_dict()) + + +async def stats(db, reports): + print("Inspecting the last 24 hours of records...") + cursor = None + batch_size = 50 + summary = {} + resolved = {} + total = 0 + while True: + count = 0 + if cursor is None: + stream = reports.limit(batch_size).stream() + else: + stream = reports.limit(batch_size).start_after(cursor).stream() + for report in stream: + cursor = report + data = report.to_dict() + count += 1 + total += 1 + if 'report' not in data: + continue + if 'error' not in data['report']: + resolved[data['client']] = data['report']['duration'] + continue + if data['report']['error'] in summary: + summary[data['report']['error']].append(data) + else: + summary[data['report']['error']] = [data] + print("Processed " + str(count) + " records") + if count == 0: + break + for error in summary.keys(): + print("") + distinct = set() + unresolved = {} + versions = set() + for report in summary[error]: + distinct.add(report['client']) + if report['client'] not in resolved: + unresolved[report['client']] = report + versions.add(report['version']) + print("Unresolved: " + str(len(unresolved))) + print("Count: " + str(len(summary[error]))) + print("Distinct: " + str(len(distinct))) + print("Versions: " + str(versions)) + print("Error: " + error) + if len(unresolved) > 0: + print("Unresolved: ") + for unres in unresolved.values(): + print(" " + unres['report']['arch'] + " " + unres['client']) + print("Total records: " + str(total)) + + +if __name__ == '__main__': + asyncio.run(main()) diff --git a/hassio-google-drive-backup/backup/util/estimator.py b/hassio-google-drive-backup/backup/util/estimator.py new file mode 100644 index 0000000..0d78767 --- /dev/null +++ b/hassio-google-drive-backup/backup/util/estimator.py @@ -0,0 +1,105 @@ +import os +import platform + +from injector import inject, singleton + +from ..config import Config, Setting +from ..exceptions import LowSpaceError +from .globalinfo import GlobalInfo +from ..logger import getLogger + +logger = getLogger(__name__) + +SIZE_SI = ["B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"] + + +@singleton +class Estimator(): + @inject + def __init__(self, config: Config, global_info: GlobalInfo): + super().__init__() + self.config = config + self._blocksUsed = 0 + self._blocksTotal = 1 + self._blockSize = 0 + self._global_info = global_info + + def refresh(self): + if platform.system() == "Windows": + # Unsupported on windows + return self + # This roughly matches the results you get by running "df", + # except its a bit more conservative, which isn't necessarily + # correct, but we're aiming for ballpark numbers here so it + # should be ok. + stats = os.statvfs(self.config.get(Setting.BACKUP_DIRECTORY_PATH)) + total = stats.f_blocks + available = stats.f_bavail + availableToRoot = stats.f_bfree + self._blocksUsed = total - availableToRoot + self._blocksTotal = self._blocksUsed + available + self._blockSize = stats.f_frsize + return self + + def checkSpace(self, backups): + if platform.system() == "Windows": + # Unsupported on windows + return + if not self.config.get(Setting.WARN_FOR_LOW_SPACE): + # Don't check, just go + return + try: + self._checkSpace(backups) + except Exception as e: + if isinstance(e, LowSpaceError): + raise e + # Just log the error and continue otherwise + logger.error( + "Encountered an error while trying to check disk space remaining: " + str(e)) + + def _checkSpace(self, backups): + # get the most recent backup size + space_needed = self.config.get(Setting.LOW_SPACE_THRESHOLD) + backups.sort(key=lambda s: s.date(), reverse=True) + for backup in backups: + latest_size = backup.sizeInt() + if latest_size > 1: + # Bump the size a little to estimate organic growth. + space_needed = latest_size * 1.1 + break + + if space_needed > self.getBytesFree() and not self._global_info.isSkipSpaceCheckOnce(): + raise LowSpaceError("{0}%".format( + int(self.getUsagePercent())), Estimator.asSizeString(self.getBytesFree())) + + def getUsagePercent(self): + if self.getBlocksTotal() == 0: + return 0 + return 100.0 * float(self.getBlocksUsed()) / float(self.getBlocksTotal()) + + def getBlocksUsed(self): + return self._blocksUsed + + def getBlocksTotal(self): + return self._blocksTotal + + def getBlocksFree(self): + return self.getBlocksTotal() - self.getBlocksUsed() + + def getBytesFree(self): + return self._blockSize * self.getBlocksFree() + + def getBytesUsed(self): + return self._blockSize * self.getBlocksUsed() + + def getBytesTotal(self): + return self._blockSize * self.getBlocksTotal() + + @classmethod + def asSizeString(cls, size): + current = float(size) + for id in SIZE_SI: + if current < 1024: + return "{0} {1}".format(round(current, 1), id) + current /= 1024 + return "Beyond mortal comprehension" diff --git a/hassio-google-drive-backup/backup/util/globalinfo.py b/hassio-google-drive-backup/backup/util/globalinfo.py new file mode 100644 index 0000000..972ccda --- /dev/null +++ b/hassio-google-drive-backup/backup/util/globalinfo.py @@ -0,0 +1,125 @@ + +from datetime import timedelta +from threading import Lock + +from injector import inject, singleton + +from ..time import Time +from ..logger import getLogger + +logger = getLogger(__name__) + + +@singleton +class GlobalInfo(): + @inject + def __init__(self, time: Time): + self._time = time + self._syncs = 0 + self._failures = 0 + self._last_error = 0 + self._successes = 0 + self._last_failure_time = None + self._uploads = 0 + self._last_upload = None + self._last_success = time.now() + self._last_sync_success = None + self._last_upload_size = None + self._last_sync_start = None + self._last_error = None + self._supress_error = False + self.credVersion = 0 + self._first_sync = True + self._multipleDeletesPermitted = False + self._dns_info = None + self._skip_space_check_once = False + self._ignore_errors_for_now = False + self._start_time = time.now() + + self.ha_ssl = False + self.addons = None + self.ha_port = None + self.slug = None + self.url = "" + self.debug = {} + self.lock = Lock() + self.backup_cooldown_time = time.now() + + def ignoreErrorsForNow(self): + return self._ignore_errors_for_now + + def setIngoreErrorsForNow(self, value): + self._ignore_errors_for_now = value + + def failureCount(self): + return self._failures + + def refresh(self): + pass + + def setDnsInfo(self, info): + self._dns_info = info + + @property + def start_time(self): + return self._start_time + + def getDnsInfo(self): + return self._dns_info + + def success(self): + self._first_sync = False + self._last_error = None + self._last_success = self._time.now() + self._last_sync_success = self._time.now() + self._successes += 1 + self._multipleDeletesPermitted = False + self.setIngoreErrorsForNow(False) + + def sync(self): + self._last_sync_start = self._time.now() + self._syncs += 1 + + def failed(self, error): + self._first_sync = False + self._last_error = error + self._failures += 1 + self._last_failure_time = self._time.now() + self._supress_error = False + self.setIngoreErrorsForNow(False) + + def suppressError(self): + self._supress_error = True + + def isErrorSuppressed(self): + return self.suppressError + + def upload(self, size): + self._last_upload = self._time.now() + self._uploads += 1 + self._last_upload_size = size + + def credsSaved(self): + self.credVersion += 1 + + def isPermitMultipleDeletes(self) -> bool: + return self._multipleDeletesPermitted + + def allowMultipleDeletes(self) -> bool: + self._multipleDeletesPermitted = True + + def addDebugInfo(self, key, value): + with self.lock: + self.debug[key] = value + + def isSkipSpaceCheckOnce(self): + return self._skip_space_check_once + + def setSkipSpaceCheckOnce(self, val): + self._skip_space_check_once = val + + def triggerBackupCooldown(self, delay: timedelta): + self.backup_cooldown_time = self._time.now() + delay + + def backupCooldownTime(self): + return self.backup_cooldown_time diff --git a/hassio-google-drive-backup/backup/util/rangelookup.py b/hassio-google-drive-backup/backup/util/rangelookup.py new file mode 100644 index 0000000..167741e --- /dev/null +++ b/hassio-google-drive-backup/backup/util/rangelookup.py @@ -0,0 +1,56 @@ +from typing import List, TypeVar, Generic, Mapping + +T = TypeVar('T') +K = TypeVar('K') + + +class RangeLookup(Generic[T, K]): + def __init__(self, source: List[T], mapping: Mapping[T, K]): + self.source = source + self.map = mapping + + def matches(self, start: K, end: K): + range_start = self._searchFirstGreaterOrEqual(start) + range_end = self._searchLastLessOrEqual(end) + + x = range_start + while (x <= range_end): + if x >= 0 and x < len(self.source): + yield self.source[x] + x += 1 + + def matchList(self, start, end): + ret = [] + for x in self.matches(start, end): + ret.append(x) + return ret + + def _searchFirstGreaterOrEqual(self, val): + first = 0 + last = len(self.source) - 1 + if self.map(self.source[last]) < val: + return last + 1 + while first != last: + mid = int((first + last) / 2) + if self.map(self.source[mid]) < val: + first = mid + 1 + else: + last = mid + return first + + def _searchLastLessOrEqual(self, val): + first = 0 + last = len(self.source) - 1 + if self.map(self.source[0]) > val: + return -1 + while first != last: + mid = int((first + last) / 2) + if self.map(self.source[mid]) > val: + last = mid - 1 + elif first != mid: + first = mid + elif self.map(self.source[last]) <= val: + return last + else: + return first + return first diff --git a/hassio-google-drive-backup/backup/util/resolver.py b/hassio-google-drive-backup/backup/util/resolver.py new file mode 100644 index 0000000..74f7de9 --- /dev/null +++ b/hassio-google-drive-backup/backup/util/resolver.py @@ -0,0 +1,58 @@ +import socket +from typing import Any, Dict, List + +import aiodns +from aiohttp.resolver import AsyncResolver +from injector import inject, singleton + +from ..config import Config, Setting +from ..logger import getLogger + +logger = getLogger(__name__) + +TTL_HOURS = 12 + + +@singleton +class Resolver(AsyncResolver): + @inject + def __init__(self, config: Config): + super().__init__() + self.config = config + self._original_dns = self._resolver + self.setAlternateResolver() + config.subscribe(self.updateConfig) + + async def resolve(self, host: str, port: int = 0, + family: int = socket.AF_INET) -> List[Dict[str, Any]]: + if host == self.config.get(Setting.DRIVE_HOST_NAME) and len(self.config.get(Setting.DRIVE_IPV4)) > 0 and family == 0: + # return the "mocked" drive address instead. + return [{ + 'family': 0, + 'flags': socket.AddressInfo.AI_NUMERICHOST, + 'port': port, + 'proto': 0, + 'host': self.config.get(Setting.DRIVE_IPV4), + 'hostname': host + }] + addresses = await super().resolve(host, port, family) + return addresses + + def updateConfig(self): + if self._alt_ns != self.config.get(Setting.ALTERNATE_DNS_SERVERS): + self.setAlternateResolver() + self._resolver = self._alt_dns + + def setAlternateResolver(self): + if len(self.config.get(Setting.ALTERNATE_DNS_SERVERS)) > 0: + self._alt_dns = aiodns.DNSResolver(loop=self._loop, nameservers=self.config.get( + Setting.ALTERNATE_DNS_SERVERS).split(",")) + else: + self._alt_dns = self._original_dns + self._alt_ns = self.config.get(Setting.ALTERNATE_DNS_SERVERS) + + def toggle(self): + if self._resolver == self._original_dns: + self._resolver = self._alt_dns + else: + self._resolver = self._original_dns diff --git a/hassio-google-drive-backup/backup/watcher.py b/hassio-google-drive-backup/backup/watcher.py new file mode 100644 index 0000000..de3950d --- /dev/null +++ b/hassio-google-drive-backup/backup/watcher.py @@ -0,0 +1,96 @@ +from datetime import timedelta +from threading import Lock +from asyncio import Event + +from injector import inject, singleton +from watchdog.events import FileSystemEvent, FileSystemEventHandler +from watchdog.observers import Observer + +from backup.config import Config, Setting, Startable +from backup.time import Time +from backup.worker import Trigger +from backup.logger import getLogger +from backup.ha import HaSource +from asyncio import get_event_loop +from os.path import join, abspath + +logger = getLogger(__name__) + +REPORT_DELAY_SECONDS = 5 +LOG_INTERVAL = timedelta(seconds=30) +CHANGES_CHECK_DELAY = timedelta(seconds=10) + + +@singleton +class Watcher(Trigger, FileSystemEventHandler, Startable): + @inject + def __init__(self, time: Time, config: Config, source: HaSource): + super().__init__() + self.time = time + self.config: Config = config + self.observer: Observer = Observer() + self._source = source + self.lock: Lock = Lock() + self.noticed_change_signal = Event() + self._changes_have_happened = False + self._last_change_time = None + self._last_log_time = None + self._last_notified_time = None + self._loop = get_event_loop() + + async def start(self): + if not self.config.get(Setting.WATCH_BACKUP_DIRECTORY): + return + self.observer.schedule(self, self.config.get( + Setting.BACKUP_DIRECTORY_PATH), recursive=False) + self.observer.start() + + def name(self): + return "Backup Directory Watcher" + + def on_any_event(self, event: FileSystemEvent): + if event.is_directory: + # ignore any directory level events that bubble up + return + if event.event_type in ['modified', 'created']: + return + logger.trace("Backup directory modified: %s %s", event.event_type, event.src_path) + with self.lock: + self._last_change_time = self.time.now() + self._changes_have_happened = True + + # Provide periodic log messages to indicate we'll backup soon. + if not self._last_log_time: + logger.info("A backup directory file was modified, we'll check for new backups soon.") + self._last_log_time = self.time.now() + elif (self.time.now() - self._last_log_time) > LOG_INTERVAL: + logger.info("The backup directory is still being written to, waiting...") + self._last_log_time = self.time.now() + if not self.noticed_change_signal.is_set(): + self._loop.call_soon_threadsafe(self.noticed_change_signal.set) + + async def check(self): + if not self._changes_have_happened: + return + + check_backup_source = False + with self.lock: + if not self._last_change_time: + return + if self.time.now() - self._last_change_time > CHANGES_CHECK_DELAY: + check_backup_source = True + self._last_change_time = None + self._changes_have_happened = False + self._last_log_time = None + if check_backup_source: + logger.debug("Checking backup source for changes...") + await self._source.get() + if self._source.query_had_changes: + self.trigger() + return await super().check() + + async def stop(self): + if not self.config.get(Setting.WATCH_BACKUP_DIRECTORY): + return + self.observer.stop() + self.observer.join() diff --git a/hassio-google-drive-backup/backup/worker/__init__.py b/hassio-google-drive-backup/backup/worker/__init__.py new file mode 100644 index 0000000..877dddf --- /dev/null +++ b/hassio-google-drive-backup/backup/worker/__init__.py @@ -0,0 +1,3 @@ +# flake8: noqa +from .worker import Worker, StopWorkException +from .trigger import Trigger diff --git a/hassio-google-drive-backup/backup/worker/trigger.py b/hassio-google-drive-backup/backup/worker/trigger.py new file mode 100644 index 0000000..9b38db2 --- /dev/null +++ b/hassio-google-drive-backup/backup/worker/trigger.py @@ -0,0 +1,26 @@ +from ..logger import getLogger + +logger = getLogger(__name__) + + +class Trigger(): + def __init__(self): + self._triggered = False + + def trigger(self) -> None: + self._triggered = True + + def reset(self) -> None: + self._triggered = False + + def triggered(self) -> bool: + return self._triggered + + def name(self): + return "Unnamed Trigger" + + async def check(self) -> bool: + if self.triggered(): + self.reset() + return True + return False diff --git a/hassio-google-drive-backup/backup/worker/worker.py b/hassio-google-drive-backup/backup/worker/worker.py new file mode 100644 index 0000000..947be83 --- /dev/null +++ b/hassio-google-drive-backup/backup/worker/worker.py @@ -0,0 +1,63 @@ +import asyncio + +from ..time import Time +from ..config import Startable +from ..logger import getLogger + +logger = getLogger(__name__) + + +class StopWorkException(Exception): + pass + + +class Worker(Startable): + def __init__(self, name, method, time: Time, interval=1): + super().__init__() + self._method = method + self._time = time + self._name = name + self._last_error = None + self._interval = interval + self._task = None + self._should_stop = False + self._wait_event = asyncio.Event() + + async def work(self): + while not self._should_stop: + self._wait_event.clear() + try: + await self._method() + except StopWorkException: + break + except asyncio.CancelledError: + break + except Exception as e: + self._last_error = e + logger.error( + "Worker {0} got an unexpected error".format(self._name)) + logger.printException(e) + if isinstance(self._interval, float) or isinstance(self._interval, int): + interval = self._interval + else: + interval = self._interval() + await self._time.sleepAsync(interval, self._wait_event) + + async def start(self): + self._should_stop = False + self._task = asyncio.create_task(self.work(), name=self._name) + return self._task + + async def stop(self): + self._should_stop = True + if self._task is not None: + self._task.cancel() + await asyncio.wait([self._task]) + + def isRunning(self): + if self._task is None: + return False + return not self._task.done() + + def getLastError(self): + return self._last_error diff --git a/hassio-google-drive-backup/cloudbuild-dev.yaml b/hassio-google-drive-backup/cloudbuild-dev.yaml new file mode 100644 index 0000000..bc1bebb --- /dev/null +++ b/hassio-google-drive-backup/cloudbuild-dev.yaml @@ -0,0 +1,17 @@ +# How to use: +# cd hassio-google-drive-backup +# gcloud config set project hassio-drive-backup +# gcloud builds submit --config cloudbuild-dev.yaml --substitutions _DOCKERHUB_PASSWORD= + +steps: +- name: "gcr.io/cloud-builders/docker" + entrypoint: "bash" + args: ["-c", "docker login --username=sabeechen --password=${_DOCKERHUB_PASSWORD}"] +- name: 'gcr.io/cloud-builders/docker' + args: [ 'build', '-f', 'Dockerfile-addon', '-t', 'sabeechen/hassio-google-drive-backup-dev-amd64:${_VERSION}', "--build-arg", "BUILD_FROM=homeassistant/amd64-base", '.' ] +substitutions: + _DOCKERHUB_PASSWORD: "define me" # default value + _VERSION: "dev-testing" # default value +images: +- "sabeechen/hassio-google-drive-backup-dev-amd64:${_VERSION}" + \ No newline at end of file diff --git a/hassio-google-drive-backup/cloudbuild-server.yaml b/hassio-google-drive-backup/cloudbuild-server.yaml new file mode 100644 index 0000000..b0432e9 --- /dev/null +++ b/hassio-google-drive-backup/cloudbuild-server.yaml @@ -0,0 +1,22 @@ +# How to use: +# gcloud config set project hassio-drive-backup +# gcloud builds submit --config cloudbuild-server.yaml + +#steps: +#- name: 'gcr.io/cloud-builders/docker' +# args: [ 'build', '-f', 'Dockerfile-server', '-t', 'gcr.io/$PROJECT_ID/authserver', '.' ] +#images: +#- 'gcr.io/$PROJECT_ID/authserver' + +steps: +# Build the container image +- name: 'gcr.io/cloud-builders/docker' + args: ['build', '-f', 'Dockerfile-server', '-t', 'gcr.io/$PROJECT_ID/${_SERVICE_NAME}:${_VERSION}', '.'] +# Push the container image to Container Registry +- name: 'gcr.io/cloud-builders/docker' + args: ['push', 'gcr.io/$PROJECT_ID/${_SERVICE_NAME}:${_VERSION}'] +substitutions: + _SERVICE_NAME: "authserver-dev" # default value + _VERSION: "test-deployment" # default value +images: +- 'gcr.io/$PROJECT_ID/${_SERVICE_NAME}:${_VERSION}' \ No newline at end of file diff --git a/hassio-google-drive-backup/config.json b/hassio-google-drive-backup/config.json new file mode 100644 index 0000000..7674f2b --- /dev/null +++ b/hassio-google-drive-backup/config.json @@ -0,0 +1,106 @@ +{ + "name": "Home Assistant Google Drive Backup", + "version": "0.110.3", + "slug": "hassio_google_drive_backup", + "description": "Automatically manage backups between Home Assistant and Google Drive", + "arch": ["armhf", "armv7", "aarch64", "amd64", "i386"], + "url": "https://github.com/sabeechen/hassio-google-drive-backup", + "homeassistant_api": true, + "hassio_api": true, + "hassio_role": "manager", + "auth_api": true, + "ingress": true, + "panel_icon": "mdi:cloud", + "panel_title": "Backups", + "map": ["ssl", "backup:rw", "config"], + "options": { + "max_backups_in_ha": 4, + "max_backups_in_google_drive": 4, + "days_between_backups": 3 + }, + "schema": { + "max_backups_in_ha": "int(0,)?", + "max_backups_in_google_drive": "int(0,)?", + "days_between_backups": "float(0,)?", + "ignore_other_backups": "bool?", + "ignore_upgrade_backups": "bool?", + + "delete_after_upload": "bool?", + "delete_before_new_backup": "bool?", + "verbose": "bool?", + "use_ssl": "bool?", + "certfile": "str?", + "keyfile": "str?", + "require_login": "bool?", + + "backup_name": "str?", + "backup_time_of_day": "match(^[0-2]\\d:[0-5]\\d$)?", + "specify_backup_folder": "bool?", + "warn_for_low_space": "bool?", + "watch_backup_directory": "bool?", + "trace_requests": "bool?", + + "generational_days": "int(0,)?", + "generational_weeks": "int(0,)?", + "generational_months": "int(0,)?", + "generational_years": "int(0,)?", + "generational_day_of_year": "int(1,365)?", + "generational_day_of_month": "int(1,31)?", + "generational_day_of_week": "list(mon|tue|wed|thu|fri|sat|sun)?", + "generational_delete_early": "bool?", + + "notify_for_stale_backups": "bool?", + "enable_backup_stale_sensor": "bool?", + "enable_backup_state_sensor": "bool?", + "send_error_reports": "bool?", + "backup_password": "str?", + "exclude_folders": "str?", + "exclude_addons": "str?", + "stop_addons": "str?", + "disable_watchdog_when_stopping": "bool?", + "expose_extra_server": "bool?", + "drive_experimental": "bool?", + "drive_ipv4": "match(^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$)?", + "ignore_ipv6_addresses": "bool?", + "confirm_multiple_deletes": "bool?", + "google_drive_timeout_seconds": "float(1,)?", + "alternate_dns_servers": "match(^([0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3})(,[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3})*$)?", + "enable_drive_upload": "bool?", + "call_backup_snapshot": "bool?", + + "background_color": "match(^(#[0-9ABCDEFabcdef]{6}|)$)?", + "accent_color": "match(^(#[0-9ABCDEFabcdef]{6}|)$)?", + + "max_sync_interval_seconds": "float(300,)?", + "default_sync_interval_variation": "float(0,1)?", + "port": "int(0,)?", + "debugger_port": "int(100,)?", + "log_level": "list(DEBUG|TRACE|INFO|WARN|CRITICAL|WARNING)?", + "console_log_level": "list(DEBUG|TRACE|INFO|WARN|CRITICAL|WARNING)?", + "max_backoff_seconds": "int(3600,)?", + + "max_snapshots_in_hassio": "int(0,)?", + "max_snapshots_in_google_drive": "int(0,)?", + "days_between_snapshots": "float(0,)?", + "ignore_other_snapshots": "bool?", + "ignore_upgrade_snapshots": "bool?", + "delete_before_new_snapshot": "bool?", + "delete_ignored_after_days": "float(0,)?", + "snapshot_name": "str?", + "snapshot_time_of_day": "match(^[0-2]\\d:[0-5]\\d$)?", + "specify_snapshot_folder": "bool?", + "notify_for_stale_snapshots": "bool?", + "enable_snapshot_stale_sensor": "bool?", + "enable_snapshot_state_sensor": "bool?", + "snapshot_password": "str?", + "maximum_upload_chunk_bytes": "float(262144,)?", + "ha_reporting_interval_seconds": "int(1,)?" + }, + "ports": { + "1627/tcp": 1627 + }, + "ports_description": { + "1627/tcp": "Direct access to the add-on without ingress. Must be enabled in the settings, see 'expose_extra_server'." + }, + "image": "sabeechen/hassio-google-drive-backup-{arch}" +} diff --git a/hassio-google-drive-backup/dev/__init__.py b/hassio-google-drive-backup/dev/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/hassio-google-drive-backup/dev/apiingress.py b/hassio-google-drive-backup/dev/apiingress.py new file mode 100644 index 0000000..bbf2e85 --- /dev/null +++ b/hassio-google-drive-backup/dev/apiingress.py @@ -0,0 +1,404 @@ +from injector import singleton, inject +import asyncio +from ipaddress import ip_address +from typing import Any, Dict, Union, Optional + +import aiohttp +from aiohttp import hdrs, web, ClientSession +from aiohttp.web_exceptions import ( + HTTPBadGateway, + HTTPServiceUnavailable, + HTTPUnauthorized, + HTTPNotFound +) +from multidict import CIMultiDict, istr + +from backup.logger import getLogger +from .ports import Ports +from .base_server import BaseServer +from .simulated_supervisor import SimulatedSupervisor + +ATTR_ADMIN = "admin" +ATTR_ENABLE = "enable" +ATTR_ICON = "icon" +ATTR_PANELS = "panels" +ATTR_SESSION = "session" +ATTR_TITLE = "title" +COOKIE_INGRESS = "ingress_session" +HEADER_TOKEN = "X-Supervisor-Token" +HEADER_TOKEN_OLD = "X-Hassio-Key" +REQUEST_FROM = "HASSIO_FROM" +JSON_RESULT = "result" +JSON_DATA = "data" +JSON_MESSAGE = "message" +RESULT_ERROR = "error" +RESULT_OK = "ok" + +_LOGGER = getLogger(__name__) + + +def api_return_error(message: Optional[str] = None) -> web.Response: + """Return an API error message.""" + return web.json_response( + {JSON_RESULT: RESULT_ERROR, JSON_MESSAGE: message}, status=400 + ) + + +def api_return_ok(data: Optional[Dict[str, Any]] = None) -> web.Response: + """Return an API ok answer.""" + return web.json_response({JSON_RESULT: RESULT_OK, JSON_DATA: data or {}}) + + +def api_process(method): + """Wrap function with true/false calls to rest api.""" + + async def wrap_api(api, *args, **kwargs): + """Return API information.""" + try: + answer = await method(api, *args, **kwargs) + except Exception as err: + return api_return_error(message=str(err)) + + if isinstance(answer, dict): + return api_return_ok(data=answer) + if isinstance(answer, web.Response): + return answer + elif isinstance(answer, bool) and not answer: + return api_return_error() + return api_return_ok() + + return wrap_api + + +class Addon(): + def __init__(self, ports: Ports, token: str): + self.ports = ports + self.ip_address = "127.0.0.1" + self.ingress_port = ports.ingress + self.token = token + + +class SysIngress(): + def __init__(self, ports: Ports, token: str, cookie_value: str): + self.ports = ports + self.token = token + self.cookie_value = cookie_value + + def validate_session(self, session): + return session == self.cookie_value + + def get(self, token): + if token == self.token: + return Addon(self.ports, self.token) + return None + + +class CoreSysAttributes(): + def __init__(self, ports: Ports, session: ClientSession, token: str, cookie_value: str): + self.sys_ingress = SysIngress(ports, token, cookie_value) + self.sys_websession = session + + +@singleton +class APIIngress(CoreSysAttributes, BaseServer): + @inject + def __init__(self, ports: Ports, session: ClientSession, supervisor: SimulatedSupervisor): + self.addon_token = self.generateId(10) + self.cookie_value = self.generateId(10) + super().__init__(ports, session, self.addon_token, self.cookie_value) + self.ports = ports + self.supervisor = supervisor + + def routes(self): + return [ + web.get("/startingress", self.start_ingress), + web.get("/hassio/ingress/{slug}", self.ingress_panel), + web.view("/api/hassio_ingress/{token}/{path:.*}", self.handler), + ] + + def start_ingress(self, request: web.Request): + resp = web.Response(status=303) + resp.headers[hdrs.LOCATION] = "/hassio/ingress/" + self.supervisor._addon_slug + resp.set_cookie(name=COOKIE_INGRESS, value=self.cookie_value, expires="Session", domain=request.url.host, path="/api/hassio_ingress/", httponly="false", secure="false") + return resp + + def ingress_panel(self, request: web.Request): + slug = request.match_info.get("slug") + if slug != self.supervisor._addon_slug: + raise HTTPNotFound() + body = """ + + + + + Simulated Supervisor Ingress Panel + + + +
      + The Web-UI below is loaded through an iframe. Start a new ingress session if you get permission errors. +
      + + + + """.format(self.addon_token) + resp = web.Response(body=body, content_type="text/html") + resp.set_cookie(name=COOKIE_INGRESS, value=self.cookie_value, expires="Session", domain=request.url.host, path="/api/hassio_ingress/", httponly="false", secure="false") + return resp + + """ + The class body below here is copied from + https://github.com/home-assistant/supervisor/blob/38b0aea8e2a3b9a9614bb5d94959235a0fae235e/supervisor/api/ingress.py#L35 + In order to correctly reproduce the supervisor's kooky ingress proxy behavior. + """ + + def _extract_addon(self, request: web.Request) -> Addon: + """Return addon, throw an exception it it doesn't exist.""" + token = request.match_info.get("token") + + # Find correct add-on + addon = self.sys_ingress.get(token) + if not addon: + _LOGGER.warning("Ingress for %s not available", token) + raise HTTPServiceUnavailable() + + return addon + + def _check_ha_access(self, request: web.Request) -> None: + # always allow + pass + + def _create_url(self, addon: Addon, path: str) -> str: + """Create URL to container.""" + return f"http://{addon.ip_address}:{addon.ingress_port}/{path}" + + @api_process + async def panels(self, request: web.Request) -> Dict[str, Any]: + """Create a list of panel data.""" + addons = {} + for addon in self.sys_ingress.addons: + addons[addon.slug] = { + ATTR_TITLE: addon.panel_title, + ATTR_ICON: addon.panel_icon, + ATTR_ADMIN: addon.panel_admin, + ATTR_ENABLE: addon.ingress_panel, + } + + return {ATTR_PANELS: addons} + + @api_process + async def create_session(self, request: web.Request) -> Dict[str, Any]: + """Create a new session.""" + self._check_ha_access(request) + + session = self.sys_ingress.create_session() + return {ATTR_SESSION: session} + + async def handler( + self, request: web.Request + ) -> Union[web.Response, web.StreamResponse, web.WebSocketResponse]: + """Route data to Supervisor ingress service.""" + self._check_ha_access(request) + + # Check Ingress Session + session = request.cookies.get(COOKIE_INGRESS) + if not self.sys_ingress.validate_session(session): + _LOGGER.warning("No valid ingress session %s", session) + raise HTTPUnauthorized() + + # Process requests + addon = self._extract_addon(request) + path = request.match_info.get("path") + try: + # Websocket + if _is_websocket(request): + return await self._handle_websocket(request, addon, path) + + # Request + return await self._handle_request(request, addon, path) + + except aiohttp.ClientError as err: + _LOGGER.error("Ingress error: %s", err) + + raise HTTPBadGateway() + + async def _handle_websocket( + self, request: web.Request, addon: Addon, path: str + ) -> web.WebSocketResponse: + """Ingress route for websocket.""" + if hdrs.SEC_WEBSOCKET_PROTOCOL in request.headers: + req_protocols = [ + str(proto.strip()) + for proto in request.headers[hdrs.SEC_WEBSOCKET_PROTOCOL].split(",") + ] + else: + req_protocols = () + + ws_server = web.WebSocketResponse( + protocols=req_protocols, autoclose=False, autoping=False + ) + await ws_server.prepare(request) + + # Preparing + url = self._create_url(addon, path) + source_header = _init_header(request, addon) + + # Support GET query + if request.query_string: + url = f"{url}?{request.query_string}" + + # Start proxy + async with self.sys_websession.ws_connect( + url, + headers=source_header, + protocols=req_protocols, + autoclose=False, + autoping=False, + ) as ws_client: + # Proxy requests + await asyncio.wait( + [ + _websocket_forward(ws_server, ws_client), + _websocket_forward(ws_client, ws_server), + ], + return_when=asyncio.FIRST_COMPLETED, + ) + + return ws_server + + async def _handle_request( + self, request: web.Request, addon: Addon, path: str + ) -> Union[web.Response, web.StreamResponse]: + """Ingress route for request.""" + url = self._create_url(addon, path) + data = await request.read() + source_header = _init_header(request, addon) + + async with self.sys_websession.request( + request.method, + url, + headers=source_header, + params=request.query, + allow_redirects=False, + data=data, + ) as result: + headers = _response_header(result) + + # Simple request + if ( + hdrs.CONTENT_LENGTH in result.headers and int(result.headers.get(hdrs.CONTENT_LENGTH, 0)) < 4_194_000 + ): + # Return Response + body = await result.read() + + return web.Response( + headers=headers, + status=result.status, + content_type=result.content_type, + body=body, + ) + + # Stream response + response = web.StreamResponse(status=result.status, headers=headers) + response.content_type = result.content_type + + try: + await response.prepare(request) + async for data in result.content.iter_chunked(4096): + await response.write(data) + + except ( + aiohttp.ClientError, + aiohttp.ClientPayloadError, + ConnectionResetError, + ) as err: + _LOGGER.error("Stream error with %s: %s", url, err) + + return response + + +def _init_header( + request: web.Request, addon: str +) -> Union[CIMultiDict, Dict[str, str]]: + """Create initial header.""" + headers = {} + + # filter flags + for name, value in request.headers.items(): + if name in ( + hdrs.CONTENT_LENGTH, + hdrs.CONTENT_ENCODING, + hdrs.SEC_WEBSOCKET_EXTENSIONS, + hdrs.SEC_WEBSOCKET_PROTOCOL, + hdrs.SEC_WEBSOCKET_VERSION, + hdrs.SEC_WEBSOCKET_KEY, + istr(HEADER_TOKEN), + istr(HEADER_TOKEN_OLD), + ): + continue + headers[name] = value + + # Update X-Forwarded-For + forward_for = request.headers.get(hdrs.X_FORWARDED_FOR) + connected_ip = ip_address(request.transport.get_extra_info("peername")[0]) + headers[hdrs.X_FORWARDED_FOR] = f"{forward_for}, {connected_ip!s}" + + return headers + + +def _response_header(response: aiohttp.ClientResponse) -> Dict[str, str]: + """Create response header.""" + headers = {} + + for name, value in response.headers.items(): + if name in ( + hdrs.TRANSFER_ENCODING, + hdrs.CONTENT_LENGTH, + hdrs.CONTENT_TYPE, + hdrs.CONTENT_ENCODING + ): + continue + headers[name] = value + + return headers + + +def _is_websocket(request: web.Request) -> bool: + """Return True if request is a websocket.""" + headers = request.headers + + if ( + "upgrade" in headers.get(hdrs.CONNECTION, "").lower() and headers.get(hdrs.UPGRADE, "").lower() == "websocket" + ): + return True + return False + + +async def _websocket_forward(ws_from, ws_to): + """Handle websocket message directly.""" + try: + async for msg in ws_from: + if msg.type == aiohttp.WSMsgType.TEXT: + await ws_to.send_str(msg.data) + elif msg.type == aiohttp.WSMsgType.BINARY: + await ws_to.send_bytes(msg.data) + elif msg.type == aiohttp.WSMsgType.PING: + await ws_to.ping() + elif msg.type == aiohttp.WSMsgType.PONG: + await ws_to.pong() + elif ws_to.closed: + await ws_to.close(code=ws_to.close_code, message=msg.extra) + except RuntimeError: + _LOGGER.warning("Ingress Websocket runtime error") diff --git a/hassio-google-drive-backup/dev/backup/keep_me_here.txt b/hassio-google-drive-backup/dev/backup/keep_me_here.txt new file mode 100644 index 0000000..4906368 --- /dev/null +++ b/hassio-google-drive-backup/dev/backup/keep_me_here.txt @@ -0,0 +1 @@ +Created just so git doesn't ignore the directory \ No newline at end of file diff --git a/hassio-google-drive-backup/dev/base_server.py b/hassio-google-drive-backup/dev/base_server.py new file mode 100644 index 0000000..8c80085 --- /dev/null +++ b/hassio-google-drive-backup/dev/base_server.py @@ -0,0 +1,56 @@ +import random +import re +import io +from aiohttp.web import HTTPBadRequest, Request, Response +from typing import Any + +rangePattern = re.compile("bytes=\\d+-\\d+") +bytesPattern = re.compile("^bytes \\d+-\\d+/\\d+$") +intPattern = re.compile("\\d+") + + +class BaseServer: + def generateId(self, length: int = 30) -> str: + random_int = random.randint(0, 1000000) + ret = str(random_int) + return ret + ''.join(map(lambda x: str(x), range(0, length - len(ret)))) + + def timeToRfc3339String(self, time) -> str: + return time.strftime("%Y-%m-%dT%H:%M:%SZ") + + def serve_bytes(self, request: Request, bytes: bytearray, include_length: bool = True) -> Any: + if "Range" in request.headers: + # Do range request + if not rangePattern.match(request.headers['Range']): + raise HTTPBadRequest() + + numbers = intPattern.findall(request.headers['Range']) + start = int(numbers[0]) + end = int(numbers[1]) + + if start < 0: + raise HTTPBadRequest() + if start > end: + raise HTTPBadRequest() + if end > len(bytes) - 1: + raise HTTPBadRequest() + resp = Response(body=bytes[start:end + 1], status=206) + resp.headers['Content-Range'] = "bytes {0}-{1}/{2}".format( + start, end, len(bytes)) + if include_length: + resp.headers["Content-length"] = str(len(bytes)) + return resp + else: + resp = Response(body=io.BytesIO(bytes)) + resp.headers["Content-length"] = str(len(bytes)) + return resp + + async def readAll(self, request): + data = bytearray() + content = request.content + while True: + chunk, done = await content.readchunk() + data.extend(chunk) + if len(chunk) == 0: + break + return data diff --git a/hassio-google-drive-backup/dev/data/dev_addon_config.yaml b/hassio-google-drive-backup/dev/data/dev_addon_config.yaml new file mode 100644 index 0000000..661b974 --- /dev/null +++ b/hassio-google-drive-backup/dev/data/dev_addon_config.yaml @@ -0,0 +1,3 @@ +authorization_host: "https://dev.habackup.io" +token_server_hosts: "https://token1.dev.habackup.io,https://dev.habackup.io" +default_drive_client_id: "795575624694-jcdhoh1jr1ngccfsbi2f44arr4jupl79.apps.googleusercontent.com" \ No newline at end of file diff --git a/hassio-google-drive-backup/dev/data/dev_options.json b/hassio-google-drive-backup/dev/data/dev_options.json new file mode 100644 index 0000000..f538960 --- /dev/null +++ b/hassio-google-drive-backup/dev/data/dev_options.json @@ -0,0 +1,28 @@ +{ + "drive_url": "http://localhost:56153", + "supervisor_url": "http://localhost:56153/", + "hassio_header": "test_header", + "retained_file_path": "hassio-google-drive-backup/dev/data/retained.json", + "data_cache_file_path": "hassio-google-drive-backup/dev/data/data_cache.json", + "backup_directory_path": "hassio-google-drive-backup/dev/backup", + "certfile": "hassio-google-drive-backup/dev/ssl/fullchain.pem", + "keyfile": "hassio-google-drive-backup/dev/ssl/privkey.pem", + "secrets_file_path": "hassio-google-drive-backup/dev/data/secrets.yaml", + "credentials_file_path": "hassio-google-drive-backup/dev/data/credentials.dat", + "folder_file_path": "hassio-google-drive-backup/dev/data/folder.dat", + "id_file_path": "hassio-google-drive-backup/dev/data/id.json", + "stop_addon_state_path": "hassio-google-drive-backup/dev/data/stop_addon_state.json", + "authorization_host": "http://localhost:56153", + "token_server_hosts": "http://localhost:56153", + "drive_refresh_url": "http://localhost:56153/oauth2/v4/token", + "drive_authorize_url": "http://localhost:56153/o/oauth2/v2/auth", + "drive_device_code_url": "http://localhost:56153/device/code", + "drive_token_url": "http://localhost:56153/token", + "ingress_token_file_path": "hassio-google-drive-backup/dev/data/ingress.dat", + "log_level": "TRACE", + "console_log_level": "TRACE", + "ingress_port": 56152, + "port": 56151, + "max_sync_interval_seconds": 600, + "cache_warmup_max_seconds": 300 +} \ No newline at end of file diff --git a/hassio-google-drive-backup/dev/data/drive_dev_options.json b/hassio-google-drive-backup/dev/data/drive_dev_options.json new file mode 100644 index 0000000..6e5efba --- /dev/null +++ b/hassio-google-drive-backup/dev/data/drive_dev_options.json @@ -0,0 +1,20 @@ +{ + "supervisor_url": "http://localhost:56153/", + "authorization_host": "https://dev.habackup.io", + "token_server_hosts": "https://token1.dev.habackup.io,https://dev.habackup.io", + "hassio_header": "test_header", + "data_cache_file_path": "hassio-google-drive-backup/dev/data/data_cache.json", + "retained_file_path": "hassio-google-drive-backup/dev/data/retained.json", + "backup_directory_path": "hassio-google-drive-backup/dev/backup", + "certfile": "hassio-google-drive-backup/dev/ssl/fullchain.pem", + "keyfile": "hassio-google-drive-backup/dev/ssl/privkey.pem", + "secrets_file_path": "hassio-google-drive-backup/dev/data/secrets.yaml", + "credentials_file_path": "hassio-google-drive-backup/dev/data/credentials.dat", + "folder_file_path": "hassio-google-drive-backup/dev/data/folder.dat", + "id_file_path": "hassio-google-drive-backup/dev/data/id.json", + "stop_addon_state_path": "hassio-google-drive-backup/dev/data/stop_addon_state.json", + "ingress_token_file_path": "hassio-google-drive-backup/dev/data/ingress.dat", + "default_drive_client_id": "795575624694-jcdhoh1jr1ngccfsbi2f44arr4jupl79.apps.googleusercontent.com", + "ingress_port": 56152, + "port": 56151 +} \ No newline at end of file diff --git a/hassio-google-drive-backup/dev/data/drive_options.json b/hassio-google-drive-backup/dev/data/drive_options.json new file mode 100644 index 0000000..48c9545 --- /dev/null +++ b/hassio-google-drive-backup/dev/data/drive_options.json @@ -0,0 +1,17 @@ +{ + "supervisor_url": "http://localhost:56153/", + "hassio_header": "test_header", + "data_cache_file_path": "hassio-google-drive-backup/dev/data/data_cache.json", + "retained_file_path": "hassio-google-drive-backup/dev/data/retained.json", + "backup_directory_path": "hassio-google-drive-backup/dev/backup", + "certfile": "hassio-google-drive-backup/dev/ssl/fullchain.pem", + "keyfile": "hassio-google-drive-backup/dev/ssl/privkey.pem", + "secrets_file_path": "hassio-google-drive-backup/dev/data/secrets.yaml", + "credentials_file_path": "hassio-google-drive-backup/dev/data/credentials.dat", + "folder_file_path": "hassio-google-drive-backup/dev/data/folder.dat", + "ingress_token_file_path": "hassio-google-drive-backup/dev/data/ingress.dat", + "id_file_path": "hassio-google-drive-backup/dev/data/id.json", + "stop_addon_state_path": "hassio-google-drive-backup/dev/data/stop_addon_state.json", + "ingress_port": 56155, + "port": 56156 +} \ No newline at end of file diff --git a/hassio-google-drive-backup/dev/data/options.json b/hassio-google-drive-backup/dev/data/options.json new file mode 100644 index 0000000..5773758 --- /dev/null +++ b/hassio-google-drive-backup/dev/data/options.json @@ -0,0 +1,11 @@ +{ + "send_error_reports": true, + "max_backups_in_ha": 4, + "max_backups_in_google_drive": 3, + "days_between_backups": 10, + "use_ssl": false, + "backup_name": "{type} Backup {year}-{month}-{day} {hr24}:{min}:{sec}", + "backup_password": "!secret password1", + "drive_experimental": true, + "drive_ipv4": "" +} \ No newline at end of file diff --git a/hassio-google-drive-backup/dev/data/secrets.yaml b/hassio-google-drive-backup/dev/data/secrets.yaml new file mode 100644 index 0000000..ffb471e --- /dev/null +++ b/hassio-google-drive-backup/dev/data/secrets.yaml @@ -0,0 +1,2 @@ +password1: "Test value" +for_unit_tests: "password value" \ No newline at end of file diff --git a/hassio-google-drive-backup/dev/deploy.sh b/hassio-google-drive-backup/dev/deploy.sh new file mode 100755 index 0000000..0d47688 --- /dev/null +++ b/hassio-google-drive-backup/dev/deploy.sh @@ -0,0 +1,6 @@ +#!/bin/bash +sudo docker run --rm --privileged \ + -v /home/coder/.docker:/root/.docker \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v ..:/data \ + homeassistant/amd64-builder --all -t /data \ No newline at end of file diff --git a/hassio-google-drive-backup/dev/deploy_addon.py b/hassio-google-drive-backup/dev/deploy_addon.py new file mode 100644 index 0000000..af065d5 --- /dev/null +++ b/hassio-google-drive-backup/dev/deploy_addon.py @@ -0,0 +1,19 @@ +import subprocess +import os +import json +from os.path import abspath, join + +with open(abspath(join(__file__, "..", "..", "config.json"))) as f: + version = json.load(f)["version"] +print("Version will be: " + version) +subprocess.run("docker login", shell=True) + + +platforms = ["amd64", "armv7", "aarch64", "armhf", "i386"] + +os.chdir("hassio-google-drive-backup") +for platform in platforms: + subprocess.run("docker build -f Dockerfile-addon -t sabeechen/hassio-google-drive-backup-{0}:{1} --build-arg BUILD_FROM=homeassistant/{0}-base .".format(platform, version), shell=True) + +for platform in platforms: + subprocess.run("docker push sabeechen/hassio-google-drive-backup-{0}:{1}".format(platform, version), shell=True) diff --git a/hassio-google-drive-backup/dev/deploy_dev_addon.py b/hassio-google-drive-backup/dev/deploy_dev_addon.py new file mode 100644 index 0000000..badce61 --- /dev/null +++ b/hassio-google-drive-backup/dev/deploy_dev_addon.py @@ -0,0 +1,20 @@ +import getpass +import subprocess +import os +import json +from os.path import abspath, join + +with open(abspath(join(__file__, "..", "..", "config.json"))) as f: + version = json.load(f)["version"] + +try: + p = getpass.getpass("Enter DockerHub Password") +except Exception as error: + print('ERROR', error) + exit() + +os.chdir("hassio-google-drive-backup") +print("Setting the appropriate gcloud project...") +subprocess.run("gcloud config set project hassio-drive-backup", shell=True) +print("Building and uploading dev container...") +subprocess.run("gcloud builds submit --config cloudbuild-dev.yaml --substitutions _DOCKERHUB_PASSWORD={0},_VERSION={1}".format(p, version), shell=True) diff --git a/hassio-google-drive-backup/dev/deploy_dev_server.py b/hassio-google-drive-backup/dev/deploy_dev_server.py new file mode 100644 index 0000000..cd03597 --- /dev/null +++ b/hassio-google-drive-backup/dev/deploy_dev_server.py @@ -0,0 +1,8 @@ +import subprocess +import os + +os.chdir("hassio-google-drive-backup") +print("Setting the appropriate gcloud project...") +subprocess.run("gcloud config set project hassio-drive-backup-dev", shell=True) +print("Building and uploading server container...") +subprocess.run("gcloud builds submit --config cloudbuild-server.yaml", shell=True) diff --git a/hassio-google-drive-backup/dev/deploy_server.py b/hassio-google-drive-backup/dev/deploy_server.py new file mode 100644 index 0000000..936f938 --- /dev/null +++ b/hassio-google-drive-backup/dev/deploy_server.py @@ -0,0 +1,8 @@ +import subprocess +import os + +os.chdir("hassio-google-drive-backup") +print("Setting the appropriate gcloud project...") +subprocess.run("gcloud config set project hassio-drive-backup", shell=True) +print("Building and uploading server container...") +subprocess.run("gcloud builds submit --config cloudbuild-server.yaml", shell=True) diff --git a/hassio-google-drive-backup/dev/http_exception.py b/hassio-google-drive-backup/dev/http_exception.py new file mode 100644 index 0000000..6a03e19 --- /dev/null +++ b/hassio-google-drive-backup/dev/http_exception.py @@ -0,0 +1,6 @@ +from aiohttp.web import HTTPClientError + + +class HttpMultiException(HTTPClientError): + def __init__(self, code): + self.status_code = code diff --git a/hassio-google-drive-backup/dev/ports.py b/hassio-google-drive-backup/dev/ports.py new file mode 100644 index 0000000..c0ebae7 --- /dev/null +++ b/hassio-google-drive-backup/dev/ports.py @@ -0,0 +1,5 @@ +class Ports: + def __init__(self, server, ui, ingress): + self.server = server + self.ui = ui + self.ingress = ingress diff --git a/hassio-google-drive-backup/dev/request_interceptor.py b/hassio-google-drive-backup/dev/request_interceptor.py new file mode 100644 index 0000000..d5e573d --- /dev/null +++ b/hassio-google-drive-backup/dev/request_interceptor.py @@ -0,0 +1,135 @@ +import re +from aiohttp.web import Request, Response +from asyncio import Event +from aiohttp.web_response import json_response +from injector import singleton, inject +from backup.time import Time + + +class UrlMatch(): + def __init__(self, time: Time, url, fail_after=None, status=None, response=None, wait=False, sleep=None, fail_for=None): + self.time = time + self.url: str = url + self.fail_after: int = fail_after + self.status: int = status + self.wait_event: Event = Event() + self.trigger_event: Event = Event() + self.response: str = "" + self.wait: bool = wait + self.trigger_event.clear() + self.wait_event.clear() + self.sleep = sleep + self.response = response + self.fail_for = fail_for + self.responses = [] + self._calls = 0 + self.time = time + + def addResponse(self, response): + self.responses.append(response) + + def stop(self): + self.wait_event.set() + self.trigger_event.set() + + def isMatch(self, request): + return re.match(self.url, request.url.path) + + async def waitForCall(self): + await self.trigger_event.wait() + + def clear(self): + self.wait_event.set() + + def callCount(self): + return self._calls + + async def _doAction(self, request: Request): + self._calls += 1 + if len(self.responses) > 0: + return self.responses.pop(0) + if self.status is not None: + await self._readAll(request) + if self.response: + return json_response(self.response, status=self.status) + else: + return Response(status=self.status) + elif self.wait: + self.trigger_event.set() + await self.wait_event.wait() + elif self.sleep is not None: + await self.time.sleepAsync(self.sleep, early_exit=self.wait_event) + + async def called(self, request: Request): + if self.fail_after is None or self.fail_after <= 0: + if self.fail_for is not None and self.fail_for > 0: + self.fail_for -= 1 + return await self._doAction(request) + elif self.fail_for is not None: + return None + + return await self._doAction(request) + elif self.fail_after is not None: + self.fail_after -= 1 + + async def _readAll(self, request: Request): + data = bytearray() + content = request.content + while True: + chunk, done = await content.readchunk() + data.extend(chunk) + if len(chunk) == 0: + break + return data + + +@singleton +class RequestInterceptor: + @inject + def __init__(self): + self._matchers = [] + self._history = [] + self.time = Time() + + def stop(self): + for matcher in self._matchers: + matcher.stop() + + def setError(self, url, status=None, fail_after=None, fail_for=None, response=None) -> UrlMatch: + matcher = UrlMatch(self.time, url, fail_after, status=status, response=response, fail_for=fail_for) + self._matchers.append(matcher) + return matcher + + def clear(self): + self._matchers.clear() + self._history.clear() + + def setWaiter(self, url, attempts=None): + matcher = UrlMatch(self.time, url, attempts, wait=True) + self._matchers.append(matcher) + return matcher + + def setSleep(self, url, attempts=None, sleep=None, wait_for=None): + matcher = UrlMatch(self.time, url, attempts, sleep=sleep, fail_for=wait_for) + self._matchers.append(matcher) + return matcher + + async def checkUrl(self, request): + ret = None + self.record(request) + for match in self._matchers: + if match.isMatch(request): + ret = await match.called(request) + return ret + + def record(self, request: Request): + record = str(request.url.path) + if len(request.url.query_string) > 0: + record += "?" + str(request.url.query_string) + self._history.append(record) + + def urlWasCalled(self, url) -> bool: + for called_url in self._history: + if url == called_url or re.match(url, called_url): + return True + return False diff --git a/hassio-google-drive-backup/dev/simulated_google.py b/hassio-google-drive-backup/dev/simulated_google.py new file mode 100644 index 0000000..979843f --- /dev/null +++ b/hassio-google-drive-backup/dev/simulated_google.py @@ -0,0 +1,521 @@ +import re + +from yarl import URL +from datetime import timedelta +from backup.logger import getLogger +from backup.config import Setting, Config +from backup.time import Time +from backup.creds import KEY_CLIENT_SECRET, KEY_CLIENT_ID, KEY_ACCESS_TOKEN, KEY_TOKEN_EXPIRY +from aiohttp.web import (HTTPBadRequest, HTTPNotFound, + HTTPUnauthorized, Request, Response, delete, get, + json_response, patch, post, put, HTTPSeeOther) +from injector import inject, singleton +from .base_server import BaseServer, bytesPattern, intPattern +from .ports import Ports +from typing import Any, Dict +from asyncio import Event +from backup.creds import Creds + +logger = getLogger(__name__) + +mimeTypeQueryPattern = re.compile("^mimeType='.*'$") +parentsQueryPattern = re.compile("^'.*' in parents$") +resumeBytesPattern = re.compile("^bytes \\*/\\d+$") + +URL_MATCH_DRIVE_API = "^.*drive.*$" +URL_MATCH_UPLOAD = "^/upload/drive/v3/files/$" +URL_MATCH_UPLOAD_PROGRESS = "^/upload/drive/v3/files/progress/.*$" +URL_MATCH_CREATE = "^/upload/drive/v3/files/progress/.*$" +URL_MATCH_FILE = "^/drive/v3/files/.*$" +URL_MATCH_DEVICE_CODE = "^/device/code$" +URL_MATCH_TOKEN = "^/token$" + + +@singleton +class SimulatedGoogle(BaseServer): + @inject + def __init__(self, config: Config, time: Time, ports: Ports): + self._time = time + self.config = config + + # auth state + self._custom_drive_client_id = self.generateId(5) + self._custom_drive_client_secret = self.generateId(5) + self._custom_drive_client_expiration = None + self._drive_auth_code = "drive_auth_code" + self._port = ports.server + self._auth_token = "" + self._refresh_token = "test_refresh_token" + self._client_id_hack = None + + # Drive item states + self.items = {} + self.lostPermission = [] + self.space_available = 1024 * 1024 * 100 # 100 Mb + + # Upload state information + self._upload_info: Dict[str, Any] = {} + self.chunks = [] + self._upload_chunk_wait = Event() + self._upload_chunk_trigger = Event() + self._current_chunk = 1 + self._waitOnChunk = 0 + self.device_auth_params = {} + self._device_code_accepted = None + + def setDriveSpaceAvailable(self, bytes_available): + self.space_available = bytes_available + + def generateNewAccessToken(self): + new_token = self.generateId(20) + self._auth_token = new_token + + def generateNewRefreshToken(self): + new_token = self.generateId(20) + self._refresh_token = new_token + + def expireCreds(self): + self.generateNewAccessToken() + self.generateNewRefreshToken() + + def expireRefreshToken(self): + self.generateNewRefreshToken() + + def resetDriveAuth(self): + self.expireCreds() + self.config.override(Setting.DEFAULT_DRIVE_CLIENT_ID, self.generateId(5)) + self.config.override(Setting.DEFAULT_DRIVE_CLIENT_SECRET, self.generateId(5)) + + def creds(self): + return Creds(self._time, + id=self.config.get(Setting.DEFAULT_DRIVE_CLIENT_ID), + expiration=self._time.now() + timedelta(hours=1), + access_token=self._auth_token, + refresh_token=self._refresh_token) + + def routes(self): + return [ + put('/upload/drive/v3/files/progress/{id}', self._uploadProgress), + post('/upload/drive/v3/files/', self._upload), + post('/drive/v3/files/', self._create), + get('/drive/v3/files/', self._query), + delete('/drive/v3/files/{id}/', self._delete), + patch('/drive/v3/files/{id}/', self._update), + get('/drive/v3/files/{id}/', self._get), + post('/oauth2/v4/token', self._oauth2Token), + get('/o/oauth2/v2/auth', self._oAuth2Authorize), + get('/drive/customcreds', self._getCustomCred), + get('/drive/v3/about', self._driveAbout), + post('/device/code', self._deviceCode), + get('/device', self._device), + get('/debug/google', self._debug), + post('/token', self._driveToken), + ] + + async def _debug(self, request: Request): + return json_response({ + "custom_drive_client_id": self._custom_drive_client_id, + "custom_drive_client_secret": self._custom_drive_client_secret, + "device_auth_params": self.device_auth_params + }) + + async def _checkDriveHeaders(self, request: Request): + if request.headers.get("Authorization", "") != "Bearer " + self._auth_token: + raise HTTPUnauthorized() + + async def _deviceCode(self, request: Request): + params = await request.post() + client_id = params['client_id'] + scope = params['scope'] + if client_id != self._custom_drive_client_id or scope != 'https://www.googleapis.com/auth/drive.file': + raise HTTPUnauthorized() + + self.device_auth_params = { + 'device_code': self.generateId(10), + 'expires_in': 60, + 'interval': 1, + 'user_code': self.generateId(8), + 'verification_url': str(URL("http://localhost").with_port(self._port).with_path("device")) + } + self._device_code_accepted = None + return json_response(self.device_auth_params) + + async def _device(self, request: Request): + code = request.query.get('code') + if code: + if self.device_auth_params.get('user_code', "dfsdfsdfsdfs") == code: + body = "Accepted" + self._device_code_accepted = True + self.generateNewRefreshToken() + self.generateNewAccessToken() + else: + body = "Wrong code" + else: + body = """ + + + + + Simulated Drive Device Authorization + + +
      + Enter the device code provided below +
      +
      +
      + + +
      + + + """ + resp = Response(body=body, content_type="text/html") + return resp + + async def _oAuth2Authorize(self, request: Request): + query = request.query + if query.get('client_id') != self.config.get(Setting.DEFAULT_DRIVE_CLIENT_ID) and query.get('client_id') != self._custom_drive_client_id: + raise HTTPUnauthorized() + if query.get('scope') != 'https://www.googleapis.com/auth/drive.file': + raise HTTPUnauthorized() + if query.get('response_type') != 'code': + raise HTTPUnauthorized() + if query.get('include_granted_scopes') != 'true': + raise HTTPUnauthorized() + if query.get('access_type') != 'offline': + raise HTTPUnauthorized() + if 'state' not in query: + raise HTTPUnauthorized() + if 'redirect_uri' not in query: + raise HTTPUnauthorized() + if query.get('prompt') != 'consent': + raise HTTPUnauthorized() + if query.get('redirect_uri') == 'urn:ietf:wg:oauth:2.0:oob': + return json_response({"code": self._drive_auth_code}) + url = URL(query.get('redirect_uri')).with_query({'code': self._drive_auth_code, 'state': query.get('state')}) + raise HTTPSeeOther(str(url)) + + async def _getCustomCred(self, request: Request): + return json_response({ + "client_id": self._custom_drive_client_id, + "client_secret": self._custom_drive_client_secret + }) + + async def _driveToken(self, request: Request): + data = await request.post() + if not self._checkClientIdandSecret(data.get('client_id'), data.get('client_secret')): + raise HTTPUnauthorized() + if data.get('grant_type') == 'authorization_code': + if data.get('redirect_uri') not in ["http://localhost:{}/drive/authorize".format(self._port), 'urn:ietf:wg:oauth:2.0:oob']: + raise HTTPUnauthorized() + if data.get('code') != self._drive_auth_code: + raise HTTPUnauthorized() + elif data.get('grant_type') == 'urn:ietf:params:oauth:grant-type:device_code': + if data.get('device_code') != self.device_auth_params['device_code']: + raise HTTPUnauthorized() + if self._device_code_accepted is None: + return json_response({ + "error": "authorization_pending", + "error_description": "Precondition Required" + }, status=428) + elif self._device_code_accepted is False: + raise HTTPUnauthorized() + else: + raise HTTPBadRequest() + self.generateNewRefreshToken() + resp = { + 'access_token': self._auth_token, + 'refresh_token': self._refresh_token, + KEY_CLIENT_ID: data.get('client_id'), + KEY_CLIENT_SECRET: self.config.get(Setting.DEFAULT_DRIVE_CLIENT_SECRET), + KEY_TOKEN_EXPIRY: self.timeToRfc3339String(self._time.now()), + } + if self._custom_drive_client_expiration is not None: + resp[KEY_TOKEN_EXPIRY] = self.timeToRfc3339String(self._custom_drive_client_expiration) + return json_response(resp) + + def _checkClientIdandSecret(self, client_id: str, client_secret: str) -> bool: + if self._custom_drive_client_id == client_id and self._custom_drive_client_secret == client_secret: + return True + if client_id == self.config.get(Setting.DEFAULT_DRIVE_CLIENT_ID) == client_id and client_secret == self.config.get(Setting.DEFAULT_DRIVE_CLIENT_SECRET): + return True + + if self._client_id_hack is not None: + if client_id == self._client_id_hack and client_secret == self.config.get(Setting.DEFAULT_DRIVE_CLIENT_SECRET): + return True + return False + + async def _oauth2Token(self, request: Request): + params = await request.post() + if not self._checkClientIdandSecret(params['client_id'], params['client_secret']): + raise HTTPUnauthorized() + if params['refresh_token'] != self._refresh_token: + raise HTTPUnauthorized() + if params['grant_type'] == 'refresh_token': + self.generateNewAccessToken() + return json_response({ + 'access_token': self._auth_token, + 'expires_in': 3600, + 'token_type': 'doesn\'t matter' + }) + elif params['grant_type'] == 'urn:ietf:params:oauth:grant-type:device_code': + if params['device_code'] != self.device_auth_params['device_code']: + raise HTTPUnauthorized() + if not self._device_code_accepted: + return json_response({ + "error": "authorization_pending", + "error_description": "Precondition Required" + }, status=428) + return json_response({ + 'access_token': self._auth_token, + 'expires_in': 3600, + 'token_type': 'doesn\'t matter' + }) + else: + raise HTTPUnauthorized() + + def filter_fields(self, item: Dict[str, Any], fields) -> Dict[str, Any]: + ret = {} + for field in fields: + if field in item: + ret[field] = item[field] + return ret + + def parseFields(self, source: str): + fields = [] + for field in source.split(","): + if field.startswith("files("): + fields.append(field[6:]) + elif field.endswith(")"): + fields.append(field[:-1]) + else: + fields.append(field) + return fields + + def formatItem(self, base, id): + caps = base.get('capabilites', {}) + if 'capabilities' not in base: + base['capabilities'] = caps + if 'canAddChildren' not in caps: + caps['canAddChildren'] = True + if 'canListChildren' not in caps: + caps['canListChildren'] = True + if 'canDeleteChildren' not in caps: + caps['canDeleteChildren'] = True + if 'canTrashChildren' not in caps: + caps['canTrashChildren'] = True + if 'canTrash' not in caps: + caps['canTrash'] = True + if 'canDelete' not in caps: + caps['canDelete'] = True + + for parent in base.get("parents", []): + parent_item = self.items[parent] + # This simulates a very simply shared drive permissions structure + if parent_item.get("driveId", None) is not None: + base["driveId"] = parent_item["driveId"] + base["capabilities"] = parent_item["capabilities"] + base['trashed'] = False + base['id'] = id + base['modifiedTime'] = self.timeToRfc3339String(self._time.now()) + return base + + async def _get(self, request: Request): + id = request.match_info.get('id') + await self._checkDriveHeaders(request) + if id not in self.items: + raise HTTPNotFound() + if id in self.lostPermission: + return Response( + status=403, + content_type="application/json", + text='{"error": {"errors": [{"reason": "forbidden"}]}}') + request_type = request.query.get("alt", "metadata") + if request_type == "media": + # return bytes + item = self.items[id] + if 'bytes' not in item: + raise HTTPBadRequest() + return self.serve_bytes(request, item['bytes'], include_length=False) + else: + fields = request.query.get("fields", "id").split(",") + return json_response(self.filter_fields(self.items[id], fields)) + + async def _update(self, request: Request): + id = request.match_info.get('id') + await self._checkDriveHeaders(request) + if id not in self.items: + return HTTPNotFound + update = await request.json() + for key in update: + if key in self.items[id] and isinstance(self.items[id][key], dict): + self.items[id][key].update(update[key]) + else: + self.items[id][key] = update[key] + return Response() + + async def _driveAbout(self, request: Request): + return json_response({ + 'storageQuota': { + 'usage': 1024 * 1024 * 1024, + 'limit': 5 * 1024 * 1024 * 1024 + }, + 'user': { + 'emailAddress': "testing@no.where" + } + }) + + async def _delete(self, request: Request): + id = request.match_info.get('id') + await self._checkDriveHeaders(request) + if id not in self.items: + raise HTTPNotFound() + del self.items[id] + return Response() + + async def _query(self, request: Request): + await self._checkDriveHeaders(request) + query: str = request.query.get("q", "") + fields = self.parseFields(request.query.get('fields', 'id')) + if mimeTypeQueryPattern.match(query): + ret = [] + mimeType = query[len("mimeType='"):-1] + for item in self.items.values(): + if item.get('mimeType', '') == mimeType: + ret.append(self.filter_fields(item, fields)) + return json_response({'files': ret}) + elif parentsQueryPattern.match(query): + ret = [] + parent = query[1:-len("' in parents")] + if parent not in self.items: + raise HTTPNotFound() + if parent in self.lostPermission: + return Response( + status=403, + content_type="application/json", + text='{"error": {"errors": [{"reason": "forbidden"}]}}') + for item in self.items.values(): + if parent in item.get('parents', []): + ret.append(self.filter_fields(item, fields)) + return json_response({'files': ret}) + elif len(query) == 0: + ret = [] + for item in self.items.values(): + ret.append(self.filter_fields(item, fields)) + return json_response({'files': ret}) + else: + raise HTTPBadRequest + + async def _create(self, request: Request): + await self._checkDriveHeaders(request) + item = self.formatItem(await request.json(), self.generateId(30)) + self.items[item['id']] = item + return json_response({'id': item['id']}) + + async def _upload(self, request: Request): + logger.info("Drive start upload request") + await self._checkDriveHeaders(request) + if request.query.get('uploadType') != 'resumable': + raise HTTPBadRequest() + mimeType = request.headers.get('X-Upload-Content-Type', None) + if mimeType is None: + raise HTTPBadRequest() + size = int(request.headers.get('X-Upload-Content-Length', -1)) + if size < 0: + raise HTTPBadRequest() + total_size = 0 + for item in self.items.values(): + total_size += item.get('size', 0) + total_size += size + if total_size > self.space_available: + return json_response({ + "error": { + "errors": [ + {"reason": "storageQuotaExceeded"} + ] + } + }, status=400) + metadata = await request.json() + id = self.generateId() + + # Validate parents + if 'parents' in metadata: + for parent in metadata['parents']: + if parent not in self.items: + raise HTTPNotFound() + if parent in self.lostPermission: + return Response(status=403, content_type="application/json", text='{"error": {"errors": [{"reason": "forbidden"}]}}') + self._upload_info['size'] = size + self._upload_info['mime'] = mimeType + self._upload_info['item'] = self.formatItem(metadata, id) + self._upload_info['id'] = id + self._upload_info['next_start'] = 0 + metadata['bytes'] = bytearray() + metadata['size'] = size + resp = Response() + resp.headers['Location'] = "http://localhost:" + \ + str(self._port) + "/upload/drive/v3/files/progress/" + id + return resp + + async def _uploadProgress(self, request: Request): + if self._waitOnChunk > 0: + if self._current_chunk == self._waitOnChunk: + self._upload_chunk_trigger.set() + await self._upload_chunk_wait.wait() + else: + self._current_chunk += 1 + id = request.match_info.get('id') + await self._checkDriveHeaders(request) + if self._upload_info.get('id', "") != id: + raise HTTPBadRequest() + chunk_size = int(request.headers['Content-Length']) + info = request.headers['Content-Range'] + if resumeBytesPattern.match(info): + resp = Response(status=308) + if self._upload_info['next_start'] != 0: + resp.headers['Range'] = "bytes=0-{0}".format(self._upload_info['next_start'] - 1) + return resp + if not bytesPattern.match(info): + raise HTTPBadRequest() + numbers = intPattern.findall(info) + start = int(numbers[0]) + end = int(numbers[1]) + total = int(numbers[2]) + if total != self._upload_info['size']: + raise HTTPBadRequest() + if start != self._upload_info['next_start']: + raise HTTPBadRequest() + if not (end == total - 1 or chunk_size % (256 * 1024) == 0): + raise HTTPBadRequest() + if end > total - 1: + raise HTTPBadRequest() + + # get the chunk + received_bytes = await self.readAll(request) + + # validate the chunk + if len(received_bytes) != chunk_size: + raise HTTPBadRequest() + + if len(received_bytes) != end - start + 1: + raise HTTPBadRequest() + + self._upload_info['item']['bytes'].extend(received_bytes) + + if len(self._upload_info['item']['bytes']) != end + 1: + raise HTTPBadRequest() + + self.chunks.append(len(received_bytes)) + if end == total - 1: + # upload is complete, so create the item + completed = self.formatItem(self._upload_info['item'], self._upload_info['id']) + self.items[completed['id']] = completed + return json_response({"id": completed['id']}) + else: + # Return an incomplete response + # For some reason, the tests like to stop right here + resp = Response(status=308) + self._upload_info['next_start'] = end + 1 + resp.headers['Range'] = "bytes=0-{0}".format(end) + return resp diff --git a/hassio-google-drive-backup/dev/simulated_supervisor.py b/hassio-google-drive-backup/dev/simulated_supervisor.py new file mode 100644 index 0000000..ed7ba8b --- /dev/null +++ b/hassio-google-drive-backup/dev/simulated_supervisor.py @@ -0,0 +1,410 @@ +import asyncio +from asyncio.tasks import sleep +from datetime import timedelta +import random +import io + +from backup.config import Config, Version +from backup.time import Time +from aiohttp.web import (HTTPBadRequest, HTTPNotFound, + HTTPUnauthorized, Request, Response, get, + json_response, post, delete, FileResponse) +from injector import inject, singleton +from .base_server import BaseServer +from .ports import Ports +from typing import Any, Dict +from tests.helpers import all_addons, createBackupTar, parseBackupInfo + +URL_MATCH_BACKUP_FULL = "^/backups/new/full$" +URL_MATCH_BACKUP_DELETE = "^/backups/.*$" +URL_MATCH_BACKUP_DOWNLOAD = "^/backups/.*/download$" +URL_MATCH_MISC_INFO = "^/info$" +URL_MATCH_CORE_API = "^/core/api.*$" +URL_MATCH_START_ADDON = "^/addons/.*/start$" +URL_MATCH_STOP_ADDON = "^/addons/.*/stop$" +URL_MATCH_ADDON_INFO = "^/addons/.*/info$" +URL_MATCH_SELF_OPTIONS = "^/addons/self/options$" + +URL_MATCH_SNAPSHOT = "^/snapshots.*$" +URL_MATCH_BACKUPS = "^/backups.*$" + + +@singleton +class SimulatedSupervisor(BaseServer): + @inject + def __init__(self, config: Config, ports: Ports, time: Time): + self._config = config + self._time = time + self._ports = ports + self._auth_token = "test_header" + self._backups: Dict[str, Any] = {} + self._backup_data: Dict[str, bytearray] = {} + self._backup_lock = asyncio.Lock() + self._backup_inner_lock = asyncio.Lock() + self._entities = {} + self._events = [] + self._attributes = {} + self._notification = None + self._min_backup_size = 1024 * 1024 * 5 + self._max_backup_size = 1024 * 1024 * 5 + self._addon_slug = "self_slug" + self._options = self.defaultOptions() + self._username = "user" + self._password = "pass" + self._addons = all_addons.copy() + self._super_version = Version(2021, 8) + + self.installAddon(self._addon_slug, "Home Assistant Google drive Backup") + self.installAddon("42", "The answer") + self.installAddon("sgadg", "sdgsagsdgsggsd") + + def defaultOptions(self): + return { + "max_backups_in_ha": 4, + "max_backups_in_google_drive": 4, + "days_between_backups": 3 + } + + def routes(self): + return [ + post('/addons/{slug}/options', self._updateOptions), + post("/core/api/services/persistent_notification/dismiss", self._dismissNotification), + post("/core/api/services/persistent_notification/create", self._createNotification), + post("/core/api/events/{name}", self._haEventUpdate), + post("/core/api/states/{entity}", self._haStateUpdate), + post('/auth', self._authenticate), + get('/auth', self._authenticate), + get('/info', self._miscInfo), + get('/addons/self/info', self._selfInfo), + get('/addons', self._allAddons), + get('/addons/{slug}/info', self._addonInfo), + + post('/addons/{slug}/start', self._startAddon), + post('/addons/{slug}/stop', self._stopAddon), + get('/addons/{slug}/logo', self._logoAddon), + get('/addons/{slug}/icon', self._logoAddon), + + get('/core/info', self._coreInfo), + get('/supervisor/info', self._supervisorInfo), + get('/supervisor/logs', self._supervisorLogs), + get('/core/logs', self._coreLogs), + get('/debug/insert/backup', self._debug_insert_backup), + get('/debug/info', self._debugInfo), + + get('/backups', self._getBackups), + delete('/backups/{slug}', self._deletebackup), + post('/backups/new/upload', self._uploadbackup), + post('/backups/new/partial', self._newbackup), + post('/backups/new/full', self._newbackup), + get('/backups/new/full', self._newbackup), + get('/backups/{slug}/download', self._backupDownload), + get('/backups/{slug}/info', self._backupDetail), + + # TODO: remove once the api path is fully deprecated + get('/snapshots', self._getSnapshots), + post('/snapshots/{slug}/remove', self._deletebackup), + post('/snapshots/new/upload', self._uploadbackup), + post('/snapshots/new/partial', self._newbackup), + post('/snapshots/new/full', self._newbackup), + get('/snapshots/new/full', self._newbackup), + get('/snapshots/{slug}/download', self._backupDownload), + get('/snapshots/{slug}/info', self._backupDetail), + ] + + def getEvents(self): + return self._events.copy() + + def getEntity(self, entity): + return self._entities.get(entity) + + def clearEntities(self): + self._entities = {} + + def addon(self, slug): + for addon in self._addons: + if addon["slug"] == slug: + return addon + return None + + def getAttributes(self, attribute): + return self._attributes.get(attribute) + + def getNotification(self): + return self._notification + + def _formatErrorResponse(self, error: str) -> str: + return json_response({'result': error}) + + def _formatDataResponse(self, data: Any) -> str: + return json_response({'result': 'ok', 'data': data}) + + async def toggleBlockBackup(self): + if self._backup_lock.locked(): + self._backup_lock.release() + else: + await self._backup_lock.acquire() + + async def _verifyHeader(self, request) -> bool: + if request.headers.get("Authorization", None) == "Bearer " + self._auth_token: + return + if request.headers.get("X-Supervisor-Token", None) == self._auth_token: + return + raise HTTPUnauthorized() + + async def _getSnapshots(self, request: Request): + await self._verifyHeader(request) + return self._formatDataResponse({'snapshots': list(self._backups.values())}) + + async def _getBackups(self, request: Request): + await self._verifyHeader(request) + return self._formatDataResponse({'backups': list(self._backups.values())}) + + async def _stopAddon(self, request: Request): + await self._verifyHeader(request) + slug = request.match_info.get('slug') + for addon in self._addons: + if addon.get("slug", "") == slug: + if addon.get("state") == "started": + addon["state"] = "stopped" + return self._formatDataResponse({}) + raise HTTPBadRequest() + + async def _logoAddon(self, request: Request): + await self._verifyHeader(request) + return FileResponse('hassio-google-drive-backup/backup/static/images/logo.png') + + async def _startAddon(self, request: Request): + await self._verifyHeader(request) + slug = request.match_info.get('slug') + for addon in self._addons: + if addon.get("slug", "") == slug: + if addon.get("state") != "started": + addon["state"] = "started" + return self._formatDataResponse({}) + raise HTTPBadRequest() + + async def _addonInfo(self, request: Request): + await self._verifyHeader(request) + slug = request.match_info.get('slug') + for addon in self._addons: + if addon.get("slug", "") == slug: + return self._formatDataResponse({ + 'boot': addon.get("boot"), + 'watchdog': addon.get("watchdog"), + 'state': addon.get("state"), + }) + raise HTTPBadRequest() + + async def _supervisorInfo(self, request: Request): + await self._verifyHeader(request) + return self._formatDataResponse( + { + 'version': str(self._super_version) + } + ) + + async def _allAddons(self, request: Request): + await self._verifyHeader(request) + return self._formatDataResponse( + { + "addons": list(self._addons).copy() + } + ) + + async def _supervisorLogs(self, request: Request): + await self._verifyHeader(request) + return Response(body="Supervisor Log line 1\nSupervisor Log Line 2") + + async def _coreLogs(self, request: Request): + await self._verifyHeader(request) + return Response(body="Core Log line 1\nCore Log Line 2") + + async def _coreInfo(self, request: Request): + await self._verifyHeader(request) + return self._formatDataResponse( + { + "version": "1.3.3.7", + "last_version": "1.3.3.8", + "machine": "VS Dev", + "ip_address": "127.0.0.1", + "arch": "x86", + "image": "image", + "custom": "false", + "boot": "true", + "port": self._ports.server, + "ssl": "false", + "watchdog": "what is this", + "wait_boot": "so many arguments" + } + ) + + async def _internalNewBackup(self, request: Request, input_json, date=None, verify_header=True) -> str: + async with self._backup_lock: + async with self._backup_inner_lock: + if 'wait' in input_json: + await sleep(input_json['wait']) + if verify_header: + await self._verifyHeader(request) + slug = self.generateId(8) + password = input_json.get('password', None) + data = createBackupTar( + slug, + input_json.get('name', "Default name"), + date=date or self._time.now(), + padSize=int(random.uniform(self._min_backup_size, self._max_backup_size)), + included_folders=input_json.get('folders', None), + included_addons=input_json.get('addons', None), + password=password) + backup_info = parseBackupInfo(data) + self._backups[slug] = backup_info + self._backup_data[slug] = bytearray(data.getbuffer()) + return slug + + async def createBackup(self, input_json, date=None): + return await self._internalNewBackup(None, input_json, date=date, verify_header=False) + + async def _newbackup(self, request: Request): + if self._backup_lock.locked(): + raise HTTPBadRequest() + input_json = await request.json() + task = asyncio.shield(asyncio.create_task(self._internalNewBackup(request, input_json))) + return self._formatDataResponse({"slug": await task}) + + async def _uploadbackup(self, request: Request): + await self._verifyHeader(request) + try: + reader = await request.multipart() + contents = await reader.next() + received_bytes = bytearray() + while True: + chunk = await contents.read_chunk() + if not chunk: + break + received_bytes.extend(chunk) + info = parseBackupInfo(io.BytesIO(received_bytes)) + self._backups[info['slug']] = info + self._backup_data[info['slug']] = received_bytes + return self._formatDataResponse({"slug": info['slug']}) + except Exception as e: + print(str(e)) + return self._formatErrorResponse("Bad backup") + + async def _deletebackup(self, request: Request): + await self._verifyHeader(request) + slug = request.match_info.get('slug') + if slug not in self._backups: + raise HTTPNotFound() + del self._backups[slug] + del self._backup_data[slug] + return self._formatDataResponse("deleted") + + async def _backupDetail(self, request: Request): + await self._verifyHeader(request) + slug = request.match_info.get('slug') + if slug not in self._backups: + raise HTTPNotFound() + return self._formatDataResponse(self._backups[slug]) + + async def _backupDownload(self, request: Request): + await self._verifyHeader(request) + slug = request.match_info.get('slug') + if slug not in self._backup_data: + raise HTTPNotFound() + return self.serve_bytes(request, self._backup_data[slug]) + + async def _selfInfo(self, request: Request): + await self._verifyHeader(request) + return self._formatDataResponse({ + "webui": "http://some/address", + 'ingress_url': "fill me in later", + "slug": self._addon_slug, + "options": self._options + }) + + async def _debugInfo(self, request: Request): + return self._formatDataResponse({ + "config": { + " webui": "http://some/address", + 'ingress_url': "fill me in later", + "slug": self._addon_slug, + "options": self._options + } + }) + + async def _miscInfo(self, request: Request): + await self._verifyHeader(request) + return self._formatDataResponse({ + "supervisor": "super version", + "homeassistant": "ha version", + "hassos": "hassos version", + "hostname": "hostname", + "machine": "machine", + "arch": "Arch", + "supported_arch": "supported arch", + "channel": "channel" + }) + + def installAddon(self, slug, name, version="v1.0", boot=True, started=True): + self._addons.append({ + "name": 'Name for ' + name, + "slug": slug, + "description": slug + " description", + "version": version, + "watchdog": False, + "boot": "auto" if boot else "manual", + "logo": True, + "ingress_entry": "/api/hassio_ingress/" + slug, + "state": "started" if started else "stopped" + }) + + async def _authenticate(self, request: Request): + await self._verifyHeader(request) + input_json = await request.json() + if input_json.get("username") != self._username or input_json.get("password") != self._password: + raise HTTPBadRequest() + return self._formatDataResponse({}) + + async def _updateOptions(self, request: Request): + slug = request.match_info.get('slug') + + if slug == "self": + await self._verifyHeader(request) + self._options = (await request.json())['options'].copy() + else: + self.addon(slug).update(await request.json()) + return self._formatDataResponse({}) + + async def _haStateUpdate(self, request: Request): + await self._verifyHeader(request) + entity = request.match_info.get('entity') + json = await request.json() + self._entities[entity] = json['state'] + self._attributes[entity] = json['attributes'] + return Response() + + async def _haEventUpdate(self, request: Request): + await self._verifyHeader(request) + name = request.match_info.get('name') + self._events.append((name, await request.json())) + return Response() + + async def _createNotification(self, request: Request): + await self._verifyHeader(request) + notification = await request.json() + print("Created notification with: {}".format(notification)) + self._notification = notification.copy() + return Response() + + async def _dismissNotification(self, request: Request): + await self._verifyHeader(request) + print("Dismissed notification with: {}".format(await request.json())) + self._notification = None + return Response() + + async def _debug_insert_backup(self, request: Request) -> bool: + days_back = int(request.query.get("days")) + date = self._time.now() - timedelta(days=days_back) + name = date.strftime("Full Backup %Y-%m-%d %H:%M-%S") + wait = int(request.query.get("wait", 0)) + slug = await self._internalNewBackup(request, {'name': name, 'wait': wait}, date=date, verify_header=False) + return self._formatDataResponse({'slug': slug}) diff --git a/hassio-google-drive-backup/dev/simulationserver.py b/hassio-google-drive-backup/dev/simulationserver.py new file mode 100644 index 0000000..cfc555d --- /dev/null +++ b/hassio-google-drive-backup/dev/simulationserver.py @@ -0,0 +1,165 @@ +import re +from typing import Dict +from yarl import URL +import aiohttp +from aiohttp.web import (Application, + HTTPException, + Request, Response, get, + json_response, middleware, post, HTTPSeeOther) +from aiohttp.client import ClientSession +from injector import inject, singleton, Injector, provider + +from backup.time import Time +from backup.logger import getLogger +from backup.server import Server +from tests.faketime import FakeTime +from backup.module import BaseModule +from backup.config import Config, Setting +from .http_exception import HttpMultiException +from .simulated_google import SimulatedGoogle +from .base_server import BaseServer +from .ports import Ports +from .request_interceptor import RequestInterceptor +from .simulated_supervisor import SimulatedSupervisor +from .apiingress import APIIngress +import aiorun + +logger = getLogger(__name__) + +mimeTypeQueryPattern = re.compile("^mimeType='.*'$") +parentsQueryPattern = re.compile("^'.*' in parents$") +bytesPattern = re.compile("^bytes \\d+-\\d+/\\d+$") +resumeBytesPattern = re.compile("^bytes \\*/\\d+$") +intPattern = re.compile("\\d+") +rangePattern = re.compile("bytes=\\d+-\\d+") + + +@singleton +class SimulationServer(BaseServer): + @inject + def __init__(self, ports: Ports, time: Time, session: ClientSession, authserver: Server, config: Config, google: SimulatedGoogle, supervisor: SimulatedSupervisor, api_ingress: APIIngress, interceptor: RequestInterceptor): + self.interceptor = interceptor + self.google = google + self.supervisor = supervisor + self.config = config + self.id_counter = 0 + self.files: Dict[str, bytearray] = {} + self._port = ports.server + self._time: FakeTime = time + self.urls = [] + self.relative = True + self._authserver = authserver + self._api_ingress = api_ingress + + def wasUrlRequested(self, pattern): + for url in self.urls: + if pattern in url: + return True + return False + + def blockBackups(self): + self.block_backups = True + + def unBlockBackups(self): + self.block_backups = False + + async def uploadfile(self, request: Request): + name: str = str(request.query.get("name", "test")) + self.files[name] = await self.readAll(request) + return Response(text="") + + async def readFile(self, request: Request): + return self.serve_bytes(request, self.files[request.query.get("name", "test")]) + + async def slugRedirect(self, request: Request): + raise HTTPSeeOther("https://localhost:" + str(self.config.get(Setting.INGRESS_PORT))) + + @middleware + async def error_middleware(self, request: Request, handler): + self.urls.append(str(request.url)) + resp = await self.interceptor.checkUrl(request) + if resp is not None: + return resp + try: + resp = await handler(request) + return resp + except Exception as ex: + await self.readAll(request) + if isinstance(ex, HttpMultiException): + return Response(status=ex.status_code) + elif isinstance(ex, HTTPException): + raise + else: + logger.printException(ex) + return json_response(str(ex), status=500) + + def createApp(self): + app = Application(middlewares=[self.error_middleware]) + app.add_routes(self.routes()) + self._authserver.buildApp(app) + return app + + async def start(self, port): + self.runner = aiohttp.web.AppRunner(self.createApp()) + await self.runner.setup() + site = aiohttp.web.TCPSite(self.runner, "0.0.0.0", port=port) + await site.start() + + async def stop(self): + self.interceptor.stop() + await self.runner.shutdown() + await self.runner.cleanup() + + def routes(self): + return [ + get('/readfile', self.readFile), + post('/uploadfile', self.uploadfile), + get('/ingress/self_slug', self.slugRedirect), + get('/debug/config', self.debug_config) + ] + self.google.routes() + self.supervisor.routes() + self._api_ingress.routes() + + async def debug_config(self, request: Request): + return json_response(self.supervisor._options) + + +class SimServerModule(BaseModule): + def __init__(self, base_url: URL): + super().__init__(override_dns=False) + self._base_url = base_url + + @provider + @singleton + def getConfig(self) -> Config: + return Config.withOverrides({ + Setting.DRIVE_AUTHORIZE_URL: str(self._base_url.with_path("o/oauth2/v2/auth")), + Setting.AUTHORIZATION_HOST: str(self._base_url), + Setting.TOKEN_SERVER_HOSTS: str(self._base_url), + Setting.DRIVE_TOKEN_URL: str(self._base_url.with_path("token")), + Setting.DRIVE_DEVICE_CODE_URL: str(self._base_url.with_path("device/code")), + Setting.DRIVE_REFRESH_URL: str(self._base_url.with_path("oauth2/v4/token")), + Setting.INGRESS_PORT: 56152 + }) + + @provider + @singleton + def getPorts(self) -> Ports: + return Ports(56153, 56151, 56152) + + +async def main(): + port = 56153 + base = URL("http://localhost").with_port(port) + injector = Injector(SimServerModule(base)) + server = injector.get(SimulationServer) + + # start the server + runner = aiohttp.web.AppRunner(server.createApp()) + await runner.setup() + site = aiohttp.web.TCPSite(runner, "0.0.0.0", port=port) + await site.start() + print("Server started on port " + str(port)) + print("Open a browser at http://localhost:" + str(port)) + + +if __name__ == '__main__': + aiorun.run(main()) diff --git a/hassio-google-drive-backup/dev/ssl/fullchain.pem b/hassio-google-drive-backup/dev/ssl/fullchain.pem new file mode 100644 index 0000000..a83c71f --- /dev/null +++ b/hassio-google-drive-backup/dev/ssl/fullchain.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC5TCCAc2gAwIBAgIJAN+M1w1AVtigMA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNV +BAMMCWxvY2FsaG9zdDAeFw0xOTAzMjYwMzI2MDJaFw0xOTA0MjUwMzI2MDJaMBQx +EjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBANAa2QE9uHexG6b/ggk7muXB4AhEcpPU+eqGmp4kFx/cKTYe+rPfui4FbARa +QyajXrVRMukEs0wZpUJ11LeGOmuTJ1Cu6mKtk4ub35ZrTfY0W0YdTW0ASYifDNQZ +pt4S0HAcY9A6wlorADxqDkqBt3cSuXdDaR6wFhc4x2kN7xMcKgX5Exv6AS04ksLm +fu0JNSvY1PcLQOA8bFc8tm4eEQcF51xBJBchCcXwpsr5OXt33govGcgxEPLZIueO +nmzzbF0jWBzBhwmjGGnEVsHnxgTG59QshFuB2xf6uWuZolLaPg32b2CV4gomFbn1 +7j4JMFTlxw80OkWILLR6pMr1gy0CAwEAAaM6MDgwFAYDVR0RBA0wC4IJbG9jYWxo +b3N0MAsGA1UdDwQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDATANBgkqhkiG9w0B +AQsFAAOCAQEAeK7VMbYO1lQmQcNIG/X42sS5Dm/YFSKgXG0VNMwjEa0xOPS54a6P +a3n7Lb6cVgwSstCSkQa0/Paqy/OvoJlvvgSrV8ZkqwU7100d7gohrReMAhWbRRDK +GkiJDUUQLAT8DXLRry2r5zRDaHX8OzzQuF8dPbFVkjXv9EMpBISY0hmodQFxBmiK +hxiYQWDcNQOTLwRk/x/b61AFLSXduonWM3r+29e8ej7LEHh9UJeLFF7S0+8t+7W4 +F8j8rGWFjYa2KCUFgTOWSg1cUnKYqFaakcMQAlfcXCzuDOso/gwuVFeZZ1hY7gEQ +OHJt0Tu+PWE4CQ3118AIajj2pxTuEHc6Ow== +-----END CERTIFICATE----- diff --git a/hassio-google-drive-backup/dev/ssl/localhost-ca-bundle.csr b/hassio-google-drive-backup/dev/ssl/localhost-ca-bundle.csr new file mode 100644 index 0000000..72aa58e --- /dev/null +++ b/hassio-google-drive-backup/dev/ssl/localhost-ca-bundle.csr @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIDAjCCAeoCAQAwgaIxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDTzETMBEGA1UE +BwwKU291dGggUGFyazEYMBYGA1UECgwPVW5pdCBUZXN0cyBJbmMuMR4wHAYDVQQL +DBVUZXN0aW5nIERlcHQuIEkgZ3Vlc3MxEjAQBgNVBAMMCWxvY2FsaG9zdDEjMCEG +CSqGSIb3DQEJARYUc3RlcGhlbkBiZWVjaGVucy5jb20wggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQDCu0+68ol5a9ShDmeg41INbwR0QdG0khlzA54Yhu3t +yhEYv7H1XE5JKwSENc1YkBTMlnmbEySW+YMpRXy6R/GoCaNU2wnz6UCdkJQQf6l+ +xIAkaRB+tj7uPpz65olC6tx5CFD+je/A6ZrHzAoEhiKTsQhI5uxexnl191BIQvcj +u7qKaN+TXmvKGlixPrYp4T30EWMDsbONyNjcZr/C4Xs1SzicfscDKt8qiINP8Fgd +tBDxyPIa4deYVKHG/1le9L1ccPFy1+wSQQG3d4YED7h94ajc5chmjMkJnTTYlRKL +XwMZxcsqX9ngHhPvoB5ZahGOLtjyYpxrvduY4kQ8XSaxAgMBAAGgGjAYBgkqhkiG +9w0BCQcxCwwJY2hhbGxlbmdlMA0GCSqGSIb3DQEBCwUAA4IBAQCT+ZSEvz9mJhMA +v71WWd+QjTyT4+9SItLVK3EAcpPbbJWayCuD+mKCGQr5plixC3w+tjy4coIG8lUo +pCX8sXi7TKMVKw6LYvBJeaRRAJ2+exeAQWJvGtRBBohXzm2+SxJ5Zp5+XEY7L3o8 +Apk++px7kLQTSRZxFAQ/irL/cUrp5Sn33ago+bzGA2AGryrqfBbe/nCwlCGF6cV2 +2w9oqY38tPeHQK9+MLOWDE0mBZvu+ab1mpTR7hxFVaVIKOBf8BifSVc4qJ8CDS+l +N4vEnxHIGdTXVp6yjpWN86qidjbLBqS6ZvY1dw6uFuXWSZP7gRixJi4/NUCf0NSO +yd+jFL0b +-----END CERTIFICATE REQUEST----- diff --git a/hassio-google-drive-backup/dev/ssl/localhost.crt b/hassio-google-drive-backup/dev/ssl/localhost.crt new file mode 100644 index 0000000..74e65de --- /dev/null +++ b/hassio-google-drive-backup/dev/ssl/localhost.crt @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC8DCCAdigAwIBAgIUUOqXw4hsjBcEzJwlO1o9TYw+f+wwDQYJKoZIhvcNAQEL +BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIwMDIwMzA4MDYyNVoXDTIwMDMw +NDA4MDYyNVowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAwrtPuvKJeWvUoQ5noONSDW8EdEHRtJIZcwOeGIbt7coR +GL+x9VxOSSsEhDXNWJAUzJZ5mxMklvmDKUV8ukfxqAmjVNsJ8+lAnZCUEH+pfsSA +JGkQfrY+7j6c+uaJQurceQhQ/o3vwOmax8wKBIYik7EISObsXsZ5dfdQSEL3I7u6 +imjfk15ryhpYsT62KeE99BFjA7GzjcjY3Ga/wuF7NUs4nH7HAyrfKoiDT/BYHbQQ +8cjyGuHXmFShxv9ZXvS9XHDxctfsEkEBt3eGBA+4feGo3OXIZozJCZ002JUSi18D +GcXLKl/Z4B4T76AeWWoRji7Y8mKca73bmOJEPF0msQIDAQABozowODAUBgNVHREE +DTALgglsb2NhbGhvc3QwCwYDVR0PBAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMB +MA0GCSqGSIb3DQEBCwUAA4IBAQBsZ29ZHTO6yNGPKWpxfOG38Z+mk6eh6TpbIVze +b7L2cFr/ONEFyz9hnS3kf23S9VsoX0AMdqYZbGmUT/4+d9+Q8hRXv7W3zenUk4KY +SkMfvB3J27w2l9Zx7oYfonBC7SSbfYrCBHgZwsINzdP5aC2q6eFTOadIdcF2bxf9 +FU/4aUyOeCkHAtYkVyxM3F33Qmf7ym7OZYKLn4SrPLFRSYiWRd8w+ww75uinnS5W +bG96OojPYzIZu8rb3b5ISR2BMWP0JVQRdmV+8TG1ekaA6EB5gAven55OxCmIUAJm +UEOLPRtVvJN0SE1S6jZBXBHler7IRDKpxATXbdFBK01s4rDz +-----END CERTIFICATE----- diff --git a/hassio-google-drive-backup/dev/ssl/localhost.key b/hassio-google-drive-backup/dev/ssl/localhost.key new file mode 100644 index 0000000..9e78356 --- /dev/null +++ b/hassio-google-drive-backup/dev/ssl/localhost.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDCu0+68ol5a9Sh +Dmeg41INbwR0QdG0khlzA54Yhu3tyhEYv7H1XE5JKwSENc1YkBTMlnmbEySW+YMp +RXy6R/GoCaNU2wnz6UCdkJQQf6l+xIAkaRB+tj7uPpz65olC6tx5CFD+je/A6ZrH +zAoEhiKTsQhI5uxexnl191BIQvcju7qKaN+TXmvKGlixPrYp4T30EWMDsbONyNjc +Zr/C4Xs1SzicfscDKt8qiINP8FgdtBDxyPIa4deYVKHG/1le9L1ccPFy1+wSQQG3 +d4YED7h94ajc5chmjMkJnTTYlRKLXwMZxcsqX9ngHhPvoB5ZahGOLtjyYpxrvduY +4kQ8XSaxAgMBAAECggEAJ1rt0S2FRSnazjX4EZb/lUFzl/9ZX3ILfKglgnV6jo1B +CUxsrdba54SvI/0vpA9ydKqQpxumUHDa5jNp8sfpefmArfyatVXVvkJi+jaizcDu +2Oz27XTtoP68gSSoZwLKThe1Ls0GwGk1491DxQhK4qhrsTgiW0EneQTjj8cg5XKH +/2l0WDslZDwW8XkJ1iqGi/OPs/X4SHggzX3xEFS2SpDK0e6GovyTfijpaql3MLMR +jnEeF69hUKKN7ADxhWvQ8d5C0CICYUzryGScVUs5312Zl83iOoeaixxfh6UaNOmE +jjdM6Hc7VbYEcfQTdZXyIPrzcz+Tc0DSDW+QsktLMQKBgQDn7j/oCNqLhxa1XnA8 +HgQqUUTav/OWlWpieTmcyZ2LkRRw9MJTnP1FIfIvOXplWFSpbSSArAEzsjpjRt0n +2+7VxwN3qNirNGAk3PZiRXXHq7sE3z39PhLPthpNisYTDTIx8fcYK032uEPHsSSj +i13yKeYqeGOmfnu0nrlmZ9+ThQKBgQDW8MnvhqjMxZDdVdxZKlY/8ihnubVBlp59 +s2SFIrWD1/QcKawCzagJHe/YR865k3ti7XIBghmKwLSMa6ENdTxTSSLHbBXlXJtH +tlWFgfVb8eDi7zo9178W8TrWEB7dSC2F6qMN17wOKWRkyo/c4cYBiAUaNQ1inJjk +ACOvHesAPQKBgHXEttKd3EtJNzC1WYxNOZQ7XBkvqwLlr/V81NJWVhdOffC1eA95 +AeoeyJlOOGZJqgO2Ffj4XkvfzmIm05mvxeDrg0k5hXu5xrAxOzK/ToUrIHXi3dk/ +sdGjCEwjkVyPMNPHp86v/pCvFEvMGWyqEfQrbmJWa1NZmnsmtcHYMOD5AoGAD1AW +Qt9IFVaZ7HraeOvAO0wIPuOHG0Ycwn3OUoHXhq4S8RKy83wtVYDxfmoXOzdbmf+q +mJrpMO5rrnlYfvn0M0bJmIWFxdJkKaa+zwUkMsm3qNM8Rf2h2oOTGn8Jg+BJhfni +ZfERr7yZL2kS+LyI+8DyBBz1eCoJ5mxwHmC2Rk0CgYBcrhxANSpikw07XLRFcvk9 +m79qiEThhmiBf1WVZdtWNi9hR+zs6mWrTk8N8jfLzNLLNMPdAAybF8feeMTa9xpS +zXF9Gqlayzx/+wyPts7ocrdJKikDVdVZauoxG+mNE87VcVEx87ZiboirQVoKSsxe +OmwKminJ/E4GHJCY7RLQAw== +-----END PRIVATE KEY----- diff --git a/hassio-google-drive-backup/dev/ssl/privkey.pem b/hassio-google-drive-backup/dev/ssl/privkey.pem new file mode 100644 index 0000000..a10446b --- /dev/null +++ b/hassio-google-drive-backup/dev/ssl/privkey.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDQGtkBPbh3sRum +/4IJO5rlweAIRHKT1PnqhpqeJBcf3Ck2Hvqz37ouBWwEWkMmo161UTLpBLNMGaVC +ddS3hjprkydQrupirZOLm9+Wa032NFtGHU1tAEmInwzUGabeEtBwHGPQOsJaKwA8 +ag5Kgbd3Erl3Q2kesBYXOMdpDe8THCoF+RMb+gEtOJLC5n7tCTUr2NT3C0DgPGxX +PLZuHhEHBedcQSQXIQnF8KbK+Tl7d94KLxnIMRDy2SLnjp5s82xdI1gcwYcJoxhp +xFbB58YExufULIRbgdsX+rlrmaJS2j4N9m9gleIKJhW59e4+CTBU5ccPNDpFiCy0 +eqTK9YMtAgMBAAECggEADlvr4UQK+GdGCy3SIST1uSi5dpiSd1TYsa/79zFyTwZ3 +6X4VuleTlx1UqLA5te7L2CL0KlPiszuJxZ4vwUIHwehzbAPFtG1ZouZsdQqOZJCU +Q7A96Wl9qWmgDvp+IxCVRUcQNAv54RLaf1CqD8YHjLXEClCibjWkMJIAYGVPu7ez +44sbXenPi+4OfI5IHhhBm+RmXv6QpP/A4OyIg/X35NoIp+z+J/aajFsb6AMvFejU +kMCj23PUv4MGA0zrc09UDzM/d7qwCeOMCW0QqKidbkZ+UtY3lsSj7b0l50TTEYsf +2sB/xjkUVHg9sJc8ieuf8LaHedvmiQPfECjZU9VhmQKBgQDx0h359EJSvil/iQ4o +OrsmxMz40mi/9pwznF0SUuRyKOsmJsSx7zL3rVFo/YLHOE5Ju4PSDm1OL4drUE0z +2l/0S6tlN4teHU6x969Xqm2vpwKP3jFXpD0zEi4QRGXgqtY1sVFO4ZIKfTa3KKMu +wqNmAB1KczvIkU71ClzqaVUULwKBgQDcTqI1SkwmIGP4PnGbLQTRI8pmw4xx/d7X +bpgAeCegSwfCy94nX7TdDYujhxa1rp3ya5YSnkTTN7oGCXIsZkLjmfFmjiIh3uEk +YX0obydQvVUfnPTPXQP3QhZG2dQtFdUUJOsu1bJKC7a/jcLGqbJzeBUg/Sb0/gXP +KCPCCr5bYwKBgHrbVX94KXoAQvUYnKizrgG0Wq7Pt4hPsmxGNMLqekXFpDJt3+DG +tg4/b+z3X0n3wU6UhhRiYAYo/5P16EM/3yAukZWK8rOOED06qUrQu4lSQGr3Z/ou +5yjbQ6vgFCJgqRP+UmDRGXFazEGh08Yd/QYFaNw6T1VG/eZgrXQqr57hAoGBALcb +qFiQm0ApNc4T4IrwXQuTKtxE9guczUXTxwTE2XKySg4PMmMZehMs+f39/tMdAmyG +HWL2JxBDRhtUaJAcosXXorvxsM7kF88MNGGSGWRTKVgwNY3QqsYtKKTU0jRy6/pl +QRBZT2mZ2NfXdKd4TjkI+s7DekiwhZWLsETMdzEvAoGARDyJNOpPPm/VpDgV08uU +P1yPOT6j8qhQ2dN1mEab0NeyY6HGriUg8y6HJ81Obt4YyVPlEplDJe8TkphWNsby +B93FpH56WF4g8ivKD4oC2JghlWf4c0MgxiWyoNvlHSM7Dmq2UfPDyV+1UhnNH1ty +CUMs7Fjk4BeJbrYmJf3VxYU= +-----END PRIVATE KEY----- diff --git a/hassio-google-drive-backup/icon.png b/hassio-google-drive-backup/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..107f2b9e2b1e8c93a1fb9334e2c532754c78588b GIT binary patch literal 7258 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 literal 0 HcmV?d00001 diff --git a/hassio-google-drive-backup/logo.png b/hassio-google-drive-backup/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..9df76d4cb864da48741fb4ec8e651cb965418960 GIT binary patch literal 9567 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 self.bytes_total: + self.bytes_total = self.bytes_free + + +class ReaderHelper: + def __init__(self, session, ui_port, ingress_port): + self.session = session + self.ui_port = ui_port + self.ingress_port = ingress_port + self.timeout = aiohttp.ClientTimeout(total=20) + + def getUrl(self, ingress=True, ssl=False): + if ssl: + protocol = "https" + else: + protocol = "http" + if ingress: + return protocol + "://localhost:" + str(self.ingress_port) + "/" + else: + return protocol + "://localhost:" + str(self.ui_port) + "/" + + async def getjson(self, path, status=200, json=None, auth=None, ingress=True, ssl=False, sslcontext=None): + async with self.session.get(self.getUrl(ingress, ssl) + path, json=json, auth=auth, ssl=sslcontext, timeout=self.timeout) as resp: + assert resp.status == status + return await resp.json() + + async def get(self, path, status=200, json=None, auth=None, ingress=True, ssl=False): + async with self.session.get(self.getUrl(ingress, ssl) + path, json=json, auth=auth, timeout=self.timeout) as resp: + if resp.status != status: + import logging + logging.getLogger().error(resp.text()) + assert resp.status == status + return await resp.text() + + async def postjson(self, path, status=200, json=None, ingress=True): + async with self.session.post(self.getUrl(ingress) + path, json=json, timeout=self.timeout) as resp: + assert resp.status == status + return await resp.json() + + async def assertError(self, path, error_type="generic_error", status=500, ingress=True, json=None): + logging.getLogger().info("Requesting " + path) + data = await self.getjson(path, status=status, ingress=ingress, json=json) + assert data['error_type'] == error_type + + +# This module should onyl ever have bindings that can also be satisfied by MainModule +class TestModule(Module): + def __init__(self, config: Config, ports: Ports): + self.ports = ports + self.config = config + + @provider + @singleton + def getDriveCreds(self, time: Time) -> Creds: + return Creds(time, "test_client_id", time.now(), "test_access_token", "test_refresh_token", "test_client_secret") + + @provider + @singleton + def getTime(self) -> Time: + return FakeTime() + + @provider + @singleton + def getPorts(self) -> Ports: + return self.ports + + @provider + @singleton + def getConfig(self) -> Config: + return self.config + + +@pytest.fixture +def event_loop(): + if platform.system() == "Windows": + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) + return asyncio.new_event_loop() + + +@pytest.fixture +async def generate_config(server_url: URL, ports, cleandir): + return Config.withOverrides({ + Setting.DRIVE_URL: str(server_url), + Setting.SUPERVISOR_URL: str(server_url) + "/", + Setting.AUTHORIZATION_HOST: str(server_url), + Setting.TOKEN_SERVER_HOSTS: str(server_url), + Setting.DRIVE_REFRESH_URL: str(server_url.with_path("/oauth2/v4/token")), + Setting.DRIVE_AUTHORIZE_URL: str(server_url.with_path("/o/oauth2/v2/auth")), + Setting.DRIVE_TOKEN_URL: str(server_url.with_path("/token")), + Setting.DRIVE_DEVICE_CODE_URL: str(server_url.with_path("/device/code")), + Setting.SUPERVISOR_TOKEN: "test_header", + Setting.SECRETS_FILE_PATH: "secrets.yaml", + Setting.CREDENTIALS_FILE_PATH: "credentials.dat", + Setting.FOLDER_FILE_PATH: "folder.dat", + Setting.RETAINED_FILE_PATH: "retained.json", + Setting.ID_FILE_PATH: "id.json", + Setting.DATA_CACHE_FILE_PATH: "data_cache.json", + Setting.STOP_ADDON_STATE_PATH: "stop_addon.json", + Setting.INGRESS_TOKEN_FILE_PATH: "ingress.dat", + Setting.DEFAULT_DRIVE_CLIENT_ID: "test_client_id", + Setting.DEFAULT_DRIVE_CLIENT_SECRET: "test_client_secret", + Setting.BACKUP_DIRECTORY_PATH: os.path.join(cleandir, "backups"), + Setting.PORT: ports.ui, + Setting.INGRESS_PORT: ports.ingress, + Setting.BACKUP_STARTUP_DELAY_MINUTES: 0, + Setting.PING_TIMEOUT: 0.1, + }) + + +@pytest.fixture +async def injector(cleandir, ports, generate_config): + drive_creds = Creds(FakeTime(), "test_client_id", None, "test_access_token", "test_refresh_token") + + os.mkdir(os.path.join(cleandir, "backups")) + with open(os.path.join(cleandir, "secrets.yaml"), "w") as f: + f.write("for_unit_tests: \"password value\"\n") + + with open(os.path.join(cleandir, "credentials.dat"), "w") as f: + f.write(json.dumps(drive_creds.serialize())) + + return Injector([BaseModule(), TestModule(generate_config, ports)]) + + +@pytest.fixture +async def ui_server(injector, server): + os.mkdir("static") + server = injector.get(UiServer) + await server.run() + yield server + await server.shutdown() + + +@pytest.fixture +def reader(server, ui_server, session, ui_port, ingress_port): + return ReaderHelper(session, ui_port, ingress_port) + + +@pytest.fixture +async def uploader(injector: Injector, server_url): + return injector.get(ClassAssistedBuilder[Uploader]).build(host=str(server_url)) + + +@pytest.fixture +async def google(injector: Injector): + return injector.get(SimulatedGoogle) + + +@pytest.fixture +async def interceptor(injector: Injector): + return injector.get(RequestInterceptor) + + +@pytest.fixture +async def supervisor(injector: Injector, server, session): + return injector.get(SimulatedSupervisor) + + +@pytest.fixture +async def addon_stopper(injector: Injector): + return injector.get(AddonStopper) + + +@pytest.fixture +async def server(injector, port, drive_creds: Creds, session): + server = injector.get(SimulationServer) + + # start the server + logging.getLogger().info("Starting SimulationServer on port " + str(port)) + await server.start(port) + yield server + await server.stop() + + +@pytest.fixture +async def data_cache(injector): + return injector.get(DataCache) + + +@pytest.fixture +async def session(injector): + async with injector.get(ClientSession) as session: + yield session + + +@pytest.fixture +async def precache(injector): + return injector.get(DestinationPrecache) + + +@pytest.fixture +async def backup(coord, source, dest): + await coord.sync() + assert len(coord.backups()) == 1 + return coord.backups()[0] + + +@pytest.fixture +async def fs(injector): + faker = injector.get(FsFaker) + faker.start() + yield faker + faker.stop() + + +@pytest.fixture +async def estimator(injector, fs): + return injector.get(Estimator) + + +@pytest.fixture +async def device_code(injector): + return injector.get(AuthCodeQuery) + + +@pytest.fixture +async def error_store(injector): + return injector.get(ErrorStore) + + +@pytest.fixture +async def model(injector): + return injector.get(Model) + + +@pytest.fixture +async def global_info(injector): + return injector.get(GlobalInfo) + + +@pytest.fixture +async def server_url(port): + return URL("http://localhost:").with_port(port) + + +@pytest.fixture +async def ports(unused_tcp_port_factory): + return Ports(unused_tcp_port_factory(), unused_tcp_port_factory(), unused_tcp_port_factory()) + + +@pytest.fixture +async def port(ports: Ports): + return ports.server + + +@pytest.fixture +async def ui_url(ports: Ports): + return URL("http://localhost").with_port(ports.ingress) + + +@pytest.fixture +async def ui_port(ports: Ports): + return ports.ui + + +@pytest.fixture +async def ingress_port(ports: Ports): + return ports.ingress + + +@pytest.fixture +async def coord(injector): + return injector.get(Coordinator) + + +@pytest.fixture() +async def updater(injector): + return injector.get(HaUpdater) + + +@pytest.fixture() +async def cleandir(): + newpath = tempfile.mkdtemp() + os.chdir(newpath) + return newpath + + +@pytest.fixture +async def time(injector): + reset() + return injector.get(Time) + + +@pytest.fixture +async def config(injector): + return injector.get(Config) + + +@pytest.fixture +async def drive_creds(injector): + return injector.get(Creds) + + +@pytest.fixture +async def drive(injector, server, session): + return injector.get(DriveSource) + + +@pytest.fixture +async def ha(injector, server, session): + return injector.get(HaSource) + + +@pytest.fixture +async def ha_requests(injector, server): + return injector.get(HaRequests) + + +@pytest.fixture +async def drive_requests(injector, server): + return injector.get(DriveRequests) + + +@pytest.fixture +async def resolver(injector): + return injector.get(Resolver) + + +@pytest.fixture +async def client_identifier(injector): + return injector.get(Config).clientIdentifier() + + +@pytest.fixture +async def debug_worker(injector): + return injector.get(DebugWorker) + + +@pytest.fixture() +async def folder_finder(injector): + return injector.get(FolderFinder) + + +@pytest.fixture() +async def watcher(injector): + watcher = injector.get(Watcher) + yield watcher + await watcher.stop() + + +class BackupHelper(): + def __init__(self, uploader, time): + self.time = time + self.uploader = uploader + + async def createFile(self, size=1024 * 1024 * 2, slug="testslug", name="Test Name"): + from_backup: DummyBackup = DummyBackup( + name, self.time.toUtc(self.time.local(1985, 12, 6)), "fake source", slug) + data = await self.uploader.upload(createBackupTar(slug, name, self.time.now(), size)) + return from_backup, data + + +@pytest.fixture +def backup_helper(uploader, time): + return BackupHelper(uploader, time) diff --git a/hassio-google-drive-backup/tests/faketime.py b/hassio-google-drive-backup/tests/faketime.py new file mode 100644 index 0000000..37ab3a6 --- /dev/null +++ b/hassio-google-drive-backup/tests/faketime.py @@ -0,0 +1,50 @@ +import asyncio +from datetime import datetime, timedelta +from backup.time import Time +from pytz import timezone + + +class FakeTime(Time): + def __init__(self, now: datetime = None): + super().__init__(local_tz=timezone('EST')) + if now: + self._now = now + else: + self._now = self.toUtc( + datetime(1985, 12, 6, 0, 0, 0, tzinfo=timezone('EST'))) + self.sleeps = [] + + def setTimeZone(self, tz): + if isinstance(tz, str): + self.local_tz = timezone(tz) + else: + self.local_tz = tz + + def setNow(self, now: datetime): + self._now = now + return self + + def advanceDay(self, days=1): + return self.advance(days=1) + + def advance(self, days=0, hours=0, minutes=0, seconds=0, duration=None): + self._now = self._now + \ + timedelta(days=days, hours=hours, seconds=seconds, minutes=minutes) + if duration is not None: + self._now = self._now + duration + return self + + def now(self) -> datetime: + return self._now + + def nowLocal(self) -> datetime: + return self.toLocal(self._now) + + async def sleepAsync(self, seconds: float, _exit_early: asyncio.Event = None): + self.sleeps.append(seconds) + self._now = self._now + timedelta(seconds=seconds) + # allow the task to be interrupted if such a thing is requested. + await asyncio.sleep(0) + + def clearSleeps(self): + self.sleeps = [] diff --git a/hassio-google-drive-backup/tests/helpers.py b/hassio-google-drive-backup/tests/helpers.py new file mode 100644 index 0000000..68a4232 --- /dev/null +++ b/hassio-google-drive-backup/tests/helpers.py @@ -0,0 +1,219 @@ +import json +import tarfile +import pytest +import platform +import os +from datetime import datetime +from io import BytesIO, IOBase + +from aiohttp import ClientSession +from injector import inject, singleton + +from backup.util import AsyncHttpGetter +from backup.model import SimulatedSource +from backup.time import Time +from backup.config import CreateOptions + +all_folders = [ + "share", + "ssl", + "addons/local" +] +all_addons = [ + { + "name": "Sexy Robots", + "slug": "sexy_robots", + "description": "The robots you already know, but sexier. See what they don't want you to see.", + "version": "0.69", + "size": 1, + "logo": True, + "state": "started" + }, + { + "name": "Particle Accelerator", + "slug": "particla_accel", + "description": "What CAN'T you do with Home Assistant?", + "version": "0.5", + "size": 500.3, + "logo": True, + "state": "started" + }, + { + "name": "Empty Addon", + "slug": "addon_empty", + "description": "Explore the meaning of the universe by contemplating whats missing.", + "version": "0.-1", + "size": 1024 * 1024 * 1024 * 21.2, + "logo": False, + "state": "started" + } +] + + +def skipForWindows(): + if platform.system() == "Windows": + pytest.skip("This test can't be run in windows environments") + + +def skipForRoot(): + if os.getuid() == 0: + pytest.skip("This test can't be run as root") + + +def createBackupTar(slug: str, name: str, date: datetime, padSize: int, included_folders=None, included_addons=None, password=None) -> BytesIO: + backup_type = "full" + haVersion = None + if included_folders is not None: + folders = [] + for folder in included_folders: + if folder == "homeassistant": + haVersion = "0.92.2" + else: + folders.append(folder) + else: + folders = all_folders.copy() + haVersion = "0.92.2" + + if included_addons is not None: + backup_type = "partial" + addons = [] + for addon in all_addons: + if addon['slug'] in included_addons: + addons.append(addon) + else: + addons = all_addons.copy() + + backup_info = { + "slug": slug, + "name": name, + "date": date.isoformat(), + "type": backup_type, + "protected": password is not None, + "homeassistant": haVersion, + "folders": folders, + "addons": addons, + "repositories": [ + "https://github.com/hassio-addons/repository" + ] + } + stream = BytesIO() + tar = tarfile.open(fileobj=stream, mode="w") + add(tar, "backup.json", BytesIO(json.dumps(backup_info).encode())) + add(tar, "padding.dat", getTestStream(padSize)) + tar.close() + stream.seek(0) + stream.size = lambda: len(stream.getbuffer()) + return stream + + +def add(tar, name, stream): + info = tarfile.TarInfo(name) + info.size = len(stream.getbuffer()) + stream.seek(0) + tar.addfile(info, stream) + + +def parseBackupInfo(stream: BytesIO): + with tarfile.open(fileobj=stream, mode="r") as tar: + info = tar.getmember("backup.json") + with tar.extractfile(info) as f: + backup_data = json.load(f) + backup_data['size'] = float( + round(len(stream.getbuffer()) / 1024.0 / 1024.0, 2)) + backup_data['version'] = 'dev' + return backup_data + + +def getTestStream(size: int): + """ + Produces a stream of repeating prime sequences to avoid accidental repetition + """ + arr = bytearray() + while True: + for prime in [4759, 4783, 4787, 4789, 4793, 4799, 4801, 4813, 4817, 4831, 4861, 4871, 4877, 4889, 4903, 4909, 4919, 4931, 4933, 4937]: + for x in range(prime): + if len(arr) < size: + arr.append(x % 255) + else: + break + if len(arr) >= size: + break + if len(arr) >= size: + break + return BytesIO(arr) + + +async def compareStreams(left, right): + await left.setup() + await right.setup() + while True: + from_left = await left.read(1024 * 1024) + from_right = await right.read(1024 * 1024) + if len(from_left.getbuffer()) == 0: + assert len(from_right.getbuffer()) == 0 + break + if from_left.getbuffer() != from_right.getbuffer(): + print("break!") + assert from_left.getbuffer() == from_right.getbuffer() + + +class IntentionalFailure(Exception): + pass + + +class HelperTestSource(SimulatedSource): + def __init__(self, name, is_destination=False): + super().__init__(name, is_destination=is_destination) + self.allow_create = True + self.allow_save = True + self.queries = 0 + + def reset(self): + self.saved = [] + self.deleted = [] + self.created = [] + self.queries = 0 + + @property + def query_count(self): + return self.queries + + async def get(self): + self.queries += 1 + return await super().get() + + def assertThat(self, created=0, deleted=0, saved=0, current=0): + assert len(self.saved) == saved + assert len(self.deleted) == deleted + assert len(self.created) == created + assert len(self.current) == current + return self + + def assertUnchanged(self): + self.assertThat(current=len(self.current)) + return self + + async def create(self, options: CreateOptions): + if not self.allow_create: + raise IntentionalFailure() + return await super().create(options) + + async def save(self, backup, bytes: IOBase = None): + if not self.allow_save: + raise IntentionalFailure() + return await super().save(backup, bytes=bytes) + + +@singleton +class Uploader(): + @inject + def __init__(self, host, session: ClientSession, time: Time): + self.host = host + self.session = session + self.time = time + + async def upload(self, data) -> AsyncHttpGetter: + async with await self.session.post(self.host + "/uploadfile", data=data) as resp: + resp.raise_for_status() + source = AsyncHttpGetter(self.host + "/readfile", {}, self.session, time=self.time) + return source diff --git a/hassio-google-drive-backup/tests/test_addon_stopper.py b/hassio-google-drive-backup/tests/test_addon_stopper.py new file mode 100644 index 0000000..6d468d7 --- /dev/null +++ b/hassio-google-drive-backup/tests/test_addon_stopper.py @@ -0,0 +1,355 @@ +import json +import pytest +import os + +from stat import S_IREAD +from backup.config import Config, Setting +from backup.ha import AddonStopper +from backup.exceptions import SupervisorFileSystemError +from .faketime import FakeTime +from dev.simulated_supervisor import SimulatedSupervisor, URL_MATCH_START_ADDON, URL_MATCH_STOP_ADDON, URL_MATCH_ADDON_INFO +from dev.request_interceptor import RequestInterceptor +from .helpers import skipForRoot + + +def getSaved(config: Config): + with open(config.get(Setting.STOP_ADDON_STATE_PATH)) as f: + data = json.load(f) + return set(data["start"]), set(data["watchdog"]) + + +def save(config: Config, to_start, to_watchdog_enable): + with open(config.get(Setting.STOP_ADDON_STATE_PATH), "w") as f: + json.dump({"start": list(to_start), "watchdog": list(to_watchdog_enable)}, f) + +@pytest.mark.asyncio +async def test_no_stop_config(supervisor: SimulatedSupervisor, addon_stopper: AddonStopper, config: Config) -> None: + slug = "test_slug_1" + supervisor.installAddon(slug, "Test decription") + addon_stopper.allowRun() + addon_stopper.isBackingUp(False) + assert supervisor.addon(slug)["state"] == "started" + await addon_stopper.stopAddons("ignore") + assert supervisor.addon(slug)["state"] == "started" + await addon_stopper.check() + await addon_stopper.startAddons() + assert supervisor.addon(slug)["state"] == "started" + + +@pytest.mark.asyncio +async def test_load_addons_on_boot(supervisor: SimulatedSupervisor, addon_stopper: AddonStopper, config: Config) -> None: + slug1 = "test_slug_1" + supervisor.installAddon(slug1, "Test decription") + slug2 = "test_slug_2" + supervisor.installAddon(slug2, "Test decription") + slug3 = "test_slug_3" + supervisor.installAddon(slug3, "Test decription") + + config.override(Setting.STOP_ADDONS, slug1) + + save(config, {slug3}, {slug2}) + + await addon_stopper.start(False) + assert addon_stopper.must_start == {slug3} + assert addon_stopper.must_enable_watchdog == {slug2} + + addon_stopper.allowRun() + assert addon_stopper.must_start == {slug1, slug3} + assert addon_stopper.must_enable_watchdog == {slug2} + + +@pytest.mark.asyncio +async def test_do_nothing_while_backing_up(supervisor: SimulatedSupervisor, addon_stopper: AddonStopper, config: Config, interceptor: RequestInterceptor) -> None: + slug1 = "test_slug_1" + supervisor.installAddon(slug1, "Test decription") + slug2 = "test_slug_2" + supervisor.installAddon(slug2, "Test decription") + config.override(Setting.STOP_ADDONS, ",".join([slug1, slug2])) + + await addon_stopper.start(False) + addon_stopper.allowRun() + addon_stopper.isBackingUp(True) + assert addon_stopper.must_start == {slug1, slug2} + + await addon_stopper.check() + + assert not interceptor.urlWasCalled(URL_MATCH_START_ADDON) + assert not interceptor.urlWasCalled(URL_MATCH_STOP_ADDON) + + +@pytest.mark.asyncio +async def test_start_and_stop(supervisor: SimulatedSupervisor, addon_stopper: AddonStopper, config: Config) -> None: + slug1 = "test_slug_1" + supervisor.installAddon(slug1, "Test decription") + config.override(Setting.STOP_ADDONS, ",".join([slug1])) + addon_stopper.allowRun() + addon_stopper.must_start = set() + assert supervisor.addon(slug1)["state"] == "started" + + await addon_stopper.stopAddons("ignore") + + assert supervisor.addon(slug1)["state"] == "stopped" + await addon_stopper.check() + assert supervisor.addon(slug1)["state"] == "stopped" + await addon_stopper.startAddons() + assert supervisor.addon(slug1)["state"] == "started" + assert getSaved(config) == (set(), set()) + + +@pytest.mark.asyncio +async def test_start_and_stop_error(supervisor: SimulatedSupervisor, addon_stopper: AddonStopper, config: Config) -> None: + slug1 = "test_slug_1" + supervisor.installAddon(slug1, "Test decription") + config.override(Setting.STOP_ADDONS, ",".join([slug1])) + addon_stopper.allowRun() + addon_stopper.must_start = set() + assert supervisor.addon(slug1)["state"] == "started" + + await addon_stopper.stopAddons("ignore") + + assert supervisor.addon(slug1)["state"] == "stopped" + await addon_stopper.check() + assert supervisor.addon(slug1)["state"] == "stopped" + supervisor.addon(slug1)["state"] = "error" + assert supervisor.addon(slug1)["state"] == "error" + await addon_stopper.startAddons() + assert supervisor.addon(slug1)["state"] == "started" + assert getSaved(config) == (set(), set()) + + +@pytest.mark.asyncio +async def test_stop_failure(supervisor: SimulatedSupervisor, addon_stopper: AddonStopper, config: Config, interceptor: RequestInterceptor) -> None: + slug1 = "test_slug_1" + supervisor.installAddon(slug1, "Test decription") + config.override(Setting.STOP_ADDONS, slug1) + addon_stopper.allowRun() + addon_stopper.must_start = set() + assert supervisor.addon(slug1)["state"] == "started" + interceptor.setError(URL_MATCH_STOP_ADDON, 400) + + await addon_stopper.stopAddons("ignore") + assert interceptor.urlWasCalled(URL_MATCH_STOP_ADDON) + assert getSaved(config) == (set(), set()) + assert supervisor.addon(slug1)["state"] == "started" + await addon_stopper.check() + await addon_stopper.startAddons() + assert supervisor.addon(slug1)["state"] == "started" + assert getSaved(config) == (set(), set()) + + +@pytest.mark.asyncio +async def test_start_failure(supervisor: SimulatedSupervisor, addon_stopper: AddonStopper, config: Config, interceptor: RequestInterceptor, time: FakeTime) -> None: + slug1 = "test_slug_1" + supervisor.installAddon(slug1, "Test decription") + config.override(Setting.STOP_ADDONS, ",".join([slug1])) + addon_stopper.allowRun() + addon_stopper.must_start = set() + assert supervisor.addon(slug1)["state"] == "started" + + await addon_stopper.stopAddons("ignore") + + assert supervisor.addon(slug1)["state"] == "stopped" + await addon_stopper.check() + assert getSaved(config) == ({slug1}, set()) + assert supervisor.addon(slug1)["state"] == "stopped" + interceptor.setError(URL_MATCH_START_ADDON, 400) + await addon_stopper.startAddons() + assert getSaved(config) == (set(), set()) + assert interceptor.urlWasCalled(URL_MATCH_START_ADDON) + assert supervisor.addon(slug1)["state"] == "stopped" + + +@pytest.mark.asyncio +async def test_delayed_start(supervisor: SimulatedSupervisor, addon_stopper: AddonStopper, config: Config, interceptor: RequestInterceptor, time: FakeTime) -> None: + slug1 = "test_slug_1" + supervisor.installAddon(slug1, "Test decription") + config.override(Setting.STOP_ADDONS, ",".join([slug1])) + addon_stopper.allowRun() + addon_stopper.must_start = set() + assert supervisor.addon(slug1)["state"] == "started" + await addon_stopper.stopAddons("ignore") + assert supervisor.addon(slug1)["state"] == "stopped" + assert getSaved(config) == ({slug1}, set()) + + # start the addon again, which simluates the supervisor's tendency to report an addon as started right after stopping it. + supervisor.addon(slug1)["state"] = "started" + await addon_stopper.check() + await addon_stopper.startAddons() + assert getSaved(config) == ({slug1}, set()) + + time.advance(seconds=30) + await addon_stopper.check() + assert getSaved(config) == ({slug1}, set()) + + time.advance(seconds=30) + await addon_stopper.check() + assert getSaved(config) == ({slug1}, set()) + + time.advance(seconds=30) + supervisor.addon(slug1)["state"] = "stopped" + await addon_stopper.check() + assert supervisor.addon(slug1)["state"] == "started" + assert getSaved(config) == (set(), set()) + + +@pytest.mark.asyncio +async def test_delayed_start_give_up(supervisor: SimulatedSupervisor, addon_stopper: AddonStopper, config: Config, interceptor: RequestInterceptor, time: FakeTime) -> None: + slug1 = "test_slug_1" + supervisor.installAddon(slug1, "Test decription") + config.override(Setting.STOP_ADDONS, ",".join([slug1])) + addon_stopper.allowRun() + addon_stopper.must_start = set() + assert supervisor.addon(slug1)["state"] == "started" + await addon_stopper.stopAddons("ignore") + assert supervisor.addon(slug1)["state"] == "stopped" + assert getSaved(config) == ({slug1}, set()) + + # start the addon again, which simluates the supervisor's tendency to report an addon as started right after stopping it. + supervisor.addon(slug1)["state"] = "started" + await addon_stopper.check() + await addon_stopper.startAddons() + assert getSaved(config) == ({slug1}, set()) + + time.advance(seconds=30) + await addon_stopper.check() + assert getSaved(config) == ({slug1}, set()) + + time.advance(seconds=30) + await addon_stopper.check() + assert getSaved(config) == ({slug1}, set()) + + # Should clear saved state after this, since it stops checking after 2 minutes. + time.advance(seconds=100) + await addon_stopper.check() + assert getSaved(config) == (set(), set()) + + +@pytest.mark.asyncio +async def test_disable_watchdog(supervisor: SimulatedSupervisor, addon_stopper: AddonStopper, config: Config) -> None: + slug1 = "test_slug_1" + supervisor.installAddon(slug1, "Test decription") + config.override(Setting.STOP_ADDONS, ",".join([slug1])) + supervisor.addon(slug1)["watchdog"] = True + + addon_stopper.allowRun() + addon_stopper.must_start = set() + assert supervisor.addon(slug1)["state"] == "started" + + await addon_stopper.stopAddons("ignore") + + assert supervisor.addon(slug1)["state"] == "stopped" + assert supervisor.addon(slug1)["watchdog"] is False + await addon_stopper.check() + assert supervisor.addon(slug1)["state"] == "stopped" + assert supervisor.addon(slug1)["watchdog"] is False + await addon_stopper.startAddons() + assert supervisor.addon(slug1)["state"] == "started" + assert supervisor.addon(slug1)["watchdog"] is True + assert getSaved(config) == (set(), set()) + + +@pytest.mark.asyncio +async def test_enable_watchdog_on_reboot(supervisor: SimulatedSupervisor, addon_stopper: AddonStopper, config: Config, time: FakeTime) -> None: + slug1 = "test_slug_1" + supervisor.installAddon(slug1, "Test decription") + config.override(Setting.STOP_ADDONS, ",".join([slug1])) + supervisor.addon(slug1)["watchdog"] = False + save(config, set(), {slug1}) + + await addon_stopper.start(False) + addon_stopper.allowRun() + assert addon_stopper.must_enable_watchdog == {slug1} + + time.advance(minutes=5) + await addon_stopper.check() + assert supervisor.addon(slug1)["watchdog"] is True + assert getSaved(config) == (set(), set()) + + +@pytest.mark.asyncio +async def test_enable_watchdog_waits_for_start(supervisor: SimulatedSupervisor, addon_stopper: AddonStopper, config: Config) -> None: + slug1 = "test_slug_1" + supervisor.installAddon(slug1, "Test decription") + config.override(Setting.STOP_ADDONS, ",".join([slug1])) + supervisor.addon(slug1)["watchdog"] = False + save(config, {slug1}, {slug1}) + + await addon_stopper.start(False) + addon_stopper.allowRun() + assert addon_stopper.must_enable_watchdog == {slug1} + + await addon_stopper.check() + assert getSaved(config) == ({slug1}, {slug1}) + + supervisor.addon(slug1)["state"] = "stopped" + await addon_stopper.check() + assert supervisor.addon(slug1)["state"] == "started" + assert supervisor.addon(slug1)["watchdog"] is True + assert getSaved(config) == (set(), set()) + + +@pytest.mark.asyncio +async def test_get_info_failure_on_stop(supervisor: SimulatedSupervisor, addon_stopper: AddonStopper, config: Config, interceptor: RequestInterceptor) -> None: + slug1 = "test_slug_1" + supervisor.installAddon(slug1, "Test decription") + config.override(Setting.STOP_ADDONS, slug1) + addon_stopper.allowRun() + addon_stopper.must_start = set() + assert supervisor.addon(slug1)["state"] == "started" + interceptor.setError(URL_MATCH_ADDON_INFO, 400) + + await addon_stopper.stopAddons("ignore") + assert interceptor.urlWasCalled(URL_MATCH_ADDON_INFO) + assert getSaved(config) == (set(), set()) + assert supervisor.addon(slug1)["state"] == "started" + await addon_stopper.check() + await addon_stopper.startAddons() + assert supervisor.addon(slug1)["state"] == "started" + assert getSaved(config) == (set(), set()) + + +@pytest.mark.asyncio +async def test_get_info_failure_on_start(supervisor: SimulatedSupervisor, addon_stopper: AddonStopper, config: Config, interceptor: RequestInterceptor) -> None: + slug1 = "test_slug_1" + supervisor.installAddon(slug1, "Test decription") + config.override(Setting.STOP_ADDONS, ",".join([slug1])) + addon_stopper.allowRun() + addon_stopper.must_start = set() + assert supervisor.addon(slug1)["state"] == "started" + + await addon_stopper.stopAddons("ignore") + + assert supervisor.addon(slug1)["state"] == "stopped" + await addon_stopper.check() + assert getSaved(config) == ({slug1}, set()) + assert supervisor.addon(slug1)["state"] == "stopped" + interceptor.setError(URL_MATCH_ADDON_INFO, 400) + await addon_stopper.startAddons() + assert getSaved(config) == (set(), set()) + assert interceptor.urlWasCalled(URL_MATCH_ADDON_INFO) + assert supervisor.addon(slug1)["state"] == "stopped" + + +@pytest.mark.asyncio +async def test_read_only_fs(supervisor: SimulatedSupervisor, addon_stopper: AddonStopper, config: Config, interceptor: RequestInterceptor) -> None: + # This test can't be run as the root user, since no file is read-only to root. + skipForRoot() + + # Stop an addon + slug1 = "test_slug_1" + supervisor.installAddon(slug1, "Test decription") + config.override(Setting.STOP_ADDONS, ",".join([slug1])) + addon_stopper.allowRun() + addon_stopper.must_start = set() + assert supervisor.addon(slug1)["state"] == "started" + await addon_stopper.stopAddons("ignore") + assert supervisor.addon(slug1)["state"] == "stopped" + await addon_stopper.check() + assert getSaved(config) == ({slug1}, set()) + + # make the state file unmodifiable + os.chmod(config.get(Setting.STOP_ADDON_STATE_PATH), S_IREAD) + + # verify we raise a known error when trying to save. + with pytest.raises(SupervisorFileSystemError): + await addon_stopper.startAddons() diff --git a/hassio-google-drive-backup/tests/test_asynchttpgetter.py b/hassio-google-drive-backup/tests/test_asynchttpgetter.py new file mode 100644 index 0000000..dd8cd40 --- /dev/null +++ b/hassio-google-drive-backup/tests/test_asynchttpgetter.py @@ -0,0 +1,117 @@ +from datetime import timedelta +import pytest +from aiohttp import ClientSession +from aiohttp.web import StreamResponse +from .conftest import Uploader +from backup.exceptions import LogicError +from dev.request_interceptor import RequestInterceptor +from .conftest import FakeTime + + +@pytest.mark.asyncio +async def test_basics(uploader: Uploader, server, session: ClientSession): + getter = await uploader.upload(bytearray([0, 1, 2, 3, 4, 5, 6, 7])) + await getter.setup() + assert (await getter.read(1)).read() == bytearray([0]) + assert (await getter.read(2)).read() == bytearray([1, 2]) + assert (await getter.read(3)).read() == bytearray([3, 4, 5]) + assert (await getter.read(3)).read() == bytearray([6, 7]) + assert (await getter.read(3)).read() == bytearray([]) + assert (await getter.read(3)).read() == bytearray([]) + + getter.position(2) + assert (await getter.read(2)).read() == bytearray([2, 3]) + assert (await getter.read(3)).read() == bytearray([4, 5, 6]) + + getter.position(2) + assert (await getter.read(2)).read() == bytearray([2, 3]) + + getter.position(2) + assert (await getter.read(2)).read() == bytearray([2, 3]) + assert (await getter.read(100)).read() == bytearray([4, 5, 6, 7]) + assert (await getter.read(3)).read() == bytearray([]) + assert (await getter.read(3)).read() == bytearray([]) + +@pytest.mark.asyncio +async def test_position_error(uploader: Uploader, server): + getter = await uploader.upload(bytearray([0, 1, 2, 3, 4, 5, 6, 7])) + await getter.setup() + assert (await getter.read(1)).read() == bytearray([0]) + + with pytest.raises(LogicError): + await getter.setup() + + +@pytest.mark.asyncio +async def test_no_content_length(uploader: Uploader, server, interceptor: RequestInterceptor): + getter = await uploader.upload(bytearray([0, 1, 2, 3, 4, 5, 6, 7])) + intercept = interceptor.setError("/readfile") + intercept.addResponse(StreamResponse(headers={})) + with pytest.raises(LogicError) as e: + await getter.setup() + assert e.value.message() == "Content size must be provided if the webserver doesn't provide it" + + +@pytest.mark.asyncio +async def test_no_setup_error(uploader: Uploader, server): + getter = await uploader.upload(bytearray([0, 1, 2, 3, 4, 5, 6, 7])) + with pytest.raises(LogicError): + await getter.read(1) + + +@pytest.mark.asyncio +async def test_progress(uploader: Uploader, server): + getter = await uploader.upload(bytearray([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])) + await getter.setup() + assert getter.progress() == 0 + assert (await getter.read(1)).read() == bytearray([0]) + assert getter.progress() == 10 + assert (await getter.read(2)).read() == bytearray([1, 2]) + assert getter.progress() == 30 + assert (await getter.read(7)).read() == bytearray([3, 4, 5, 6, 7, 8, 9]) + assert getter.progress() == 100 + assert str.format("{0}", getter) == "100" + + +@pytest.mark.asyncio +async def test_speed(uploader: Uploader, server, time: FakeTime): + getter = await uploader.upload(bytearray(x for x in range(0, 100))) + assert getter.startTime() == time.now() + await getter.setup() + assert getter.speed(period=timedelta(seconds=10)) is None + time.advance(seconds=1) + await getter.read(1) + assert getter.speed(period=timedelta(seconds=10)) == 1 + + time.advance(seconds=1) + await getter.read(1) + assert getter.speed(period=timedelta(seconds=10)) == 1 + assert getter.speed(period=timedelta(seconds=1)) == 1 + assert getter.speed(period=timedelta(seconds=1.5)) == 1 + assert getter.speed(period=timedelta(seconds=0.5)) == 1 + + time.advance(seconds=1) + assert getter.speed(period=timedelta(seconds=10)) == 1 + assert getter.speed(period=timedelta(seconds=1)) == 1 + assert getter.speed(period=timedelta(seconds=1.5)) == 1 + time.advance(seconds=0.5) + assert getter.speed(period=timedelta(seconds=1)) == 0.5 + time.advance(seconds=0.5) + assert getter.speed(period=timedelta(seconds=1)) == 0 + + # Now 4 seconds have passed, and we've transferred 4 bytes + await getter.read(2) + assert getter.speed(period=timedelta(seconds=4)) == 1 + assert getter.speed(period=timedelta(seconds=10)) == 1 + + time.advance(seconds=10) + await getter.read(10) + assert getter.speed(period=timedelta(seconds=10)) == 1 + + time.advance(seconds=10) + await getter.read(20) + assert getter.speed(period=timedelta(seconds=10)) == 2 + time.advance(seconds=10) + assert getter.speed(period=timedelta(seconds=10)) == 2 + time.advance(seconds=5) + assert getter.speed(period=timedelta(seconds=10)) == 1 diff --git a/hassio-google-drive-backup/tests/test_authcodequery.py b/hassio-google-drive-backup/tests/test_authcodequery.py new file mode 100644 index 0000000..f04eda7 --- /dev/null +++ b/hassio-google-drive-backup/tests/test_authcodequery.py @@ -0,0 +1,104 @@ +import pytest + +from backup.drive import AuthCodeQuery +from backup.exceptions import LogicError, GoogleCredGenerateError, ProtocolError +from dev.request_interceptor import RequestInterceptor +from dev.simulated_google import URL_MATCH_TOKEN, SimulatedGoogle, URL_MATCH_DEVICE_CODE +from aiohttp.web_response import json_response +from backup.config import Config, Setting + + +@pytest.mark.asyncio +async def test_invalid_sequence(device_code: AuthCodeQuery, interceptor: RequestInterceptor) -> None: + with pytest.raises(LogicError): + await device_code.waitForPermission() + + +@pytest.mark.asyncio +async def test_success(device_code: AuthCodeQuery, interceptor: RequestInterceptor, google: SimulatedGoogle, server) -> None: + await device_code.requestCredentials(google._custom_drive_client_id, google._custom_drive_client_secret) + + google._device_code_accepted = True + assert await device_code.waitForPermission() is not None + + +@pytest.mark.asyncio +async def test_google_failure_on_request(device_code: AuthCodeQuery, interceptor: RequestInterceptor, google: SimulatedGoogle, server) -> None: + interceptor.setError(URL_MATCH_DEVICE_CODE, 458) + with pytest.raises(GoogleCredGenerateError) as error: + await device_code.requestCredentials(google._custom_drive_client_id, google._custom_drive_client_secret) + assert error.value.message() == "Google responded with error status HTTP 458. Please verify your credentials are set up correctly." + + +@pytest.mark.asyncio +async def test_failure_on_http_unknown(device_code: AuthCodeQuery, interceptor: RequestInterceptor, google: SimulatedGoogle, server) -> None: + await device_code.requestCredentials(google._custom_drive_client_id, google._custom_drive_client_secret) + + interceptor.setError(URL_MATCH_TOKEN, 500) + + with pytest.raises(GoogleCredGenerateError) as error: + await device_code.waitForPermission() + assert error.value.message() == "Failed unexpectedly while trying to reach Google. See the add-on logs for details." + + +@pytest.mark.asyncio +async def test_success_after_wait(device_code: AuthCodeQuery, interceptor: RequestInterceptor, google: SimulatedGoogle, server) -> None: + await device_code.requestCredentials(google._custom_drive_client_id, google._custom_drive_client_secret) + + match = interceptor.setError(URL_MATCH_TOKEN) + match.addResponse(json_response(data={'error': "slow_down"}, status=403)) + + google._device_code_accepted = True + await device_code.waitForPermission() + + assert match.callCount() == 2 + + +@pytest.mark.asyncio +async def test_success_after_428(device_code: AuthCodeQuery, interceptor: RequestInterceptor, google: SimulatedGoogle, server) -> None: + await device_code.requestCredentials(google._custom_drive_client_id, google._custom_drive_client_secret) + + match = interceptor.setError(URL_MATCH_TOKEN) + match.addResponse(json_response(data={}, status=428)) + match.addResponse(json_response(data={}, status=428)) + match.addResponse(json_response(data={}, status=428)) + match.addResponse(json_response(data={}, status=428)) + match.addResponse(json_response(data={}, status=428)) + + google._device_code_accepted = True + await device_code.waitForPermission() + + assert match.callCount() == 6 + + +@pytest.mark.asyncio +async def test_permission_failure(device_code: AuthCodeQuery, interceptor: RequestInterceptor, google: SimulatedGoogle, server) -> None: + await device_code.requestCredentials(google._custom_drive_client_id, google._custom_drive_client_secret) + + match = interceptor.setError(URL_MATCH_TOKEN) + match.addResponse(json_response(data={}, status=403)) + + google._device_code_accepted = False + with pytest.raises(GoogleCredGenerateError) as error: + await device_code.waitForPermission() + assert error.value.message() == "Google refused the request to connect your account, either because you rejected it or they were set up incorrectly." + + +@pytest.mark.asyncio +async def test_json_parse_failure(device_code: AuthCodeQuery, interceptor: RequestInterceptor, google: SimulatedGoogle, server) -> None: + await device_code.requestCredentials(google._custom_drive_client_id, google._custom_drive_client_secret) + + interceptor.setError(URL_MATCH_TOKEN, 200) + + with pytest.raises(ProtocolError): + await device_code.waitForPermission() + + +@pytest.mark.asyncio +async def test_repeated_failure(device_code: AuthCodeQuery, interceptor: RequestInterceptor, google: SimulatedGoogle, server, config: Config) -> None: + await device_code.requestCredentials(google._custom_drive_client_id, google._custom_drive_client_secret) + + config.override(Setting.DRIVE_TOKEN_URL, "http://go.nowhere") + with pytest.raises(GoogleCredGenerateError) as error: + await device_code.waitForPermission() + error.value.message() == "Failed unexpectedly too many times while attempting to reach Google. See the logs for details." diff --git a/hassio-google-drive-backup/tests/test_backoff.py b/hassio-google-drive-backup/tests/test_backoff.py new file mode 100644 index 0000000..d643985 --- /dev/null +++ b/hassio-google-drive-backup/tests/test_backoff.py @@ -0,0 +1,104 @@ +from pytest import fixture, raises + +from backup.util import Backoff + + +@fixture +def error(): + return Exception() + + +def test_defaults(error): + backoff = Backoff() + assert backoff.backoff(error) == 2 + assert backoff.backoff(error) == 4 + assert backoff.backoff(error) == 8 + assert backoff.backoff(error) == 16 + assert backoff.backoff(error) == 32 + assert backoff.backoff(error) == 64 + assert backoff.backoff(error) == 128 + assert backoff.backoff(error) == 256 + assert backoff.backoff(error) == 512 + assert backoff.backoff(error) == 1024 + assert backoff.backoff(error) == 2048 + + for x in range(10000): + assert backoff.backoff(error) == 3600 + + +def test_max(error): + backoff = Backoff(max=500) + assert backoff.backoff(error) == 2 + assert backoff.backoff(error) == 4 + assert backoff.backoff(error) == 8 + assert backoff.backoff(error) == 16 + assert backoff.backoff(error) == 32 + assert backoff.backoff(error) == 64 + assert backoff.backoff(error) == 128 + assert backoff.backoff(error) == 256 + + for x in range(10000): + assert backoff.backoff(error) == 500 + + +def test_initial(error): + backoff = Backoff(initial=0) + assert backoff.backoff(error) == 0 + assert backoff.backoff(error) == 2 + assert backoff.backoff(error) == 4 + assert backoff.backoff(error) == 8 + assert backoff.backoff(error) == 16 + assert backoff.backoff(error) == 32 + assert backoff.backoff(error) == 64 + assert backoff.backoff(error) == 128 + assert backoff.backoff(error) == 256 + assert backoff.backoff(error) == 512 + assert backoff.backoff(error) == 1024 + assert backoff.backoff(error) == 2048 + + for x in range(10000): + assert backoff.backoff(error) == 3600 + + +def test_attempts(error): + backoff = Backoff(attempts=5) + assert backoff.backoff(error) == 2 + assert backoff.backoff(error) == 4 + assert backoff.backoff(error) == 8 + assert backoff.backoff(error) == 16 + assert backoff.backoff(error) == 32 + + for x in range(5): + with raises(type(error)): + backoff.backoff(error) + + +def test_start(error): + backoff = Backoff(base=10) + assert backoff.backoff(error) == 10 + assert backoff.backoff(error) == 20 + assert backoff.backoff(error) == 40 + assert backoff.backoff(error) == 80 + + +def test_realistic(error): + backoff = Backoff(base=5, initial=0, exp=1.5, attempts=5) + assert backoff.backoff(error) == 0 + assert backoff.backoff(error) == 5 + assert backoff.backoff(error) == 5 * 1.5 + assert backoff.backoff(error) == 5 * (1.5**2) + assert backoff.backoff(error) == 5 * (1.5**3) + for x in range(5): + with raises(type(error)): + backoff.backoff(error) + + +def test_maxOut(error): + backoff = Backoff(base=10, max=100) + assert backoff.backoff(error) == 10 + assert backoff.backoff(error) == 20 + backoff.maxOut() + assert backoff.backoff(error) == 100 + assert backoff.backoff(error) == 100 + backoff.reset() + assert backoff.backoff(error) == 10 diff --git a/hassio-google-drive-backup/tests/test_bytesizeasstring.py b/hassio-google-drive-backup/tests/test_bytesizeasstring.py new file mode 100644 index 0000000..fd3abda --- /dev/null +++ b/hassio-google-drive-backup/tests/test_bytesizeasstring.py @@ -0,0 +1,125 @@ +from backup.config import BytesizeAsStringValidator +from backup.exceptions import InvalidConfigurationValue +import pytest + + +def test_minimum(): + parser = BytesizeAsStringValidator("test", minimum=10) + assert parser.validate("11 bytes") == 11 + assert parser.validate(11) == 11 + with pytest.raises(InvalidConfigurationValue): + parser.validate("9 bytes") + + +def test_maximum(): + parser = BytesizeAsStringValidator("test", maximum=10) + assert parser.validate("9 bytes") == 9 + assert parser.validate(9) == 9 + with pytest.raises(InvalidConfigurationValue): + parser.validate("11 bytes") + assert parser.formatForUi(9) == "9 B" + + +def test_ui_format(): + parser = BytesizeAsStringValidator("test") + assert parser.formatForUi(25) == "25 B" + assert parser.formatForUi(25 * 1024) == "25 KB" + assert parser.formatForUi(25 * 1024 * 1024) == "25 MB" + assert parser.formatForUi(25 * 1024 * 1024 * 1024) == "25 GB" + assert parser.formatForUi(25 * 1024 * 1024 * 1024 * 1024) == "25 TB" + assert parser.formatForUi(25 * 1024 * 1024 * 1024 * 1024 * 1024) == "25 PB" + assert parser.formatForUi(25 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024) == "25 EB" + assert parser.formatForUi(25 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024) == "25 ZB" + assert parser.formatForUi(25 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024) == "25 YB" + assert parser.formatForUi(2000 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024) == "2000 YB" + + assert parser.formatForUi(2.5 * 1024 * 1024) == "2.5 MB" + assert parser.formatForUi(2.534525 * 1024 * 1024) == "2.534525 MB" + assert parser.formatForUi(98743.1234 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024) == "98743.1234 YB" + + +def test_numbers(): + parser = BytesizeAsStringValidator("test") + parser.validate(1.2) == 1 + parser.validate(1024.9) == 1024 + parser.validate(1024) == 1024 + + +def test_parsing(): + parser = BytesizeAsStringValidator("test") + assert parser.validate("1 B") == 1 + assert parser.validate("1 b") == 1 + assert parser.validate("1 bytes") == 1 + assert parser.validate("1 byte") == 1 + assert parser.validate("") is None + assert parser.validate(" ") is None + assert parser.validate(" 5. bytes ") == 5 + assert parser.validate("10b") == 10 + + assert parser.validate("1 KB") == 1024 + assert parser.validate("1 k") == 1024 + assert parser.validate("1 kb") == 1024 + assert parser.validate("1 kilobytes") == 1024 + assert parser.validate("1 kibibytes") == 1024 + assert parser.validate("1 kibi") == 1024 + assert parser.validate("2.5 KB") == 1024 * 2.5 + assert parser.validate("10k") == 10 * 1024 + + assert parser.validate("1 MB") == 1024 * 1024 + assert parser.validate("1 m") == 1024 * 1024 + assert parser.validate("1 mb") == 1024 * 1024 + assert parser.validate("1 megs") == 1024 * 1024 + assert parser.validate("1 mega") == 1024 * 1024 + assert parser.validate("1 megabytes") == 1024 * 1024 + assert parser.validate("1 mebibytes") == 1024 * 1024 + assert parser.validate("10m") == 10 * 1024 * 1024 + + assert parser.validate("1 GB") == 1024 * 1024 * 1024 + assert parser.validate("1 g") == 1024 * 1024 * 1024 + assert parser.validate("1 gb") == 1024 * 1024 * 1024 + assert parser.validate("1 gigs") == 1024 * 1024 * 1024 + assert parser.validate("1 gig") == 1024 * 1024 * 1024 + assert parser.validate("1 giga") == 1024 * 1024 * 1024 + assert parser.validate("1 gigabytes") == 1024 * 1024 * 1024 + assert parser.validate("1 gibibytes") == 1024 * 1024 * 1024 + assert parser.validate("10G") == 10 * 1024 * 1024 * 1024 + + assert parser.validate("1 TB") == 1024 * 1024 * 1024 * 1024 + assert parser.validate("1 t") == 1024 * 1024 * 1024 * 1024 + assert parser.validate("1 tb") == 1024 * 1024 * 1024 * 1024 + assert parser.validate("1 tera") == 1024 * 1024 * 1024 * 1024 + assert parser.validate("1 tebi") == 1024 * 1024 * 1024 * 1024 + assert parser.validate("1 terabytes") == 1024 * 1024 * 1024 * 1024 + assert parser.validate("10T") == 10 * 1024 * 1024 * 1024 * 1024 + + assert parser.validate("1 PB") == 1024 * 1024 * 1024 * 1024 * 1024 + assert parser.validate("1 p") == 1024 * 1024 * 1024 * 1024 * 1024 + assert parser.validate("1 pb") == 1024 * 1024 * 1024 * 1024 * 1024 + assert parser.validate("1 peta") == 1024 * 1024 * 1024 * 1024 * 1024 + assert parser.validate("1 pebi") == 1024 * 1024 * 1024 * 1024 * 1024 + assert parser.validate("1 petabytes") == 1024 * 1024 * 1024 * 1024 * 1024 + assert parser.validate("10P") == 10 * 1024 * 1024 * 1024 * 1024 * 1024 + + assert parser.validate("1 EB") == 1024 * 1024 * 1024 * 1024 * 1024 * 1024 + assert parser.validate("1 e") == 1024 * 1024 * 1024 * 1024 * 1024 * 1024 + assert parser.validate("1 eb") == 1024 * 1024 * 1024 * 1024 * 1024 * 1024 + assert parser.validate("1 exa") == 1024 * 1024 * 1024 * 1024 * 1024 * 1024 + assert parser.validate("1 exbi") == 1024 * 1024 * 1024 * 1024 * 1024 * 1024 + assert parser.validate("1 exabytes") == 1024 * 1024 * 1024 * 1024 * 1024 * 1024 + assert parser.validate("10E") == 10 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 + + assert parser.validate("1 ZB") == 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 + assert parser.validate("1 z") == 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 + assert parser.validate("1 zb") == 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 + assert parser.validate("1 zetta") == 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 + assert parser.validate("1 zebi") == 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 + assert parser.validate("1 zettabytes") == 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 + assert parser.validate("10Z") == 10 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 + + assert parser.validate("1 YB") == 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 + assert parser.validate("1 y") == 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 + assert parser.validate("1 yb") == 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 + assert parser.validate("1 yotta") == 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 + assert parser.validate("1 yobi") == 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 + assert parser.validate("1 yottabytes") == 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 + assert parser.validate("10Y") == 10 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 diff --git a/hassio-google-drive-backup/tests/test_config.py b/hassio-google-drive-backup/tests/test_config.py new file mode 100644 index 0000000..840f114 --- /dev/null +++ b/hassio-google-drive-backup/tests/test_config.py @@ -0,0 +1,307 @@ +import os +from pytest import raises + +from backup.model import GenConfig +from backup.config import Config, Setting +from backup.exceptions import InvalidConfigurationValue + + +def test_validate_empty(): + config = Config() + assert config.validate({}) == defaultAnd() + + +def test_validate_int(): + assert Config().validate({'max_backups_in_ha': 5}) == defaultAnd( + {Setting.MAX_BACKUPS_IN_HA: 5}) + assert Config().validate({'max_backups_in_ha': 5.0}) == defaultAnd( + {Setting.MAX_BACKUPS_IN_HA: 5}) + assert Config().validate({'max_backups_in_ha': "5"}) == defaultAnd( + {Setting.MAX_BACKUPS_IN_HA: 5}) + + with raises(InvalidConfigurationValue): + Config().validate({'max_backups_in_ha': -2}) + + +def test_validate_float(): + setting = Setting.DAYS_BETWEEN_BACKUPS + assert Config().validate({setting: 5}) == defaultAnd({setting: 5}) + assert Config().validate({setting.key(): 5}) == defaultAnd({setting: 5}) + assert Config().validate({setting: 5.0}) == defaultAnd({setting: 5}) + assert Config().validate({setting: "5"}) == defaultAnd({setting: 5}) + + with raises(InvalidConfigurationValue): + Config().validate({'days_between_backups': -1}) + + +def test_validate_bool(): + setting = Setting.SEND_ERROR_REPORTS + assert Config().validate({setting: True}) == defaultAnd({setting: True}) + assert Config().validate({setting: False}) == defaultAnd({setting: False}) + assert Config().validate({setting: "true"}) == defaultAnd({setting: True}) + assert Config().validate({setting: "false"}) == defaultAnd({setting: False}) + assert Config().validate({setting: "1"}) == defaultAnd({setting: True}) + assert Config().validate({setting: "0"}) == defaultAnd({setting: False}) + assert Config().validate({setting: "yes"}) == defaultAnd({setting: True}) + assert Config().validate({setting: "no"}) == defaultAnd({setting: False}) + assert Config().validate({setting: "on"}) == defaultAnd({setting: True}) + assert Config().validate({setting: "off"}) == defaultAnd({setting: False}) + + +def test_validate_string(): + assert Config().validate({Setting.BACKUP_NAME: True}) == defaultAnd({Setting.BACKUP_NAME: "True"}) + assert Config().validate({Setting.BACKUP_NAME: False}) == defaultAnd({Setting.BACKUP_NAME: "False"}) + assert Config().validate({Setting.BACKUP_NAME: "true"}) == defaultAnd({Setting.BACKUP_NAME: "true"}) + assert Config().validate({Setting.BACKUP_NAME: "false"}) == defaultAnd({Setting.BACKUP_NAME: "false"}) + assert Config().validate({Setting.BACKUP_NAME: "1"}) == defaultAnd({Setting.BACKUP_NAME: "1"}) + assert Config().validate({Setting.BACKUP_NAME: "0"}) == defaultAnd({Setting.BACKUP_NAME: "0"}) + assert Config().validate({Setting.BACKUP_NAME: "yes"}) == defaultAnd({Setting.BACKUP_NAME: "yes"}) + assert Config().validate({Setting.BACKUP_NAME: "no"}) == defaultAnd({Setting.BACKUP_NAME: "no"}) + + +def test_validate_url(): + assert Config().validate({Setting.SUPERVISOR_URL: True}) == defaultAnd( + {Setting.SUPERVISOR_URL: "True"}) + assert Config().validate({Setting.SUPERVISOR_URL: False}) == defaultAnd( + {Setting.SUPERVISOR_URL: "False"}) + assert Config().validate({Setting.SUPERVISOR_URL: "true"}) == defaultAnd( + {Setting.SUPERVISOR_URL: "true"}) + assert Config().validate({Setting.SUPERVISOR_URL: "false"}) == defaultAnd( + {Setting.SUPERVISOR_URL: "false"}) + assert Config().validate({Setting.SUPERVISOR_URL: "1"}) == defaultAnd( + {Setting.SUPERVISOR_URL: "1"}) + assert Config().validate({Setting.SUPERVISOR_URL: "0"}) == defaultAnd( + {Setting.SUPERVISOR_URL: "0"}) + assert Config().validate({Setting.SUPERVISOR_URL: "yes"}) == defaultAnd( + {Setting.SUPERVISOR_URL: "yes"}) + assert Config().validate({Setting.SUPERVISOR_URL: "no"}) == defaultAnd( + {Setting.SUPERVISOR_URL: "no"}) + + +def test_validate_regex(): + assert Config().validate({Setting.DRIVE_IPV4: "192.168.1.1"}) == defaultAnd( + {Setting.DRIVE_IPV4: "192.168.1.1"}) + with raises(InvalidConfigurationValue): + Config().validate({Setting.DRIVE_IPV4: -1}) + with raises(InvalidConfigurationValue): + Config().validate({Setting.DRIVE_IPV4: "192.168.1"}) + + +def test_remove_ssl(): + assert Config().validate({Setting.USE_SSL: True}) == defaultAnd({Setting.USE_SSL: True}) + assert Config().validate({Setting.USE_SSL: False}) == defaultAnd() + assert Config().validate({ + Setting.USE_SSL: False, + Setting.CERTFILE: "removed", + Setting.KEYFILE: 'removed' + }) == defaultAnd() + assert Config().validate({ + Setting.USE_SSL: True, + Setting.CERTFILE: "kept", + Setting.KEYFILE: 'kept' + }) == defaultAnd({ + Setting.USE_SSL: True, + Setting.CERTFILE: "kept", + Setting.KEYFILE: 'kept' + }) + + +def test_send_error_reports(): + assert Config().validate({Setting.SEND_ERROR_REPORTS: False}) == defaultAnd( + {Setting.SEND_ERROR_REPORTS: False}) + assert Config().validate({Setting.SEND_ERROR_REPORTS: True}) == defaultAnd( + {Setting.SEND_ERROR_REPORTS: True}) + assert Config().validate( + {Setting.SEND_ERROR_REPORTS: None}) == defaultAnd() + + +def test_unrecognized_values_filter(): + assert Config().validate({'blah': "bloo"}) == defaultAnd() + + +def test_removes_defaults(): + assert Config().validate( + {Setting.BACKUP_TIME_OF_DAY: ""}) == defaultAnd() + + +def defaultAnd(config={}): + ret = { + Setting.DAYS_BETWEEN_BACKUPS: 3, + Setting.MAX_BACKUPS_IN_HA: 4, + Setting.MAX_BACKUPS_IN_GOOGLE_DRIVE: 4 + } + ret.update(config) + return (ret, False) + + +def test_GenerationalConfig() -> None: + assert Config().getGenerationalConfig() is None + + assert Config().override(Setting.GENERATIONAL_DAYS, 5).getGenerationalConfig() == GenConfig(days=5) + assert Config().override(Setting.GENERATIONAL_WEEKS, 3).getGenerationalConfig() == GenConfig(days=1, weeks=3) + assert Config().override(Setting.GENERATIONAL_MONTHS, 3).getGenerationalConfig() == GenConfig(days=1, months=3) + assert Config().override(Setting.GENERATIONAL_YEARS, 3).getGenerationalConfig() == GenConfig(days=1, years=3) + assert Config().override(Setting.GENERATIONAL_DELETE_EARLY, True).override( + Setting.GENERATIONAL_DAYS, 2).getGenerationalConfig() == GenConfig(days=2, aggressive=True) + assert Config().override(Setting.GENERATIONAL_DAYS, 1).override( + Setting.GENERATIONAL_DAY_OF_YEAR, 3).getGenerationalConfig() == GenConfig(days=1, day_of_year=3) + assert Config().override(Setting.GENERATIONAL_DAYS, 1).override( + Setting.GENERATIONAL_DAY_OF_MONTH, 3).getGenerationalConfig() == GenConfig(days=1, day_of_month=3) + assert Config().override(Setting.GENERATIONAL_DAYS, 1).override( + Setting.GENERATIONAL_DAY_OF_WEEK, "tue").getGenerationalConfig() == GenConfig(days=1, day_of_week="tue") + + assert Config().override(Setting.GENERATIONAL_DAY_OF_MONTH, 3).override(Setting.GENERATIONAL_DAY_OF_WEEK, "tue").override(Setting.GENERATIONAL_DAY_OF_YEAR, "4").getGenerationalConfig() is None + + +def test_from_environment(): + assert Config.fromEnvironment().get(Setting.PORT) != 1000 + + os.environ["PORT"] = str(1000) + assert Config.fromEnvironment().get(Setting.PORT) == 1000 + + del os.environ["PORT"] + assert Config.fromEnvironment().get(Setting.PORT) != 1000 + + os.environ["port"] = str(1000) + assert Config.fromEnvironment().get(Setting.PORT) == 1000 + + +def test_config_upgrade(): + # Test specifying one value + config = Config() + config.update({Setting.DEPRECTAED_BACKUP_TIME_OF_DAY: "00:01"}) + assert (config.getAllConfig(), False) == defaultAnd({ + Setting.BACKUP_TIME_OF_DAY: "00:01", + Setting.CALL_BACKUP_SNAPSHOT: True + }) + assert config.mustSaveUpgradeChanges() + + # Test specifying multiple values + config = Config() + config.update({ + Setting.DEPRECTAED_MAX_BACKUPS_IN_GOOGLE_DRIVE: 21, + Setting.DEPRECTAED_MAX_BACKUPS_IN_HA: 20, + Setting.DEPRECATED_BACKUP_PASSWORD: "boop" + }) + assert config.getAllConfig() == defaultAnd({ + Setting.MAX_BACKUPS_IN_HA: 20, + Setting.MAX_BACKUPS_IN_GOOGLE_DRIVE: 21, + Setting.BACKUP_PASSWORD: "boop", + Setting.CALL_BACKUP_SNAPSHOT: True + })[0] + assert config.mustSaveUpgradeChanges() + + # test specifying value that don't get upgraded + config = Config() + config.update({Setting.EXCLUDE_ADDONS: "test"}) + assert config.getAllConfig() == defaultAnd({ + Setting.EXCLUDE_ADDONS: "test" + })[0] + assert not config.mustSaveUpgradeChanges() + + # Test specifying both + config = Config() + config.update({ + Setting.DEPRECTAED_BACKUP_TIME_OF_DAY: "00:01", + Setting.EXCLUDE_ADDONS: "test" + }) + assert config.getAllConfig() == defaultAnd({ + Setting.BACKUP_TIME_OF_DAY: "00:01", + Setting.EXCLUDE_ADDONS: "test", + Setting.CALL_BACKUP_SNAPSHOT: True + })[0] + assert config.mustSaveUpgradeChanges() + + +def test_overwrite_on_upgrade(): + config = Config() + config.update({ + Setting.DEPRECTAED_MAX_BACKUPS_IN_HA: 5, + Setting.MAX_BACKUPS_IN_HA: 6 + }) + assert (config.getAllConfig(), False) == defaultAnd({ + Setting.MAX_BACKUPS_IN_HA: 6, + Setting.CALL_BACKUP_SNAPSHOT: True + }) + assert config.mustSaveUpgradeChanges() + + config = Config() + config.update({ + Setting.MAX_BACKUPS_IN_HA: 6, + Setting.DEPRECTAED_MAX_BACKUPS_IN_HA: 5 + }) + assert (config.getAllConfig(), False) == defaultAnd({ + Setting.MAX_BACKUPS_IN_HA: 6, + Setting.CALL_BACKUP_SNAPSHOT: True + }) + assert config.mustSaveUpgradeChanges() + + config = Config() + config.update({ + Setting.MAX_BACKUPS_IN_HA: 6, + Setting.DEPRECTAED_MAX_BACKUPS_IN_HA: 4 + }) + assert (config.getAllConfig(), False) == defaultAnd({ + Setting.MAX_BACKUPS_IN_HA: 6, + Setting.CALL_BACKUP_SNAPSHOT: True + }) + assert config.mustSaveUpgradeChanges() + + +def test_overwrite_on_upgrade_default_value(): + # Test specifying one value + config = Config() + config.update({ + Setting.DEPRECTAED_MAX_BACKUPS_IN_HA: Setting.MAX_BACKUPS_IN_HA.default() + 1, + Setting.MAX_BACKUPS_IN_HA: Setting.MAX_BACKUPS_IN_HA.default() + }) + assert (config.getAllConfig(), False) == defaultAnd({ + Setting.MAX_BACKUPS_IN_HA: Setting.MAX_BACKUPS_IN_HA.default() + 1, + Setting.CALL_BACKUP_SNAPSHOT: True + }) + assert config.mustSaveUpgradeChanges() + + config = Config() + config.update({ + Setting.MAX_BACKUPS_IN_HA: Setting.MAX_BACKUPS_IN_HA.default(), + Setting.DEPRECTAED_MAX_BACKUPS_IN_HA: Setting.MAX_BACKUPS_IN_HA.default() + 1 + }) + assert (config.getAllConfig(), False) == defaultAnd({ + Setting.MAX_BACKUPS_IN_HA: Setting.MAX_BACKUPS_IN_HA.default() + 1, + Setting.CALL_BACKUP_SNAPSHOT: True + }) + assert config.mustSaveUpgradeChanges() + + +def test_empty_colors(): + # Test specifying one value + config = Config() + config.update({Setting.BACKGROUND_COLOR: "", Setting.ACCENT_COLOR: ""}) + assert config.get(Setting.BACKGROUND_COLOR) == Setting.BACKGROUND_COLOR.default() + assert config.get(Setting.ACCENT_COLOR) == Setting.ACCENT_COLOR.default() + + +def test_ignore_upgrades_default(): + # Test specifying one value + config = Config() + assert config.get(Setting.IGNORE_UPGRADE_BACKUPS) + + config.useLegacyIgnoredBehavior(True) + assert not config.get(Setting.IGNORE_UPGRADE_BACKUPS) + + config.useLegacyIgnoredBehavior(False) + assert config.get(Setting.IGNORE_UPGRADE_BACKUPS) + + +def getGenConfig(update): + base = { + "days": 1, + "weeks": 0, + "months": 0, + "years": 0, + "day_of_week": "mon", + "day_of_year": 1, + "day_of_month": 1 + } + base.update(update) + return base diff --git a/hassio-google-drive-backup/tests/test_coordinator.py b/hassio-google-drive-backup/tests/test_coordinator.py new file mode 100644 index 0000000..0dca529 --- /dev/null +++ b/hassio-google-drive-backup/tests/test_coordinator.py @@ -0,0 +1,552 @@ +import asyncio +from datetime import timedelta + +import pytest +from pytest import raises + +from backup.config import Config, Setting, CreateOptions +from backup.exceptions import LogicError, LowSpaceError, NoBackup, PleaseWait, UserCancelledError +from backup.util import GlobalInfo, DataCache +from backup.model import Coordinator, Model, Backup, DestinationPrecache +from .conftest import FsFaker +from .faketime import FakeTime +from .helpers import HelperTestSource, skipForWindows + + +@pytest.fixture +def source(): + return HelperTestSource("Source") + + +@pytest.fixture +def dest(): + return HelperTestSource("Dest") + + +@pytest.fixture +def simple_config(): + config = Config() + config.override(Setting.BACKUP_STARTUP_DELAY_MINUTES, 0) + return config + + +@pytest.fixture +def model(source, dest, time, simple_config, global_info, estimator, data_cache: DataCache): + return Model(simple_config, time, source, dest, global_info, estimator, data_cache) + + +@pytest.fixture +def coord(model, time, simple_config, global_info, estimator): + return Coordinator(model, time, simple_config, global_info, estimator) + + +@pytest.fixture +def precache(coord, time, dest, simple_config): + return DestinationPrecache(coord, time, dest, simple_config) + + +@pytest.mark.asyncio +async def test_enabled(coord: Coordinator, dest, time): + dest.setEnabled(True) + assert coord.enabled() + dest.setEnabled(False) + assert not coord.enabled() + + +@pytest.mark.asyncio +async def test_sync(coord: Coordinator, global_info: GlobalInfo, time: FakeTime): + await coord.sync() + assert global_info._syncs == 1 + assert global_info._successes == 1 + assert global_info._last_sync_start == time.now() + assert len(coord.backups()) == 1 + + +@pytest.mark.asyncio +async def test_blocking(coord: Coordinator): + # This just makes sure the wait thread is blocked while we do stuff + event_start = asyncio.Event() + event_end = asyncio.Event() + asyncio.create_task(coord._withSoftLock(lambda: sleepHelper(event_start, event_end))) + await event_start.wait() + + # Make sure PleaseWait gets called on these + with raises(PleaseWait): + await coord.delete(None, None) + with raises(PleaseWait): + await coord.sync() + with raises(PleaseWait): + await coord.uploadBackups(None) + with raises(PleaseWait): + await coord.startBackup(None) + event_end.set() + + +async def sleepHelper(event_start: asyncio.Event, event_end: asyncio.Event): + event_start.set() + await event_end.wait() + + +@pytest.mark.asyncio +async def test_new_backup(coord: Coordinator, time: FakeTime, source, dest): + await coord.startBackup(CreateOptions(time.now(), "Test Name")) + backups = coord.backups() + assert len(backups) == 1 + assert backups[0].name() == "Test Name" + assert backups[0].getSource(source.name()) is not None + assert backups[0].getSource(dest.name()) is None + + +@pytest.mark.asyncio +async def test_sync_error(coord: Coordinator, global_info: GlobalInfo, time: FakeTime, model): + error = Exception("BOOM") + old_sync = model.sync + model.sync = lambda s: doRaise(error) + await coord.sync() + assert global_info._last_error is error + assert global_info._last_failure_time == time.now() + assert global_info._successes == 0 + model.sync = old_sync + await coord.sync() + assert global_info._last_error is None + assert global_info._successes == 1 + assert global_info._last_success == time.now() + await coord.sync() + + +def doRaise(error): + raise error + + +@pytest.mark.asyncio +async def test_delete(coord: Coordinator, backup, source, dest): + assert backup.getSource(source.name()) is not None + assert backup.getSource(dest.name()) is not None + await coord.delete([source.name()], backup.slug()) + assert len(coord.backups()) == 1 + assert backup.getSource(source.name()) is None + assert backup.getSource(dest.name()) is not None + await coord.delete([dest.name()], backup.slug()) + assert backup.getSource(source.name()) is None + assert backup.getSource(dest.name()) is None + assert backup.isDeleted() + assert len(coord.backups()) == 0 + + await coord.sync() + assert len(coord.backups()) == 1 + await coord.delete([source.name(), dest.name()], coord.backups()[0].slug()) + assert len(coord.backups()) == 0 + + +@pytest.mark.asyncio +async def test_delete_errors(coord: Coordinator, source, dest, backup): + with raises(NoBackup): + await coord.delete([source.name()], "badslug") + bad_source = HelperTestSource("bad") + with raises(NoBackup): + await coord.delete([bad_source.name()], backup.slug()) + + +@pytest.mark.asyncio +async def test_retain(coord: Coordinator, source, dest, backup): + assert not backup.getSource(source.name()).retained() + assert not backup.getSource(dest.name()).retained() + await coord.retain({ + source.name(): True, + dest.name(): True + }, backup.slug()) + assert backup.getSource(source.name()).retained() + assert backup.getSource(dest.name()).retained() + + +@pytest.mark.asyncio +async def test_retain_errors(coord: Coordinator, source, dest, backup): + with raises(NoBackup): + await coord.retain({source.name(): True}, "badslug") + bad_source = HelperTestSource("bad") + with raises(NoBackup): + await coord.delete({bad_source.name(): True}, backup.slug()) + + +@pytest.mark.asyncio +async def test_freshness(coord: Coordinator, source: HelperTestSource, dest: HelperTestSource, backup: Backup, time: FakeTime): + source.setMax(2) + dest.setMax(2) + await coord.sync() + assert backup.getPurges() == { + source.name(): False, + dest.name(): False + } + + source.setMax(1) + dest.setMax(1) + await coord.sync() + assert backup.getPurges() == { + source.name(): True, + dest.name(): True + } + + dest.setMax(0) + await coord.sync() + assert backup.getPurges() == { + source.name(): True, + dest.name(): False + } + + source.setMax(0) + await coord.sync() + assert backup.getPurges() == { + source.name(): False, + dest.name(): False + } + + source.setMax(2) + dest.setMax(2) + time.advance(days=7) + await coord.sync() + assert len(coord.backups()) == 2 + assert backup.getPurges() == { + source.name(): True, + dest.name(): True + } + assert coord.backups()[1].getPurges() == { + source.name(): False, + dest.name(): False + } + + # should refresh on delete + source.setMax(1) + dest.setMax(1) + await coord.delete([source.name()], backup.slug()) + assert coord.backups()[0].getPurges() == { + dest.name(): True + } + assert coord.backups()[1].getPurges() == { + source.name(): True, + dest.name(): False + } + + # should update on retain + await coord.retain({dest.name(): True}, backup.slug()) + assert coord.backups()[0].getPurges() == { + dest.name(): False + } + assert coord.backups()[1].getPurges() == { + source.name(): True, + dest.name(): True + } + + # should update on upload + await coord.uploadBackups(coord.backups()[0].slug()) + assert coord.backups()[0].getPurges() == { + dest.name(): False, + source.name(): True + } + assert coord.backups()[1].getPurges() == { + source.name(): False, + dest.name(): True + } + + +@pytest.mark.asyncio +async def test_upload(coord: Coordinator, source: HelperTestSource, dest: HelperTestSource, backup): + await coord.delete([source.name()], backup.slug()) + assert backup.getSource(source.name()) is None + await coord.uploadBackups(backup.slug()) + assert backup.getSource(source.name()) is not None + + with raises(LogicError): + await coord.uploadBackups(backup.slug()) + + with raises(NoBackup): + await coord.uploadBackups("bad slug") + + await coord.delete([dest.name()], backup.slug()) + with raises(NoBackup): + await coord.uploadBackups(backup.slug()) + + +@pytest.mark.asyncio +async def test_download(coord: Coordinator, source, dest, backup): + await coord.download(backup.slug()) + await coord.delete([source.name()], backup.slug()) + await coord.download(backup.slug()) + + with raises(NoBackup): + await coord.download("bad slug") + + +@pytest.mark.asyncio +async def test_backoff(coord: Coordinator, model, source: HelperTestSource, dest: HelperTestSource, backup, time: FakeTime, simple_config: Config): + assert await coord.check() + simple_config.override(Setting.DAYS_BETWEEN_BACKUPS, 1) + simple_config.override(Setting.MAX_SYNC_INTERVAL_SECONDS, 60 * 60 * 6) + simple_config.override(Setting.DEFAULT_SYNC_INTERVAL_VARIATION, 0) + + assert coord.nextSyncAttempt() == time.now() + timedelta(hours=6) + assert not await coord.check() + old_sync = model.sync + model.sync = lambda s: doRaise(Exception("BOOM")) + await coord.sync() + + # first backoff should be 0 seconds + assert coord.nextSyncAttempt() == time.now() + assert await coord.check() + + # backoff maxes out at 2 hr = 7200 seconds + for seconds in [10, 20, 40, 80, 160, 320, 640, 1280, 2560, 5120, 7200, 7200]: + await coord.sync() + assert coord.nextSyncAttempt() == time.now() + timedelta(seconds=seconds) + assert not await coord.check() + assert not await coord.check() + assert not await coord.check() + + # a good sync resets it back to 6 hours from now + model.sync = old_sync + await coord.sync() + assert coord.nextSyncAttempt() == time.now() + timedelta(hours=6) + assert not await coord.check() + + # if the next backup is less that 6 hours from the last one, that that shoudl be when we sync + simple_config.override(Setting.DAYS_BETWEEN_BACKUPS, 1.0 / 24.0) + assert coord.nextSyncAttempt() == time.now() + timedelta(hours=1) + assert not await coord.check() + + time.advance(hours=2) + assert coord.nextSyncAttempt() == time.now() - timedelta(hours=1) + assert await coord.check() + + +def test_save_creds(coord: Coordinator, source, dest): + pass + + +@pytest.mark.asyncio +async def test_check_size_new_backup(coord: Coordinator, source: HelperTestSource, dest: HelperTestSource, time, fs: FsFaker): + skipForWindows() + fs.setFreeBytes(0) + with raises(LowSpaceError): + await coord.startBackup(CreateOptions(time.now(), "Test Name")) + + +@pytest.mark.asyncio +async def test_check_size_sync(coord: Coordinator, source: HelperTestSource, dest: HelperTestSource, time, fs: FsFaker, global_info: GlobalInfo): + skipForWindows() + fs.setFreeBytes(0) + await coord.sync() + assert len(coord.backups()) == 0 + assert global_info._last_error is not None + + await coord.sync() + assert len(coord.backups()) == 0 + assert global_info._last_error is not None + + # Verify it resets the global size skip check, but gets through once + global_info.setSkipSpaceCheckOnce(True) + await coord.sync() + assert len(coord.backups()) == 1 + assert global_info._last_error is None + assert not global_info.isSkipSpaceCheckOnce() + + # Next attempt to backup shoudl fail again. + time.advance(days=7) + await coord.sync() + assert len(coord.backups()) == 1 + assert global_info._last_error is not None + + +@pytest.mark.asyncio +async def test_cancel(coord: Coordinator, global_info: GlobalInfo): + coord._sync_wait.clear() + asyncio.create_task(coord.sync()) + await coord._sync_start.wait() + await coord.cancel() + assert isinstance(global_info._last_error, UserCancelledError) + + +@pytest.mark.asyncio +async def test_working_through_upload(coord: Coordinator, global_info: GlobalInfo, dest): + coord._sync_wait.clear() + assert not coord.isWorkingThroughUpload() + sync_task = asyncio.create_task(coord.sync()) + await coord._sync_start.wait() + assert not coord.isWorkingThroughUpload() + dest.working = True + assert coord.isWorkingThroughUpload() + coord._sync_wait.set() + await asyncio.wait([sync_task]) + assert not coord.isWorkingThroughUpload() + + +@pytest.mark.asyncio +async def test_alternate_timezone(coord: Coordinator, time: FakeTime, model: Model, dest, source, simple_config: Config): + time.setTimeZone("Europe/Stockholm") + simple_config.override(Setting.BACKUP_TIME_OF_DAY, "12:00") + simple_config.override(Setting.DAYS_BETWEEN_BACKUPS, 1) + + source.setMax(10) + source.insert("Fri", time.toUtc(time.local(2020, 3, 16, 18, 5))) + time.setNow(time.local(2020, 3, 16, 18, 6)) + model.reinitialize() + coord.reset() + await coord.sync() + assert not await coord.check() + assert coord.nextBackupTime() == time.local(2020, 3, 17, 12) + + time.setNow(time.local(2020, 3, 17, 11, 59)) + await coord.sync() + assert not await coord.check() + time.setNow(time.local(2020, 3, 17, 12)) + assert await coord.check() + + +@pytest.mark.asyncio +async def test_disabled_at_install(coord: Coordinator, dest, time): + """ + Verifies that at install time, if some backups are already present the + addon doesn't try to sync over and over when drive is disabled. This was + a problem at one point. + """ + dest.setEnabled(True) + await coord.sync() + assert len(coord.backups()) == 1 + + dest.setEnabled(False) + time.advance(days=5) + assert await coord.check() + await coord.sync() + assert not await coord.check() + + +@pytest.mark.asyncio +async def test_only_source_configured(coord: Coordinator, dest: HelperTestSource, time, source: HelperTestSource): + source.setEnabled(True) + dest.setEnabled(False) + dest.setNeedsConfiguration(False) + await coord.sync() + assert len(coord.backups()) == 1 + + +@pytest.mark.asyncio +async def test_schedule_backup_next_sync_attempt(coord: Coordinator, model, source: HelperTestSource, dest: HelperTestSource, backup, time: FakeTime, simple_config: Config): + """ + Next backup is before max sync interval is reached + """ + simple_config.override(Setting.DAYS_BETWEEN_BACKUPS, 1) + simple_config.override(Setting.MAX_SYNC_INTERVAL_SECONDS, 60 * 60) + simple_config.override(Setting.DEFAULT_SYNC_INTERVAL_VARIATION, 0) + + time.setTimeZone("Europe/Stockholm") + simple_config.override(Setting.BACKUP_TIME_OF_DAY, "03:23") + simple_config.override(Setting.DAYS_BETWEEN_BACKUPS, 1) + + source.setMax(10) + source.insert("Fri", time.toUtc(time.local(2020, 3, 16, 3, 33))) + + time.setNow(time.local(2020, 3, 17, 2, 29)) + model.reinitialize() + coord.reset() + await coord.sync() + assert coord.nextBackupTime() == time.local(2020, 3, 17, 3, 23) + assert coord.nextBackupTime() == coord.nextSyncAttempt() + + +@pytest.mark.asyncio +async def test_max_sync_interval_next_sync_attempt(coord: Coordinator, model, source: HelperTestSource, dest: HelperTestSource, backup, time: FakeTime, simple_config: Config): + """ + Next backup is after max sync interval is reached + """ + simple_config.override(Setting.DAYS_BETWEEN_BACKUPS, 1) + simple_config.override(Setting.MAX_SYNC_INTERVAL_SECONDS, 60 * 60) + simple_config.override(Setting.DEFAULT_SYNC_INTERVAL_VARIATION, 0) + + time.setTimeZone("Europe/Stockholm") + simple_config.override(Setting.BACKUP_TIME_OF_DAY, "03:23") + simple_config.override(Setting.DAYS_BETWEEN_BACKUPS, 1) + + source.setMax(10) + source.insert("Fri", time.toUtc(time.local(2020, 3, 16, 3, 33))) + time.setNow(time.local(2020, 3, 17, 1, 29)) + model.reinitialize() + coord.reset() + await coord.sync() + assert coord.nextSyncAttempt() == time.local(2020, 3, 17, 2, 29) + assert coord.nextBackupTime() > coord.nextSyncAttempt() + + +@pytest.mark.asyncio +async def test_generational_only_ignored_snapshots(coord: Coordinator, model, source: HelperTestSource, dest: HelperTestSource, time: FakeTime, simple_config: Config, global_info: GlobalInfo): + """ + Verifies a sync with generational settings and only ignored snapshots doesn't cause an error. + Setup is taken from https://github.com/sabeechen/hassio-google-drive-backup/issues/727 + """ + simple_config.override(Setting.DAYS_BETWEEN_BACKUPS, 1) + simple_config.override(Setting.GENERATIONAL_DAYS, 3) + simple_config.override(Setting.GENERATIONAL_WEEKS, 4) + simple_config.override(Setting.GENERATIONAL_DELETE_EARLY, True) + simple_config.override(Setting.MAX_BACKUPS_IN_HA, 2) + simple_config.override(Setting.MAX_BACKUPS_IN_GOOGLE_DRIVE, 6) + + backup = source.insert("Fri", time.toUtc(time.local(2020, 3, 16, 3, 33))) + backup.setIgnore(True) + time.setNow(time.local(2020, 3, 16, 4, 0)) + dest.setEnabled(False) + source.setEnabled(True) + + await coord.sync() + assert global_info._last_error is None + + +@pytest.mark.asyncio +async def test_max_sync_interval_randomness(coord: Coordinator, model, source: HelperTestSource, dest: HelperTestSource, backup, time: FakeTime, simple_config: Config): + simple_config.override(Setting.DAYS_BETWEEN_BACKUPS, 1) + simple_config.override(Setting.MAX_SYNC_INTERVAL_SECONDS, 60 * 60) + simple_config.override(Setting.DEFAULT_SYNC_INTERVAL_VARIATION, 0.5) + + time.setTimeZone("Europe/Stockholm") + simple_config.override(Setting.BACKUP_TIME_OF_DAY, "03:23") + simple_config.override(Setting.DAYS_BETWEEN_BACKUPS, 1) + + source.setMax(10) + source.insert("Fri", time.toUtc(time.local(2020, 3, 16, 3, 33))) + time.setNow(time.local(2020, 3, 17, 1, 29)) + model.reinitialize() + coord.reset() + await coord.sync() + next_attempt = coord.nextSyncAttempt() + + # verify its within expected range + assert next_attempt - time.now() <= timedelta(hours=1) + assert next_attempt - time.now() >= timedelta(hours=0.5) + + # verify it doesn't change + assert coord.nextSyncAttempt() == next_attempt + time.advance(minutes=1) + assert coord.nextSyncAttempt() == next_attempt + + # sync, and verify it does change + await coord.sync() + assert coord.nextSyncAttempt() != next_attempt + + +@pytest.mark.asyncio +async def test_precaching(coord: Coordinator, precache: DestinationPrecache, dest: HelperTestSource, time: FakeTime, global_info: GlobalInfo): + await coord.sync() + dest.reset() + + # Warm the cache + assert precache.getNextWarmDate() < coord.nextSyncAttempt() + assert precache.cached(dest.name(), time.now()) is None + assert dest.query_count == 0 + time.setNow(precache.getNextWarmDate()) + await precache.checkForSmoothing() + assert precache.cached(dest.name(), time.now()) is not None + assert dest.query_count == 1 + + # No queries should have been made to dest, and the cache should now be cleared + time.setNow(coord.nextSyncAttempt()) + assert precache.cached(dest.name(), time.now()) is not None + await coord.sync() + assert dest.query_count == 1 + assert precache.cached(dest.name(), time.now()) is None + assert global_info._last_error is None diff --git a/hassio-google-drive-backup/tests/test_data_cache.py b/hassio-google-drive-backup/tests/test_data_cache.py new file mode 100644 index 0000000..91401ab --- /dev/null +++ b/hassio-google-drive-backup/tests/test_data_cache.py @@ -0,0 +1,210 @@ +import pytest +import os +import json +from injector import Injector +from datetime import timedelta +from backup.config import Config, Setting, VERSION, Version +from backup.util import DataCache, UpgradeFlags, KEY_CREATED, KEY_LAST_SEEN, CACHE_EXPIRATION_DAYS +from backup.time import Time +from os.path import join + + +@pytest.mark.asyncio +async def test_read_and_write(config: Config, time: Time) -> None: + cache = DataCache(config, time) + assert len(cache.backups) == 0 + + cache.backup("test")[KEY_CREATED] = time.now().isoformat() + assert not cache._dirty + cache.makeDirty() + assert cache._dirty + cache.saveIfDirty() + assert not cache._dirty + + cache = DataCache(config, time) + assert cache.backup("test")[KEY_CREATED] == time.now().isoformat() + assert not cache._dirty + + +@pytest.mark.asyncio +async def test_backup_expiration(config: Config, time: Time) -> None: + cache = DataCache(config, time) + assert len(cache.backups) == 0 + + cache.backup("new")[KEY_LAST_SEEN] = time.now().isoformat() + cache.backup("old")[KEY_LAST_SEEN] = ( + time.now() - timedelta(days=CACHE_EXPIRATION_DAYS + 1)) .isoformat() + cache.makeDirty() + cache.saveIfDirty() + + assert len(cache.backups) == 1 + assert "new" in cache.backups + assert "old" not in cache.backups + + +@pytest.mark.asyncio +async def test_version_upgrades(time: Time, injector: Injector, config: Config) -> None: + # Simluate upgrading from an un-tracked version + assert not os.path.exists(config.get(Setting.DATA_CACHE_FILE_PATH)) + cache = injector.get(DataCache) + upgrade_time = time.now() + assert cache.previousVersion == Version.default() + assert cache.currentVersion == Version.parse(VERSION) + + assert os.path.exists(config.get(Setting.DATA_CACHE_FILE_PATH)) + with open(config.get(Setting.DATA_CACHE_FILE_PATH)) as f: + data = json.load(f) + assert data["upgrades"] == [{ + "prev_version": str(Version.default()), + "new_version": VERSION, + "date": upgrade_time.isoformat() + }] + + # Reload the data cache, verify there is no upgrade. + time.advance(days=1) + cache = DataCache(config, time) + assert cache.previousVersion == Version.parse(VERSION) + assert cache.currentVersion == Version.parse(VERSION) + assert os.path.exists(config.get(Setting.DATA_CACHE_FILE_PATH)) + + with open(config.get(Setting.DATA_CACHE_FILE_PATH)) as f: + data = json.load(f) + assert data["upgrades"] == [{ + "prev_version": str(Version.default()), + "new_version": VERSION, + "date": upgrade_time.isoformat() + }] + + # simulate upgrading to a new version, verify an upgrade gets identified. + upgrade_version = Version.parse("200") + + class UpgradeCache(DataCache): + def __init__(self): + super().__init__(config, time) + + @property + def currentVersion(self): + return upgrade_version + + cache = UpgradeCache() + assert cache.previousVersion == Version.parse(VERSION) + assert cache.currentVersion == upgrade_version + assert os.path.exists(config.get(Setting.DATA_CACHE_FILE_PATH)) + + with open(config.get(Setting.DATA_CACHE_FILE_PATH)) as f: + data = json.load(f) + assert data["upgrades"] == [ + { + "prev_version": str(Version.default()), + "new_version": VERSION, + "date": upgrade_time.isoformat() + }, + { + "prev_version": VERSION, + "new_version": str(upgrade_version), + "date": time.now().isoformat() + } + ] + + next_upgrade_time = time.now() + time.advance(days=1) + # Verify version upgrade time queries work as expected + assert cache.getUpgradeTime(Version.parse(VERSION)) == upgrade_time + assert cache.getUpgradeTime(Version.default()) == upgrade_time + assert cache.getUpgradeTime(upgrade_version) == next_upgrade_time + + # degenerate case, should never happen but a sensible value needs to be returned + assert cache.getUpgradeTime(Version.parse("201")) == time.now() + + +@pytest.mark.asyncio +async def test_flag(config: Config, time: Time): + cache = DataCache(config, time) + assert not cache.checkFlag(UpgradeFlags.TESTING_FLAG) + assert not cache.dirty + + cache.addFlag(UpgradeFlags.TESTING_FLAG) + assert cache.dirty + assert cache.checkFlag(UpgradeFlags.TESTING_FLAG) + cache.saveIfDirty() + + cache = DataCache(config, time) + assert cache.checkFlag(UpgradeFlags.TESTING_FLAG) + + +@pytest.mark.asyncio +async def test_warn_upgrade_new_install(config: Config, time: Time): + """A fresh install of the addon should never warn about upgrade snapshots""" + cache = DataCache(config, time) + assert not cache.notifyForIgnoreUpgrades + assert cache._config.get(Setting.IGNORE_UPGRADE_BACKUPS) + + +@pytest.mark.asyncio +async def test_warn_upgrade_old_install(config: Config, time: Time): + """An old install of the addon warn about upgrade snapshots""" + with open(config.get(Setting.DATA_CACHE_FILE_PATH), "w") as f: + data = { + "upgrades": [ + { + "prev_version": str(Version.default()), + "new_version": "0.108.1", + "date": time.now().isoformat() + } + ] + } + json.dump(data, f) + cache = DataCache(config, time) + assert cache.notifyForIgnoreUpgrades + assert not cache._config.get(Setting.IGNORE_UPGRADE_BACKUPS) + + +@pytest.mark.asyncio +async def test_warn_upgrade_old_install_explicit_ignore_upgrades(config: Config, time: Time, cleandir: str): + """An old install of the addon should not warn about upgrade snapshots if it explicitly ignores them""" + with open(config.get(Setting.DATA_CACHE_FILE_PATH), "w") as f: + data = { + "upgrades": [ + { + "prev_version": str(Version.default()), + "new_version": "0.108.1", + "date": time.now().isoformat() + } + ] + } + json.dump(data, f) + config_path = join(cleandir, "config.json") + with open(config_path, "w") as f: + data = { + Setting.IGNORE_UPGRADE_BACKUPS.value: True, + Setting.DATA_CACHE_FILE_PATH.value: config.get(Setting.DATA_CACHE_FILE_PATH) + } + json.dump(data, f) + cache = DataCache(Config.fromFile(config_path), time) + assert not cache.notifyForIgnoreUpgrades + assert cache._config.get(Setting.IGNORE_UPGRADE_BACKUPS) + + +@pytest.mark.asyncio +async def test_warn_upgrade_old_install_explicit_ignore_others(config: Config, time: Time, cleandir: str): + """An old install of the addon should not warn about upgrade snapshots if it explicitly ignores them""" + with open(config.get(Setting.DATA_CACHE_FILE_PATH), "w") as f: + data = { + "upgrades": [ + { + "prev_version": str(Version.default()), + "new_version": "0.108.1", + "date": time.now().isoformat() + } + ] + } + json.dump(data, f) + config_path = join(cleandir, "config.json") + with open(config_path, "w") as f: + data = { + Setting.IGNORE_OTHER_BACKUPS.value: True, + Setting.DATA_CACHE_FILE_PATH.value: config.get(Setting.DATA_CACHE_FILE_PATH) + } + json.dump(data, f) + cache = DataCache(Config.fromFile(config_path), time) + assert not cache.notifyForIgnoreUpgrades diff --git a/hassio-google-drive-backup/tests/test_debugworker.py b/hassio-google-drive-backup/tests/test_debugworker.py new file mode 100644 index 0000000..fdc2213 --- /dev/null +++ b/hassio-google-drive-backup/tests/test_debugworker.py @@ -0,0 +1,142 @@ +import pytest + +from backup.config import Config, Setting +from backup.debugworker import DebugWorker +from backup.util import GlobalInfo +from backup.logger import getLogger +from dev.simulationserver import SimulationServer +from .helpers import skipForWindows +from backup.server import ErrorStore +from .conftest import FakeTime + + +@pytest.mark.asyncio +async def test_dns_info(debug_worker: DebugWorker, config: Config): + skipForWindows() + config.override(Setting.SEND_ERROR_REPORTS, True) + config.override(Setting.DRIVE_HOST_NAME, "localhost") + await debug_worker.doWork() + assert '127.0.0.1' in debug_worker.dns_info['localhost'] + assert 'localhost' in debug_worker.dns_info['localhost'] + + +@pytest.mark.asyncio +async def test_bad_host(debug_worker: DebugWorker, config: Config): + skipForWindows() + config.override(Setting.DRIVE_HOST_NAME, "dasdfdfgvxcvvsoejbr.com") + await debug_worker.doWork() + assert "Name or service not known" in debug_worker.dns_info['dasdfdfgvxcvvsoejbr.com']['dasdfdfgvxcvvsoejbr.com'] + +@pytest.mark.asyncio +async def test_send_error_report(time, debug_worker: DebugWorker, config: Config, global_info: GlobalInfo, server, error_store: ErrorStore): + config.override(Setting.SEND_ERROR_REPORTS, True) + config.override(Setting.DRIVE_HOST_NAME, "localhost") + global_info.sync() + global_info.success() + global_info.sync() + global_info.success() + global_info.sync() + global_info.failed(Exception()) + await debug_worker.doWork() + report = error_store.last_error + assert report['report']['sync_success_count'] == 2 + assert report['report']['sync_count'] == 3 + assert report['report']['failure_count'] == 1 + assert report['report']['sync_last_start'] == time.now().isoformat() + assert report['report']['failure_time'] == time.now().isoformat() + assert report['report']['error'] == getLogger("test").formatException(Exception()) + + +@pytest.mark.asyncio +async def test_dont_send_error_report(time, debug_worker: DebugWorker, config: Config, global_info: GlobalInfo, server: SimulationServer, error_store: ErrorStore): + config.override(Setting.SEND_ERROR_REPORTS, False) + config.override(Setting.DRIVE_HOST_NAME, "localhost") + global_info.failed(Exception()) + await debug_worker.doWork() + assert error_store.last_error is None + + +@pytest.mark.asyncio +async def test_only_send_duplicates(time, debug_worker: DebugWorker, config: Config, global_info: GlobalInfo, server, error_store: ErrorStore): + config.override(Setting.SEND_ERROR_REPORTS, True) + config.override(Setting.DRIVE_HOST_NAME, "localhost") + global_info.failed(Exception("boom1")) + firstExceptionTime = time.now() + await debug_worker.doWork() + report = error_store.last_error + assert report['report']["error"] == getLogger("test").formatException(Exception("boom1")) + assert report['report']["time"] == firstExceptionTime.isoformat() + + # Same exception shouldn't cause us to send the error report again + time.advance(days=1) + global_info.failed(Exception("boom1")) + await debug_worker.doWork() + report = error_store.last_error + assert report['report']["error"] == getLogger("test").formatException(Exception("boom1")) + assert report['report']["time"] == firstExceptionTime.isoformat() + + # Btu a new one will send a new report + global_info.failed(Exception("boom2")) + await debug_worker.doWork() + report = error_store.last_error + assert report['report']["error"] == getLogger("test").formatException(Exception("boom2")) + assert report['report']["time"] == time.now().isoformat() + + +@pytest.mark.asyncio +async def test_send_clear(time, debug_worker: DebugWorker, config: Config, global_info: GlobalInfo, server, error_store: ErrorStore): + config.override(Setting.SEND_ERROR_REPORTS, True) + config.override(Setting.DRIVE_HOST_NAME, "localhost") + + # Simulate failure + global_info.failed(Exception("boom")) + await debug_worker.doWork() + + # And then success + global_info.success() + time.advance(days=1) + await debug_worker.doWork() + report = error_store.last_error + assert report['report'] == { + 'duration': '1 day, 0:00:00' + } + + +@pytest.mark.asyncio +async def test_health_check_timing_success(server_url, time: FakeTime, debug_worker: DebugWorker, config: Config, server: SimulationServer): + # Only do successfull checks once a day + await debug_worker.doWork() + assert server.interceptor.urlWasCalled("/health") + server.interceptor.clear() + + await debug_worker.doWork() + assert not server.interceptor.urlWasCalled("/health") + + time.advance(hours=23) + await debug_worker.doWork() + assert not server.interceptor.urlWasCalled("/health") + + time.advance(hours=2) + await debug_worker.doWork() + assert server.interceptor.urlWasCalled("/health") + + +@pytest.mark.asyncio +async def test_health_check_timing_failure(server_url, time: FakeTime, debug_worker: DebugWorker, config: Config, server: SimulationServer): + # Failed helath checks retry after a minute + server.interceptor.setError("/health", 500) + + await debug_worker.doWork() + assert server.interceptor.urlWasCalled("/health") + server.interceptor.clear() + + await debug_worker.doWork() + assert not server.interceptor.urlWasCalled("/health") + + time.advance(seconds=59) + await debug_worker.doWork() + assert not server.interceptor.urlWasCalled("/health") + + time.advance(seconds=2) + await debug_worker.doWork() + assert server.interceptor.urlWasCalled("/health") diff --git a/hassio-google-drive-backup/tests/test_destinationprecache.py b/hassio-google-drive-backup/tests/test_destinationprecache.py new file mode 100644 index 0000000..d97bd53 --- /dev/null +++ b/hassio-google-drive-backup/tests/test_destinationprecache.py @@ -0,0 +1,119 @@ + + +from backup.model import DestinationPrecache, Model, Coordinator +from backup.config import Config, Setting +from tests.faketime import FakeTime +from dev.request_interceptor import RequestInterceptor +from dev.simulated_google import URL_MATCH_DRIVE_API +from backup.drive import DriveSource +from datetime import timedelta +import pytest + + +@pytest.mark.asyncio +async def test_no_caching_before_cache_time(server, precache: DestinationPrecache, model: Model, drive: DriveSource, interceptor: RequestInterceptor, coord: Coordinator, time: FakeTime) -> None: + await coord.sync() + + interceptor.clear() + await precache.checkForSmoothing() + assert precache.getNextWarmDate() > time.now() + assert not interceptor.urlWasCalled(URL_MATCH_DRIVE_API) + assert precache.cached(drive.name(), time.now()) is None + + +@pytest.mark.asyncio +async def test_no_caching_after_sync_time(server, precache: DestinationPrecache, model: Model, drive: DriveSource, interceptor: RequestInterceptor, coord: Coordinator, time: FakeTime) -> None: + await coord.sync() + + time.setNow(coord.nextSyncAttempt()) + interceptor.clear() + await precache.checkForSmoothing() + assert precache.getNextWarmDate() < time.now() + assert not interceptor.urlWasCalled(URL_MATCH_DRIVE_API) + assert precache.cached(drive.name(), time.now()) is None + + +@pytest.mark.asyncio +async def test_cache_after_warm_date(server, precache: DestinationPrecache, model: Model, drive: DriveSource, interceptor: RequestInterceptor, coord: Coordinator, time: FakeTime) -> None: + await coord.sync() + interceptor.clear() + assert precache.getNextWarmDate() < coord.nextSyncAttempt() + + time.setNow(precache.getNextWarmDate()) + await precache.checkForSmoothing() + assert interceptor.urlWasCalled(URL_MATCH_DRIVE_API) + assert precache.cached(drive.name(), time.now()) is not None + + +async def test_no_double_caching(server, precache: DestinationPrecache, model: Model, drive: DriveSource, interceptor: RequestInterceptor, coord: Coordinator, time: FakeTime) -> None: + await coord.sync() + interceptor.clear() + + time.setNow(precache.getNextWarmDate()) + await precache.checkForSmoothing() + assert precache.cached(drive.name(), time.now()) is not None + + interceptor.clear() + time.setNow(precache.getNextWarmDate() + (coord.nextSyncAttempt() - precache.getNextWarmDate()) / 2) + await precache.checkForSmoothing() + assert not interceptor.urlWasCalled(URL_MATCH_DRIVE_API) + assert precache.cached(drive.name(), time.now()) is not None + + +async def test_cache_expiration(server, precache: DestinationPrecache, model: Model, drive: DriveSource, interceptor: RequestInterceptor, coord: Coordinator, time: FakeTime) -> None: + await coord.sync() + + time.setNow(precache.getNextWarmDate()) + await precache.checkForSmoothing() + assert precache.cached(drive.name(), time.now()) is not None + + time.setNow(coord.nextSyncAttempt() + timedelta(minutes=2)) + assert precache.cached(drive.name(), time.now()) is None + + +async def test_cache_clear(server, precache: DestinationPrecache, model: Model, drive: DriveSource, interceptor: RequestInterceptor, coord: Coordinator, time: FakeTime) -> None: + await coord.sync() + + time.setNow(precache.getNextWarmDate()) + await precache.checkForSmoothing() + assert precache.cached(drive.name(), time.now()) is not None + + precache.clear() + assert precache.cached(drive.name(), time.now()) is None + + +async def test_cache_error_backoff(server, precache: DestinationPrecache, model: Model, drive: DriveSource, interceptor: RequestInterceptor, coord: Coordinator, time: FakeTime) -> None: + await coord.sync() + + time.setNow(precache.getNextWarmDate()) + interceptor.setError(URL_MATCH_DRIVE_API, status=503) + await precache.checkForSmoothing() + + assert precache.cached(drive.name(), time.now()) is None + delta = precache.getNextWarmDate() - time.now() + assert delta >= timedelta(days=1) + + +async def test_cache_warm_date_stability(server, precache: DestinationPrecache, model: Model, drive: DriveSource, interceptor: RequestInterceptor, coord: Coordinator, time: FakeTime) -> None: + await coord.sync() + + # The warm date shouldn't change + last_warm = precache.getNextWarmDate() + assert precache.getNextWarmDate() == last_warm + time.setNow(last_warm - timedelta(minutes=1)) + assert precache.getNextWarmDate() == last_warm + + # Until the cached is warmed + time.setNow(last_warm) + await precache.checkForSmoothing() + assert precache.cached(drive.name(), time.now()) is not None + assert precache.getNextWarmDate() != last_warm + + +async def test_disable_caching(server, precache: DestinationPrecache, model: Model, drive: DriveSource, interceptor: RequestInterceptor, coord: Coordinator, time: FakeTime, config: Config) -> None: + await coord.sync() + config.override(Setting.CACHE_WARMUP_MAX_SECONDS, 0) + + time.setNow(precache.getNextWarmDate()) + await precache.checkForSmoothing() + assert precache.cached(drive.name(), time.now()) is None diff --git a/hassio-google-drive-backup/tests/test_drivesource.py b/hassio-google-drive-backup/tests/test_drivesource.py new file mode 100644 index 0000000..52d385c --- /dev/null +++ b/hassio-google-drive-backup/tests/test_drivesource.py @@ -0,0 +1,991 @@ +import os +import json +from time import sleep + +import pytest +import asyncio +from yarl import URL +from aiohttp.client_exceptions import ClientResponseError +from backup.config import Config, Setting +from dev.simulationserver import SimulationServer +from dev.simulated_google import SimulatedGoogle, URL_MATCH_UPLOAD_PROGRESS, URL_MATCH_FILE +from dev.request_interceptor import RequestInterceptor +from backup.drive import DriveSource, FolderFinder, DriveRequests, RETRY_SESSION_ATTEMPTS, UPLOAD_SESSION_EXPIRATION_DURATION, URL_START_UPLOAD +from backup.drive.driverequests import (BASE_CHUNK_SIZE, CHUNK_UPLOAD_TARGET_SECONDS) +from backup.drive.drivesource import FOLDER_MIME_TYPE +from backup.exceptions import (BackupFolderInaccessible, BackupFolderMissingError, + DriveQuotaExceeded, ExistingBackupFolderError, + GoogleCantConnect, GoogleCredentialsExpired, + GoogleInternalError, + GoogleSessionError, GoogleTimeoutError, CredRefreshMyError, CredRefreshGoogleError) +from backup.creds import Creds +from backup.model import DriveBackup, DummyBackup +from .faketime import FakeTime +from .helpers import compareStreams, createBackupTar + +RETRY_EXHAUSTION_SLEEPS = [2, 4, 8, 16, 32] + + +class BackupHelper(): + def __init__(self, uploader, time): + self.time = time + self.uploader = uploader + + async def createFile(self, size=1024 * 1024 * 2, slug="testslug", name="Test Name", note=None): + from_backup: DummyBackup = DummyBackup( + name, self.time.toUtc(self.time.local(1985, 12, 6)), "fake source", slug, note=note) + data = await self.uploader.upload(createBackupTar(slug, name, self.time.now(), size)) + return from_backup, data + + +@pytest.fixture +def backup_helper(uploader, time): + return BackupHelper(uploader, time) + + +@pytest.mark.asyncio +async def test_sync_empty(drive) -> None: + assert len(await drive.get()) == 0 + + +@pytest.mark.asyncio +async def test_CRUD(backup_helper, drive, time, session) -> None: + from_backup, data = await backup_helper.createFile() + backup: DriveBackup = await drive.save(from_backup, data) + assert backup.name() == "Test Name" + assert backup.date() == time.local(1985, 12, 6) + assert not backup.retained() + assert backup.size() == data.size() + assert backup.slug() == "testslug" + assert len(backup.id()) > 0 + assert backup.backupType() == from_backup.backupType() + assert backup.protected() == from_backup.protected() + from_backup.addSource(backup) + + # downlaod the item, its bytes should match up + download = await drive.read(from_backup) + data.position(0) + await compareStreams(data, download) + + # read the item, make sure its data matches up + backups = await drive.get() + assert len(backups) == 1 + backup = backups[from_backup.slug()] + assert backup.name() == "Test Name" + assert backup.date() == time.local(1985, 12, 6) + assert not backup.retained() + assert backup.size() == data.size() + assert backup.slug() == "testslug" + assert len(backup.id()) > 0 + assert backup.backupType() == from_backup.backupType() + assert backup.protected() == from_backup.protected() + + # update retention + assert not backup.retained() + await drive.retain(from_backup, True) + assert (await drive.get())[from_backup.slug()].retained() + await drive.retain(from_backup, False) + assert not (await drive.get())[from_backup.slug()].retained() + + # Delete the item, make sure its gone + await drive.delete(from_backup) + backups = await drive.get() + assert len(backups) == 0 + + +@pytest.mark.asyncio +async def test_folder_creation(drive, time, config): + assert len(await drive.get()) == 0 + + folderId = await drive.getFolderId() + assert len(folderId) > 0 + + item = await drive.drivebackend.get(folderId) + assert not item["trashed"] + assert item["name"] == "Home Assistant Backups" + assert item["mimeType"] == FOLDER_MIME_TYPE + assert item["appProperties"]['backup_folder'] == 'true' + + # sync again, assert the folder is reused + time.advanceDay() + os.remove(config.get(Setting.FOLDER_FILE_PATH)) + assert len(await drive.get()) == 0 + assert await drive.getFolderId() == folderId + + # trash the folder, assert we create a new one on sync + await drive.drivebackend.update(folderId, {"trashed": True}) + assert (await drive.drivebackend.get(folderId))["trashed"] is True + assert len(await drive.get()) == 0 + time.advanceDay() + assert await drive.getFolderId() != folderId + + # delete the folder, assert we create a new one + folderId = await drive.getFolderId() + await drive.drivebackend.delete(folderId) + time.advanceDay() + assert len(await drive.get()) == 0 + time.advanceDay() + assert await drive.getFolderId() != folderId + + +@pytest.mark.asyncio +async def test_folder_selection(drive, time): + folder_metadata = { + 'name': "Junk Data", + 'mimeType': FOLDER_MIME_TYPE, + 'appProperties': { + "backup_folder": "true", + }, + } + + # create two fodlers at different times + id_old = (await drive.drivebackend.createFolder(folder_metadata))['id'] + sleep(2) + id_new = (await drive.drivebackend.createFolder(folder_metadata))['id'] + + # Verify we use the newest + await drive.get() + assert await drive.getFolderId() == id_new + assert await drive.getFolderId() != id_old + + +@pytest.mark.asyncio +async def test_bad_auth_creds(drive: DriveSource, time): + drive.drivebackend.creds._refresh_token = "not_allowed" + with pytest.raises(GoogleCredentialsExpired): + await drive.get() + assert time.sleeps == [] + + +@pytest.mark.asyncio +async def test_out_of_space(backup_helper, drive: DriveSource, google: SimulatedGoogle): + google.setDriveSpaceAvailable(100) + from_backup, data = await backup_helper.createFile() + with pytest.raises(DriveQuotaExceeded): + await drive.save(from_backup, data) + + +@pytest.mark.asyncio +async def test_drive_dns_resolution_error(drive: DriveSource, config: Config, time): + config.override(Setting.DRIVE_URL, + "http://fsdfsdasdasdf.saasdsdfsdfsd.com:2567") + with pytest.raises(GoogleCantConnect): + await drive.get() + assert time.sleeps == [] + + +@pytest.mark.asyncio +async def test_drive_connect_error(drive: DriveSource, config: Config, time): + config.override(Setting.DRIVE_URL, "http://localhost:1034") + with pytest.raises(GoogleCantConnect): + await drive.get() + assert time.sleeps == [] + + +@pytest.mark.asyncio +async def test_upload_session_expired(drive, time, backup_helper, interceptor: RequestInterceptor): + from_backup, data = await backup_helper.createFile() + interceptor.setError(URL_MATCH_UPLOAD_PROGRESS, status=404) + with pytest.raises(GoogleSessionError): + await drive.save(from_backup, data) + assert time.sleeps == [] + + +@pytest.mark.asyncio +async def test_upload_resume(drive: DriveSource, time, backup_helper: BackupHelper, google: SimulatedGoogle, interceptor: RequestInterceptor): + from_backup, data = await backup_helper.createFile() + interceptor.setError(URL_MATCH_UPLOAD_PROGRESS, fail_after=1, status=500) + + # Upload, which will fail + with pytest.raises(GoogleInternalError): + await drive.save(from_backup, data) + + # Verify we uploaded one chunk + assert google.chunks == [BASE_CHUNK_SIZE] + + # Retry the upload, which shoudl now pass + interceptor.clear() + data.position(0) + drive_backup = await drive.save(from_backup, data) + from_backup.addSource(drive_backup) + assert google.chunks == [BASE_CHUNK_SIZE, + BASE_CHUNK_SIZE, (data.size()) - BASE_CHUNK_SIZE * 2] + + # Verify the data is correct + data.position(0) + await compareStreams(data, await drive.read(from_backup)) + + +def test_chunk_size(drive: DriveSource, config: Config): + max = config.get(Setting.MAXIMUM_UPLOAD_CHUNK_BYTES) + assert drive.drivebackend._getNextChunkSize( + 1000000000, 0) == max + assert drive.drivebackend._getNextChunkSize( + 1, CHUNK_UPLOAD_TARGET_SECONDS) == BASE_CHUNK_SIZE + assert drive.drivebackend._getNextChunkSize( + 1000000000, CHUNK_UPLOAD_TARGET_SECONDS) == max + assert drive.drivebackend._getNextChunkSize( + BASE_CHUNK_SIZE, CHUNK_UPLOAD_TARGET_SECONDS) == BASE_CHUNK_SIZE + assert drive.drivebackend._getNextChunkSize( + BASE_CHUNK_SIZE, 1) == BASE_CHUNK_SIZE * CHUNK_UPLOAD_TARGET_SECONDS + assert drive.drivebackend._getNextChunkSize( + BASE_CHUNK_SIZE, 1.01) == BASE_CHUNK_SIZE * (CHUNK_UPLOAD_TARGET_SECONDS - 1) + + +def test_chunk_size_limits(drive: DriveSource, config: Config): + config.override(Setting.MAXIMUM_UPLOAD_CHUNK_BYTES, 1) + assert drive.drivebackend._getNextChunkSize(1000000000, 0) == BASE_CHUNK_SIZE + assert drive.drivebackend._getNextChunkSize(1, 1000000) == BASE_CHUNK_SIZE + + config.override(Setting.MAXIMUM_UPLOAD_CHUNK_BYTES, BASE_CHUNK_SIZE * 1.5) + assert drive.drivebackend._getNextChunkSize(1000000000, 0) == BASE_CHUNK_SIZE + assert drive.drivebackend._getNextChunkSize(1, 1000000) == BASE_CHUNK_SIZE + + config.override(Setting.MAXIMUM_UPLOAD_CHUNK_BYTES, BASE_CHUNK_SIZE * 3.5) + assert drive.drivebackend._getNextChunkSize(1000000000, 0) == BASE_CHUNK_SIZE * 3 + assert drive.drivebackend._getNextChunkSize(1, 1000000) == BASE_CHUNK_SIZE + + +@pytest.mark.asyncio +async def test_working_through_upload(drive: DriveSource, server: SimulationServer, backup_helper: BackupHelper, interceptor: RequestInterceptor): + assert not drive.isWorking() + + # Let a single chunk upload, then wait + matcher = interceptor.setWaiter(URL_MATCH_UPLOAD_PROGRESS, attempts=1) + from_backup, data = await backup_helper.createFile(size=1024 * 1024 * 10) + save_task = asyncio.create_task(drive.save(from_backup, data)) + await matcher.waitForCall() + assert drive.isWorking() + + # let it complete + matcher.clear() + await save_task + assert not drive.isWorking() + + +@pytest.mark.asyncio +async def test_drive_timeout(drive, config, time: FakeTime): + # Ensure we have credentials + await drive.get() + + config.override(Setting.GOOGLE_DRIVE_TIMEOUT_SECONDS, 0.000001) + with pytest.raises(GoogleTimeoutError): + await drive.get() + assert time.sleeps == [] + + +@pytest.mark.asyncio +async def test_resume_upload_attempts_exhausted(drive: DriveSource, time, backup_helper, interceptor: RequestInterceptor, google: SimulatedGoogle): + # Allow an upload to update one chunk and then fail. + from_backup, data = await backup_helper.createFile() + interceptor.setError(URL_MATCH_UPLOAD_PROGRESS, fail_after=1, status=500) + with pytest.raises(GoogleInternalError): + await drive.save(from_backup, data) + assert google.chunks == [BASE_CHUNK_SIZE] + + # Verify we have a cached location + assert drive.drivebackend.last_attempt_location is not None + assert drive.drivebackend.last_attempt_count == 1 + last_location = drive.drivebackend.last_attempt_location + + for x in range(1, RETRY_SESSION_ATTEMPTS): + data.position(0) + with pytest.raises(GoogleInternalError): + await drive.save(from_backup, data) + assert drive.drivebackend.last_attempt_count == x + 1 + + # We should still be using the same location url + assert drive.drivebackend.last_attempt_location == last_location + + # Another attempt should use another location url + with pytest.raises(GoogleInternalError): + data.position(0) + await drive.save(from_backup, data) + assert drive.drivebackend.last_attempt_count == 0 + assert drive.drivebackend.last_attempt_location is not None + assert drive.drivebackend.last_attempt_location != last_location + + # Now let it succeed + interceptor.clear() + data.position(0) + drive_backup = await drive.save(from_backup, data) + from_backup.addSource(drive_backup) + + # And verify the bytes are correct + data.position(0) + await compareStreams(data, await drive.read(from_backup)) + + +@pytest.mark.asyncio +async def test_google_internal_error(drive, server, time: FakeTime, interceptor: RequestInterceptor): + interceptor.setError(URL_MATCH_FILE, 500) + with pytest.raises(GoogleInternalError): + await drive.get() + assert time.sleeps == RETRY_EXHAUSTION_SLEEPS + time.clearSleeps() + + interceptor.clear() + interceptor.setError(URL_MATCH_FILE, 500) + with pytest.raises(GoogleInternalError): + await drive.get() + assert time.sleeps == RETRY_EXHAUSTION_SLEEPS + + +@pytest.mark.asyncio +async def test_check_time(drive: DriveSource, drive_creds: Creds): + assert not await drive.check() + drive.saveCreds(drive_creds) + assert await drive.check() + + +@pytest.mark.asyncio +async def test_disable_upload(drive: DriveSource, config: Config): + assert drive.upload() + config.override(Setting.ENABLE_DRIVE_UPLOAD, False) + assert not drive.upload() + + +@pytest.mark.asyncio +async def test_resume_session_abandoned_on_http4XX(time, drive: DriveSource, config: Config, server, backup_helper, interceptor: RequestInterceptor): + from_backup, data = await backup_helper.createFile() + + # Configure the upload to fail after the first upload chunk + interceptor.setError(URL_MATCH_UPLOAD_PROGRESS, 402, 1) + with pytest.raises(ClientResponseError): + await drive.save(from_backup, data) + + # Verify a requst was made to start the upload but not cached + assert server.wasUrlRequested( + "/upload/drive/v3/files/?uploadType=resumable&supportsAllDrives=true") + assert drive.drivebackend.last_attempt_count == 1 + assert drive.drivebackend.last_attempt_location is None + assert drive.drivebackend.last_attempt_metadata is None + + # upload again, which should retry + server.urls.clear() + interceptor.clear() + data.position(0) + backup = await drive.save(from_backup, data) + assert server.wasUrlRequested(URL_START_UPLOAD) + + # Verify the uploaded bytes are identical + from_backup.addSource(backup) + download = await drive.read(from_backup) + data.position(0) + await compareStreams(data, download) + + +@pytest.mark.asyncio +async def test_resume_session_abandoned_after_a_long_time(time: FakeTime, drive: DriveSource, config: Config, server: SimulationServer, backup_helper, interceptor: RequestInterceptor): + from_backup, data = await backup_helper.createFile() + + # Configure the upload to fail after the first upload chunk + interceptor.setError(URL_MATCH_UPLOAD_PROGRESS, 501, 1) + with pytest.raises(ClientResponseError): + await drive.save(from_backup, data) + + # Verify it reuses the session a few times + assert server.wasUrlRequested(URL_START_UPLOAD) + assert drive.drivebackend.last_attempt_count == 1 + assert drive.drivebackend.last_attempt_location is not None + assert drive.drivebackend.last_attempt_metadata is not None + + data.position(0) + with pytest.raises(ClientResponseError): + await drive.save(from_backup, data) + assert drive.drivebackend.last_attempt_count == 2 + assert drive.drivebackend.last_attempt_location is not None + assert drive.drivebackend.last_attempt_metadata is not None + last_location = drive.drivebackend.last_attempt_location + + # Fast forward a lot, then verify the session is restarted + server.urls.clear() + interceptor.clear() + time.advance(duration=UPLOAD_SESSION_EXPIRATION_DURATION) + data.position(0) + await drive.save(from_backup, data) + assert interceptor.urlWasCalled(URL_START_UPLOAD) + assert not interceptor.urlWasCalled(last_location) + + +@pytest.mark.asyncio +async def test_chunk_upload_resets_attempt_counter(time: FakeTime, drive: DriveSource, config: Config, server: SimulationServer, backup_helper: BackupHelper, interceptor: RequestInterceptor): + from_backup, data = await backup_helper.createFile(size=1024 * 1024 * 10) + + # Configure the upload to fail after the first upload chunk + interceptor.setError(URL_MATCH_UPLOAD_PROGRESS, 501, 1) + with pytest.raises(ClientResponseError): + await drive.save(from_backup, data) + + data.position(0) + with pytest.raises(ClientResponseError): + await drive.save(from_backup, data) + + # Verify the session was started + assert interceptor.urlWasCalled(URL_START_UPLOAD) + assert interceptor.urlWasCalled(URL_MATCH_UPLOAD_PROGRESS) + assert drive.drivebackend.last_attempt_count == 2 + location = drive.drivebackend.last_attempt_location + assert location is not None + + # Allow one more chunk to succeed + interceptor.clear() + interceptor.setError(URL_MATCH_UPLOAD_PROGRESS, 501, 2) + data.position(0) + with pytest.raises(ClientResponseError): + await drive.save(from_backup, data) + + # Verify the session was reused and the attempt counter was reset + assert not interceptor.urlWasCalled(URL_START_UPLOAD) + assert interceptor.urlWasCalled(URL_MATCH_UPLOAD_PROGRESS) + assert interceptor.urlWasCalled(URL(location).path) + assert drive.drivebackend.last_attempt_count == 1 + assert drive.drivebackend.last_attempt_location == location + + +@pytest.mark.asyncio +async def test_resume_session_reused_on_http5XX(time, drive: DriveSource, config: Config, server, backup_helper, interceptor: RequestInterceptor): + await verify_upload_resumed(time, drive, config, server, interceptor, 550, backup_helper) + + +@pytest.mark.asyncio +async def test_resume_session_reused_abonded_after_retries(time, drive: DriveSource, config: Config, server: SimulationServer, backup_helper, interceptor: RequestInterceptor): + from_backup, data = await backup_helper.createFile() + + # Configure the upload to fail after the first upload chunk + interceptor.setError(URL_MATCH_UPLOAD_PROGRESS, 501, 1) + with pytest.raises(ClientResponseError): + await drive.save(from_backup, data) + + # Verify a requst was made to start the upload but not cached + assert server.wasUrlRequested(URL_START_UPLOAD) + assert drive.drivebackend.last_attempt_count == 1 + assert drive.drivebackend.last_attempt_location is not None + assert drive.drivebackend.last_attempt_metadata is not None + last_location = drive.drivebackend.last_attempt_location + + for x in range(1, RETRY_SESSION_ATTEMPTS): + server.urls.clear() + interceptor.clear() + interceptor.setError(URL_MATCH_UPLOAD_PROGRESS, 501) + data.position(0) + with pytest.raises(ClientResponseError): + await drive.save(from_backup, data) + assert not server.wasUrlRequested(URL_START_UPLOAD) + assert server.wasUrlRequested(last_location) + assert drive.drivebackend.last_attempt_count == x + 1 + assert drive.drivebackend.last_attempt_location is last_location + assert drive.drivebackend.last_attempt_metadata is not None + + # Next attempt should give up and restart the upload + server.urls.clear() + interceptor.clear() + interceptor.setError(URL_MATCH_UPLOAD_PROGRESS, 501, 1) + data.position(0) + with pytest.raises(ClientResponseError): + await drive.save(from_backup, data) + assert server.wasUrlRequested(URL_START_UPLOAD) + assert not server.wasUrlRequested(last_location) + assert drive.drivebackend.last_attempt_count == 1 + + # upload again, which should retry + server.urls.clear() + interceptor.clear() + data.position(0) + backup = await drive.save(from_backup, data) + assert not server.wasUrlRequested(URL_START_UPLOAD) + + # Verify the uploaded bytes are identical + from_backup.addSource(backup) + download = await drive.read(from_backup) + data.position(0) + await compareStreams(data, download) + + +async def verify_upload_resumed(time, drive: DriveSource, config: Config, server: SimulationServer, interceptor: RequestInterceptor, status, backup_helper, expected=ClientResponseError): + from_backup, data = await backup_helper.createFile() + + # Configure the upload to fail after the first upload chunk + interceptor.setError(URL_MATCH_UPLOAD_PROGRESS, status, 1) + with pytest.raises(expected): + await drive.save(from_backup, data) + + # Verify a requst was made to start the upload + assert server.wasUrlRequested(URL_START_UPLOAD) + assert drive.drivebackend.last_attempt_location is not None + assert drive.drivebackend.last_attempt_metadata is not None + last_location = drive.drivebackend.last_attempt_location + + # Retry the upload and let is succeed + server.urls.clear() + interceptor.clear() + data.position(0) + backup = await drive.save(from_backup, data) + + # We shoudl nto see the upload "initialize" url + assert not server.wasUrlRequested(URL_START_UPLOAD) + + # We should see the last location url (which has a unique token) reused to resume the upload + assert server.wasUrlRequested(last_location) + + # The saved metadata should be cleared out. + assert drive.drivebackend.last_attempt_count == 1 + assert drive.drivebackend.last_attempt_location is None + assert drive.drivebackend.last_attempt_metadata is None + + # Verify the uploaded bytes are identical + from_backup.addSource(backup) + download = await drive.read(from_backup) + data.position(0) + await compareStreams(data, download) + + +@pytest.mark.asyncio +async def test_recreate_folder_when_deleted(time, drive: DriveSource, config: Config, backup_helper, folder_finder: FolderFinder): + await drive.get() + id = await drive.getFolderId() + await drive.drivebackend.delete(id) + assert len(await drive.get()) == 0 + assert id != await drive.getFolderId() + + +@pytest.mark.asyncio +async def test_recreate_folder_when_losing_permissions(time, drive: DriveSource, config: Config, backup_helper, google: SimulatedGoogle): + await drive.get() + id = await drive.getFolderId() + google.lostPermission.append(id) + assert len(await drive.get()) == 0 + assert id != await drive.getFolderId() + + +@pytest.mark.asyncio +async def test_folder_missing_on_upload(time, drive: DriveSource, config: Config, backup_helper): + # Make the folder + await drive.get() + + # Require a specified folder so we don't query + config.override(Setting.SPECIFY_BACKUP_FOLDER, True) + + # Delete the folder + await drive.drivebackend.delete(await drive.getFolderId()) + + # Then try to make one + from_backup, data = await backup_helper.createFile() + + # Configure the upload to fail after the first upload chunk + with pytest.raises(BackupFolderInaccessible): + await drive.save(from_backup, data) + + +@pytest.mark.asyncio +async def test_folder_error_on_upload_lost_permission(time, drive: DriveSource, config: Config, google: SimulatedGoogle, backup_helper, session): + # Make the folder + await drive.get() + + # Require a specified folder so we don't query + config.override(Setting.SPECIFY_BACKUP_FOLDER, True) + + # Make the folder inaccessible + google.lostPermission.append(await drive.getFolderId()) + time.advanceDay() + + # Fail to upload + with pytest.raises(BackupFolderInaccessible): + await drive.save(*await backup_helper.createFile()) + + +@pytest.mark.asyncio +async def test_folder_error_on_upload_lost_permission_custom_client(time, drive: DriveSource, config: Config, google: SimulatedGoogle, backup_helper, session): + # Make the folder + await drive.get() + + # Require a specified folder so we don't query + config.override(Setting.SPECIFY_BACKUP_FOLDER, True) + + google._client_id_hack = config.get(Setting.DEFAULT_DRIVE_CLIENT_ID) + config.override(Setting.DEFAULT_DRIVE_CLIENT_ID, "something-else") + + # Make the folder inaccessible + google.lostPermission.append(await drive.getFolderId()) + time.advanceDay() + + # Fail to upload + with pytest.raises(BackupFolderInaccessible): + await drive.save(*await backup_helper.createFile()) + + +@pytest.mark.asyncio +async def test_folder_error_on_query_lost_permission(time, drive: DriveSource, config: Config, google: SimulatedGoogle): + # Make the folder + await drive.get() + + # Require a specified folder so we don't query + config.override(Setting.SPECIFY_BACKUP_FOLDER, "true") + config.override(Setting.DEFAULT_DRIVE_CLIENT_ID, "something") + + # Make the folder inaccessible + google.lostPermission.append(await drive.getFolderId()) + + # It shoudl fail! + with pytest.raises(BackupFolderInaccessible): + await drive.get() + + +@pytest.mark.asyncio +async def test_folder_error_on_query_deleted(time, drive: DriveSource, config: Config, server): + # Make the folder + await drive.get() + + # Require a specified folder so we don't query + config.override(Setting.SPECIFY_BACKUP_FOLDER, "true") + config.override(Setting.DEFAULT_DRIVE_CLIENT_ID, "something") + + # Delete the folder + await drive.drivebackend.delete(await drive.getFolderId()) + + # It should fail! + with pytest.raises(BackupFolderInaccessible): + await drive.get() + + +@pytest.mark.asyncio +async def test_backup_folder_not_specified(time, drive: DriveSource, config: Config, server, backup_helper): + config.override(Setting.SPECIFY_BACKUP_FOLDER, "true") + + with pytest.raises(BackupFolderMissingError): + await drive.get() + + from_backup, data = await backup_helper.createFile() + with pytest.raises(BackupFolderMissingError): + await drive.save(from_backup, data) + + config.override(Setting.DEFAULT_DRIVE_CLIENT_ID, "something") + with pytest.raises(BackupFolderMissingError): + await drive.get() + with pytest.raises(BackupFolderMissingError): + await drive.save(from_backup, data) + + +@pytest.mark.asyncio +async def test_folder_invalid_when_specified(time, drive: DriveSource, config: Config, server): + await drive.get() + + config.override(Setting.SPECIFY_BACKUP_FOLDER, "true") + await drive.drivebackend.update(await drive.getFolderId(), {"trashed": True}) + + time.advanceDay() + + with pytest.raises(BackupFolderInaccessible): + await drive.get() + + +@pytest.mark.asyncio +async def test_no_folder_when_required(time, drive: DriveSource, config: Config): + config.override(Setting.SPECIFY_BACKUP_FOLDER, "true") + with pytest.raises(BackupFolderMissingError): + await drive.get() + + +@pytest.mark.asyncio +async def test_existing_folder_already_exists(time, drive: DriveSource, config: Config, folder_finder: FolderFinder): + await drive.get() + drive.checkBeforeChanges() + + # Reset folder, try again + folder_finder.reset() + await drive.get() + with pytest.raises(ExistingBackupFolderError): + drive.checkBeforeChanges() + + +@pytest.mark.asyncio +async def test_existing_resolved_use_existing(time, drive: DriveSource, config: Config, folder_finder: FolderFinder): + await drive.get() + drive.checkBeforeChanges() + + folder_id = await drive.getFolderId() + + # Reset folder, try again + folder_finder.reset() + await drive.get() + with pytest.raises(ExistingBackupFolderError): + drive.checkBeforeChanges() + + folder_finder.resolveExisting(True) + await drive.get() + drive.checkBeforeChanges() + assert await drive.getFolderId() == folder_id + + +@pytest.mark.asyncio +async def test_existing_resolved_create_new(time, drive: DriveSource, config: Config, folder_finder: FolderFinder): + await drive.get() + drive.checkBeforeChanges() + + folder_id = await drive.getFolderId() + + # Reset folder, try again + folder_finder.reset() + await drive.get() + with pytest.raises(ExistingBackupFolderError): + drive.checkBeforeChanges() + + folder_finder.resolveExisting(False) + await drive.get() + drive.checkBeforeChanges() + assert await drive.getFolderId() != folder_id + + +@pytest.mark.asyncio +async def test_cred_refresh_with_secret(drive: DriveSource, google: SimulatedGoogle, time: FakeTime, config: Config): + google.resetDriveAuth() + with open(config.get(Setting.CREDENTIALS_FILE_PATH), "w") as f: + creds = google.creds() + creds._secret = config.get(Setting.DEFAULT_DRIVE_CLIENT_SECRET) + json.dump(creds.serialize(), f) + drive.drivebackend.tryLoadCredentials() + await drive.get() + old_creds = drive.drivebackend.creds + + # valid creds should be reused + await drive.get() + assert old_creds.access_token == drive.drivebackend.creds.access_token + + # then refreshed when they expire + time.advanceDay() + await drive.get() + assert old_creds.access_token != drive.drivebackend.creds.access_token + + # verify the client_secret is kept + with open(config.get(Setting.CREDENTIALS_FILE_PATH)) as f: + assert "client_secret" in json.load(f) + + +@pytest.mark.asyncio +async def test_cred_refresh_no_secret(drive: DriveSource, google: SimulatedGoogle, time: FakeTime, config: Config): + drive.saveCreds(google.creds()) + await drive.get() + old_creds = drive.drivebackend.creds + await drive.get() + assert old_creds.access_token == drive.drivebackend.creds.access_token + time.advanceDay() + await drive.get() + assert old_creds.access_token != drive.drivebackend.creds.access_token + with open(config.get(Setting.CREDENTIALS_FILE_PATH)) as f: + assert "client_secret" not in json.load(f) + + +@pytest.mark.asyncio +async def test_cred_refresh_upgrade_default_client(drive: DriveSource, server: SimulationServer, time: FakeTime, config: Config): + return + # TODO: Enable this when we start removing the default client_secret + config.override(Setting.DEFAULT_DRIVE_CLIENT_ID, server.getSetting("drive_client_id")) + creds = server.getCurrentCreds() + creds_with_secret = server.getCurrentCreds() + creds_with_secret._secret = server.getSetting("drive_client_secret") + with open(config.get(Setting.CREDENTIALS_FILE_PATH), "w") as f: + json.dump(creds_with_secret.serialize(), f) + + # reload the creds + drive.drivebackend.tryLoadCredentials() + + # Verify the "client secret" was removed + with open(config.get(Setting.CREDENTIALS_FILE_PATH)) as f: + saved_creds = json.load(f) + assert saved_creds == creds.serialize() + + await drive.get() + old_creds = drive.drivebackend.cred_bearer + await drive.get() + assert old_creds == drive.drivebackend.cred_bearer + time.advanceDay() + await drive.get() + assert old_creds != drive.drivebackend.cred_bearer + + +@pytest.mark.asyncio +async def test_cant_reach_refresh_server(drive: DriveSource, server: SimulationServer, config: Config, time): + config.override(Setting.TOKEN_SERVER_HOSTS, "http://lkasdpoiwehjhcty.com") + drive.drivebackend.creds._secret = None + time.advanceDay() + with pytest.raises(CredRefreshMyError) as error: + await drive.get() + assert error.value.data() == {"reason": "Couldn't communicate with lkasdpoiwehjhcty.com"} + + +@pytest.mark.asyncio +async def test_refresh_problem_with_google(drive: DriveSource, interceptor: RequestInterceptor, config: Config, time): + time.advanceDay() + interceptor.setError(".*/oauth2/v4/token.*", status=510) + drive.drivebackend.creds._secret = None + with pytest.raises(CredRefreshGoogleError) as error: + await drive.get() + assert error.value.data() == {"from_google": "Google returned HTTP 510"} + + +@pytest.mark.asyncio +async def test_ignore_trashed_backups(time, drive: DriveSource, config: Config, server, backup_helper): + backup = await backup_helper.createFile() + drive_backup = await drive.save(*backup) + + assert len(await drive.get()) == 1 + await drive.drivebackend.update(drive_backup.id(), {"trashed": True}) + + assert len(await drive.get()) == 0 + + +@pytest.mark.asyncio +async def test_download_timeout(time, drive: DriveSource, config: Config, interceptor: RequestInterceptor, backup_helper): + config.override(Setting.DOWNLOAD_TIMEOUT_SECONDS, 0.1) + from_backup, data = await backup_helper.createFile() + backup = await drive.save(from_backup, data) + + # Verify the uploaded bytes are identical + from_backup.addSource(backup) + interceptor.setSleep(URL_MATCH_FILE, sleep=100) + download = await drive.read(from_backup) + data.position(0) + + with pytest.raises(GoogleTimeoutError): + await compareStreams(data, download) + + +@pytest.mark.asyncio +async def test_resume_session_reused_on_http410(time, drive: DriveSource, config: Config, server: SimulationServer, backup_helper: BackupHelper, interceptor: RequestInterceptor): + from_backup, data = await backup_helper.createFile() + + # Configure the upload to fail + interceptor.setError(URL_MATCH_UPLOAD_PROGRESS, 500) + with pytest.raises(GoogleInternalError): + await drive.save(from_backup, data) + + # Verify a requst was made to start the upload + assert server.wasUrlRequested(URL_START_UPLOAD) + assert drive.drivebackend.last_attempt_location is not None + + server.urls.clear() + interceptor.clear() + data.position(0) + + interceptor.setError(drive.drivebackend.last_attempt_location, 0, 410) + await drive.save(from_backup, data) + + +@pytest.mark.asyncio +async def test_resume_session_reused_on_http408(time, drive: DriveSource, config: Config, server: SimulationServer, backup_helper: BackupHelper, interceptor: RequestInterceptor): + from_backup, data = await backup_helper.createFile() + + # Configure the upload to fail + interceptor.setError(URL_MATCH_UPLOAD_PROGRESS, 408) + with pytest.raises(GoogleTimeoutError): + await drive.save(from_backup, data) + + # Verify a requst was made to start the upload + assert server.wasUrlRequested(URL_START_UPLOAD) + location = drive.drivebackend.last_attempt_location + assert location is not None + + server.urls.clear() + interceptor.clear() + data.position(0) + + await drive.save(from_backup, data) + assert interceptor.urlWasCalled(URL(location).path) + + +@pytest.mark.asyncio +async def test_shared_drive_manager(drive: DriveSource, time: FakeTime, folder_finder: FolderFinder, backup_helper: BackupHelper, drive_requests: DriveRequests): + # Make a shared drive folder + folder_metadata = { + 'name': "Shared Drive", + 'mimeType': FOLDER_MIME_TYPE, + 'driveId': "test_shared_drive_id", + 'appProperties': { + "backup_folder": "true", + }, + } + shared_drive_folder_id = (await drive.drivebackend.createFolder(folder_metadata))['id'] + await folder_finder.save(shared_drive_folder_id) + + # Save a backup + from_backup, data = await backup_helper.createFile() + backup = await drive.save(from_backup, data) + assert len(await drive.get()) == 1 + from_backup.addSource(backup) + + # Delete the backup, and verify it was deleted instead of trashed + await drive.delete(from_backup) + assert len(await drive.get()) == 0 + with pytest.raises(ClientResponseError) as exc: + await drive_requests.get(backup.id()) + assert exc.value.code == 404 + + +@pytest.mark.asyncio +async def test_shared_drive_content_manager(drive: DriveSource, time: FakeTime, folder_finder: FolderFinder, backup_helper: BackupHelper, drive_requests: DriveRequests): + # Make a shared drive folder where the user has capabilities consistent with a "content manager" role. + folder_metadata = { + 'name': "Shared Drive", + 'mimeType': FOLDER_MIME_TYPE, + 'driveId': "test_shared_drive_id", + 'appProperties': { + "backup_folder": "true", + }, + 'capabilities': { + 'canDeleteChildren': False, + 'canTrashChildren': True, + 'canDelete': False, + 'canTrash': True, + } + } + + shared_drive_folder_id = (await drive.drivebackend.createFolder(folder_metadata))['id'] + await folder_finder.save(shared_drive_folder_id) + + # Save a backup + from_backup, data = await backup_helper.createFile() + backup = await drive.save(from_backup, data) + assert len(await drive.get()) == 1 + from_backup.addSource(backup) + + # Delete the backup, and verify it was onyl trashed + await drive.delete(from_backup) + assert len(await drive.get()) == 0 + assert (await drive_requests.get(backup.id()))['trashed'] + + +@pytest.mark.asyncio +async def test_note(drive: DriveSource, time: FakeTime, folder_finder: FolderFinder, backup_helper: BackupHelper, drive_requests: DriveRequests): + from_backup, data = await backup_helper.createFile() + backup = await drive.save(from_backup, data) + assert backup.note() is None + + full = DummyBackup(backup.name(), backup.date(), + backup.size(), backup.slug(), "dummy") + full.addSource(backup) + await drive.note(full, "new note") + assert backup.note() == "new note" + + await drive.note(full, None) + assert backup.note() is None + + +@pytest.mark.asyncio +async def test_note_truncation(drive: DriveSource, time: FakeTime, folder_finder: FolderFinder, backup_helper: BackupHelper, drive_requests: DriveRequests): + from_backup, data = await backup_helper.createFile() + backup = await drive.save(from_backup, data) + assert backup.note() is None + + full = DummyBackup(backup.name(), backup.date(), + backup.size(), backup.slug(), "dummy") + full.addSource(backup) + long_note = "".join(["䷷" for x in range(500)]) + await drive.note(full, long_note) + assert backup.note() == "".join(["䷷" for x in range(38)]) + + +@pytest.mark.asyncio +async def test_note_creation(drive: DriveSource, time: FakeTime, folder_finder: FolderFinder, backup_helper: BackupHelper, drive_requests: DriveRequests): + from_backup, data = await backup_helper.createFile(note="test") + backup = await drive.save(from_backup, data) + assert backup.note() == "test" diff --git a/hassio-google-drive-backup/tests/test_duration_parser.py b/hassio-google-drive-backup/tests/test_duration_parser.py new file mode 100644 index 0000000..9d91920 --- /dev/null +++ b/hassio-google-drive-backup/tests/test_duration_parser.py @@ -0,0 +1,89 @@ +from backup.config.durationparser import DurationParser +from datetime import timedelta + + +def test_parse_days(): + parser = DurationParser() + assert parser.parse("1 days") == timedelta(days=1) + assert parser.parse("5 days") == timedelta(days=5) + assert parser.parse("5 d") == timedelta(days=5) + assert parser.parse("5d") == timedelta(days=5) + assert parser.parse("5.0d") == timedelta(days=5) + assert parser.parse("5.0day") == timedelta(days=5) + assert parser.parse("5.0 day") == timedelta(days=5) + assert parser.parse("5.5 days") == timedelta(days=5, hours=12) + + +def test_parse_hours(): + parser = DurationParser() + assert parser.parse("1 hours") == timedelta(hours=1) + assert parser.parse("5 hours") == timedelta(hours=5) + assert parser.parse("5 h") == timedelta(hours=5) + assert parser.parse("5hour") == timedelta(hours=5) + assert parser.parse("5.0h") == timedelta(hours=5) + assert parser.parse("5.0 hour") == timedelta(hours=5) + assert parser.parse("5.5 h") == timedelta(hours=5, minutes=30) + + +def test_parse_minutes(): + parser = DurationParser() + assert parser.parse("1 minutes") == timedelta(minutes=1) + assert parser.parse("5 min") == timedelta(minutes=5) + assert parser.parse("5 m") == timedelta(minutes=5) + assert parser.parse("5mins") == timedelta(minutes=5) + assert parser.parse("5.0m") == timedelta(minutes=5) + assert parser.parse("5.0 min") == timedelta(minutes=5) + assert parser.parse("5.5 m") == timedelta(minutes=5, seconds=30) + + +def test_parse_seconds(): + parser = DurationParser() + assert parser.parse("1 seconds") == timedelta(seconds=1) + assert parser.parse("5 sec") == timedelta(seconds=5) + assert parser.parse("5 s") == timedelta(seconds=5) + assert parser.parse("5secs") == timedelta(seconds=5) + assert parser.parse("5.0s") == timedelta(seconds=5) + assert parser.parse("5.0 secs") == timedelta(seconds=5) + assert parser.parse("5.5 s") == timedelta(seconds=5, milliseconds=500) + + +def test_parse_multiple(): + parser = DurationParser() + assert parser.parse("1 day, 5 hours, 30 seconds") == timedelta(days=1, hours=5, seconds=30) + assert parser.parse("1 day 5 hours 30 seconds") == timedelta(days=1, hours=5, seconds=30) + assert parser.parse("1d 5 hours 30s") == timedelta(days=1, hours=5, seconds=30) + assert parser.parse("1d 5h 30s") == timedelta(days=1, hours=5, seconds=30) + assert parser.parse("5m 1d 5h 30s") == timedelta(days=1, hours=5, minutes=5, seconds=30) + + +def test_format(): + parser = DurationParser() + assert parser.format(timedelta(days=1)) == "1 days" + assert parser.format(timedelta(seconds=86400)) == "1 days" + assert parser.format(timedelta(hours=1)) == "1 hours" + assert parser.format(timedelta(minutes=1)) == "1 minutes" + assert parser.format(timedelta(seconds=60)) == "1 minutes" + assert parser.format(timedelta(seconds=5)) == "5 seconds" + assert parser.format(timedelta(seconds=1)) == "1 seconds" + assert parser.format(timedelta(days=5, hours=6, minutes=7)) == "5 days, 6 hours, 7 minutes" + assert parser.format(timedelta(days=5, hours=6, minutes=7, seconds=8)) == "5 days, 6 hours, 7 minutes, 8 seconds" + + +def test_back_and_forth(): + doTestConvert(timedelta(hours=5)) + doTestConvert(timedelta(minutes=600)) + doTestConvert(timedelta(days=30)) + doTestConvert(timedelta(days=5, minutes=6, hours=10, seconds=20)) + + +def doTestConvert(duration): + parser = DurationParser() + assert parser.parse(parser.format(duration)) == duration + + +def test_convert_empty_seconds(): + parser = DurationParser() + assert parser.parse("") == timedelta(seconds=0) + assert parser.parse("0") == timedelta(seconds=0) + assert parser.parse("30") == timedelta(seconds=30) + assert parser.parse(str(60 * 60)) == timedelta(seconds=60 * 60) diff --git a/hassio-google-drive-backup/tests/test_durationasstringvalidator.py b/hassio-google-drive-backup/tests/test_durationasstringvalidator.py new file mode 100644 index 0000000..933fe2b --- /dev/null +++ b/hassio-google-drive-backup/tests/test_durationasstringvalidator.py @@ -0,0 +1,28 @@ +from backup.config import DurationAsStringValidator +from backup.exceptions import InvalidConfigurationValue +from datetime import timedelta +import pytest + + +def test_minimum(): + parser = DurationAsStringValidator("test", minimum=10) + assert parser.validate("11 seconds") == 11 + assert parser.validate(11) == 11 + with pytest.raises(InvalidConfigurationValue): + parser.validate("9 seconds") + + +def test_maximum(): + parser = DurationAsStringValidator("test", maximum=10) + assert parser.validate("9 seconds") == 9 + assert parser.validate(9) == 9 + with pytest.raises(InvalidConfigurationValue): + parser.validate("11 seconds") + assert parser.formatForUi(9) == "9 seconds" + + +def test_base(): + parser = DurationAsStringValidator("test", base_seconds=60) + assert parser.validate("60 seconds") == 1 + assert parser.validate(60) == 60 + assert parser.formatForUi(1) == "1 minutes" diff --git a/hassio-google-drive-backup/tests/test_estimator.py b/hassio-google-drive-backup/tests/test_estimator.py new file mode 100644 index 0000000..d2f11eb --- /dev/null +++ b/hassio-google-drive-backup/tests/test_estimator.py @@ -0,0 +1,13 @@ +import pytest +from backup.util import Estimator +from backup.config import Config, Setting +from backup.exceptions import LowSpaceError + +@pytest.mark.asyncio +async def test_check_space(estimator: Estimator, coord, config: Config): + estimator.refresh() + estimator.checkSpace(coord.backups()) + + config.override(Setting.LOW_SPACE_THRESHOLD, estimator.getBytesFree() + 1) + with pytest.raises(LowSpaceError): + estimator.checkSpace(coord.backups()) diff --git a/hassio-google-drive-backup/tests/test_exceptions.py b/hassio-google-drive-backup/tests/test_exceptions.py new file mode 100644 index 0000000..2af73ad --- /dev/null +++ b/hassio-google-drive-backup/tests/test_exceptions.py @@ -0,0 +1,49 @@ +from bs4 import BeautifulSoup +import backup.exceptions +import inspect +import pytest +from backup.exceptions import GoogleCredGenerateError, KnownError, KnownTransient, SimulatedError, GoogleDrivePermissionDenied, InvalidConfigurationValue, LogicError, ProtocolError, NoBackup, NotUploadable, PleaseWait, UploadFailed +from .conftest import ReaderHelper + + +@pytest.mark.asyncio +async def test_verify_coverage(ui_server, reader: ReaderHelper): + # Get the list of exception codes + ignore = [ + KnownError, + KnownTransient, + SimulatedError, + GoogleDrivePermissionDenied, + InvalidConfigurationValue, + LogicError, + NoBackup, + NotUploadable, + PleaseWait, + ProtocolError, + UploadFailed, + GoogleCredGenerateError, + ] + codes = {} + for name, obj in inspect.getmembers(backup.exceptions): + if inspect.isclass(obj) and (KnownError in obj.__bases__) and obj not in ignore: + codes[obj().code()] = obj + + # Get the list of ui dialogs + document = await reader.get("", json=False) + page = BeautifulSoup(document, 'html.parser') + + dialogs = {} + for div in page.find_all("div"): + cls = div.get("class") + if cls is None: + continue + if "error_card" in cls: + for specific_class in cls: + if specific_class in dialogs: + dialogs[specific_class] = dialogs[specific_class] + 1 + else: + dialogs[specific_class] = 1 + + # Make sure exactly one dialog has the class + for code in codes.keys(): + assert dialogs[code] == 1 diff --git a/hassio-google-drive-backup/tests/test_exchanger.py b/hassio-google-drive-backup/tests/test_exchanger.py new file mode 100644 index 0000000..9293e21 --- /dev/null +++ b/hassio-google-drive-backup/tests/test_exchanger.py @@ -0,0 +1,186 @@ +import pytest + +from dev.simulationserver import SimulationServer, RequestInterceptor +from backup.time import Time +from backup.config import Config, Setting +from backup.drive import DriveRequests +from backup.exceptions import CredRefreshMyError, GoogleCredentialsExpired, CredRefreshGoogleError +from backup.tracing_session import TracingSession +from yarl import URL + + +@pytest.mark.asyncio +async def test_correct_host(time: Time, session: TracingSession, config: Config, server: SimulationServer, drive_requests: DriveRequests, server_url, interceptor: RequestInterceptor): + # Verify the correct endpoitns get called for a successful request + session.record = True + await drive_requests.exchanger.refresh(drive_requests.creds) + assert interceptor.urlWasCalled("/drive/refresh") + session._records[0]['url'] == server_url.with_path("/drive/refresh") + + +@pytest.mark.asyncio +async def test_some_bad_hosts(time: Time, session: TracingSession, config: Config, server: SimulationServer, drive_requests: DriveRequests, server_url, interceptor: RequestInterceptor): + session.record = True + config.override(Setting.EXCHANGER_TIMEOUT_SECONDS, 1) + config.override(Setting.TOKEN_SERVER_HOSTS, "https://this.goes.nowhere.info," + str(server_url)) + + await drive_requests.exchanger.refresh(drive_requests.creds) + assert interceptor.urlWasCalled("/drive/refresh") + + # Verify both hosts were checked + session._records[0]['url'] == URL("https://this.goes.nowhere.info").with_path("/drive/refresh") + session._records[1]['url'] == server_url.with_path("/drive/refresh") + + +@pytest.mark.asyncio +async def test_all_bad_hosts(time: Time, session: TracingSession, config: Config, server: SimulationServer, drive_requests: DriveRequests, interceptor: RequestInterceptor): + session.record = True + config.override(Setting.EXCHANGER_TIMEOUT_SECONDS, 1) + config.override(Setting.TOKEN_SERVER_HOSTS, "https://this.goes.nowhere.info,http://also.a.bad.host") + + with pytest.raises(CredRefreshMyError) as e: + await drive_requests.exchanger.refresh(drive_requests.creds) + + # Error should be about the last host name + assert e.value.reason.index("also.a.bad.host") >= 0 + + # Verify both hosts were checked + session._records[0]['url'] == URL("https://this.goes.nowhere.info").with_path("/drive/refresh") + session._records[1]['url'] == URL("http://also.a.bad.host").with_path("/drive/refresh") + + +@pytest.mark.asyncio +async def test_exchange_timeout(time: Time, session: TracingSession, config: Config, server: SimulationServer, drive_requests: DriveRequests, interceptor: RequestInterceptor, server_url: URL): + session.record = True + interceptor.setSleep("/drive/refresh", sleep=10) + + config.override(Setting.EXCHANGER_TIMEOUT_SECONDS, 0.1) + + with pytest.raises(CredRefreshMyError) as e: + await drive_requests.exchanger.refresh(drive_requests.creds) + + # Error should be about the last host name + assert e.value.reason == "Timed out communicating with localhost" + + # Verify both hosts were checked + session._records[0]['url'] == server_url.with_path("/drive/refresh") + + +@pytest.mark.asyncio +async def test_exchange_invalid_creds(time: Time, session: TracingSession, config: Config, server: SimulationServer, drive_requests: DriveRequests, interceptor: RequestInterceptor, server_url: URL): + session.record = True + drive_requests.creds._refresh_token = "fail" + with pytest.raises(GoogleCredentialsExpired): + await drive_requests.exchanger.refresh(drive_requests.creds) + + # Verify both hosts were checked + session._records[0]['url'] == server_url.with_path("/drive/refresh") + + +@pytest.mark.asyncio +async def test_fail_503_with_error(time: Time, session: TracingSession, config: Config, server: SimulationServer, drive_requests: DriveRequests, interceptor: RequestInterceptor, server_url: URL): + session.record = True + interceptor.setError("^/drive/refresh$", 503, response={'error': 'test_value'}) + with pytest.raises(CredRefreshGoogleError) as e: + await drive_requests.exchanger.refresh(drive_requests.creds) + assert e.value.message() == "Couldn't refresh your credentials with Google because: 'test_value'" + + # Verify both hosts were checked + session._records[0]['url'] == server_url.with_path("/drive/refresh") + + +@pytest.mark.asyncio +async def test_fail_503_invalid_grant(time: Time, session: TracingSession, config: Config, server: SimulationServer, drive_requests: DriveRequests, interceptor: RequestInterceptor, server_url: URL): + session.record = True + interceptor.setError("^/drive/refresh$", 503, response={'error': 'invalid_grant'}) + with pytest.raises(GoogleCredentialsExpired): + await drive_requests.exchanger.refresh(drive_requests.creds) + + # Verify both hosts were checked + session._records[0]['url'] == server_url.with_path("/drive/refresh") + + +@pytest.mark.asyncio +async def test_fail_503_with_invalid_json(time: Time, session: TracingSession, config: Config, server: SimulationServer, drive_requests: DriveRequests, interceptor: RequestInterceptor, server_url: URL): + session.record = True + interceptor.setError("^/drive/refresh$", 503, response={'ignored': 'nothing'}) + with pytest.raises(CredRefreshMyError) as e: + await drive_requests.exchanger.refresh(drive_requests.creds) + assert e.value.message() == "Couldn't refresh Google Drive credentials because: HTTP 503 from localhost" + + # Verify both hosts were checked + session._records[0]['url'] == server_url.with_path("/drive/refresh") + + +@pytest.mark.asyncio +async def test_fail_503_with_no_data(time: Time, session: TracingSession, config: Config, server: SimulationServer, drive_requests: DriveRequests, interceptor: RequestInterceptor, server_url: URL): + session.record = True + interceptor.setError("^/drive/refresh$", 503) + with pytest.raises(CredRefreshMyError) as e: + await drive_requests.exchanger.refresh(drive_requests.creds) + assert e.value.message() == "Couldn't refresh Google Drive credentials because: HTTP 503 from localhost" + + # Verify both hosts were checked + session._records[0]['url'] == server_url.with_path("/drive/refresh") + + +@pytest.mark.asyncio +async def test_fail_401(time: Time, session: TracingSession, config: Config, server: SimulationServer, drive_requests: DriveRequests, interceptor: RequestInterceptor, server_url: URL): + session.record = True + interceptor.setError("^/drive/refresh$", 401) + with pytest.raises(GoogleCredentialsExpired): + await drive_requests.exchanger.refresh(drive_requests.creds) + + # Verify both hosts were checked + session._records[0]['url'] == server_url.with_path("/drive/refresh") + + +@pytest.mark.asyncio +async def test_fail_401_no_fall_through(time: Time, session: TracingSession, config: Config, server: SimulationServer, drive_requests: DriveRequests, interceptor: RequestInterceptor, server_url: URL): + session.record = True + config.override(Setting.TOKEN_SERVER_HOSTS, str(server_url) + "," + str(server_url)) + interceptor.setError("^/drive/refresh$", 401) + with pytest.raises(GoogleCredentialsExpired): + await drive_requests.exchanger.refresh(drive_requests.creds) + + # Verify both hosts were checked + session._records[0]['url'] == server_url.with_path("/drive/refresh") + assert len(session._records) == 1 + + +@pytest.mark.asyncio +async def test_invalid_grant_no_fall_through(time: Time, session: TracingSession, config: Config, server: SimulationServer, drive_requests: DriveRequests, interceptor: RequestInterceptor, server_url: URL): + session.record = True + config.override(Setting.TOKEN_SERVER_HOSTS, str(server_url) + "," + str(server_url)) + interceptor.setError("^/drive/refresh$", 503, response={'error': 'invalid_grant'}) + with pytest.raises(GoogleCredentialsExpired): + await drive_requests.exchanger.refresh(drive_requests.creds) + + # Verify both hosts were checked + session._records[0]['url'] == server_url.with_path("/drive/refresh") + assert len(session._records) == 1 + + +@pytest.mark.asyncio +async def test_timeout_fall_through(time: Time, session: TracingSession, config: Config, server: SimulationServer, drive_requests: DriveRequests, interceptor: RequestInterceptor, server_url: URL): + session.record = True + config.override(Setting.EXCHANGER_TIMEOUT_SECONDS, 0.1) + config.override(Setting.TOKEN_SERVER_HOSTS, str(server_url) + "," + str(server_url)) + interceptor.setSleep("^/drive/refresh$", sleep=10, wait_for=1) + await drive_requests.exchanger.refresh(drive_requests.creds) + + # Verify both hosts were checked + session._records[0]['url'] == server_url.with_path("/drive/refresh") + session._records[1]['url'] == server_url.with_path("/drive/refresh") + + +@pytest.mark.asyncio +async def test_anything_else_through(time: Time, session: TracingSession, config: Config, server: SimulationServer, drive_requests: DriveRequests, interceptor: RequestInterceptor, server_url: URL): + session.record = True + config.override(Setting.TOKEN_SERVER_HOSTS, str(server_url) + "," + str(server_url)) + interceptor.setError("^/drive/refresh$", status=500, fail_for=1) + await drive_requests.exchanger.refresh(drive_requests.creds) + + # Verify both hosts were checked + session._records[0]['url'] == server_url.with_path("/drive/refresh") + session._records[1]['url'] == server_url.with_path("/drive/refresh") diff --git a/hassio-google-drive-backup/tests/test_file.py b/hassio-google-drive-backup/tests/test_file.py new file mode 100644 index 0000000..3c93e35 --- /dev/null +++ b/hassio-google-drive-backup/tests/test_file.py @@ -0,0 +1,60 @@ + + +from backup.file import File +from os.path import exists, join +from os import remove +import pytest +import json + +TEST_DATA = "when you press my special key I play a little melody" + + +def readfile(path): + with open(path) as f: + return f.read() + + +@pytest.mark.asyncio +async def test_basic(tmpdir: str) -> None: + path = join(tmpdir, "test.json") + backup_path = join(tmpdir, "test.json.backup") + + assert not File.exists(path) + File.write(path, TEST_DATA) + assert File.exists(path) + assert readfile(path) == TEST_DATA + assert readfile(backup_path) == TEST_DATA + assert File.read(path) == TEST_DATA + + File.delete(path) + assert not exists(path) + assert not exists(backup_path) + assert not File.exists(path) + + +@pytest.mark.asyncio +async def test_file_deleted(tmpdir: str) -> None: + path = join(tmpdir, "test.json") + File.write(path, TEST_DATA) + remove(path) + assert File.read(path) == TEST_DATA + + +@pytest.mark.asyncio +async def test_backup_deleted(tmpdir: str) -> None: + path = join(tmpdir, "test.json") + backup_path = join(tmpdir, "test.json.backup") + File.write(path, TEST_DATA) + remove(backup_path) + assert File.read(path) == TEST_DATA + +@pytest.mark.asyncio +async def test_decode_error(tmpdir: str) -> None: + path = join(tmpdir, "test.json") + File.write(path, TEST_DATA) + with open(path, "w"): + # emptys the file contents + pass + with open(path) as f: + assert len(f.read()) == 0 + assert File.read(path) == TEST_DATA diff --git a/hassio-google-drive-backup/tests/test_hasource.py b/hassio-google-drive-backup/tests/test_hasource.py new file mode 100644 index 0000000..13dec8d --- /dev/null +++ b/hassio-google-drive-backup/tests/test_hasource.py @@ -0,0 +1,1087 @@ +import asyncio +from datetime import timedelta +import os + +import pytest +from aiohttp.client_exceptions import ClientResponseError + +from backup.config import Config, Setting, CreateOptions, Version +from backup.const import SOURCE_HA +from backup.exceptions import (HomeAssistantDeleteError, BackupInProgress, + BackupPasswordKeyInvalid, UploadFailed, SupervisorConnectionError, SupervisorPermissionError, SupervisorTimeoutError) +from backup.util import GlobalInfo, DataCache, KEY_CREATED, KEY_LAST_SEEN, KEY_NAME +from backup.ha import HaSource, PendingBackup, EVENT_BACKUP_END, EVENT_BACKUP_START, HABackup, Password, AddonStopper +from backup.model import DummyBackup +from dev.simulationserver import SimulationServer +from .faketime import FakeTime +from .helpers import all_addons, all_folders, createBackupTar, getTestStream +from dev.simulated_supervisor import SimulatedSupervisor, URL_MATCH_SELF_OPTIONS, URL_MATCH_START_ADDON, URL_MATCH_STOP_ADDON, URL_MATCH_BACKUP_FULL, URL_MATCH_BACKUP_DELETE, URL_MATCH_MISC_INFO, URL_MATCH_BACKUP_DOWNLOAD, URL_MATCH_BACKUPS, URL_MATCH_SNAPSHOT +from dev.request_interceptor import RequestInterceptor +from backup.model import Model +from backup.time import Time +from yarl import URL + + +@pytest.mark.asyncio +async def test_sync_empty(ha) -> None: + assert len(await ha.get()) == 0 + + +@pytest.mark.asyncio +async def test_CRUD(ha: HaSource, time, interceptor: RequestInterceptor, data_cache: DataCache) -> None: + backup: HABackup = await ha.create(CreateOptions(time.now(), "Test Name")) + + assert backup.name() == "Test Name" + assert type(backup) is HABackup + assert not backup.retained() + assert backup.backupType() == "full" + assert not backup.protected() + assert backup.name() == "Test Name" + assert backup.source() == SOURCE_HA + assert not backup.ignore() + assert backup.madeByTheAddon() + assert "pending" not in data_cache.backups + + # read the item directly, its metadata should match + from_ha = await ha.harequests.backup(backup.slug()) + assert from_ha.size() == backup.size() + assert from_ha.slug() == backup.slug() + assert from_ha.source() == SOURCE_HA + + backups = await ha.get() + assert len(backups) == 1 + assert backup.slug() in backups + + full = DummyBackup(from_ha.name(), from_ha.date(), + from_ha.size(), from_ha.slug(), "dummy") + full.addSource(backup) + + # download the item, its bytes should match up + download = await ha.read(full) + await download.setup() + direct_download = await ha.harequests.download(backup.slug()) + await direct_download.setup() + while True: + from_file = await direct_download.read(1024 * 1024) + from_download = await download.read(1024 * 1024) + if len(from_file.getbuffer()) == 0: + assert len(from_download.getbuffer()) == 0 + break + assert from_file.getbuffer() == from_download.getbuffer() + + # update retention + assert not backup.retained() + await ha.retain(full, True) + assert (await ha.get())[full.slug()].retained() + await ha.retain(full, False) + assert not (await ha.get())[full.slug()].retained() + + # Delete the item, make sure its gone + await ha.delete(full) + assert full.getSource(ha.name()) is None + backups = await ha.get() + assert len(backups) == 0 + + +@pytest.mark.asyncio +@pytest.mark.timeout(10) +async def test_pending_backup_nowait(ha: HaSource, time: Time, supervisor: SimulatedSupervisor, interceptor: RequestInterceptor, config: Config, data_cache: DataCache): + interceptor.setSleep(URL_MATCH_BACKUP_FULL, sleep=5) + config.override(Setting.NEW_BACKUP_TIMEOUT_SECONDS, 0.1) + backup_immediate: PendingBackup = await ha.create(CreateOptions(time.now(), "Test Name")) + assert isinstance(backup_immediate, PendingBackup) + backup_pending: HABackup = (await ha.get())['pending'] + + assert isinstance(backup_immediate, PendingBackup) + assert isinstance(backup_pending, PendingBackup) + assert backup_immediate is backup_pending + assert backup_immediate.name() == "Test Name" + assert backup_immediate.slug() == "pending" + assert not backup_immediate.uploadable() + assert backup_immediate.backupType() == "Full" + assert backup_immediate.source() == SOURCE_HA + assert backup_immediate.date() == time.now() + assert not backup_immediate.protected() + assert not backup_immediate.ignore() + assert backup_immediate.madeByTheAddon() + assert data_cache.backup("pending") == { + KEY_CREATED: time.now().isoformat(), + KEY_LAST_SEEN: time.now().isoformat(), + KEY_NAME: "Test Name" + } + + # Might be a little flaky but...whatever + await asyncio.wait({ha._pending_backup_task}) + + backups = await ha.get() + assert 'pending' not in backups + assert len(backups) == 1 + backup = next(iter(backups.values())) + assert isinstance(backup, HABackup) + assert not backup.ignore() + assert backup.madeByTheAddon() + assert data_cache.backup(backup.slug())[KEY_LAST_SEEN] == time.now().isoformat() + assert "pending" not in data_cache.backups + + return + # ignroe events for now + assert supervisor.getEvents() == [ + (EVENT_BACKUP_START, { + 'backup_name': backup_immediate.name(), + 'backup_time': str(backup_immediate.date())})] + ha.backup_thread.join() + assert supervisor.getEvents() == [ + (EVENT_BACKUP_START, { + 'backup_name': backup_immediate.name(), + 'backup_time': str(backup_immediate.date())}), + (EVENT_BACKUP_END, { + 'completed': True, + 'backup_name': backup_immediate.name(), + 'backup_time': str(backup_immediate.date())})] + + +@pytest.mark.asyncio +async def test_pending_backup_already_in_progress(ha, time, config: Config, supervisor: SimulatedSupervisor): + await ha.create(CreateOptions(time.now(), "Test Name")) + assert len(await ha.get()) == 1 + + config.override(Setting.NEW_BACKUP_TIMEOUT_SECONDS, 100) + await supervisor.toggleBlockBackup() + with pytest.raises(BackupInProgress): + await ha.create(CreateOptions(time.now(), "Test Name")) + backups = list((await ha.get()).values()) + assert len(backups) == 2 + backup = backups[1] + + assert isinstance(backup, PendingBackup) + assert backup.name() == "Pending Backup" + assert backup.slug() == "pending" + assert not backup.uploadable() + assert backup.backupType() == "unknown" + assert backup.source() == SOURCE_HA + assert backup.date() == time.now() + assert not backup.protected() + + with pytest.raises(BackupInProgress): + await ha.create(CreateOptions(time.now(), "Test Name")) + + +@pytest.mark.asyncio +async def test_partial_backup(ha, time, server, config: Config): + config.override(Setting.NEW_BACKUP_TIMEOUT_SECONDS, 100) + for folder in all_folders: + config.override(Setting.EXCLUDE_FOLDERS, folder) + backup: HABackup = await ha.create(CreateOptions(time.now(), "Test Name")) + + assert backup.backupType() == "partial" + for search in all_folders: + if search == folder: + assert search not in backup.details()['folders'] + else: + assert search in backup.details()['folders'] + + for addon in all_addons: + config.override(Setting.EXCLUDE_ADDONS, addon['slug']) + backup: HABackup = await ha.create(CreateOptions(time.now(), "Test Name")) + assert backup.backupType() == "partial" + list_of_addons = [] + for included in backup.details()['addons']: + list_of_addons.append(included['slug']) + for search in list_of_addons: + if search == addon: + assert search not in list_of_addons + else: + assert search in list_of_addons + + # excluding addon/folders that don't exist should actually make a full backup + config.override(Setting.EXCLUDE_ADDONS, "none,of.these,are.addons") + config.override(Setting.EXCLUDE_FOLDERS, "not,folders,either") + backup: HABackup = await ha.create(CreateOptions(time.now(), "Test Name")) + assert backup.backupType() == "full" + + +@pytest.mark.asyncio +async def test_backup_password(ha: HaSource, config: Config, time): + config.override(Setting.NEW_BACKUP_TIMEOUT_SECONDS, 100) + backup: HABackup = await ha.create(CreateOptions(time.now(), "Test Name")) + assert not backup.protected() + + config.override(Setting.BACKUP_PASSWORD, 'test') + backup = await ha.create(CreateOptions(time.now(), "Test Name")) + assert backup.protected() + + config.override(Setting.BACKUP_PASSWORD, 'test') + assert Password(ha.config).resolve() == 'test' + + config.override(Setting.BACKUP_PASSWORD, '!secret for_unit_tests') + assert Password(ha.config).resolve() == 'password value' + + config.override(Setting.BACKUP_PASSWORD, '!secret bad_key') + with pytest.raises(BackupPasswordKeyInvalid): + Password(config).resolve() + + config.override(Setting.SECRETS_FILE_PATH, "/bad/file/path") + config.override(Setting.BACKUP_PASSWORD, '!secret for_unit_tests') + with pytest.raises(BackupPasswordKeyInvalid): + Password(ha.config).resolve() + + +@pytest.mark.asyncio +async def test_backup_name(time: FakeTime, ha): + time.setNow(time.local(1985, 12, 6, 15, 8, 9, 10)) + await assertName(ha, time.now(), "{type}", "Full") + await assertName(ha, time.now(), "{year}", "1985") + await assertName(ha, time.now(), "{year_short}", "85") + await assertName(ha, time.now(), "{weekday}", "Friday") + await assertName(ha, time.now(), "{weekday_short}", "Fri") + await assertName(ha, time.now(), "{month}", "12") + await assertName(ha, time.now(), "{month_long}", "December") + await assertName(ha, time.now(), "{month_short}", "Dec") + await assertName(ha, time.now(), "{ms}", "000010") + await assertName(ha, time.now(), "{day}", "06") + await assertName(ha, time.now(), "{hr24}", "15") + await assertName(ha, time.now(), "{hr12}", "03") + await assertName(ha, time.now(), "{min}", "08") + await assertName(ha, time.now(), "{sec}", "09") + await assertName(ha, time.now(), "{ampm}", "PM") + await assertName(ha, time.now(), "{version_ha}", "ha version") + await assertName(ha, time.now(), "{version_hassos}", "hassos version") + await assertName(ha, time.now(), "{version_super}", "super version") + await assertName(ha, time.now(), "{date}", "12/06/85") + await assertName(ha, time.now(), "{time}", "15:08:09") + await assertName(ha, time.now(), "{datetime}", "Fri Dec 6 15:08:09 1985") + await assertName(ha, time.now(), "{isotime}", "1985-12-06T15:08:09.000010-05:00") + + +async def assertName(ha: HaSource, time, template: str, expected: str): + backup: HABackup = await ha.create(CreateOptions(time, template)) + assert backup.name() == expected + + +@pytest.mark.asyncio +async def test_default_name(time: FakeTime, ha, server): + backup = await ha.create(CreateOptions(time.now(), "")) + assert backup.name() == "Full Backup 1985-12-06 00:00:00" + + +@pytest.mark.asyncio +async def test_pending_backup_timeout(time: FakeTime, ha: HaSource, config: Config, interceptor: RequestInterceptor, supervisor: SimulatedSupervisor): + config.override(Setting.NEW_BACKUP_TIMEOUT_SECONDS, 1) + config.override(Setting.FAILED_BACKUP_TIMEOUT_SECONDS, 1) + config.override(Setting.PENDING_BACKUP_TIMEOUT_SECONDS, 1) + + async with supervisor._backup_inner_lock: + backup_immediate: PendingBackup = await ha.create(CreateOptions(time.now(), "Test Name")) + assert isinstance(backup_immediate, PendingBackup) + assert backup_immediate.name() == "Test Name" + assert not await ha.check() + assert ha.pending_backup is backup_immediate + + await asyncio.wait({ha._pending_backup_task}) + assert ha.pending_backup is backup_immediate + assert await ha.check() + assert not await ha.check() + + time.advance(minutes=1) + assert await ha.check() + assert len(await ha.get()) == 0 + assert not await ha.check() + assert ha.pending_backup is None + assert backup_immediate.isStale() + + +@pytest.mark.asyncio +async def test_pending_backup_timeout_external(time, config, ha: HaSource, supervisor: SimulatedSupervisor): + # now configure a snapshto to start outside of the addon + config.override(Setting.NEW_BACKUP_TIMEOUT_SECONDS, 100) + await supervisor.toggleBlockBackup() + with pytest.raises(BackupInProgress): + await ha.create(CreateOptions(time.now(), "Ignored")) + backup_immediate = (await ha.get())['pending'] + await supervisor.toggleBlockBackup() + assert isinstance(backup_immediate, PendingBackup) + assert backup_immediate.name() == "Pending Backup" + assert await ha.check() + assert not await ha.check() + assert ha.pending_backup is backup_immediate + + # should clean up after a day, since we're still waiting on the backup thread. + time.advanceDay() + assert await ha.check() + assert len(await ha.get()) == 0 + + +@pytest.mark.asyncio +async def test_pending_backup_replaces_original(time, ha: HaSource, config: Config, supervisor: SimulatedSupervisor): + # now configure a snapshto to start outside of the addon + config.override(Setting.NEW_BACKUP_TIMEOUT_SECONDS, 100) + await supervisor.toggleBlockBackup() + with pytest.raises(BackupInProgress): + await ha.create(CreateOptions(time.now(), "Ignored")) + backup_immediate = (await ha.get())['pending'] + await supervisor.toggleBlockBackup() + assert isinstance(backup_immediate, PendingBackup) + assert backup_immediate.name() == "Pending Backup" + assert await ha.check() + assert ha.pending_backup is backup_immediate + assert await ha.get() == {backup_immediate.slug(): backup_immediate} + + # create a new backup behind the scenes, the pending backup should get replaced with the new one + slug = (await ha.harequests.createBackup({'name': "Suddenly Appears", "hardlock": True}))['slug'] + results = await ha.get() + assert len(results) == 1 + assert slug in results + assert results[slug].name() == "Suddenly Appears" + assert not results[slug].retained() + + +def test_retryable_errors(): + # SOMEDAY: retryable errors should be retried in the future + pass + + +@pytest.mark.asyncio +async def test_retained_on_finish(ha: HaSource, server, time, config: Config, supervisor: SimulatedSupervisor): + async with supervisor._backup_inner_lock: + retention = {ha.name(): True} + config.override(Setting.NEW_BACKUP_TIMEOUT_SECONDS, 0.0001) + pending = await ha.create(CreateOptions(time.now(), "Test Name", retention)) + results = await ha.get() + assert pending.name() == "Test Name" + assert results == {pending.slug(): pending} + assert type(pending) == PendingBackup + assert not ha._pending_backup_task.done() + + await asyncio.wait({ha._pending_backup_task}) + results = list((await ha.get()).values()) + assert len(results) == 1 + assert results[0].name() == "Test Name" + assert type(results[0]) == HABackup + assert results[0].retained() + assert config.isRetained(results[0].slug()) + + +@pytest.mark.asyncio +async def test_upload(time, ha, server, uploader): + data = await uploader.upload(createBackupTar("slug", "Test Name", time.now(), 1024 * 1024)) + dummy = DummyBackup("Test Name", time.now(), "src", "slug", "dummy") + backup: HABackup = await ha.save(dummy, data) + assert backup.name() == "Test Name" + assert backup.slug() == "slug" + assert backup.size() == round(data.size() / 1024.0 / 1024.0, 2) * 1024 * 1024 + assert backup.retained() + # ensure its still retained on a refresh + assert list((await ha.get()).values())[0].retained() + + +@pytest.mark.asyncio +async def test_corrupt_upload(time, ha, server, uploader): + # verify a corrupt backup throws the right exception + bad_data = await uploader.upload(getTestStream(100)) + dummy = DummyBackup("Test Name", time.now(), "src", "slug2", "dummy") + + with pytest.raises(UploadFailed): + await ha.save(dummy, bad_data) + + +@pytest.mark.asyncio +async def test_upload_wrong_slug(time, ha, server, uploader): + # verify a backup with the wrong slug also throws + bad_data = await uploader.upload(createBackupTar("wrongslug", "Test Name", time.now(), 1024 * 1024)) + dummy = DummyBackup("Test Name", time.now(), "src", "slug", "dummy") + with pytest.raises(UploadFailed): + await ha.save(dummy, bad_data) + + +@pytest.mark.asyncio +async def test_failed_backup(time, ha: HaSource, supervisor: SimulatedSupervisor, config: Config, interceptor: RequestInterceptor): + # create a blocking backup + interceptor.setError(URL_MATCH_BACKUP_FULL, 524) + config.override(Setting.NEW_BACKUP_TIMEOUT_SECONDS, 0) + await supervisor.toggleBlockBackup() + backup_immediate = await ha.create(CreateOptions(time.now(), "Some Name")) + assert isinstance(backup_immediate, PendingBackup) + assert backup_immediate.name() == "Some Name" + assert not await ha.check() + assert not backup_immediate.isFailed() + await supervisor.toggleBlockBackup() + + # let the backup attempt to complete + await asyncio.wait({ha._pending_backup_task}) + + # verify it failed with the expected http error + assert backup_immediate.isFailed() + assert backup_immediate._exception.status == 524 + + backups = list((await ha.get()).values()) + assert len(backups) == 1 + assert backups[0] is backup_immediate + + # verify we can create a new backup immediately + interceptor.clear() + await ha.create(CreateOptions(time.now(), "Some Name")) + assert len(await ha.get()) == 1 + + +@pytest.mark.asyncio +async def test_failed_backup_retry(ha: HaSource, time: FakeTime, config: Config, supervisor: SimulatedSupervisor, interceptor: RequestInterceptor): + # create a blocking backup + interceptor.setError(URL_MATCH_BACKUP_FULL, 524) + config.override(Setting.NEW_BACKUP_TIMEOUT_SECONDS, 0) + await supervisor.toggleBlockBackup() + backup_immediate = await ha.create(CreateOptions(time.now(), "Some Name")) + assert isinstance(backup_immediate, PendingBackup) + assert backup_immediate.name() == "Some Name" + assert not await ha.check() + assert not backup_immediate.isFailed() + await supervisor.toggleBlockBackup() + + # let the backup attempt to complete + await asyncio.wait({ha._pending_backup_task}) + + # verify it failed with the expected http error + assert backup_immediate.isFailed() + assert backup_immediate._exception.status == 524 + + assert await ha.check() + assert not await ha.check() + time.advance(seconds=config.get(Setting.FAILED_BACKUP_TIMEOUT_SECONDS)) + + # should trigger a sync after the failed backup timeout + assert await ha.check() + await ha.get() + assert not await ha.check() + + +@pytest.mark.asyncio +async def test_immediate_backup_failure(time: FakeTime, ha: HaSource, config: Config, interceptor: RequestInterceptor): + interceptor.setError(URL_MATCH_BACKUP_FULL, 524) + with pytest.raises(ClientResponseError) as thrown: + await ha.create(CreateOptions(time.now(), "Some Name")) + assert thrown.value.status == 524 + + assert ha.pending_backup is not None + backups = list((await ha.get()).values()) + assert len(backups) == 1 + assert backups[0].isFailed() + + # Failed backup should go away after it times out + assert await ha.check() + assert not await ha.check() + time.advance(seconds=config.get( + Setting.FAILED_BACKUP_TIMEOUT_SECONDS) + 1) + assert await ha.check() + + assert len(await ha.get()) == 0 + assert not await ha.check() + + +@pytest.mark.asyncio +async def test_delete_error(time, ha: HaSource, interceptor: RequestInterceptor): + backup = await ha.create(CreateOptions(time.now(), "Some Name")) + full = DummyBackup(backup.name(), backup.date(), + backup.size(), backup.slug(), "dummy") + full.addSource(backup) + interceptor.setError(URL_MATCH_BACKUP_DELETE, 400) + with pytest.raises(HomeAssistantDeleteError): + await ha.delete(full) + + interceptor.clear() + await ha.delete(full) + + +@pytest.mark.asyncio +async def test_hostname(time, ha: HaSource, server, global_info: GlobalInfo): + await ha.init() + assert global_info.url == "/hassio/ingress/self_slug" + + +@pytest.mark.asyncio +async def test_supervisor_error(time, ha: HaSource, server: SimulationServer, global_info: GlobalInfo): + await server.stop() + with pytest.raises(SupervisorConnectionError): + await ha.init() + + +@pytest.mark.asyncio +async def test_supervisor_permission_error(time, ha: HaSource, interceptor: RequestInterceptor, global_info: GlobalInfo): + interceptor.setError(URL_MATCH_MISC_INFO, 403) + with pytest.raises(SupervisorPermissionError): + await ha.init() + + interceptor.clear() + interceptor.setError(URL_MATCH_MISC_INFO, 404) + with pytest.raises(ClientResponseError): + await ha.init() + + +@pytest.mark.asyncio +async def test_download_timeout(ha: HaSource, time, interceptor: RequestInterceptor, config: Config) -> None: + config.override(Setting.NEW_BACKUP_TIMEOUT_SECONDS, 100) + backup: HABackup = await ha.create(CreateOptions(time.now(), "Test Name")) + from_ha = await ha.harequests.backup(backup.slug()) + full = DummyBackup(from_ha.name(), from_ha.date(), + from_ha.size(), from_ha.slug(), "dummy") + full.addSource(backup) + + interceptor.setSleep(URL_MATCH_BACKUP_DOWNLOAD, sleep=100) + config.override(Setting.DOWNLOAD_TIMEOUT_SECONDS, 1) + direct_download = await ha.harequests.download(backup.slug()) + + with pytest.raises(SupervisorTimeoutError): + await direct_download.setup() + await direct_download.read(1) + + +@pytest.mark.asyncio +async def test_start_and_stop_addon(ha: HaSource, time, interceptor: RequestInterceptor, config: Config, supervisor: SimulatedSupervisor, addon_stopper: AddonStopper) -> None: + addon_stopper.allowRun() + slug = "test_slug" + supervisor.installAddon(slug, "Test decription") + config.override(Setting.STOP_ADDONS, slug) + config.override(Setting.NEW_BACKUP_TIMEOUT_SECONDS, 0.001) + + assert supervisor.addon(slug)["state"] == "started" + async with supervisor._backup_inner_lock: + await ha.create(CreateOptions(time.now(), "Test Name")) + assert supervisor.addon(slug)["state"] == "stopped" + await ha._pending_backup_task + assert supervisor.addon(slug)["state"] == "started" + + +@pytest.mark.asyncio +async def test_start_and_stop_two_addons(ha: HaSource, time, interceptor: RequestInterceptor, config: Config, supervisor: SimulatedSupervisor, addon_stopper: AddonStopper) -> None: + addon_stopper.allowRun() + slug1 = "test_slug_1" + supervisor.installAddon(slug1, "Test decription") + + slug2 = "test_slug_2" + supervisor.installAddon(slug2, "Test decription") + config.override(Setting.STOP_ADDONS, ",".join([slug1, slug2])) + config.override(Setting.NEW_BACKUP_TIMEOUT_SECONDS, 0.001) + + assert supervisor.addon(slug1)["state"] == "started" + assert supervisor.addon(slug2)["state"] == "started" + async with supervisor._backup_inner_lock: + await ha.create(CreateOptions(time.now(), "Test Name")) + assert supervisor.addon(slug1)["state"] == "stopped" + assert supervisor.addon(slug2)["state"] == "stopped" + await ha._pending_backup_task + assert supervisor.addon(slug1)["state"] == "started" + assert supervisor.addon(slug2)["state"] == "started" + + +@pytest.mark.asyncio +async def test_stop_addon_failure(ha: HaSource, time, interceptor: RequestInterceptor, config: Config, supervisor: SimulatedSupervisor, addon_stopper: AddonStopper) -> None: + addon_stopper.allowRun() + slug = "test_slug" + supervisor.installAddon(slug, "Test decription") + config.override(Setting.STOP_ADDONS, slug) + config.override(Setting.NEW_BACKUP_TIMEOUT_SECONDS, 0.001) + interceptor.setError(URL_MATCH_STOP_ADDON, 400) + + assert supervisor.addon(slug)["state"] == "started" + async with supervisor._backup_inner_lock: + await ha.create(CreateOptions(time.now(), "Test Name")) + assert supervisor.addon(slug)["state"] == "started" + await ha._pending_backup_task + assert supervisor.addon(slug)["state"] == "started" + assert len(await ha.get()) == 1 + + +@pytest.mark.asyncio +async def test_start_addon_failure(ha: HaSource, time, interceptor: RequestInterceptor, config: Config, supervisor: SimulatedSupervisor, addon_stopper: AddonStopper) -> None: + addon_stopper.allowRun() + slug = "test_slug" + supervisor.installAddon(slug, "Test decription") + config.override(Setting.STOP_ADDONS, slug) + config.override(Setting.NEW_BACKUP_TIMEOUT_SECONDS, 0.001) + interceptor.setError(URL_MATCH_START_ADDON, 400) + + assert supervisor.addon(slug)["state"] == "started" + async with supervisor._backup_inner_lock: + await ha.create(CreateOptions(time.now(), "Test Name")) + assert supervisor.addon(slug)["state"] == "stopped" + await ha._pending_backup_task + assert supervisor.addon(slug)["state"] == "stopped" + assert len(await ha.get()) == 1 + + +@pytest.mark.asyncio +async def test_ingore_self_when_stopping(ha: HaSource, time, interceptor: RequestInterceptor, config: Config, supervisor: SimulatedSupervisor, addon_stopper: AddonStopper) -> None: + addon_stopper.allowRun() + slug = supervisor._addon_slug + config.override(Setting.STOP_ADDONS, slug) + config.override(Setting.NEW_BACKUP_TIMEOUT_SECONDS, 0.001) + interceptor.setError(URL_MATCH_START_ADDON, 400) + + assert supervisor.addon(slug)["state"] == "started" + async with supervisor._backup_inner_lock: + await ha.create(CreateOptions(time.now(), "Test Name")) + assert supervisor.addon(slug)["state"] == "started" + await ha._pending_backup_task + assert supervisor.addon(slug)["state"] == "started" + assert not interceptor.urlWasCalled(URL_MATCH_START_ADDON) + assert not interceptor.urlWasCalled(URL_MATCH_STOP_ADDON) + assert len(await ha.get()) == 1 + + +@pytest.mark.asyncio +async def test_dont_purge_pending_backup(ha: HaSource, time, config: Config, supervisor: SimulatedSupervisor, model: Model, interceptor): + config.override(Setting.MAX_BACKUPS_IN_HA, 4) + await ha.create(CreateOptions(time.now(), "Test Name 1")) + await ha.create(CreateOptions(time.now(), "Test Name 2")) + await ha.create(CreateOptions(time.now(), "Test Name 3")) + await ha.create(CreateOptions(time.now(), "Test Name 4")) + await model.sync(time.now()) + + config.override(Setting.NEW_BACKUP_TIMEOUT_SECONDS, 0.1) + interceptor.setSleep(URL_MATCH_BACKUP_FULL, sleep=2) + await ha.create(CreateOptions(time.now(), "Test Name")) + backups = list((await ha.get()).values()) + assert len(backups) == 5 + backup = backups[4] + assert isinstance(backup, PendingBackup) + + # no backup should get purged yet because the ending backup isn't considered for purging. + await model.sync(time.now()) + backups = list((await ha.get()).values()) + assert len(backups) == 5 + + # Wait for the backup to finish, then verify one gets purged. + await ha._pending_backup_task + await model.sync(time.now()) + backups = list((await ha.get()).values()) + assert len(backups) == 4 + + +@pytest.mark.asyncio +async def test_matching_pending_backup(ha: HaSource, time: Time, config: Config, supervisor: SimulatedSupervisor, model: Model, interceptor, data_cache: DataCache): + ''' + A pending backups with the same name and within a day of the backup time should be considered + made by the addon + ''' + data_cache.backup("pending")[KEY_NAME] = "Test Backup" + data_cache.backup("pending")[KEY_CREATED] = time.now().isoformat() + data_cache.backup("pending")[KEY_LAST_SEEN] = time.now().isoformat() + + await supervisor.createBackup({"name": "Test Backup"}, date=time.now() - timedelta(hours=12)) + + backups = await ha.get() + assert len(backups) == 1 + backup = next(iter(backups.values())) + assert backup.madeByTheAddon() + + +@pytest.mark.asyncio +async def test_date_match_wrong_pending_backup(ha: HaSource, time: Time, config: Config, supervisor: SimulatedSupervisor, model: Model, interceptor, data_cache: DataCache): + ''' + A pending backups with the same name but with the wrong date shoudl nto be considered made by the addon + ''' + data_cache.backup("pending")[KEY_NAME] = "Test Backup" + data_cache.backup("pending")[KEY_CREATED] = time.now().isoformat() + data_cache.backup("pending")[KEY_LAST_SEEN] = time.now().isoformat() + + await supervisor.createBackup({"name": "Test Backup"}, date=time.now() - timedelta(hours=25)) + + backups = await ha.get() + assert len(backups) == 1 + backups = next(iter(backups.values())) + assert not backups.madeByTheAddon() + + +@pytest.mark.asyncio +async def test_name_wrong_match_pending_backup(ha: HaSource, time: Time, config: Config, supervisor: SimulatedSupervisor, model: Model, interceptor, data_cache: DataCache): + ''' + A pending backups with the wrong name shoudl not be considered made by the addon + ''' + data_cache.backup("pending")[KEY_NAME] = "Test Backup" + data_cache.backup("pending")[KEY_CREATED] = time.now().isoformat() + data_cache.backup("pending")[KEY_LAST_SEEN] = time.now().isoformat() + + await supervisor.createBackup({"name": "Wrong Name"}, date=time.now() - timedelta(hours=12)) + + backups = await ha.get() + assert len(backups) == 1 + backup = next(iter(backups.values())) + assert not backup.madeByTheAddon() + + +@pytest.mark.asyncio +async def test_bump_last_seen(ha: HaSource, time: Time, config: Config, supervisor: SimulatedSupervisor, model: Model, interceptor, data_cache: DataCache): + backup = await ha.create(CreateOptions(time.now(), "Test Name")) + time.advance(days=1) + assert backup.slug() in await ha.get() + assert data_cache.backup(backup.slug())[KEY_LAST_SEEN] == time.now().isoformat() + + time.advance(days=1) + assert backup.slug() in await ha.get() + assert data_cache.backup(backup.slug())[KEY_LAST_SEEN] == time.now().isoformat() + + +@pytest.mark.asyncio +async def test_backup_supervisor_path(ha: HaSource, supervisor: SimulatedSupervisor, interceptor: RequestInterceptor): + supervisor._super_version = Version(2021, 8) + await ha.get() + assert interceptor.urlWasCalled(URL_MATCH_BACKUPS) + assert not interceptor.urlWasCalled(URL_MATCH_SNAPSHOT) + + +@pytest.mark.asyncio +async def test_backup_supervisor_path_old_version(ha: HaSource, supervisor: SimulatedSupervisor, interceptor: RequestInterceptor): + supervisor._super_version = Version(2021, 7) + await ha.get() + assert not interceptor.urlWasCalled(URL_MATCH_BACKUPS) + assert interceptor.urlWasCalled(URL_MATCH_SNAPSHOT) + + +@pytest.mark.asyncio +async def test_supervisor_host(ha: HaSource, supervisor: SimulatedSupervisor, interceptor: RequestInterceptor, config: Config, server_url): + assert ha.harequests.getSupervisorURL() == server_url + + config.override(Setting.SUPERVISOR_URL, "") + assert ha.harequests.getSupervisorURL() == URL("http://hassio") + + os.environ['SUPERVISOR_TOKEN'] = "test" + assert ha.harequests.getSupervisorURL() == URL("http://supervisor") + + +@pytest.mark.asyncio +async def test_upgrade_default_config(ha: HaSource, supervisor: SimulatedSupervisor, interceptor: RequestInterceptor, config: Config, server_url): + """Verify that converting the original default config optiosn works as expected""" + + # overwrite the addon options with old values + supervisor._options = { + Setting.DEPRECTAED_MAX_BACKUPS_IN_HA.value: 4, + Setting.DEPRECTAED_MAX_BACKUPS_IN_GOOGLE_DRIVE.value: 4, + Setting.DEPRECATED_DAYS_BETWEEN_BACKUPS.value: 3, + Setting.USE_SSL.value: False, + } + + await ha.init() + + assert not config.mustSaveUpgradeChanges() + assert interceptor.urlWasCalled(URL_MATCH_SELF_OPTIONS) + + # Verify the config was upgraded + assert supervisor._options == { + Setting.MAX_BACKUPS_IN_HA.value: 4, + Setting.MAX_BACKUPS_IN_GOOGLE_DRIVE.value: 4, + Setting.DAYS_BETWEEN_BACKUPS.value: 3, + Setting.CALL_BACKUP_SNAPSHOT.value: True, + } + + +@pytest.mark.asyncio +async def test_upgrade_all_config(ha: HaSource, supervisor: SimulatedSupervisor, interceptor: RequestInterceptor, config: Config, server_url): + """Verify that converting all upgradeable config optiosn works as expected""" + + # overwrite the addon options with old values + supervisor._options = { + Setting.DEPRECTAED_MAX_BACKUPS_IN_HA.value: 1, + Setting.DEPRECTAED_MAX_BACKUPS_IN_GOOGLE_DRIVE.value: 2, + Setting.DEPRECATED_DAYS_BETWEEN_BACKUPS.value: 5, + Setting.DEPRECTAED_IGNORE_OTHER_BACKUPS.value: True, + Setting.DEPRECTAED_IGNORE_UPGRADE_BACKUPS.value: True, + Setting.DEPRECTAED_BACKUP_TIME_OF_DAY.value: "01:11", + Setting.DEPRECTAED_DELETE_BEFORE_NEW_BACKUP.value: True, + Setting.DEPRECTAED_BACKUP_NAME.value: "test", + Setting.DEPRECTAED_SPECIFY_BACKUP_FOLDER.value: True, + Setting.DEPRECTAED_NOTIFY_FOR_STALE_BACKUPS.value: False, + Setting.DEPRECTAED_ENABLE_BACKUP_STALE_SENSOR.value: False, + Setting.DEPRECTAED_ENABLE_BACKUP_STATE_SENSOR.value: False, + Setting.DEPRECATED_BACKUP_PASSWORD.value: "test password", + } + + await ha.init() + assert not config.mustSaveUpgradeChanges() + assert interceptor.urlWasCalled(URL_MATCH_SELF_OPTIONS) + + # Verify the config was upgraded + assert supervisor._options == { + Setting.MAX_BACKUPS_IN_HA.value: 1, + Setting.MAX_BACKUPS_IN_GOOGLE_DRIVE.value: 2, + Setting.DAYS_BETWEEN_BACKUPS.value: 5, + Setting.IGNORE_OTHER_BACKUPS.value: True, + Setting.IGNORE_UPGRADE_BACKUPS.value: True, + Setting.BACKUP_TIME_OF_DAY.value: "01:11", + Setting.DELETE_BEFORE_NEW_BACKUP.value: True, + Setting.BACKUP_NAME.value: "test", + Setting.SPECIFY_BACKUP_FOLDER.value: True, + Setting.NOTIFY_FOR_STALE_BACKUPS.value: False, + Setting.ENABLE_BACKUP_STALE_SENSOR.value: False, + Setting.ENABLE_BACKUP_STATE_SENSOR.value: False, + Setting.BACKUP_PASSWORD.value: "test password", + Setting.CALL_BACKUP_SNAPSHOT.value: True, + } + + interceptor.clear() + + await ha.init() + assert not interceptor.urlWasCalled(URL_MATCH_SELF_OPTIONS) + + +@pytest.mark.asyncio +async def test_upgrade_some_config(ha: HaSource, supervisor: SimulatedSupervisor, interceptor: RequestInterceptor, config: Config, server_url): + """Verify that converting a mix of upgradeable and not upgradeable config works""" + + # overwrite the addon options with old values + supervisor._options = { + Setting.DEPRECTAED_MAX_BACKUPS_IN_HA.value: 4, + Setting.DEPRECTAED_MAX_BACKUPS_IN_GOOGLE_DRIVE.value: 4, + Setting.DEPRECATED_DAYS_BETWEEN_BACKUPS.value: 3, + Setting.DEPRECTAED_BACKUP_TIME_OF_DAY.value: "01:11", + Setting.EXCLUDE_ADDONS.value: "test", + Setting.USE_SSL.value: False, + } + + await ha.init() + + assert not config.mustSaveUpgradeChanges() + assert interceptor.urlWasCalled(URL_MATCH_SELF_OPTIONS) + + # Verify the config was upgraded + assert supervisor._options == { + Setting.MAX_BACKUPS_IN_HA.value: 4, + Setting.MAX_BACKUPS_IN_GOOGLE_DRIVE.value: 4, + Setting.DAYS_BETWEEN_BACKUPS.value: 3, + Setting.EXCLUDE_ADDONS.value: "test", + Setting.BACKUP_TIME_OF_DAY.value: "01:11", + Setting.CALL_BACKUP_SNAPSHOT.value: True, + } + + +@pytest.mark.asyncio +async def test_upgrade_no_config(ha: HaSource, supervisor: SimulatedSupervisor, interceptor: RequestInterceptor, config: Config, server_url): + """Verifies that config not in need of an upgrade doesn't get upgraded""" + + # overwrite the addon options with old values + supervisor._options = { + Setting.MAX_BACKUPS_IN_HA.value: 4, + Setting.MAX_BACKUPS_IN_GOOGLE_DRIVE.value: 4, + Setting.DAYS_BETWEEN_BACKUPS.value: 3, + Setting.BACKUP_TIME_OF_DAY.value: "01:11", + Setting.EXCLUDE_ADDONS.value: "test" + } + + await ha.init() + + assert not config.mustSaveUpgradeChanges() + assert not interceptor.urlWasCalled(URL_MATCH_SELF_OPTIONS) + + # Verify the config was upgraded + assert supervisor._options == { + Setting.MAX_BACKUPS_IN_HA.value: 4, + Setting.MAX_BACKUPS_IN_GOOGLE_DRIVE.value: 4, + Setting.DAYS_BETWEEN_BACKUPS.value: 3, + Setting.BACKUP_TIME_OF_DAY.value: "01:11", + Setting.EXCLUDE_ADDONS.value: "test", + } + + +@pytest.mark.asyncio +async def test_old_delete_path(ha: HaSource, supervisor: SimulatedSupervisor, interceptor: RequestInterceptor, time: FakeTime): + supervisor._super_version = Version(2020, 8) + await ha.get() + backup: HABackup = await ha.create(CreateOptions(time.now(), "Test Name")) + full = DummyBackup(backup.name(), backup.date(), + backup.size(), backup.slug(), "dummy") + full.addSource(backup) + await ha.delete(full) + assert interceptor.urlWasCalled("/snapshots/{0}/remove".format(backup.slug())) + + +@pytest.mark.asyncio +async def test_ignore_upgrade_backup_ha_config(ha: HaSource, time: Time, supervisor: SimulatedSupervisor, interceptor: RequestInterceptor, config: Config, data_cache: DataCache): + config.override(Setting.IGNORE_UPGRADE_BACKUPS, True) + slug = (await ha.harequests.createBackup({'name': "Suddenly Appears", 'folders': ['homeassistant'], 'addons': []}))['slug'] + + backups = await ha.get() + assert len(backups) == 1 + assert slug in backups + assert backups[slug].ignore() + + +@pytest.mark.asyncio +async def test_ignore_upgrade_backup_single_folder(ha: HaSource, time: Time, supervisor: SimulatedSupervisor, interceptor: RequestInterceptor, config: Config, data_cache: DataCache): + config.override(Setting.IGNORE_UPGRADE_BACKUPS, True) + slug = (await ha.harequests.createBackup({'name': "Suddenly Appears", 'folders': ['share'], 'addons': []}))['slug'] + + backups = await ha.get() + assert len(backups) == 1 + assert slug in backups + assert backups[slug].ignore() + + +@pytest.mark.asyncio +async def test_ignore_upgrade_backup_single_addon(ha: HaSource, time: Time, supervisor: SimulatedSupervisor, interceptor: RequestInterceptor, config: Config, data_cache: DataCache): + config.override(Setting.IGNORE_UPGRADE_BACKUPS, True) + slug = (await ha.harequests.createBackup({'name': "Suddenly Appears", 'folders': [], 'addons': ["particla_accel"]}))['slug'] + + backups = await ha.get() + assert len(backups) == 1 + assert slug in backups + assert backups[slug].ignore() + + +@pytest.mark.asyncio +async def test_ignore_upgrade_backup_two_folders(ha: HaSource, time: Time, supervisor: SimulatedSupervisor, interceptor: RequestInterceptor, config: Config, data_cache: DataCache): + config.override(Setting.IGNORE_UPGRADE_BACKUPS, True) + slug = (await ha.harequests.createBackup({'name': "Suddenly Appears", 'folders': ['homeassistant', "share"], 'addons': []}))['slug'] + + backups = await ha.get() + assert len(backups) == 1 + assert slug in backups + assert not backups[slug].ignore() + + +@pytest.mark.asyncio +async def test_ignore_upgrade_backup_empty(ha: HaSource, time: Time, supervisor: SimulatedSupervisor, interceptor: RequestInterceptor, config: Config, data_cache: DataCache): + config.override(Setting.IGNORE_UPGRADE_BACKUPS, True) + slug = (await ha.harequests.createBackup({'name': "Suddenly Appears", 'folders': [], 'addons': []}))['slug'] + + backups = await ha.get() + assert len(backups) == 1 + assert slug in backups + assert not backups[slug].ignore() + + +@pytest.mark.asyncio +async def test_very_long_running_backup(time, config, ha: HaSource, supervisor: SimulatedSupervisor): + config.override(Setting.NEW_BACKUP_TIMEOUT_SECONDS, 1) + config.override(Setting.PENDING_BACKUP_TIMEOUT_SECONDS, 2) + config.override(Setting.IGNORE_OTHER_BACKUPS, True) + + async with supervisor._backup_inner_lock: + await ha.create(CreateOptions(time.now(), "Actually gets made")) + + for _ in range(4): + time.advance(hours=1) + assert "pending" in (await ha.get()) + + # Let the task fail + await ha._pending_backup_task + + # after 4 hours the pending backup should be assumed to have failed and cleaned up + time.advance(hours=1) + assert len(await ha.get()) == 0 + + for _ in range(4): + time.advance(hours=1) + # Making a backup should keep failing + with pytest.raises(BackupInProgress): + await ha.create(CreateOptions(time.now(), "Ignored")) + backups = await ha.get() + assert len(backups) == 1 + assert "pending" in backups + + # Wait for the backup to complete + async with supervisor._backup_lock: + pass + + time.advance(hours=1) + backups = await ha.get() + assert len(backups) == 1 + assert "pending" not in backups + assert list(backups.values())[0].madeByTheAddon() + + +@pytest.mark.asyncio +async def test_note(time, config, ha: HaSource, supervisor: SimulatedSupervisor): + backup = await ha.create(CreateOptions(time.now(), "Backup")) + assert isinstance(backup, HABackup) + assert backup.note() is None + + full = DummyBackup(backup.name(), backup.date(), + backup.size(), backup.slug(), "dummy") + full.addSource(backup) + await ha.note(full, "new note") + assert backup.note() == "new note" + + await ha.note(full, None) + assert backup.note() is None + + +@pytest.mark.asyncio +async def test_note_creation(time, config, ha: HaSource, supervisor: SimulatedSupervisor): + backup = await ha.create(CreateOptions(time.now(), "Backup", note="creation note")) + assert isinstance(backup, HABackup) + assert backup.note() == "creation note" + assert (await ha.get())[backup.slug()].note() == "creation note" + + full = DummyBackup(backup.name(), backup.date(), + backup.size(), backup.slug(), "dummy") + full.addSource(backup) + await ha.note(full, "new note") + assert backup.note() == "new note" + assert (await ha.get())[full.slug()].note() == "new note" + + await ha.note(full, None) + assert backup.note() is None + + +@pytest.mark.asyncio +async def test_note_long_backup(time, config, ha: HaSource, supervisor: SimulatedSupervisor): + config.override(Setting.NEW_BACKUP_TIMEOUT_SECONDS, 1) + + async with supervisor._backup_inner_lock: + backup = await ha.create(CreateOptions(time.now(), "Backup", note="Creation note")) + assert isinstance(backup, PendingBackup) + assert backup.note() == "Creation note" + + pending = (await ha.get())[backup.slug()] + assert pending.note() == "Creation note" + await ha._pending_backup_task + completed = next(iter((await ha.get()).values())) + assert not isinstance(completed, PendingBackup) + assert completed.note() == "Creation note" + + +@pytest.mark.asyncio +async def test_note_long_backup_changed_during_creation(time, config, ha: HaSource, supervisor: SimulatedSupervisor): + config.override(Setting.NEW_BACKUP_TIMEOUT_SECONDS, 1) + + async with supervisor._backup_inner_lock: + backup = await ha.create(CreateOptions(time.now(), "Backup", note="Creation note")) + assert isinstance(backup, PendingBackup) + assert backup.note() == "Creation note" + + pending = (await ha.get())[backup.slug()] + assert pending.note() == "Creation note" + + full = DummyBackup(pending.name(), pending.date(), pending.size(), pending.slug(), "dummy") + full.addSource(pending) + await ha.note(full, "changed") + + still_pending = next(iter((await ha.get()).values())) + assert isinstance(still_pending, PendingBackup) + assert still_pending.note() == "changed" + + await ha._pending_backup_task + completed = next(iter((await ha.get()).values())) + assert not isinstance(completed, PendingBackup) + assert completed.note() == "changed" + + +@pytest.mark.asyncio +async def test_note_change_external_backup(time, config, ha: HaSource, supervisor: SimulatedSupervisor): + config.override(Setting.NEW_BACKUP_TIMEOUT_SECONDS, 100) + + await supervisor.toggleBlockBackup() + with pytest.raises(BackupInProgress): + await ha.create(CreateOptions(time.now(), "Ignored", note="ignored")) + pending = next(iter((await ha.get()).values())) + assert isinstance(pending, PendingBackup) + assert pending.note() is None + + full = DummyBackup(pending.name(), pending.date(), pending.size(), pending.slug(), "dummy") + full.addSource(pending) + await ha.note(full, "changed note") + assert full.note() == "changed note" + + # create a new backup in the background, this should get the note of the pending backup. + await supervisor.toggleBlockBackup() + await supervisor.createBackup({"name": "Test Backup"}, date=time.now() - timedelta(hours=12)) + completed = next(iter((await ha.get()).values())) + assert not isinstance(completed, PendingBackup) + assert completed.note() == "changed note" diff --git a/hassio-google-drive-backup/tests/test_haupdater.py b/hassio-google-drive-backup/tests/test_haupdater.py new file mode 100644 index 0000000..af196d9 --- /dev/null +++ b/hassio-google-drive-backup/tests/test_haupdater.py @@ -0,0 +1,416 @@ +from datetime import timedelta +from backup.model.backups import Backup +import pytest + +from backup.util import GlobalInfo +from backup.ha import HaUpdater +from backup.ha.haupdater import REASSURING_MESSAGE +from .faketime import FakeTime +from .helpers import HelperTestSource +from dev.simulationserver import SimulationServer +from backup.logger import getLast +from backup.util import Estimator +from dev.simulated_supervisor import SimulatedSupervisor, URL_MATCH_CORE_API +from dev.request_interceptor import RequestInterceptor +from backup.model import Coordinator +from backup.config import Config, Setting + +STALE_ATTRIBUTES = { + "friendly_name": "Backups Stale", + "device_class": "problem" +} + + +@pytest.fixture +def source(): + return HelperTestSource("Source") + + +@pytest.fixture +def dest(): + return HelperTestSource("Dest") + + +@pytest.mark.asyncio +async def test_init(updater: HaUpdater, global_info, supervisor: SimulatedSupervisor, server, time: FakeTime): + await updater.update() + assert not updater._stale() + assert updater._state() == "waiting" + verifyEntity(supervisor, "binary_sensor.backups_stale", + "off", STALE_ATTRIBUTES) + verifyEntity(supervisor, "sensor.backup_state", "waiting", { + 'friendly_name': 'Backup State', + 'last_backup': 'Never', + 'next_backup': time.now().isoformat(), + 'last_uploaded': 'Never', + 'backups': [], + 'backups_in_google_drive': 0, + 'free_space_in_google_drive': "", + 'backups_in_home_assistant': 0, + 'size_in_google_drive': "0.0 B", + 'size_in_home_assistant': '0.0 B' + }) + assert supervisor.getNotification() is None + + global_info.success() + assert not updater._stale() + assert updater._state() == "backed_up" + + +@pytest.mark.asyncio +async def test_init_failure(updater: HaUpdater, global_info: GlobalInfo, time: FakeTime, server, supervisor: SimulatedSupervisor): + await updater.update() + assert not updater._stale() + assert updater._state() == "waiting" + + global_info.failed(Exception()) + assert not updater._stale() + assert updater._state() == "backed_up" + assert supervisor.getNotification() is None + + time.advanceDay() + assert updater._stale() + assert updater._state() == "error" + await updater.update() + assert supervisor.getNotification() == { + 'message': 'The add-on is having trouble making backups and needs attention. Please visit the add-on status page for details.', + 'title': 'Home Assistant Google Drive Backup is Having Trouble', + 'notification_id': 'backup_broken' + } + + +@pytest.mark.asyncio +async def test_failure_backoff_502(updater: HaUpdater, server, time: FakeTime, interceptor: RequestInterceptor): + interceptor.setError(URL_MATCH_CORE_API, 502) + for x in range(9): + await updater.update() + assert time.sleeps == [60, 120, 240, 300, 300, 300, 300, 300, 300] + + interceptor.clear() + await updater.update() + assert time.sleeps == [60, 120, 240, 300, 300, 300, 300, 300, 300] + + +@pytest.mark.asyncio +async def test_failure_backoff_510(updater: HaUpdater, server, time: FakeTime, interceptor: RequestInterceptor): + interceptor.setError(URL_MATCH_CORE_API, 502) + for x in range(9): + await updater.update() + assert time.sleeps == [60, 120, 240, 300, 300, 300, 300, 300, 300] + + interceptor.clear() + await updater.update() + assert time.sleeps == [60, 120, 240, 300, 300, 300, 300, 300, 300] + + +@pytest.mark.asyncio +async def test_failure_backoff_other(updater: HaUpdater, server, time: FakeTime, interceptor: RequestInterceptor): + interceptor.setError(URL_MATCH_CORE_API, 400) + for x in range(9): + await updater.update() + assert time.sleeps == [60, 120, 240, 300, 300, 300, 300, 300, 300] + interceptor.clear() + await updater.update() + assert time.sleeps == [60, 120, 240, 300, 300, 300, 300, 300, 300] + + +@pytest.mark.asyncio +async def test_update_backups(updater: HaUpdater, server, time: FakeTime, supervisor: SimulatedSupervisor): + await updater.update() + assert not updater._stale() + assert updater._state() == "waiting" + verifyEntity(supervisor, "binary_sensor.backups_stale", + "off", STALE_ATTRIBUTES) + verifyEntity(supervisor, "sensor.backup_state", "waiting", { + 'friendly_name': 'Backup State', + 'last_backup': 'Never', + 'next_backup': time.now().isoformat(), + 'last_uploaded': 'Never', + 'backups': [], + 'backups_in_google_drive': 0, + 'backups_in_home_assistant': 0, + 'size_in_home_assistant': "0.0 B", + 'size_in_google_drive': "0.0 B", + 'free_space_in_google_drive': '' + }) + + +@pytest.mark.asyncio +async def test_update_backups_no_next_backup(updater: HaUpdater, server, time: FakeTime, supervisor: SimulatedSupervisor, config: Config): + config.override(Setting.DAYS_BETWEEN_BACKUPS, 0) + await updater.update() + assert not updater._stale() + assert updater._state() == "waiting" + verifyEntity(supervisor, "binary_sensor.backups_stale", + "off", STALE_ATTRIBUTES) + verifyEntity(supervisor, "sensor.backup_state", "waiting", { + 'friendly_name': 'Backup State', + 'last_backup': 'Never', + 'next_backup': None, + 'last_uploaded': 'Never', + 'backups': [], + 'backups_in_google_drive': 0, + 'backups_in_home_assistant': 0, + 'size_in_home_assistant': "0.0 B", + 'size_in_google_drive': "0.0 B", + 'free_space_in_google_drive': '' + }) + + +@pytest.mark.asyncio +async def test_update_backups_sync(updater: HaUpdater, server, time: FakeTime, backup: Backup, supervisor: SimulatedSupervisor, config: Config): + await updater.update() + assert not updater._stale() + assert updater._state() == "backed_up" + verifyEntity(supervisor, "binary_sensor.backups_stale", + "off", STALE_ATTRIBUTES) + date = '1985-12-06T05:00:00+00:00' + verifyEntity(supervisor, "sensor.backup_state", "backed_up", { + 'friendly_name': 'Backup State', + 'last_backup': date, + 'last_uploaded': date, + 'next_backup': (backup.date() + timedelta(days=config.get(Setting.DAYS_BETWEEN_BACKUPS))).isoformat(), + 'backups': [{ + 'date': date, + 'name': backup.name(), + 'size': backup.sizeString(), + 'state': backup.status(), + 'slug': backup.slug() + } + ], + 'backups_in_google_drive': 1, + 'backups_in_home_assistant': 1, + 'size_in_home_assistant': Estimator.asSizeString(backup.size()), + 'size_in_google_drive': Estimator.asSizeString(backup.size()), + 'free_space_in_google_drive': '4.0 GB' + }) + + +@pytest.mark.asyncio +async def test_notification_link(updater: HaUpdater, server, time: FakeTime, global_info, supervisor: SimulatedSupervisor): + await updater.update() + assert not updater._stale() + assert updater._state() == "waiting" + verifyEntity(supervisor, "binary_sensor.backups_stale", + "off", STALE_ATTRIBUTES) + verifyEntity(supervisor, "sensor.backup_state", "waiting", { + 'friendly_name': 'Backup State', + 'last_backup': 'Never', + 'next_backup': time.now().isoformat(), + 'last_uploaded': 'Never', + 'backups': [], + 'backups_in_google_drive': 0, + 'backups_in_home_assistant': 0, + 'size_in_home_assistant': "0.0 B", + 'size_in_google_drive': "0.0 B", + 'free_space_in_google_drive': '' + }) + assert supervisor.getNotification() is None + + global_info.failed(Exception()) + global_info.url = "http://localhost/test" + time.advanceDay() + await updater.update() + assert supervisor.getNotification() == { + 'message': 'The add-on is having trouble making backups and needs attention. Please visit the add-on [status page](http://localhost/test) for details.', + 'title': 'Home Assistant Google Drive Backup is Having Trouble', + 'notification_id': 'backup_broken' + } + + +@pytest.mark.asyncio +async def test_notification_clears(updater: HaUpdater, server, time: FakeTime, global_info, supervisor: SimulatedSupervisor): + await updater.update() + assert not updater._stale() + assert updater._state() == "waiting" + assert supervisor.getNotification() is None + + global_info.failed(Exception()) + time.advance(hours=8) + await updater.update() + assert supervisor.getNotification() is not None + + global_info.success() + await updater.update() + assert supervisor.getNotification() is None + + +@pytest.mark.asyncio +async def test_publish_for_failure(updater: HaUpdater, server, time: FakeTime, global_info: GlobalInfo, supervisor: SimulatedSupervisor): + global_info.success() + await updater.update() + assert supervisor.getNotification() is None + + time.advance(hours=8) + global_info.failed(Exception()) + await updater.update() + assert supervisor.getNotification() is not None + + time.advance(hours=8) + global_info.failed(Exception()) + await updater.update() + assert supervisor.getNotification() is not None + + global_info.success() + await updater.update() + assert supervisor.getNotification() is None + + +@pytest.mark.asyncio +async def test_failure_logging(updater: HaUpdater, server, time: FakeTime, interceptor: RequestInterceptor): + interceptor.setError(URL_MATCH_CORE_API, 501) + assert getLast() is None + await updater.update() + assert getLast() is None + + time.advance(minutes=1) + await updater.update() + assert getLast() is None + + time.advance(minutes=5) + await updater.update() + assert getLast().msg == REASSURING_MESSAGE.format(501) + + last_log = getLast() + time.advance(minutes=5) + await updater.update() + assert getLast() is not last_log + assert getLast().msg == REASSURING_MESSAGE.format(501) + + last_log = getLast() + interceptor.clear() + await updater.update() + assert getLast() is last_log + + +@pytest.mark.asyncio +async def test_publish_retries(updater: HaUpdater, server: SimulationServer, time: FakeTime, backup, drive, supervisor: SimulatedSupervisor): + await updater.update() + assert supervisor.getEntity("sensor.backup_state") is not None + + # Shoudlnt update after 59 minutes + supervisor.clearEntities() + time.advance(minutes=59) + await updater.update() + assert supervisor.getEntity("sensor.backup_state") is None + + # after that it should + supervisor.clearEntities() + time.advance(minutes=2) + await updater.update() + assert supervisor.getEntity("sensor.backup_state") is not None + + supervisor.clearEntities() + await drive.delete(backup) + await updater.update() + assert supervisor.getEntity("sensor.backup_state") is not None + + +@pytest.mark.asyncio +async def test_ignored_backups(updater: HaUpdater, time: FakeTime, server: SimulationServer, backup: Backup, supervisor: SimulatedSupervisor, coord: Coordinator, config: Config): + config.override(Setting.IGNORE_OTHER_BACKUPS, True) + time.advance(hours=1) + await supervisor.createBackup({'name': "test_backup"}, date=time.now()) + await coord.sync() + await updater.update() + state = supervisor.getAttributes("sensor.backup_state") + assert state["backups_in_google_drive"] == 1 + assert state["backups_in_home_assistant"] == 1 + assert len(state["backups"]) == 1 + assert state['last_backup'] == backup.date().isoformat() + + +@pytest.mark.asyncio +async def test_update_backups_old_names(updater: HaUpdater, server, backup: Backup, time: FakeTime, supervisor: SimulatedSupervisor, config: Config): + config.override(Setting.CALL_BACKUP_SNAPSHOT, True) + await updater.update() + assert not updater._stale() + assert updater._state() == "backed_up" + verifyEntity(supervisor, "binary_sensor.snapshots_stale", + "off", {"friendly_name": "Snapshots Stale", + "device_class": "problem"}) + date = '1985-12-06T05:00:00+00:00' + verifyEntity(supervisor, "sensor.snapshot_backup", "backed_up", { + 'friendly_name': 'Snapshot State', + 'last_snapshot': date, + 'snapshots': [{ + 'date': date, + 'name': backup.name(), + 'size': backup.sizeString(), + 'state': backup.status(), + 'slug': backup.slug() + } + ], + 'snapshots_in_google_drive': 1, + 'snapshots_in_home_assistant': 1, + 'snapshots_in_hassio': 1, + 'size_in_home_assistant': Estimator.asSizeString(backup.size()), + 'size_in_google_drive': Estimator.asSizeString(backup.size()) + }) + + +@pytest.mark.asyncio +async def test_drive_free_space(updater: HaUpdater, time: FakeTime, server: SimulationServer, supervisor: SimulatedSupervisor, coord: Coordinator, config: Config): + await updater.update() + state = supervisor.getAttributes("sensor.backup_state") + assert state["free_space_in_google_drive"] == "" + + await coord.sync() + await updater.update() + state = supervisor.getAttributes("sensor.backup_state") + assert state["free_space_in_google_drive"] == "4.0 GB" + + +@pytest.mark.asyncio +async def test_stale_backup_is_error(updater: HaUpdater, server, backup: Backup, time: FakeTime, supervisor: SimulatedSupervisor, config: Config): + config.override(Setting.DAYS_BETWEEN_BACKUPS, 1) + await updater.update() + assert supervisor.getEntity("sensor.backup_state") == "backed_up" + + time.advance(days=1) + await updater.update() + assert supervisor.getEntity("sensor.backup_state") == "backed_up" + + time.advance(days=1) + await updater.update() + assert supervisor.getEntity("sensor.backup_state") == "error" + + time.advance(days=1) + await updater.update() + assert supervisor.getEntity("sensor.backup_state") == "error" + + +@pytest.mark.asyncio +async def test_stale_backup_ignores_pending(updater: HaUpdater, server, backup: Backup, time: FakeTime, supervisor: SimulatedSupervisor, config: Config, coord: Coordinator): + config.override(Setting.DAYS_BETWEEN_BACKUPS, 1) + + config.override(Setting.NEW_BACKUP_TIMEOUT_SECONDS, 1) + await updater.update() + assert supervisor.getEntity("sensor.backup_state") == "backed_up" + + time.advance(days=2) + await updater.update() + assert supervisor.getEntity("sensor.backup_state") == "error" + + async with supervisor._backup_inner_lock: + await coord.sync() + assert coord.getBackup("pending") is not None + await updater.update() + assert supervisor.getEntity("sensor.backup_state") == "error" + + +@pytest.mark.asyncio +async def test_stale_backups_fine_for_no_creation(updater: HaUpdater, server, backup: Backup, time: FakeTime, supervisor: SimulatedSupervisor, config: Config, coord: Coordinator): + config.override(Setting.DAYS_BETWEEN_BACKUPS, 0) + await updater.update() + assert supervisor.getEntity("sensor.backup_state") == "backed_up" + + # backups shouldn't become stale because the addon doesn't create them. + time.advance(days=100) + await updater.update() + assert supervisor.getEntity("sensor.backup_state") == "backed_up" + + +def verifyEntity(backend: SimulatedSupervisor, name, state, attributes): + assert backend.getEntity(name) == state + assert backend.getAttributes(name) == attributes diff --git a/hassio-google-drive-backup/tests/test_jsonfilesaver.py b/hassio-google-drive-backup/tests/test_jsonfilesaver.py new file mode 100644 index 0000000..7baf6c6 --- /dev/null +++ b/hassio-google-drive-backup/tests/test_jsonfilesaver.py @@ -0,0 +1,63 @@ + + +from backup.file import JsonFileSaver +from os.path import exists, join +from os import remove +import pytest +import json + +TEST_DATA = { + 'info': "and the value", + 'some': 3 +} + + +def readfile(path): + with open(path) as f: + return json.load(f) + + +@pytest.mark.asyncio +async def test_basic(tmpdir: str) -> None: + path = join(tmpdir, "test.json") + backup_path = join(tmpdir, "test.json.backup") + + assert not JsonFileSaver.exists(path) + JsonFileSaver.write(path, TEST_DATA) + assert JsonFileSaver.exists(path) + assert readfile(path) == TEST_DATA + assert readfile(backup_path) == TEST_DATA + assert JsonFileSaver.read(path) == TEST_DATA + + JsonFileSaver.delete(path) + assert not exists(path) + assert not exists(backup_path) + assert not JsonFileSaver.exists(path) + + +@pytest.mark.asyncio +async def test_file_deleted(tmpdir: str) -> None: + path = join(tmpdir, "test.json") + JsonFileSaver.write(path, TEST_DATA) + remove(path) + assert JsonFileSaver.read(path) == TEST_DATA + + +@pytest.mark.asyncio +async def test_backup_deleted(tmpdir: str) -> None: + path = join(tmpdir, "test.json") + backup_path = join(tmpdir, "test.json.backup") + JsonFileSaver.write(path, TEST_DATA) + remove(backup_path) + assert JsonFileSaver.read(path) == TEST_DATA + +@pytest.mark.asyncio +async def test_decode_error(tmpdir: str) -> None: + path = join(tmpdir, "test.json") + JsonFileSaver.write(path, TEST_DATA) + with open(path, "w"): + # emptys the file contents + pass + with open(path) as f: + assert len(f.read()) == 0 + assert JsonFileSaver.read(path) == TEST_DATA diff --git a/hassio-google-drive-backup/tests/test_model.py b/hassio-google-drive-backup/tests/test_model.py new file mode 100644 index 0000000..34cd7d1 --- /dev/null +++ b/hassio-google-drive-backup/tests/test_model.py @@ -0,0 +1,1349 @@ +from datetime import datetime, timedelta, timezone + +import pytest +from dateutil.tz import gettz + +from backup.config import Config, Setting, CreateOptions +from backup.exceptions import DeleteMutlipleBackupsError +from backup.util import GlobalInfo, DataCache +from backup.model import Model, BackupSource +from .faketime import FakeTime +from .helpers import HelperTestSource, IntentionalFailure + +test_tz = gettz('EST') + +default_source = BackupSource() + + +@pytest.fixture +def source(): + return HelperTestSource("Source", is_destination=False) + + +@pytest.fixture +def dest(): + return HelperTestSource("Dest", is_destination=True) + + +@pytest.fixture +def simple_config() -> Config: + config = createConfig() + return config + + +@pytest.fixture +def model(source, dest, time, simple_config, global_info, estimator, data_cache): + return Model(simple_config, time, source, dest, global_info, estimator, data_cache) + + +def createConfig() -> Config: + return Config().override(Setting.BACKUP_STARTUP_DELAY_MINUTES, 0) + + +def test_timeOfDay(estimator, model: Model) -> None: + assert model.getTimeOfDay() is None + + model.config.override(Setting.BACKUP_TIME_OF_DAY, '00:00') + model.reinitialize() + assert model.getTimeOfDay() == (0, 0) + + model.config.override(Setting.BACKUP_TIME_OF_DAY, '23:59') + model.reinitialize() + assert model.getTimeOfDay() == (23, 59) + + model.config.override(Setting.BACKUP_TIME_OF_DAY, '24:59') + model.reinitialize() + assert model.getTimeOfDay() is None + + model.config.override(Setting.BACKUP_TIME_OF_DAY, '24:60') + model.reinitialize() + assert model.getTimeOfDay() is None + + model.config.override(Setting.BACKUP_TIME_OF_DAY, '-1:60') + model.reinitialize() + assert model.getTimeOfDay() is None + + model.config.override(Setting.BACKUP_TIME_OF_DAY, '24:-1') + model.reinitialize() + assert model.getTimeOfDay() is None + + model.config.override(Setting.BACKUP_TIME_OF_DAY, 'boop:60') + model.reinitialize() + assert model.getTimeOfDay() is None + + model.config.override(Setting.BACKUP_TIME_OF_DAY, '24:boop') + model.reinitialize() + assert model.getTimeOfDay() is None + + model.config.override(Setting.BACKUP_TIME_OF_DAY, '24:10:22') + model.reinitialize() + assert model.getTimeOfDay() is None + + model.config.override(Setting.BACKUP_TIME_OF_DAY, '10') + model.reinitialize() + assert model.getTimeOfDay() is None + + +def test_next_time(estimator, data_cache): + time: FakeTime = FakeTime() + now: datetime = datetime(1985, 12, 6, 1, 0, 0).astimezone(timezone.utc) + time.setNow(now) + info = GlobalInfo(time) + config: Config = createConfig().override(Setting.DAYS_BETWEEN_BACKUPS, 0) + model: Model = Model(config, time, default_source, + default_source, info, estimator, data_cache) + assert model._nextBackup(now=now, last_backup=None) is None + assert model._nextBackup(now=now, last_backup=now) is None + + config: Config = createConfig().override(Setting.DAYS_BETWEEN_BACKUPS, 1) + model: Model = Model(config, time, default_source, + default_source, info, estimator, data_cache) + assert model._nextBackup( + now=now, last_backup=None) == now + assert model._nextBackup( + now=now, last_backup=now) == now + timedelta(days=1) + assert model._nextBackup( + now=now, last_backup=now - timedelta(days=1)) == now + assert model._nextBackup( + now=now, last_backup=now + timedelta(days=1)) == now + timedelta(days=2) + + +def test_next_time_of_day(estimator, data_cache): + time: FakeTime = FakeTime() + now: datetime = datetime(1985, 12, 6, 1, 0, 0).astimezone(timezone.utc) + time.setNow(now) + info = GlobalInfo(time) + config: Config = createConfig().override(Setting.DAYS_BETWEEN_BACKUPS, 1).override( + Setting.BACKUP_TIME_OF_DAY, '08:00') + model: Model = Model(config, time, default_source, + default_source, info, estimator, data_cache) + + assert model._nextBackup( + now=now, last_backup=None) == now + assert model._nextBackup( + now=now, last_backup=now - timedelta(days=1)) == now + assert model._nextBackup(now=now, last_backup=now) == datetime( + 1985, 12, 6, 8, 0, tzinfo=test_tz) + assert model._nextBackup(now=now, last_backup=datetime( + 1985, 12, 6, 8, 0, tzinfo=test_tz)) == datetime(1985, 12, 7, 8, 0, tzinfo=test_tz) + assert model._nextBackup(now=datetime(1985, 12, 6, 8, 0, tzinfo=test_tz), last_backup=datetime( + 1985, 12, 6, 8, 0, tzinfo=test_tz)) == datetime(1985, 12, 7, 8, 0, tzinfo=test_tz) + + +def test_next_time_of_day_drift(estimator, data_cache): + time: FakeTime = FakeTime() + now: datetime = datetime(1985, 12, 6, 1, 0, 0).astimezone(timezone.utc) + time.setNow(now) + info = GlobalInfo(time) + + # ignore the backup cooldown for this test + info.triggerBackupCooldown(timedelta(days=-7)) + config: Config = createConfig().override(Setting.DAYS_BETWEEN_BACKUPS, 1).override( + Setting.BACKUP_TIME_OF_DAY, '08:00') + model: Model = Model(config, time, default_source, + default_source, info, estimator, data_cache) + + assert model._nextBackup( + now=now, last_backup=None) == now + assert model._nextBackup( + now=now, last_backup=now - timedelta(days=1) + timedelta(minutes=1)) == datetime(1985, 12, 5, 8, 0, tzinfo=test_tz) + + +def test_next_time_of_day_dest_disabled(model, time, source, dest): + dest.setEnabled(True) + assert model._nextBackup( + now=time.now(), last_backup=None) == time.now() + dest.setEnabled(False) + assert model._nextBackup(now=time.now(), last_backup=None) is None + + +@pytest.mark.asyncio +async def test_sync_empty(model, time, source, dest): + source.setEnabled(False) + dest.setEnabled(False) + await model.sync(time.now()) + assert len(model.backups) == 0 + + +@pytest.mark.asyncio +async def test_sync_single_source(model: Model, source, dest, time): + backup = await source.create(CreateOptions(time.now(), "name")) + dest.setEnabled(False) + await model.sync(time.now()) + assert len(model.backups) == 1 + assert backup.slug() in model.backups + assert model.backups[backup.slug()].getSource( + source.name()) is backup + assert model.backups[backup.slug()].getSource(dest.name()) is None + + +@pytest.mark.asyncio +async def test_sync_source_and_dest(model: Model, time, source, dest: HelperTestSource): + backup_source = await source.create(CreateOptions(time.now(), "name")) + await model._syncBackups([source, dest], time.now()) + assert len(model.backups) == 1 + + backup_dest = await dest.save(model.backups[backup_source.slug()]) + await model._syncBackups([source, dest], time.now()) + assert len(model.backups) == 1 + assert model.backups[backup_source.slug()].getSource( + source.name()) is backup_source + assert model.backups[backup_source.slug()].getSource( + dest.name()) is backup_dest + + +@pytest.mark.asyncio +async def test_sync_different_sources(model: Model, time, source, dest): + backup_source = await source.create(CreateOptions(time.now(), "name")) + backup_dest = await dest.create(CreateOptions(time.now(), "name")) + + await model._syncBackups([source, dest], time.now()) + assert len(model.backups) == 2 + assert model.backups[backup_source.slug()].getSource( + source.name()) is backup_source + assert model.backups[backup_dest.slug()].getSource( + dest.name()) is backup_dest + + +@pytest.mark.asyncio +async def test_removal(model: Model, time, source, dest): + await source.create(CreateOptions(time.now(), "name")) + await model._syncBackups([source, dest], time.now()) + assert len(model.backups) == 1 + source.current = {} + await model._syncBackups([source, dest], time.now()) + assert len(model.backups) == 0 + + +@pytest.mark.asyncio +async def test_new_backup(model: Model, source, dest, time): + await model.sync(time.now()) + assert len(model.backups) == 1 + assert len(source.created) == 1 + assert source.created[0].date() == time.now() + assert len(source.current) == 1 + assert len(dest.current) == 1 + + +@pytest.mark.asyncio +async def test_upload_backup(time, model: Model, dest, source): + dest.setEnabled(True) + await model.sync(time.now()) + assert len(model.backups) == 1 + source.assertThat(created=1, current=1) + assert len(source.created) == 1 + assert source.created[0].date() == time.now() + assert len(source.current) == 1 + assert len(dest.current) == 1 + assert len(dest.saved) == 1 + + +@pytest.mark.asyncio +async def test_disabled(time, model: Model, source, dest): + # create two disabled sources + source.setEnabled(False) + source.insert("newer", time.now(), "slug1") + dest.setEnabled(False) + dest.insert("s2", time.now(), "slug2") + await model.sync(time.now()) + source.assertUnchanged() + dest.assertUnchanged() + assert len(model.backups) == 0 + + +@pytest.mark.asyncio +async def test_delete_source(time, model: Model, source, dest): + time = FakeTime() + now = time.now() + + # create two source backups + source.setMax(1) + older = source.insert("older", now - timedelta(minutes=1), "older") + newer = source.insert("newer", now, "newer") + + # configure only one to be kept + await model.sync(now) + assert len(model.backups) == 1 + assert len(source.saved) == 0 + assert source.deleted == [older] + assert len(source.saved) == 0 + assert newer.slug() in model.backups + assert model.backups[newer.slug()].getSource(source.name()) == newer + + +@pytest.mark.asyncio +async def test_delete_dest(time, model: Model, source, dest): + now = time.now() + + # create two source backups + dest.setMax(1) + older = dest.insert("older", now - timedelta(minutes=1), "older") + newer = dest.insert("newer", now, "newer") + + # configure only one to be kept + await model.sync(now) + assert len(model.backups) == 1 + assert len(dest.saved) == 0 + assert dest.deleted == [older] + assert len(source.saved) == 0 + assert newer.slug() in model.backups + assert model.backups[newer.slug()].getSource(dest.name()) == newer + source.assertUnchanged() + + +@pytest.mark.asyncio +async def test_new_upload_with_delete(time, model: Model, source, dest, simple_config): + now = time.now() + + # create a single old backups + source.setMax(1) + dest.setMax(1) + backup_dest = dest.insert("older", now - timedelta(days=1), "older") + backup_source = source.insert("older", now - timedelta(days=1), "older") + + # configure only one to be kept in both places + simple_config.config.update({ + "days_between_backups": 1 + }) + model.reinitialize() + await model.sync(now) + + # Old snapshto shoudl be deleted, new one shoudl be created and uploaded. + source.assertThat(current=1, created=1, deleted=1) + dest.assertThat(current=1, saved=1, deleted=1) + assert dest.deleted == [backup_dest] + assert source.deleted == [backup_source] + + assert len(model.backups) == 1 + assertBackup(model, [source.created[0], dest.saved[0]]) + + +@pytest.mark.asyncio +async def test_new_upload_no_delete(time, model: Model, source, dest, simple_config): + now = time.now() + + # create a single old backup + source.setMax(2) + dest.setMax(2) + backup_dest = dest.insert("older", now - timedelta(days=1), "older") + backup_source = source.insert("older", now - timedelta(days=1), "older") + + # configure keeping two in both places + simple_config.config.update({ + "days_between_backups": 1 + }) + model.reinitialize() + await model.sync(now) + + # Another backup should have been created and saved + source.assertThat(current=2, created=1) + dest.assertThat(current=2, saved=1) + assert len(model.backups) == 2 + assertBackup(model, [source.created[0], dest.saved[0]]) + assertBackup(model, [backup_dest, backup_source]) + + +@pytest.mark.asyncio +async def test_multiple_deletes_allowed(time, model: Model, source, dest, simple_config): + now = time.now() + simple_config.config.update({"confirm_multiple_deletes": False}) + # create 4 backups in dest + dest.setMax(1) + + current = dest.insert("current", now, "current") + old = dest.insert("old", now - timedelta(days=1), "old") + older = dest.insert("older", now - timedelta(days=2), "older") + oldest = dest.insert("oldest", now - timedelta(days=3), "oldest") + + # configure keeping 1 + simple_config.config.update({ + "max_backups_in_google_drive": 1, + }) + model.reinitialize() + await model.sync(now) + + source.assertUnchanged() + dest.assertThat(current=1, deleted=3) + assert dest.deleted == [oldest, older, old] + assert len(model.backups) == 1 + assertBackup(model, [current]) + + +@pytest.mark.asyncio +async def test_confirm_multiple_deletes(time, model: Model, source, dest, simple_config): + now = time.now() + dest.setMax(1) + source.setMax(1) + + dest.insert("current", now, "current") + dest.insert("old", now - timedelta(days=1), "old") + dest.insert("older", now - timedelta(days=2), "older") + dest.insert("oldest", now - timedelta(days=2), "olderest") + + source.insert("current", now, "current") + source.insert("old", now - timedelta(days=1), "old") + source.insert("older", now - timedelta(days=2), "older") + + with pytest.raises(DeleteMutlipleBackupsError) as thrown: + await model.sync(now) + + thrown.value.data() == { + source.name(): 2, + dest.name(): 3 + } + + source.assertUnchanged() + dest.assertUnchanged() + + +@pytest.mark.asyncio +async def test_dont_upload_deletable(time, model: Model, source, dest): + now = time.now() + + # a new backup in Drive and an old backup in HA + dest.setMax(1) + current = dest.insert("current", now, "current") + old = source.insert("old", now - timedelta(days=1), "old") + + # configure keeping 1 + await model.sync(now) + + # Nothing should happen, because the upload from hassio would have to be deleted right after it's uploaded. + source.assertUnchanged() + dest.assertUnchanged() + assert len(model.backups) == 2 + assertBackup(model, [current]) + assertBackup(model, [old]) + + +@pytest.mark.asyncio +async def test_dont_upload_when_disabled(time, model: Model, source, dest): + now = time.now() + + # Make an enabled destination but with upload diabled. + dest.setMax(1) + dest.setUpload(False) + + await model.sync(now) + + # Verify the backup was created at the source but not uploaded. + source.assertThat(current=1, created=1) + dest.assertUnchanged() + assert len(model.backups) == 1 + + +@pytest.mark.asyncio +async def test_dont_delete_purgable(time, model: Model, source, dest, simple_config): + now = time.now() + + # create a single old backup, retained + source.setMax(1) + dest.setMax(1) + backup_dest = dest.insert("older", now - timedelta(days=1), "older") + backup_dest.setRetained(True) + backup_source = source.insert("older", now - timedelta(days=1), "older") + backup_source.setRetained(True) + + # configure only one to be kept in both places + simple_config.config.update({ + "days_between_backups": 1 + }) + model.reinitialize() + await model.sync(now) + + # Old snapshto shoudl be kept, new one should be created and uploaded. + source.assertThat(current=2, created=1) + dest.assertThat(current=2, saved=1) + + assert len(model.backups) == 2 + assertBackup(model, [backup_dest, backup_source]) + assertBackup(model, [source.created[0], dest.saved[0]]) + + +@pytest.mark.asyncio +async def test_generational_delete(time, model: Model, dest, source, simple_config): + time.setNow(time.local(2019, 5, 10)) + now = time.now() + + # Create 4 backups, configured to keep 3 + source.setMax(3) + source.insert("Fri", time.local(2019, 5, 10, 1)) + source.insert("Thu", time.local(2019, 5, 9, 1)) + wed = source.insert("Wed", time.local(2019, 5, 8, 1)) + source.insert("Mon", time.local(2019, 5, 6, 1)) + + # configure only one to be kept in both places + simple_config.config.update({ + "days_between_backups": 1, + "generational_weeks": 1, + "generational_days": 2 + }) + model.reinitialize() + await model.sync(now) + + # Shoud only delete wed, since it isn't kept in the generational backup config + source.assertThat(current=3, deleted=1) + assert source.deleted == [wed] + assert len(model.backups) == 3 + dest.assertThat(current=3, saved=3) + + +@pytest.mark.asyncio +async def test_delete_when_drive_disabled(time, model: Model, dest: HelperTestSource, source: HelperTestSource, simple_config): + time.setNow(time.local(2019, 5, 10)) + now = time.now() + dest.setEnabled(False) + dest.setNeedsConfiguration(False) + + # Create 4 backups, configured to keep 3 + source.setMax(3) + source.insert("Fri", time.local(2019, 5, 10, 1)) + source.insert("Thu", time.local(2019, 5, 9, 1)) + source.insert("Wed", time.local(2019, 5, 8, 1)) + mon = source.insert("Mon", time.local(2019, 5, 7, 1)) + + await model.sync(now) + + # Shoud only delete mon, the oldest one + source.assertThat(current=3, deleted=1) + assert source.deleted == [mon] + assert len(model.backups) == 3 + dest.assertThat(current=0) + + +@pytest.mark.asyncio +async def test_wait_for_startup_no_backup(time: FakeTime, model: Model, dest: HelperTestSource, source: HelperTestSource, global_info: GlobalInfo): + time.setNow(time.toUtc(time.local(2019, 5, 10))) + global_info.__init__(time) + global_info.triggerBackupCooldown(timedelta(minutes=10)) + assert model.nextBackup(time.now()) == time.now() + timedelta(minutes=10) + assert model.nextBackup(time.now()) == global_info.backupCooldownTime() + assert model.waiting_for_startup + + time.advance(minutes=10) + assert model.nextBackup(time.now()) == time.now() + assert not model.waiting_for_startup + + +@pytest.mark.asyncio +async def test_wait_for_startup_with_backup(time: FakeTime, model: Model, dest: HelperTestSource, source: HelperTestSource, global_info: GlobalInfo): + time.setNow(time.local(2019, 5, 10)) + global_info.__init__(time) + global_info.triggerBackupCooldown(timedelta(minutes=10)) + + source.setMax(3) + source.insert("old", time.now() - timedelta(days=7)) + + assert model.nextBackup(time.now()) == time.now() + timedelta(minutes=10) + assert model.nextBackup(time.now()) == global_info.backupCooldownTime() + assert model.waiting_for_startup + + time.advance(minutes=10) + assert model.nextBackup(time.now()) == time.now() + assert not model.waiting_for_startup + + +@pytest.mark.asyncio +async def test_wait_for_startup_with_backup_during_cooldown(time: FakeTime, model: Model, dest: HelperTestSource, source: HelperTestSource, global_info: GlobalInfo, simple_config: Config): + global_info.triggerBackupCooldown(timedelta(minutes=10)) + + source.setMax(3) + source.insert("old", time.now()) + + backup_time = time.toLocal(time.now()) + timedelta(minutes=5) + simple_config.override(Setting.BACKUP_TIME_OF_DAY, f"{backup_time.hour}:{backup_time.minute}") + model.reinitialize() + await model.sync(time.now()) + assert model.nextBackup(time.now()) == global_info.backupCooldownTime() + assert model.waiting_for_startup + + time.advance(minutes=15) + assert model.nextBackup(time.now()) == global_info.backupCooldownTime() + assert not model.waiting_for_startup + + + +@pytest.mark.asyncio +async def test_ignore_startup_delay(time: FakeTime, model: Model, dest: HelperTestSource, source: HelperTestSource, global_info: GlobalInfo): + time.setNow(time.local(2019, 5, 10)) + global_info.__init__(time) + global_info.triggerBackupCooldown(timedelta(minutes=10)) + model.ignore_startup_delay = True + assert model.nextBackup(time.now()) == time.now() + assert not model.waiting_for_startup + + +def assertBackup(model, sources): + matches = {} + for source in sources: + matches[source.source()] = source + slug = source.slug() + assert slug in model.backups + assert model.backups[slug].sources == matches + + +@pytest.mark.asyncio +async def test_delete_after_upload(time: FakeTime, model: Model, dest: HelperTestSource, source: HelperTestSource, global_info: GlobalInfo): + model.config.override(Setting.DELETE_AFTER_UPLOAD, True) + source.setMax(100) + dest.setMax(100) + dest.insert("Destination 1", time.now()) + dest.reset() + + # Nothing should happen on a sync, the backup is already backed up. + await model.sync(time.now()) + dest.assertThat(current=1) + source.assertThat() + + time.advance(days=7) + source.insert("Source 1", time.now()) + source.reset() + + # Source backup should get backed up and the deleted + await model.sync(time.now()) + source.assertThat(deleted=1, current=0) + dest.assertThat(saved=1, current=2) + + +@pytest.mark.asyncio +async def test_delete_after_upload_multiple_deletes(time: FakeTime, model: Model, dest: HelperTestSource, source: HelperTestSource, global_info: GlobalInfo): + model.config.override(Setting.DELETE_AFTER_UPLOAD, True) + source.setMax(100) + dest.setMax(100) + source.insert("Src 1", time.now()) + time.advance(days=1) + source.insert("Src 2", time.now()) + source.reset() + + # Deleteing multiple backups should still fail with DELETE_AFTER_UPLOAD:True + with pytest.raises(DeleteMutlipleBackupsError): + await model.sync(time.now()) + + # But the backup should still get backed up + source.assertThat(current=2) + dest.assertThat(saved=2, current=2) + + +@pytest.mark.asyncio +async def test_delete_after_upload_simple_sync(time: FakeTime, model: Model, dest: HelperTestSource, source: HelperTestSource, global_info: GlobalInfo): + model.config.override(Setting.DELETE_AFTER_UPLOAD, True) + source.setMax(100) + dest.setMax(100) + + # A sync should create a backup, back it up to dest, and then delete it from source. + await model.sync(time.now()) + source.assertThat(created=1, deleted=1, current=0) + dest.assertThat(saved=1, current=1) + + time.advance(hours=1) + source.reset() + dest.reset() + + # Next sync should do nothing + await model.sync(time.now()) + source.assertThat() + dest.assertThat(current=1) + + +@pytest.mark.asyncio +async def test_never_delete_ignored_backups(time: FakeTime, model: Model, dest: HelperTestSource, source: HelperTestSource): + source.setMax(1) + dest.setMax(1) + + # A sync should create a backup and back it up to dest. + await model.sync(time.now()) + source.assertThat(created=1, current=1) + dest.assertThat(saved=1, current=1) + + source.reset() + dest.reset() + + # Another sync shoudl delete a backup, which is just a sanity check. + time.advance(days=5) + await model.sync(time.now()) + source.assertThat(created=1, current=1, deleted=1) + dest.assertThat(saved=1, current=1, deleted=1) + assert model.nextBackup(time.now()) == time.now() + timedelta(days=3) + source.reset() + dest.reset() + + # Make the backup ignored, which should cause a new backup to be created + # and synced without the ignored one getting deleted. + next(iter((await dest.get()).values())).setIgnore(True) + next(iter((await source.get()).values())).setIgnore(True) + assert model.nextBackup(time.now()) < time.now() + await model.sync(time.now()) + source.assertThat(created=1, current=2) + dest.assertThat(saved=1, current=2) + + +@pytest.mark.asyncio +async def test_ignored_backups_dont_upload(time: FakeTime, model: Model, dest: HelperTestSource, source: HelperTestSource): + source.setMax(2) + dest.setMax(2) + + older = source.insert("older", time.now() - timedelta(days=1), slug="older") + older.setIgnore(True) + source.insert("newer", time.now(), slug="newer") + source.reset() + + # A sync should backup the last backup and ignore the older one + await model.sync(time.now()) + source.assertThat(created=0, current=2) + dest.assertThat(saved=1, current=1) + + uploaded = await dest.get() + assert len(uploaded) == 1 + assert next(iter(uploaded.values())).name() == "newer" + + +@pytest.mark.asyncio +async def test_dirty_cache_gets_saved(time: FakeTime, model: Model, data_cache: DataCache): + data_cache.makeDirty() + await model.sync(time.now()) + assert not data_cache.dirty + + +@pytest.mark.asyncio +async def test_delete_after_upload_with_no_backups(source: HelperTestSource, dest: HelperTestSource, time: FakeTime, model: Model, data_cache: DataCache, simple_config: Config): + source.setMax(0) + dest.setMax(2) + simple_config.override(Setting.DELETE_AFTER_UPLOAD, True) + + source.insert("older", time.now() - timedelta(days=1), slug="older") + source.insert("newer", time.now(), slug="newer") + source.reset() + + with pytest.raises(DeleteMutlipleBackupsError): + await model.sync(time.now()) + + simple_config.override(Setting.CONFIRM_MULTIPLE_DELETES, False) + await model.sync(time.now()) + + dest.assertThat(saved=2, current=2) + source.assertThat(deleted=2, current=0) + + +@pytest.mark.asyncio +async def test_purge_before_upload(source: HelperTestSource, dest: HelperTestSource, time: FakeTime, model: Model, data_cache: DataCache, simple_config: Config): + source.setMax(2) + dest.setMax(2) + older = source.insert("older", time.now() - timedelta(days=7), slug="older") + oldest = source.insert("oldest", time.now() - timedelta(days=14), slug="oldest") + await model.sync(time.now() - timedelta(days=7)) + + source.allow_create = False + dest.allow_save = False + + dest.reset() + source.reset() + + # trying to sync now should do nothing. + with pytest.raises(IntentionalFailure): + await model.sync(time.now()) + source.assertThat(current=2) + dest.assertThat(current=2) + + simple_config.override(Setting.DELETE_BEFORE_NEW_BACKUP, True) + # Trying to sync should delete the backup before syncing and then fail to create a new one. + with pytest.raises(IntentionalFailure): + await model.sync(time.now()) + source.assertThat(deleted=1, current=1) + assert oldest.slug() not in (await source.get()).keys() + dest.assertThat(current=2) + + # Trying to do it again should do nothing (eg not delete another backup) + with pytest.raises(IntentionalFailure): + await model.sync(time.now()) + source.assertThat(deleted=1, current=1) + dest.assertThat(current=2) + + # let the new source backup get created, which then deletes the destination but fails to save + source.allow_create = True + with pytest.raises(IntentionalFailure): + await model.sync(time.now()) + source.assertThat(deleted=1, current=2, created=1) + dest.assertThat(current=1, deleted=1) + assert oldest.slug() not in (await dest.get()).keys() + + # now let the new backup get saved. + dest.allow_save = True + await model.sync(time.now()) + source.assertThat(deleted=1, current=2, created=1) + dest.assertThat(current=2, deleted=1, saved=1) + + assert oldest.slug() not in (await source.get()).keys() + assert older.slug() in (await source.get()).keys() + assert oldest.slug() not in (await dest.get()).keys() + assert older.slug() in (await dest.get()).keys() + + +@pytest.mark.asyncio +async def test_generational_empty(time, model: Model, dest, source, simple_config: Config): + time.setNow(time.local(2019, 5, 10)) + now = time.now() + + simple_config.config.update({ + "days_between_backups": 1, + "generational_weeks": 1, + "generational_days": 2 + }) + + simple_config.override(Setting.DAYS_BETWEEN_BACKUPS, 1) + + model.reinitialize() + assert len(model.backups) == 0 + await model.sync(now) + assert len(model.backups) == 1 + + +@pytest.mark.asyncio +async def test_delete_ignored_other_backup_after_some_time(time: FakeTime, model: Model, dest: HelperTestSource, source: HelperTestSource, simple_config: Config): + # Create a few backups, one ignored + source.setMax(2) + dest.setMax(2) + simple_config.override(Setting.DAYS_BETWEEN_BACKUPS, 0) + simple_config.override(Setting.IGNORE_OTHER_BACKUPS, True) + simple_config.override(Setting.DELETE_IGNORED_AFTER_DAYS, 1) + ignored = source.insert("Ignored", time.now()) + source.insert("Existing", time.now()) + ignored.setIgnore(True) + await model.sync(time.now()) + source.assertThat(current=2) + dest.assertThat(saved=1, current=1) + source.reset() + dest.reset() + + time.advance(hours=23) + await model.sync(time.now()) + + source.assertThat(current=2) + dest.assertThat(current=1) + + time.advance(hours=2) + await model.sync(time.now()) + source.assertThat(current=1, deleted=1) + dest.assertThat(current=1) + + +@pytest.mark.asyncio +async def test_delete_ignored_upgrade_backup_after_some_time(time: FakeTime, model: Model, dest: HelperTestSource, source: HelperTestSource, simple_config: Config): + # Create a few backups, one ignored + source.setMax(2) + dest.setMax(2) + simple_config.override(Setting.DAYS_BETWEEN_BACKUPS, 0) + simple_config.override(Setting.IGNORE_UPGRADE_BACKUPS, True) + simple_config.override(Setting.DELETE_IGNORED_AFTER_DAYS, 1) + ignored = source.insert("Ignored", time.now()) + source.insert("Existing", time.now()) + ignored.setIgnore(True) + await model.sync(time.now()) + source.assertThat(current=2) + dest.assertThat(saved=1, current=1) + source.reset() + dest.reset() + + time.advance(hours=23) + await model.sync(time.now()) + + source.assertThat(current=2) + dest.assertThat(current=1) + + time.advance(hours=2) + await model.sync(time.now()) + source.assertThat(current=1, deleted=1) + dest.assertThat(current=1) + + +@pytest.mark.asyncio +async def test_zero_config_whiled_deleting_backups(time: FakeTime, model: Model, dest: HelperTestSource, source: HelperTestSource, simple_config: Config): + """ + Issue #745 identified that setting setting destination max backups to 0 and "delete after upload"=True would cause destination + backups to get deleted due to an error in the logic for handling purges. This test verifies that no longer happens. + """ + source.setMax(1) + dest.setMax(1) + simple_config.override(Setting.DAYS_BETWEEN_BACKUPS, 1) + simple_config.override(Setting.DELETE_AFTER_UPLOAD, True) + source.insert("Backup", time.now()) + await model.sync(time.now()) + source.assertThat(current=0, deleted=1) + dest.assertThat(current=1, saved=1) + source.reset() + dest.reset() + + dest.setMax(0) + await model.sync(time.now()) + dest.assertThat(current=1) + source.assertThat() + + +async def simulate_backups_timeline(time: FakeTime, model: Model, start: datetime, end: datetime): + time.setNow(time.toUtc(start)) + await model.sync(time.now()) + while time.now() < end: + next = model.nextBackup(time.now()) + assert next is not None + assert next > time.now() + time.setNow(next) + await model.sync(time.now()) + + +@pytest.mark.asyncio +async def test_generational_delete_issue602(time: FakeTime, model: Model, dest: HelperTestSource, source: HelperTestSource, simple_config: Config): + time.setTimeZone("Europe/Rome") + start = time.local(2021, 1, 1, 1) + end = time.local(2023, 5, 1, 1) + + simple_config.override(Setting.MAX_BACKUPS_IN_HA, 1) + simple_config.override(Setting.MAX_BACKUPS_IN_GOOGLE_DRIVE, 50) + simple_config.override(Setting.DAYS_BETWEEN_BACKUPS, 1) + simple_config.override(Setting.IGNORE_OTHER_BACKUPS, True) + simple_config.override(Setting.IGNORE_UPGRADE_BACKUPS, True) + simple_config.override(Setting.BACKUP_TIME_OF_DAY, "00:00") + simple_config.override(Setting.GENERATIONAL_DAYS, 14) + simple_config.override(Setting.GENERATIONAL_WEEKS, 5) + simple_config.override(Setting.GENERATIONAL_MONTHS, 13) + simple_config.override(Setting.GENERATIONAL_YEARS, 1) + simple_config.override(Setting.GENERATIONAL_DELETE_EARLY, True) + + source.setMax(1) + dest.setMax(30) + model.reinitialize() + + await simulate_backups_timeline(time, model, start, end) + + dates = list([x.date() for x in dest.current.values()]) + dates.sort() + assert dates == [time.parse('2022-04-30T22:00:00+00:00'), + time.parse('2022-05-31T22:00:00+00:00'), + time.parse('2022-06-30T22:00:00+00:00'), + time.parse('2022-07-31T22:00:00+00:00'), + time.parse('2022-08-31T22:00:00+00:00'), + time.parse('2022-09-30T22:00:00+00:00'), + time.parse('2022-10-31T23:00:00+00:00'), + time.parse('2022-11-30T23:00:00+00:00'), + time.parse('2022-12-31T23:00:00+00:00'), + time.parse('2023-01-31T23:00:00+00:00'), + time.parse('2023-02-28T23:00:00+00:00'), + time.parse('2023-03-31T22:00:00+00:00'), + time.parse('2023-04-02T22:00:00+00:00'), + time.parse('2023-04-09T22:00:00+00:00'), + time.parse('2023-04-16T22:00:00+00:00'), + time.parse('2023-04-18T22:00:00+00:00'), + time.parse('2023-04-19T22:00:00+00:00'), + time.parse('2023-04-20T22:00:00+00:00'), + time.parse('2023-04-21T22:00:00+00:00'), + time.parse('2023-04-22T22:00:00+00:00'), + time.parse('2023-04-23T22:00:00+00:00'), + time.parse('2023-04-24T22:00:00+00:00'), + time.parse('2023-04-25T22:00:00+00:00'), + time.parse('2023-04-26T22:00:00+00:00'), + time.parse('2023-04-27T22:00:00+00:00'), + time.parse('2023-04-28T22:00:00+00:00'), + time.parse('2023-04-29T22:00:00+00:00'), + time.parse('2023-04-30T22:00:00+00:00'), + time.parse('2023-05-01T22:00:00+00:00')] + + +@pytest.mark.asyncio +async def test_generational_delete_issue809(time: FakeTime, model: Model, dest: HelperTestSource, source: HelperTestSource, simple_config: Config): + time.setTimeZone("America/Los_Angeles") + + simple_config.override(Setting.MAX_BACKUPS_IN_HA, 10) + simple_config.override(Setting.MAX_BACKUPS_IN_GOOGLE_DRIVE, 32) + simple_config.override(Setting.DAYS_BETWEEN_BACKUPS, 1) + simple_config.override(Setting.BACKUP_TIME_OF_DAY, "02:00") + simple_config.override(Setting.GENERATIONAL_DAYS, 7) + simple_config.override(Setting.GENERATIONAL_WEEKS, 4) + simple_config.override(Setting.GENERATIONAL_MONTHS, 6) + simple_config.override(Setting.GENERATIONAL_YEARS, 10) + + source.setMax(simple_config.get(Setting.MAX_BACKUPS_IN_HA)) + dest.setMax(simple_config.get(Setting.MAX_BACKUPS_IN_GOOGLE_DRIVE)) + model.reinitialize() + + start = time.local(2021, 1, 1) + end = time.local(2024, 5, 1) + await simulate_backups_timeline(time, model, start, end) + + dates = list([x.date() for x in dest.current.values()]) + dates.sort() + assert dates == [time.parse('2021-01-01 10:00:00+00:00'), + time.parse('2022-01-01 10:00:00+00:00'), + time.parse('2023-01-01 10:00:00+00:00'), + time.parse('2023-12-01 10:00:00+00:00'), + time.parse('2024-01-01 10:00:00+00:00'), + time.parse('2024-02-01 10:00:00+00:00'), + time.parse('2024-03-01 10:00:00+00:00'), + time.parse('2024-04-01 09:00:00+00:00'), + time.parse('2024-04-08 09:00:00+00:00'), + time.parse('2024-04-09 09:00:00+00:00'), + time.parse('2024-04-10 09:00:00+00:00'), + time.parse('2024-04-11 09:00:00+00:00'), + time.parse('2024-04-12 09:00:00+00:00'), + time.parse('2024-04-13 09:00:00+00:00'), + time.parse('2024-04-14 09:00:00+00:00'), + time.parse('2024-04-15 09:00:00+00:00'), + time.parse('2024-04-16 09:00:00+00:00'), + time.parse('2024-04-17 09:00:00+00:00'), + time.parse('2024-04-18 09:00:00+00:00'), + time.parse('2024-04-19 09:00:00+00:00'), + time.parse('2024-04-20 09:00:00+00:00'), + time.parse('2024-04-21 09:00:00+00:00'), + time.parse('2024-04-22 09:00:00+00:00'), + time.parse('2024-04-23 09:00:00+00:00'), + time.parse('2024-04-24 09:00:00+00:00'), + time.parse('2024-04-25 09:00:00+00:00'), + time.parse('2024-04-26 09:00:00+00:00'), + time.parse('2024-04-27 09:00:00+00:00'), + time.parse('2024-04-28 09:00:00+00:00'), + time.parse('2024-04-29 09:00:00+00:00'), + time.parse('2024-04-30 09:00:00+00:00'), + time.parse('2024-05-01 09:00:00+00:00')] + + +async def test_generational_delete_dst_start_rome(time: FakeTime, model: Model, dest: HelperTestSource, source: HelperTestSource, simple_config: Config): + time.setTimeZone("Europe/Rome") + + simple_config.override(Setting.MAX_BACKUPS_IN_HA, 1) + simple_config.override(Setting.MAX_BACKUPS_IN_GOOGLE_DRIVE, 8) + simple_config.override(Setting.DAYS_BETWEEN_BACKUPS, 1) + simple_config.override(Setting.BACKUP_TIME_OF_DAY, "02:00") + simple_config.override(Setting.GENERATIONAL_DAYS, 3) + simple_config.override(Setting.GENERATIONAL_WEEKS, 3) + simple_config.override(Setting.GENERATIONAL_MONTHS, 2) + simple_config.override(Setting.GENERATIONAL_DELETE_EARLY, True) + + source.setMax(simple_config.get(Setting.MAX_BACKUPS_IN_HA)) + dest.setMax(simple_config.get(Setting.MAX_BACKUPS_IN_GOOGLE_DRIVE)) + model.reinitialize() + + start = time.local(2023, 1, 1) + end = time.local(2023, 3, 25) + await simulate_backups_timeline(time, model, start, end) + + dates = list([x.date() for x in dest.current.values()]) + dates.sort() + assert dates == [time.local(2023, 2, 1, 2), + time.local(2023, 3, 1, 2), + time.local(2023, 3, 6, 2), + time.local(2023, 3, 13, 2), + time.local(2023, 3, 20, 2), + time.local(2023, 3, 23, 2), + time.local(2023, 3, 24, 2), + time.local(2023, 3, 25, 2)] + + assert time.now() == time.local(2023, 3, 25, 2) + assert model.nextBackup(time.now()) == time.local(2023, 3, 26, 2) + + for x in range(0, 24 * 15): + time.advance(minutes=15) + assert model.nextBackup(time.now()) == time.local(2023, 3, 26, 2) + + time.setNow(time.toUtc(time.local(2023, 3, 26, 2))) + await model.sync(time.now()) + assert max([x.date() for x in dest.current.values()]) == time.local(2023, 3, 26, 2) + + for x in range(0, 24 * 15): + time.advance(minutes=15) + assert model.nextBackup(time.now()) == time.local(2023, 3, 27, 2) + + +async def test_generational_delete_dst_start_los_angeles(time: FakeTime, model: Model, dest: HelperTestSource, source: HelperTestSource, simple_config: Config): + time.setTimeZone("America/Los_Angeles") + + simple_config.override(Setting.MAX_BACKUPS_IN_HA, 1) + simple_config.override(Setting.MAX_BACKUPS_IN_GOOGLE_DRIVE, 8) + simple_config.override(Setting.DAYS_BETWEEN_BACKUPS, 1) + simple_config.override(Setting.BACKUP_TIME_OF_DAY, "02:00") + simple_config.override(Setting.GENERATIONAL_DAYS, 3) + simple_config.override(Setting.GENERATIONAL_WEEKS, 3) + simple_config.override(Setting.GENERATIONAL_MONTHS, 2) + simple_config.override(Setting.GENERATIONAL_DELETE_EARLY, True) + + source.setMax(simple_config.get(Setting.MAX_BACKUPS_IN_HA)) + dest.setMax(simple_config.get(Setting.MAX_BACKUPS_IN_GOOGLE_DRIVE)) + model.reinitialize() + + start = time.local(2023, 1, 1) + end = time.local(2023, 3, 11) + await simulate_backups_timeline(time, model, start, end) + + dates = list([x.date() for x in dest.current.values()]) + dates.sort() + assert dates == [time.local(2023, 2, 1, 2), + time.local(2023, 2, 20, 2), + time.local(2023, 2, 27, 2), + time.local(2023, 3, 1, 2), + time.local(2023, 3, 6, 2), + time.local(2023, 3, 9, 2), + time.local(2023, 3, 10, 2), + time.local(2023, 3, 11, 2)] + + assert time.now() == time.local(2023, 3, 11, 2) + assert model.nextBackup(time.now()) == time.local(2023, 3, 12, 2) + + for x in range(0, 24 * 15): + time.advance(minutes=15) + assert model.nextBackup(time.now()) == time.local(2023, 3, 12, 2) + + time.setNow(time.toUtc(time.local(2023, 3, 12, 2))) + await model.sync(time.now()) + assert max([x.date() for x in dest.current.values()]) == time.local(2023, 3, 12, 2) + + for x in range(0, 24 * 15): + time.advance(minutes=15) + assert model.nextBackup(time.now()) == time.local(2023, 3, 13, 2) + + +async def test_generational_delete_dst_start_rome_2_30(time: FakeTime, model: Model, dest: HelperTestSource, source: HelperTestSource, simple_config: Config): + time.setTimeZone("Europe/Rome") + + simple_config.override(Setting.MAX_BACKUPS_IN_HA, 1) + simple_config.override(Setting.MAX_BACKUPS_IN_GOOGLE_DRIVE, 8) + simple_config.override(Setting.DAYS_BETWEEN_BACKUPS, 1) + simple_config.override(Setting.BACKUP_TIME_OF_DAY, "02:30") + simple_config.override(Setting.GENERATIONAL_DAYS, 3) + simple_config.override(Setting.GENERATIONAL_WEEKS, 3) + simple_config.override(Setting.GENERATIONAL_MONTHS, 2) + simple_config.override(Setting.GENERATIONAL_DELETE_EARLY, True) + + source.setMax(simple_config.get(Setting.MAX_BACKUPS_IN_HA)) + dest.setMax(simple_config.get(Setting.MAX_BACKUPS_IN_GOOGLE_DRIVE)) + model.reinitialize() + + start = time.local(2023, 1, 1) + end = time.local(2023, 3, 25) + await simulate_backups_timeline(time, model, start, end) + + dates = list([x.date() for x in dest.current.values()]) + dates.sort() + assert dates == [time.local(2023, 2, 1, 2, 30), + time.local(2023, 3, 1, 2, 30), + time.local(2023, 3, 6, 2, 30), + time.local(2023, 3, 13, 2, 30), + time.local(2023, 3, 20, 2, 30), + time.local(2023, 3, 23, 2, 30), + time.local(2023, 3, 24, 2, 30), + time.local(2023, 3, 25, 2, 30)] + + assert time.now() == time.local(2023, 3, 25, 2, 30) + assert model.nextBackup(time.now()) == time.local(2023, 3, 26, 2, 30) + + for x in range(0, 24 * 15): + time.advance(minutes=15) + assert model.nextBackup(time.now()) == time.local(2023, 3, 26, 2, 30) + + time.setNow(time.toUtc(time.local(2023, 3, 26, 2, 30))) + await model.sync(time.now()) + assert max([x.date() for x in dest.current.values()]) == time.local(2023, 3, 26, 2, 30) + + for x in range(0, 24 * 15): + time.advance(minutes=15) + assert model.nextBackup(time.now()) == time.local(2023, 3, 27, 2, 30) + + +async def test_generational_delete_dst_start_rome_3_00(time: FakeTime, model: Model, dest: HelperTestSource, source: HelperTestSource, simple_config: Config): + time.setTimeZone("Europe/Rome") + + simple_config.override(Setting.MAX_BACKUPS_IN_HA, 1) + simple_config.override(Setting.MAX_BACKUPS_IN_GOOGLE_DRIVE, 8) + simple_config.override(Setting.DAYS_BETWEEN_BACKUPS, 1) + simple_config.override(Setting.BACKUP_TIME_OF_DAY, "03:00") + simple_config.override(Setting.GENERATIONAL_DAYS, 3) + simple_config.override(Setting.GENERATIONAL_WEEKS, 3) + simple_config.override(Setting.GENERATIONAL_MONTHS, 2) + simple_config.override(Setting.GENERATIONAL_DELETE_EARLY, True) + + source.setMax(simple_config.get(Setting.MAX_BACKUPS_IN_HA)) + dest.setMax(simple_config.get(Setting.MAX_BACKUPS_IN_GOOGLE_DRIVE)) + model.reinitialize() + + start = time.local(2023, 1, 1) + end = time.local(2023, 3, 25) + await simulate_backups_timeline(time, model, start, end) + + dates = list([x.date() for x in dest.current.values()]) + dates.sort() + assert dates == [time.local(2023, 2, 1, 3), + time.local(2023, 3, 1, 3), + time.local(2023, 3, 6, 3), + time.local(2023, 3, 13, 3), + time.local(2023, 3, 20, 3), + time.local(2023, 3, 23, 3), + time.local(2023, 3, 24, 3), + time.local(2023, 3, 25, 3)] + + assert time.now() == time.local(2023, 3, 25, 3) + assert model.nextBackup(time.now()) == time.local(2023, 3, 26, 3) + + for x in range(0, 24 * 15): + time.advance(minutes=15) + assert model.nextBackup(time.now()) == time.local(2023, 3, 26, 3) + + time.setNow(time.toUtc(time.local(2023, 3, 26, 3))) + await model.sync(time.now()) + assert max([x.date() for x in dest.current.values()]) == time.local(2023, 3, 26, 3) + + for x in range(0, 24 * 15): + time.advance(minutes=15) + assert model.nextBackup(time.now()) == time.local(2023, 3, 27, 3) + + +async def test_generational_delete_dst_end_rome(time: FakeTime, model: Model, dest: HelperTestSource, source: HelperTestSource, simple_config: Config): + time.setTimeZone("Europe/Rome") + + simple_config.override(Setting.MAX_BACKUPS_IN_HA, 1) + simple_config.override(Setting.MAX_BACKUPS_IN_GOOGLE_DRIVE, 8) + simple_config.override(Setting.DAYS_BETWEEN_BACKUPS, 1) + simple_config.override(Setting.BACKUP_TIME_OF_DAY, "02:00") + simple_config.override(Setting.GENERATIONAL_DAYS, 3) + simple_config.override(Setting.GENERATIONAL_WEEKS, 3) + simple_config.override(Setting.GENERATIONAL_MONTHS, 2) + simple_config.override(Setting.GENERATIONAL_DELETE_EARLY, True) + + source.setMax(simple_config.get(Setting.MAX_BACKUPS_IN_HA)) + dest.setMax(simple_config.get(Setting.MAX_BACKUPS_IN_GOOGLE_DRIVE)) + model.reinitialize() + + start = time.local(2023, 6, 1) + end = time.local(2023, 10, 28) + await simulate_backups_timeline(time, model, start, end) + + dates = list([x.date() for x in dest.current.values()]) + dates.sort() + assert dates == [time.local(2023, 9, 1, 2), + time.local(2023, 10, 1, 2), + time.local(2023, 10, 9, 2), + time.local(2023, 10, 16, 2), + time.local(2023, 10, 23, 2), + time.local(2023, 10, 26, 2), + time.local(2023, 10, 27, 2), + time.local(2023, 10, 28, 2)] + + assert time.now() == time.local(2023, 10, 28, 2) + assert model.nextBackup(time.now()) == time.local(2023, 10, 29, 2) + + for x in range(0, 24 * 15): + time.advance(minutes=15) + assert model.nextBackup(time.now()) == time.local(2023, 10, 29, 2) + + time.setNow(time.toUtc(time.local(2023, 10, 29, 2))) + await model.sync(time.now()) + assert max([x.date() for x in dest.current.values()]) == time.local(2023, 10, 29, 2) + + for x in range(0, 24 * 15): + time.advance(minutes=15) + assert model.nextBackup(time.now()) == time.local(2023, 10, 30, 2) + + +async def test_generational_delete_dst_end_rome_2_30(time: FakeTime, model: Model, dest: HelperTestSource, source: HelperTestSource, simple_config: Config): + time.setTimeZone("Europe/Rome") + + simple_config.override(Setting.MAX_BACKUPS_IN_HA, 1) + simple_config.override(Setting.MAX_BACKUPS_IN_GOOGLE_DRIVE, 8) + simple_config.override(Setting.DAYS_BETWEEN_BACKUPS, 1) + simple_config.override(Setting.BACKUP_TIME_OF_DAY, "02:30") + simple_config.override(Setting.GENERATIONAL_DAYS, 3) + simple_config.override(Setting.GENERATIONAL_WEEKS, 3) + simple_config.override(Setting.GENERATIONAL_MONTHS, 2) + simple_config.override(Setting.GENERATIONAL_DELETE_EARLY, True) + + source.setMax(simple_config.get(Setting.MAX_BACKUPS_IN_HA)) + dest.setMax(simple_config.get(Setting.MAX_BACKUPS_IN_GOOGLE_DRIVE)) + model.reinitialize() + + start = time.local(2023, 6, 1) + end = time.local(2023, 10, 28) + await simulate_backups_timeline(time, model, start, end) + + dates = list([x.date() for x in dest.current.values()]) + dates.sort() + assert dates == [time.local(2023, 9, 1, 2, 30), + time.local(2023, 10, 1, 2, 30), + time.local(2023, 10, 9, 2, 30), + time.local(2023, 10, 16, 2, 30), + time.local(2023, 10, 23, 2, 30), + time.local(2023, 10, 26, 2, 30), + time.local(2023, 10, 27, 2, 30), + time.local(2023, 10, 28, 2, 30)] + + assert time.now() == time.local(2023, 10, 28, 2, 30) + assert model.nextBackup(time.now()) == time.local(2023, 10, 29, 2, 30) + + for x in range(0, 24 * 15): + time.advance(minutes=15) + assert model.nextBackup(time.now()) == time.local(2023, 10, 29, 2, 30) + + time.setNow(time.toUtc(time.local(2023, 10, 29, 2, 30))) + await model.sync(time.now()) + assert max([x.date() for x in dest.current.values()]) == time.local(2023, 10, 29, 2, 30) + + for x in range(0, 24 * 15): + time.advance(minutes=15) + assert model.nextBackup(time.now()) == time.local(2023, 10, 30, 2, 30) + + +async def test_generational_delete_dst_end_rome_3_00(time: FakeTime, model: Model, dest: HelperTestSource, source: HelperTestSource, simple_config: Config): + time.setTimeZone("Europe/Rome") + + simple_config.override(Setting.MAX_BACKUPS_IN_HA, 1) + simple_config.override(Setting.MAX_BACKUPS_IN_GOOGLE_DRIVE, 8) + simple_config.override(Setting.DAYS_BETWEEN_BACKUPS, 1) + simple_config.override(Setting.BACKUP_TIME_OF_DAY, "03:00") + simple_config.override(Setting.GENERATIONAL_DAYS, 3) + simple_config.override(Setting.GENERATIONAL_WEEKS, 3) + simple_config.override(Setting.GENERATIONAL_MONTHS, 2) + simple_config.override(Setting.GENERATIONAL_DELETE_EARLY, True) + + source.setMax(simple_config.get(Setting.MAX_BACKUPS_IN_HA)) + dest.setMax(simple_config.get(Setting.MAX_BACKUPS_IN_GOOGLE_DRIVE)) + model.reinitialize() + + start = time.local(2023, 6, 1) + end = time.local(2023, 10, 28) + await simulate_backups_timeline(time, model, start, end) + + dates = list([x.date() for x in dest.current.values()]) + dates.sort() + assert dates == [time.local(2023, 9, 1, 3), + time.local(2023, 10, 1, 3), + time.local(2023, 10, 9, 3), + time.local(2023, 10, 16, 3), + time.local(2023, 10, 23, 3), + time.local(2023, 10, 26, 3), + time.local(2023, 10, 27, 3), + time.local(2023, 10, 28, 3)] + + assert time.now() == time.local(2023, 10, 28, 3) + assert model.nextBackup(time.now()) == time.local(2023, 10, 29, 3) + + for x in range(0, 24 * 15): + time.advance(minutes=15) + assert model.nextBackup(time.now()) == time.local(2023, 10, 29, 3) + + time.setNow(time.toUtc(time.local(2023, 10, 29, 3))) + await model.sync(time.now()) + assert max([x.date() for x in dest.current.values()]) == time.local(2023, 10, 29, 3) + + for x in range(0, 24 * 15): + time.advance(minutes=15) + assert model.nextBackup(time.now()) == time.local(2023, 10, 30, 3) + + +def test_next_time_over_a_day(estimator, data_cache): + time: FakeTime = FakeTime() + now: datetime = time.localize(datetime(1985, 12, 6)) + time.setNow(now) + info = GlobalInfo(time) + + config: Config = createConfig() + config.override(Setting.BACKUP_TIME_OF_DAY, "00:00") + config.override(Setting.DAYS_BETWEEN_BACKUPS, 2.0) + model: Model = Model(config, time, default_source, + default_source, info, estimator, data_cache) + assert model._nextBackup( + now=now, last_backup=None) == now + assert model._nextBackup( + now=now, last_backup=now) == now + timedelta(days=2) diff --git a/hassio-google-drive-backup/tests/test_rangelookup.py b/hassio-google-drive-backup/tests/test_rangelookup.py new file mode 100644 index 0000000..5003ab1 --- /dev/null +++ b/hassio-google-drive-backup/tests/test_rangelookup.py @@ -0,0 +1,26 @@ +from backup.util import RangeLookup + + +def test_lookup(): + data = [1, 3, 5] + lookup = RangeLookup(data, lambda x: x) + assert list(lookup.matches(-1, 0)) == [] + assert list(lookup.matches(6, 7)) == [] + assert list(lookup.matches(2, 2)) == [] + assert list(lookup.matches(4, 4)) == [] + assert list(lookup.matches(6, 6)) == [] + + assert list(lookup.matches(0, 6)) == [1, 3, 5] + assert list(lookup.matches(1, 5)) == [1, 3, 5] + + assert list(lookup.matches(1, 3)) == [1, 3] + assert list(lookup.matches(0, 4)) == [1, 3] + assert list(lookup.matches(3, 5)) == [3, 5] + assert list(lookup.matches(2, 6)) == [3, 5] + + assert list(lookup.matches(0, 2)) == [1] + assert list(lookup.matches(1, 1)) == [1] + assert list(lookup.matches(3, 3)) == [3] + assert list(lookup.matches(2, 4)) == [3] + assert list(lookup.matches(5, 5)) == [5] + assert list(lookup.matches(4, 5)) == [5] diff --git a/hassio-google-drive-backup/tests/test_resolver.py b/hassio-google-drive-backup/tests/test_resolver.py new file mode 100644 index 0000000..26f7e5f --- /dev/null +++ b/hassio-google-drive-backup/tests/test_resolver.py @@ -0,0 +1,46 @@ +import pytest +import socket + +from backup.config import Config, Setting +from backup.util import Resolver + + +@pytest.mark.asyncio +async def test_empty_name_server(resolver: Resolver, config: Config): + assert resolver._alt_dns.nameservers == ["8.8.8.8", "8.8.4.4"] + assert resolver._resolver is resolver._original_dns + config.override(Setting.ALTERNATE_DNS_SERVERS, "") + resolver.updateConfig() + assert resolver._resolver is resolver._alt_dns + + # make sure the value is cached + prev = resolver._alt_dns + resolver.updateConfig() + assert resolver._alt_dns is prev + + +@pytest.mark.asyncio +async def test_toggle(resolver: Resolver): + assert resolver._resolver is resolver._original_dns + resolver.toggle() + assert resolver._resolver is resolver._alt_dns + resolver.toggle() + assert resolver._resolver is resolver._original_dns + + +@pytest.mark.asyncio +async def test_hard_resolve(resolver: Resolver, config: Config): + expected = [{ + 'family': 0, + 'flags': socket.AddressInfo.AI_NUMERICHOST, + 'port': 1234, + 'proto': 0, + 'host': "1.2.3.4", + 'hostname': "www.googleapis.com" + }] + config.override(Setting.DRIVE_IPV4, "1.2.3.4") + assert await resolver.resolve("www.googleapis.com", 1234, 0) == expected + resolver.toggle() + assert await resolver.resolve("www.googleapis.com", 1234, 0) == expected + resolver.toggle() + assert await resolver.resolve("www.googleapis.com", 1234, 0) == expected diff --git a/hassio-google-drive-backup/tests/test_scheme.py b/hassio-google-drive-backup/tests/test_scheme.py new file mode 100644 index 0000000..b4fe62b --- /dev/null +++ b/hassio-google-drive-backup/tests/test_scheme.py @@ -0,0 +1,442 @@ +from datetime import datetime, timedelta + +import pytest +from dateutil.tz import tzutc +from pytest import fail + +from backup.model import GenConfig, GenerationalScheme, DummyBackup, Backup +from backup.time import Time + + +def test_timezone(time) -> None: + assert time.local_tz is not None + + +def test_trivial(time) -> None: + config = GenConfig(days=1) + + scheme = GenerationalScheme(time, config, count=0) + + backups = [ + makeBackup("single", time.local(1928, 12, 6)) + ] + + assert scheme.getOldest(backups)[1].date() == time.local(1928, 12, 6) + + +def test_trivial_empty(time): + config = GenConfig(days=1) + scheme = GenerationalScheme(time, config, count=0) + assert scheme.getOldest([])[1] is None + + +def test_trivial_oldest(time: Time) -> None: + config = GenConfig(days=1) + scheme = GenerationalScheme(time, config, count=0) + + backups = [ + makeBackup("test", time.local(1985, 12, 6, 10)), + makeBackup("test", time.local(1985, 12, 6, 12)), + makeBackup("test", time.local(1985, 12, 6, 13)) + ] + assertRemovalOrder(scheme, backups, [ + time.local(1985, 12, 6, 10), + time.local(1985, 12, 6, 12), + time.local(1985, 12, 6, 13) + ]) + + +def test_duplicate_weeks(time): + config = GenConfig(weeks=1, day_of_week='wed') + + scheme = GenerationalScheme(time, config, count=0) + + backups = [ + makeBackup("test", time.local(1985, 12, 5)), + makeBackup("test", time.local(1985, 12, 4)), + makeBackup("test", time.local(1985, 12, 1)), + makeBackup("test", time.local(1985, 12, 2)) + ] + assertRemovalOrder(scheme, backups, [ + time.local(1985, 12, 1), + time.local(1985, 12, 2), + time.local(1985, 12, 5), + time.local(1985, 12, 4) + ]) + + +def test_duplicate_months(time) -> None: + config = GenConfig(months=2, day_of_month=15) + + scheme = GenerationalScheme(time, config, count=0) + + backups = [ + makeBackup("test", time.local(1985, 12, 6)), + makeBackup("test", time.local(1985, 12, 15)), + makeBackup("test", time.local(1985, 11, 20)), + makeBackup("test", time.local(1985, 11, 15)) + ] + assertRemovalOrder(scheme, backups, [ + time.local(1985, 11, 20), + time.local(1985, 12, 6), + time.local(1985, 11, 15), + time.local(1985, 12, 15) + ]) + + +def test_duplicate_years(time): + config = GenConfig(years=2, day_of_year=1) + + scheme = GenerationalScheme(time, config, count=0) + + backups = [ + makeBackup("test", time.local(1985, 12, 31)), + makeBackup("test", time.local(1985, 1, 1)), + makeBackup("test", time.local(1984, 12, 31)), + makeBackup("test", time.local(1984, 1, 1)) + ] + assertRemovalOrder(scheme, backups, [ + time.local(1984, 12, 31), + time.local(1985, 12, 31), + time.local(1984, 1, 1), + time.local(1985, 1, 1) + ]) + + +def test_removal_order(time) -> None: + config = GenConfig(days=5, weeks=2, months=2, years=2, + day_of_week='mon', day_of_month=15, day_of_year=1) + + scheme = GenerationalScheme(time, config, count=0) + + backups = [ + # 5 days, week 1 + makeBackup("test", time.local(1985, 12, 7)), # day 1 + makeBackup("test", time.local(1985, 12, 6)), # day 2 + makeBackup("test", time.local(1985, 12, 5)), # day 3 + makeBackup("test", time.local(1985, 12, 4)), # day 4 + makeBackup("test", time.local(1985, 12, 3)), # day 5 + + makeBackup("test", time.local(1985, 12, 1)), # 1st week pref + + # week 2 + makeBackup("test", time.local(1985, 11, 25)), # 1st month pref + + # month2 + makeBackup("test", time.local(1985, 11, 15)), # 2nd month pref + + # year 1 + makeBackup("test", time.local(1985, 1, 1)), # 1st year preference + makeBackup("test", time.local(1985, 1, 2)), + + # year 2 + makeBackup("test", time.local(1984, 6, 1)), # 2nd year pref + makeBackup("test", time.local(1984, 7, 1)), + + # year 3 + makeBackup("test", time.local(1983, 1, 1)), + ] + assertRemovalOrder(scheme, backups, [ + time.local(1983, 1, 1), + time.local(1984, 7, 1), + time.local(1985, 1, 2), + + time.local(1984, 6, 1), + time.local(1985, 1, 1), + time.local(1985, 11, 15), + time.local(1985, 11, 25), + time.local(1985, 12, 1), + time.local(1985, 12, 3), + time.local(1985, 12, 4), + time.local(1985, 12, 5), + time.local(1985, 12, 6), + time.local(1985, 12, 7) + ]) + + +@pytest.mark.timeout(60) +def test_simulate_daily_backup_for_4_years(time): + config = GenConfig(days=4, weeks=4, months=4, years=4, + day_of_week='mon', day_of_month=1, day_of_year=1) + scheme = GenerationalScheme(time, config, count=16) + backups = simulate(time.local(2019, 1, 1), + time.local(2022, 12, 31), + scheme) + assertRemovalOrder(GenerationalScheme(time, config, count=0), backups, [ + # 4 years + time.local(2019, 1, 1), + time.local(2020, 1, 1), + time.local(2021, 1, 1), + time.local(2022, 1, 1), + + # 4 months + time.local(2022, 9, 1), + time.local(2022, 10, 1), + time.local(2022, 11, 1), + time.local(2022, 12, 1), + + # 4 weeks + time.local(2022, 12, 5), + time.local(2022, 12, 12), + time.local(2022, 12, 19), + time.local(2022, 12, 26), + + # 4 days + time.local(2022, 12, 28), + time.local(2022, 12, 29), + time.local(2022, 12, 30), + time.local(2022, 12, 31) + ]) + + +@pytest.mark.timeout(60) +def test_simulate_agressive_daily_backup_for_4_years(time): + config = GenConfig(days=4, weeks=4, months=4, years=4, + day_of_week='mon', day_of_month=1, day_of_year=1, aggressive=True) + scheme = GenerationalScheme(time, config, count=16) + backups = simulate(time.local(2019, 1, 1), + time.local(2022, 12, 31), + scheme) + + assertRemovalOrder(GenerationalScheme(time, config, count=0), backups, [ + # 4 years + time.local(2019, 1, 1), + time.local(2020, 1, 1), + time.local(2021, 1, 1), + time.local(2022, 1, 1), + + # 4 months + time.local(2022, 9, 1), + time.local(2022, 10, 1), + time.local(2022, 11, 1), + time.local(2022, 12, 1), + + # 4 weeks + time.local(2022, 12, 5), + time.local(2022, 12, 12), + time.local(2022, 12, 19), + time.local(2022, 12, 26), + + # 4 days + time.local(2022, 12, 28), + time.local(2022, 12, 29), + time.local(2022, 12, 30), + time.local(2022, 12, 31), + ]) + + +def test_count_limit(time): + config = GenConfig(years=2, day_of_year=1) + scheme = GenerationalScheme(time, config, count=1) + backups = [ + makeBackup("test", time.local(1985, 1, 1)), + makeBackup("test", time.local(1984, 1, 1)) + ] + assertRemovalOrder(scheme, backups, [ + time.local(1984, 1, 1) + ]) + + +def test_aggressive_removal_below_limit(time): + config = GenConfig(years=2, day_of_year=1, aggressive=True) + scheme = GenerationalScheme(time, config, count=5) + backups = [ + makeBackup("test", time.local(1985, 1, 1)), + makeBackup("test", time.local(1985, 1, 2)) + ] + assertRemovalOrder(scheme, backups, [ + time.local(1985, 1, 2) + ]) + + +def test_aggressive_removal_at_limit_ok(time): + config = GenConfig(years=2, day_of_year=1, aggressive=True) + scheme = GenerationalScheme(time, config, count=2) + backups = [ + makeBackup("test", time.local(1985, 1, 1)), + makeBackup("test", time.local(1984, 1, 1)) + ] + assertRemovalOrder(scheme, backups, []) + + +def test_aggressive_removal_over_limit(time): + config = GenConfig(years=2, day_of_year=1, aggressive=True) + scheme = GenerationalScheme(time, config, count=2) + backups = [ + makeBackup("test", time.local(1985, 1, 1)), + makeBackup("test", time.local(1984, 1, 1)), + makeBackup("test", time.local(1983, 1, 1)), + makeBackup("test", time.local(1983, 1, 2)) + ] + assertRemovalOrder(scheme, backups, [ + time.local(1983, 1, 1), + time.local(1983, 1, 2) + ]) + + +def test_removal_order_week(time: Time): + config = GenConfig(weeks=1, day_of_week='wed', aggressive=True) + scheme = GenerationalScheme(time, config, count=1) + backups = [ + makeBackup("test", time.local(2019, 10, 28)), + makeBackup("test", time.local(2019, 10, 29)), + makeBackup("test", time.local(2019, 10, 30, 1)), + makeBackup("test", time.local(2019, 10, 30, 2)), + makeBackup("test", time.local(2019, 10, 31)), + makeBackup("test", time.local(2019, 11, 1)), + makeBackup("test", time.local(2019, 11, 2)), + makeBackup("test", time.local(2019, 11, 3)), + ] + assertRemovalOrder(scheme, backups, [ + time.local(2019, 10, 28), + time.local(2019, 10, 29), + time.local(2019, 10, 30, 1), + time.local(2019, 10, 31), + time.local(2019, 11, 1), + time.local(2019, 11, 2), + time.local(2019, 11, 3) + ]) + + +def test_removal_order_month(time): + config = GenConfig(months=1, day_of_month=20, aggressive=True) + + scheme = GenerationalScheme(time, config, count=1) + + backups = [ + makeBackup("test", time.local(2019, 1, 1)), + makeBackup("test", time.local(2019, 1, 2)), + makeBackup("test", time.local(2019, 1, 20, 1)), + makeBackup("test", time.local(2019, 1, 20, 2)), + makeBackup("test", time.local(2019, 1, 21)), + makeBackup("test", time.local(2019, 1, 25)), + makeBackup("test", time.local(2019, 1, 26)), + makeBackup("test", time.local(2019, 1, 27)), + ] + assertRemovalOrder(scheme, backups, [ + time.local(2019, 1, 1), + time.local(2019, 1, 2), + time.local(2019, 1, 20, 1), + time.local(2019, 1, 21), + time.local(2019, 1, 25), + time.local(2019, 1, 26), + time.local(2019, 1, 27) + ]) + + +def test_removal_order_many_months(time): + config = GenConfig(months=70, day_of_month=20, aggressive=True) + + scheme = GenerationalScheme(time, config, count=10) + + backups = [ + makeBackup("test", time.local(2019, 7, 20)), # preferred + makeBackup("test", time.local(2018, 7, 18)), # preferred + makeBackup("test", time.local(2018, 7, 21)), + makeBackup("test", time.local(2017, 1, 19)), + makeBackup("test", time.local(2017, 1, 20)), # preferred + makeBackup("test", time.local(2017, 1, 31)), + makeBackup("test", time.local(2016, 12, 1)), # preferred + makeBackup("test", time.local(2014, 1, 31)), + makeBackup("test", time.local(2014, 1, 1)), # preferred + ] + assertRemovalOrder(scheme, backups, [ + time.local(2014, 1, 31), + time.local(2017, 1, 19), + time.local(2017, 1, 31), + time.local(2018, 7, 21), + ]) + + +def test_removal_order_years(time): + config = GenConfig(years=2, day_of_year=15, aggressive=True) + + scheme = GenerationalScheme(time, config, count=10) + + backups = [ + makeBackup("test", time.local(2019, 2, 15)), + makeBackup("test", time.local(2019, 1, 15)), # keep + makeBackup("test", time.local(2018, 1, 14)), + makeBackup("test", time.local(2018, 1, 15)), # keep + makeBackup("test", time.local(2018, 1, 16)), + makeBackup("test", time.local(2017, 1, 15)), + ] + assertRemovalOrder(scheme, backups, [ + time.local(2017, 1, 15), + time.local(2018, 1, 14), + time.local(2018, 1, 16), + time.local(2019, 2, 15), + ]) + + +@pytest.mark.asyncio +async def test_ignored_generational_labels(time): + config = GenConfig(days=2) + + scheme = GenerationalScheme(time, config, count=10) + backup1 = makeBackup("test", time.local(2019, 2, 15)) + backup2 = makeBackup("test", time.local(2019, 2, 14)) + backup3 = makeBackup("test", time.local(2019, 2, 13), ignore=True) + backups = [backup1, backup2, backup3] + scheme.handleNaming(backups) + assert backup1.getStatusDetail() == ['Day 1 of 2'] + assert backup2.getStatusDetail() == ['Day 2 of 2'] + assert backup3.getStatusDetail() is None + + +def getRemovalOrder(scheme, toCheck): + backups = list(toCheck) + removed = [] + while True: + oldest = scheme.getOldest(backups) + if not oldest: + break + removed.append(oldest.date()) + backups.remove(oldest) + return removed + + +def assertRemovalOrder(scheme, toCheck, expected): + backups = list(toCheck) + removed = [] + index = 0 + time = scheme.time + while True: + reason, oldest = scheme.getOldest(backups) + if index >= len(expected): + if oldest is not None: + fail("at index {0}, expected 'None' but got {1}".format( + index, time.toLocal(oldest.date()))) + break + if oldest.date() != expected[index]: + fail("at index {0}, expected {1} but got {2}".format( + index, time.toLocal(expected[index]), time.toLocal(oldest.date()))) + removed.append(oldest.date()) + backups.remove(oldest) + index += 1 + return removed + + +def makeBackup(slug, date, name=None, ignore=False) -> Backup: + if not name: + name = slug + return DummyBackup(name, date.astimezone(tzutc()), "src", slug, ignore=ignore) + + +def simulate(start: datetime, end: datetime, scheme: GenerationalScheme, backups=[]): + today = start + while today <= end: + backups.append(makeBackup("test", today)) + test = scheme.getOldest(backups) + if test is None: + pass + reason, oldest = test + while oldest is not None: + backups.remove(oldest) + test = scheme.getOldest(backups) + if test is None: + pass + reason, oldest = test + today = today + timedelta(hours=27) + today = scheme.time.local(today.year, today.month, today.day) + return backups diff --git a/hassio-google-drive-backup/tests/test_server.py b/hassio-google-drive-backup/tests/test_server.py new file mode 100644 index 0000000..9f88bc1 --- /dev/null +++ b/hassio-google-drive-backup/tests/test_server.py @@ -0,0 +1,50 @@ + +import pytest +from yarl import URL +from dev.simulationserver import SimulationServer +from aiohttp import ClientSession, hdrs +from backup.config import Config + + +@pytest.mark.asyncio +async def test_refresh_known_error(server: SimulationServer, session: ClientSession, config: Config, server_url: URL): + async with session.post(server_url.with_path("drive/refresh"), json={"blah": "blah"}) as r: + assert r.status == 503 + assert await r.json() == { + 'error': "Required key 'refresh_token' was missing from the request payload" + } + + +@pytest.mark.asyncio +async def test_refresh_unknown_error(server: SimulationServer, session: ClientSession, config: Config, server_url: URL): + async with session.post(server_url.with_path("drive/refresh"), data={}) as r: + assert r.status == 500 + assert len((await r.json())["error"]) > 0 + + +@pytest.mark.asyncio +async def test_old_auth_method(server: SimulationServer, session: ClientSession, server_url: URL): + start_auth = server_url.with_path("drive/authorize").with_query({ + "redirectbacktoken": "http://example.com" + }) + + # Verify the redirect to Drive's oauthv2 endpoint + async with session.get(start_auth, data={}, allow_redirects=False) as r: + assert r.status == 303 + redirect = URL(r.headers[hdrs.LOCATION]) + assert redirect.path == "/o/oauth2/v2/auth" + assert redirect.host == "localhost" + + # Verify the redirect back to the server's oauth page + async with session.get(redirect, data={}, allow_redirects=False) as r: + assert r.status == 303 + redirect = URL(r.headers[hdrs.LOCATION]) + assert redirect.path == "/drive/authorize" + assert redirect.host == "localhost" + + # Verify we gte redirected back to the addon (example.com) with creds + async with session.get(redirect, data={}, allow_redirects=False) as r: + assert r.status == 303 + redirect = URL(r.headers[hdrs.LOCATION]) + assert redirect.query.get("creds") is not None + assert redirect.host == "example.com" diff --git a/hassio-google-drive-backup/tests/test_settings.py b/hassio-google-drive-backup/tests/test_settings.py new file mode 100644 index 0000000..9b204fe --- /dev/null +++ b/hassio-google-drive-backup/tests/test_settings.py @@ -0,0 +1,37 @@ +from backup.config import Setting, addon_config, _CONFIG + + +def test_defaults(): + # all settings should have a default + for setting in Setting: + if setting is not Setting.DEBUGGER_PORT: + assert setting.default() is not None, setting.value + " has no default" + + +def test_validators(): + # all defaults shoudl have a validator + for setting in Setting: + assert setting.validator() is not None, setting.value + " has no validator" + + +def test_defaults_are_valid(): + # all defaults values should be valid and validate to their own value + for setting in Setting: + assert setting.validator().validate(setting.default()) == setting.default() + + +def test_setting_configuration(): + # All settings in the default config should have the exact same parse expression + for setting in Setting: + if setting.value in addon_config["schema"]: + if setting != Setting.GENERATIONAL_DAY_OF_WEEK: + assert _CONFIG[setting] == addon_config["schema"][setting.value], setting.value + + +def test_settings_present(): + all = set() + for setting in Setting: + all.add(setting.value) + + for setting in addon_config["schema"]: + assert setting in all, setting + " not present in config.json" diff --git a/hassio-google-drive-backup/tests/test_starter.py b/hassio-google-drive-backup/tests/test_starter.py new file mode 100644 index 0000000..eb8192c --- /dev/null +++ b/hassio-google-drive-backup/tests/test_starter.py @@ -0,0 +1,22 @@ +import pytest +import os +from backup.module import MainModule, BaseModule +from backup.starter import Starter +from backup.config import Config, Setting +from injector import Injector + + +@pytest.mark.asyncio +async def test_bootstrap_requirements(cleandir): + # This just verifies we're able to satisfy starter's injector requirements. + injector = Injector([BaseModule(), MainModule()]) + config = injector.get(Config) + config.override(Setting.DATA_CACHE_FILE_PATH, os.path.join(cleandir, "data_cache.json")) + injector.get(Starter) + + +@pytest.mark.asyncio +async def test_start_and_stop(injector): + starter = injector.get(Starter) + await starter.start() + await starter.stop() diff --git a/hassio-google-drive-backup/tests/test_timezone.py b/hassio-google-drive-backup/tests/test_timezone.py new file mode 100644 index 0000000..2dd694b --- /dev/null +++ b/hassio-google-drive-backup/tests/test_timezone.py @@ -0,0 +1,59 @@ +import datetime +import os +from backup.time import Time, _infer_timezone_from_env, _infer_timezone_from_name, _infer_timezone_from_offset, _infer_timezone_from_system +from .faketime import FakeTime + + +def test_parse() -> None: + time = Time.parse("1985-12-06 01:01:01.0001") + assert str(time) == "1985-12-06 01:01:01.000100+00:00" + + time = Time.parse("1985-12-06 01:01:01.0001+01:00") + assert str(time) == "1985-12-06 01:01:01.000100+01:00" + + +def test_parse_timezone(time) -> None: + assertUtc(Time.parse("1985-12-06")) + assertUtc(Time.parse("1985-12-06 21:21")) + assertUtc(Time.parse("1985-12-06 21:21+00:00")) + assertUtc(Time.parse("1985-12-06 21:21 UTC")) + assertUtc(Time.parse("1985-12-06 21:21 GGGR")) + + assertOffset(Time.parse("1985-12-06 21:21+10"), 10) + assertOffset(Time.parse("1985-12-06 21:21-10"), -10) + + +def assertOffset(time, hours): + assert time.tzinfo.utcoffset(time) == datetime.timedelta(hours=hours) + + +def assertUtc(time): + assertOffset(time, 0) + + +def test_common_timezones(time: FakeTime): + assert _infer_timezone_from_system() is not None + assert _infer_timezone_from_name() is not None + assert _infer_timezone_from_offset() is not None + assert _infer_timezone_from_env() is None + + os.environ["TZ"] = "America/Denver" + assert _infer_timezone_from_env() is not None + + os.environ["TZ"] = "Australia/Brisbane" + + assert _infer_timezone_from_env() is not None + + tzs = [_infer_timezone_from_system(), + _infer_timezone_from_env(), + _infer_timezone_from_offset(), + _infer_timezone_from_name()] + + for tz in tzs: + time.setTimeZone(tz) + time.now() + time.nowLocal() + time.localize(datetime.datetime(1985, 12, 6)) + time.local(1985, 12, 6) + time.toLocal(time.now()) + time.toUtc(time.nowLocal()) diff --git a/hassio-google-drive-backup/tests/test_uiserver.py b/hassio-google-drive-backup/tests/test_uiserver.py new file mode 100644 index 0000000..c015a3b --- /dev/null +++ b/hassio-google-drive-backup/tests/test_uiserver.py @@ -0,0 +1,1176 @@ +from datetime import timedelta +import os +import json +from os.path import abspath, join +from shutil import copyfile +from urllib.parse import quote + +import aiohttp +import pytest +import asyncio +import base64 +from aiohttp import BasicAuth +from aiohttp.client import ClientSession + +from backup.file import File +from backup.util import AsyncHttpGetter, GlobalInfo, DataCache, UpgradeFlags +from backup.ui import UiServer, Restarter +from backup.config import Config, Setting, CreateOptions +from backup.const import (ERROR_CREDS_EXPIRED, ERROR_EXISTING_FOLDER, + ERROR_MULTIPLE_DELETES, ERROR_NO_BACKUP, + SOURCE_GOOGLE_DRIVE, SOURCE_HA) +from backup.creds import Creds +from backup.model import Coordinator, Backup +from backup.drive import DriveSource, FolderFinder, OOB_CRED_CUTOFF +from backup.drive.drivesource import FOLDER_MIME_TYPE, DriveRequests +from backup.ha import HaSource, HaUpdater +from backup.config import VERSION +from .faketime import FakeTime +from .helpers import compareStreams +from yarl import URL +from dev.ports import Ports +from dev.simulated_supervisor import SimulatedSupervisor +from dev.simulationserver import SimulationServer +from dev.simulated_google import SimulatedGoogle +from bs4 import BeautifulSoup +from .conftest import ReaderHelper + + +@pytest.fixture +def source(ha): + return ha + + +@pytest.fixture +def dest(drive): + return drive + + +@pytest.fixture +def simple_config(config): + return config + + +@pytest.fixture +async def restarter(injector, server): + restarter = injector.get(Restarter) + await restarter.start() + return restarter + + +@pytest.mark.asyncio +async def test_uiserver_start(ui_server: UiServer): + assert ui_server.running + + +@pytest.mark.asyncio +@pytest.mark.timeout(10) +async def test_uiserver_static_files(reader: ReaderHelper): + await reader.get("") + await reader.get("reauthenticate") + await reader.get("pp") + await reader.get("tos") + + +@pytest.mark.asyncio +async def test_getstatus(reader, config: Config, ha, server, ports: Ports): + File.touch(config.get(Setting.INGRESS_TOKEN_FILE_PATH)) + await ha.init() + data = await reader.getjson("getstatus") + assert data['ask_error_reports'] is True + assert data['cred_version'] == 0 + assert data['firstSync'] is True + assert data['folder_id'] is None + assert data['last_error'] is None + assert data['last_backup_text'] == "Never" + assert data['next_backup_text'] == "right now" + assert data['backup_name_template'] == config.get(Setting.BACKUP_NAME) + assert data['warn_ingress_upgrade'] is False + assert len(data['backups']) == 0 + assert data['sources'][SOURCE_GOOGLE_DRIVE] == { + 'deletable': 0, + 'name': SOURCE_GOOGLE_DRIVE, + 'retained': 0, + 'backups': 0, + 'latest': None, + 'size': '0.0 B', + 'enabled': True, + 'max': config.get(Setting.MAX_BACKUPS_IN_GOOGLE_DRIVE), + 'title': "Google Drive", + 'icon': 'google-drive', + 'ignored': 0, + 'ignored_size': '0.0 B', + 'detail': "", + } + assert data['sources'][SOURCE_HA] == { + 'deletable': 0, + 'name': SOURCE_HA, + 'retained': 0, + 'backups': 0, + 'latest': None, + 'size': '0.0 B', + 'enabled': True, + 'max': config.get(Setting.MAX_BACKUPS_IN_HA), + 'title': "Home Assistant", + 'free_space': "0.0 B", + 'icon': 'home-assistant', + 'ignored': 0, + 'ignored_size': '0.0 B', + 'detail': "", + } + assert len(data['sources']) == 2 + + +@pytest.mark.asyncio +async def test_getstatus_sync(reader, config: Config, backup: Backup, time: FakeTime): + data = await reader.getjson("getstatus") + assert data['firstSync'] is False + assert data['folder_id'] is not None + assert data['last_error'] is None + assert data['last_backup_text'] != "Never" + assert data['next_backup_text'] != "right now" + assert len(data['backups']) == 1 + assert data['sources'][SOURCE_GOOGLE_DRIVE] == { + 'deletable': 1, + 'name': SOURCE_GOOGLE_DRIVE, + 'retained': 0, + 'backups': 1, + 'latest': time.asRfc3339String(time.now()), + 'size': data['sources'][SOURCE_GOOGLE_DRIVE]['size'], + 'enabled': True, + 'max': config.get(Setting.MAX_BACKUPS_IN_GOOGLE_DRIVE), + 'title': "Google Drive", + 'icon': 'google-drive', + 'free_space': "4.0 GB", + 'ignored': 0, + 'ignored_size': '0.0 B', + 'detail': "testing@no.where", + } + assert data['sources'][SOURCE_HA] == { + 'deletable': 1, + 'name': SOURCE_HA, + 'retained': 0, + 'backups': 1, + 'latest': time.asRfc3339String(time.now()), + 'size': data['sources'][SOURCE_HA]['size'], + 'enabled': True, + 'max': config.get(Setting.MAX_BACKUPS_IN_HA), + 'title': "Home Assistant", + 'free_space': data['sources'][SOURCE_HA]['free_space'], + 'icon': 'home-assistant', + 'ignored': 0, + 'ignored_size': '0.0 B', + 'detail': "", + } + assert len(data['sources']) == 2 + + +@pytest.mark.asyncio +async def test_retain(reader: ReaderHelper, config: Config, backup: Backup, coord: Coordinator, time: FakeTime): + slug = backup.slug() + assert await reader.getjson("retain", json={'slug': slug, 'sources': {"GoogleDrive": True, "HomeAssistant": True}}) == { + 'message': "Updated the backup's settings" + } + status = await reader.getjson("getstatus") + assert status['sources'][SOURCE_GOOGLE_DRIVE] == { + 'deletable': 0, + 'name': SOURCE_GOOGLE_DRIVE, + 'retained': 1, + 'backups': 1, + 'latest': time.asRfc3339String(backup.date()), + 'size': status['sources'][SOURCE_GOOGLE_DRIVE]['size'], + 'enabled': True, + 'max': config.get(Setting.MAX_BACKUPS_IN_GOOGLE_DRIVE), + 'title': "Google Drive", + 'icon': 'google-drive', + 'free_space': "4.0 GB", + 'ignored': 0, + 'ignored_size': '0.0 B', + 'detail': "testing@no.where", + } + assert status['sources'][SOURCE_HA] == { + 'deletable': 0, + 'name': SOURCE_HA, + 'retained': 1, + 'backups': 1, + 'latest': time.asRfc3339String(backup.date()), + 'size': status['sources'][SOURCE_HA]['size'], + 'enabled': True, + 'max': config.get(Setting.MAX_BACKUPS_IN_HA), + 'title': "Home Assistant", + 'free_space': status['sources'][SOURCE_HA]["free_space"], + 'icon': 'home-assistant', + 'ignored': 0, + 'ignored_size': '0.0 B', + 'detail': "", + } + + await reader.getjson("retain", json={'slug': slug, 'sources': {"GoogleDrive": False, "HomeAssistant": False}}) + status = await reader.getjson("getstatus") + assert status['sources'][SOURCE_GOOGLE_DRIVE] == { + 'deletable': 1, + 'name': SOURCE_GOOGLE_DRIVE, + 'retained': 0, + 'backups': 1, + 'latest': time.asRfc3339String(backup.date()), + 'size': status['sources'][SOURCE_GOOGLE_DRIVE]['size'], + 'enabled': True, + 'max': config.get(Setting.MAX_BACKUPS_IN_GOOGLE_DRIVE), + 'title': "Google Drive", + 'icon': 'google-drive', + 'free_space': "4.0 GB", + 'ignored': 0, + 'ignored_size': '0.0 B', + 'detail': "testing@no.where", + } + assert status['sources'][SOURCE_HA] == { + 'deletable': 1, + 'name': SOURCE_HA, + 'retained': 0, + 'backups': 1, + 'latest': time.asRfc3339String(backup.date()), + 'size': status['sources'][SOURCE_HA]['size'], + 'enabled': True, + 'max': config.get(Setting.MAX_BACKUPS_IN_HA), + 'title': "Home Assistant", + 'free_space': status['sources'][SOURCE_HA]["free_space"], + 'icon': 'home-assistant', + 'ignored': 0, + 'ignored_size': '0.0 B', + 'detail': "", + } + delete_req = { + "slug": slug, + "sources": ["GoogleDrive"] + } + await reader.getjson("deleteSnapshot", json=delete_req) + await reader.getjson("retain", json={'slug': slug, 'sources': {"HomeAssistant": True}}) + status = await reader.getjson("getstatus") + assert status['sources'][SOURCE_GOOGLE_DRIVE] == { + 'deletable': 0, + 'name': SOURCE_GOOGLE_DRIVE, + 'retained': 0, + 'backups': 0, + 'latest': None, + 'size': status['sources'][SOURCE_GOOGLE_DRIVE]['size'], + 'enabled': True, + 'max': config.get(Setting.MAX_BACKUPS_IN_GOOGLE_DRIVE), + 'title': "Google Drive", + 'icon': 'google-drive', + 'free_space': "4.0 GB", + 'ignored': 0, + 'ignored_size': '0.0 B', + 'detail': "testing@no.where", + } + assert status['sources'][SOURCE_HA] == { + 'deletable': 0, + 'name': SOURCE_HA, + 'retained': 1, + 'backups': 1, + 'latest': time.asRfc3339String(backup.date()), + 'size': status['sources'][SOURCE_HA]['size'], + 'enabled': True, + 'max': config.get(Setting.MAX_BACKUPS_IN_HA), + 'title': "Home Assistant", + 'free_space': status['sources'][SOURCE_HA]["free_space"], + 'icon': 'home-assistant', + 'ignored': 0, + 'ignored_size': '0.0 B', + 'detail': "", + } + + # sync again, which should upoload the backup to Drive + await coord.sync() + status = await reader.getjson("getstatus") + assert status['sources'][SOURCE_GOOGLE_DRIVE]['backups'] == 1 + assert status['sources'][SOURCE_GOOGLE_DRIVE]['retained'] == 0 + assert status['sources'][SOURCE_GOOGLE_DRIVE]['backups'] == 1 + + +@pytest.mark.asyncio +async def test_note(reader: ReaderHelper, config: Config, backup: Backup, coord: Coordinator, time: FakeTime): + slug = backup.slug() + assert backup.note() is None + assert await reader.getjson("note", json={'slug': slug, 'note': "This is the note"}) == { + 'message': "Updated the backup's settings" + } + status = await reader.getjson("getstatus") + assert backup.note() == "This is the note" + assert status['backups'][0]['note'] == "This is the note" + + +@pytest.mark.asyncio +async def test_sync(reader, ui_server, coord: Coordinator, time: FakeTime, session): + assert len(coord.backups()) == 0 + status = await reader.getjson("sync") + assert len(coord.backups()) == 1 + assert status == await reader.getjson("getstatus") + time.advance(days=7) + assert len((await reader.getjson("sync"))['backups']) == 2 + + +@pytest.mark.asyncio +async def test_delete(reader: ReaderHelper, ui_server, backup): + slug = backup.slug() + + data = {"slug": "bad_slug", "sources": ["GoogleDrive"]} + await reader.assertError("deleteSnapshot", json=data, error_type=ERROR_NO_BACKUP) + status = await reader.getjson("getstatus") + assert len(status['backups']) == 1 + data["slug"] = slug + assert await reader.getjson("deleteSnapshot", json=data) == {"message": "Deleted from 1 place(s)"} + await reader.assertError("deleteSnapshot", json=data, error_type=ERROR_NO_BACKUP) + status = await reader.getjson("getstatus") + assert len(status['backups']) == 1 + assert status['sources'][SOURCE_GOOGLE_DRIVE]['backups'] == 0 + data["sources"] = ["HomeAssistant"] + assert await reader.getjson("deleteSnapshot", json=data) == {"message": "Deleted from 1 place(s)"} + status = await reader.getjson("getstatus") + assert len(status['backups']) == 0 + data["sources"] = [] + await reader.assertError("deleteSnapshot", json=data, error_type=ERROR_NO_BACKUP) + + +@pytest.mark.asyncio +async def test_backup_now(reader, ui_server, time: FakeTime, backup: Backup, coord: Coordinator): + assert len(coord.backups()) == 1 + assert (await reader.getjson("getstatus"))["backups"][0]["date"] == time.toLocal(time.now()).strftime("%c") + + time.advance(hours=1) + assert await reader.getjson("backup?custom_name=TestName&retain_drive=False&retain_ha=False") == { + 'message': "Requested backup 'TestName'" + } + status = await reader.getjson('getstatus') + assert len(status["backups"]) == 2 + assert status["backups"][1]["date"] == time.toLocal(time.now()).strftime("%c") + assert status["backups"][1]["name"] == "TestName" + assert status["backups"][1]["note"] is None + assert status["backups"][1]['sources'][0]['retained'] is False + assert len(status["backups"][1]['sources']) == 1 + + time.advance(hours=1) + assert await reader.getjson("backup?custom_name=TestName2&retain_drive=True&retain_ha=False") == { + 'message': "Requested backup 'TestName2'" + } + await coord.sync() + status = await reader.getjson('getstatus') + assert len(status["backups"]) == 3 + assert status["backups"][2]["date"] == time.toLocal(time.now()).strftime("%c") + assert status["backups"][2]["name"] == "TestName2" + assert status["backups"][2]['sources'][0]['retained'] is False + assert status["backups"][2]['sources'][1]['retained'] is True + + time.advance(hours=1) + assert await reader.getjson("backup?custom_name=TestName3&retain_drive=False&retain_ha=True") == { + 'message': "Requested backup 'TestName3'" + } + await coord.sync() + status = await reader.getjson('getstatus') + assert len(status["backups"]) == 4 + assert status["backups"][3]['sources'][0]['retained'] is True + assert status["backups"][3]['sources'][1]['retained'] is False + assert status["backups"][3]["date"] == time.toLocal(time.now()).strftime("%c") + assert status["backups"][3]["name"] == "TestName3" + + +@pytest.mark.asyncio +async def test_backup_now_with_note(reader, ui_server, time: FakeTime, coord: Coordinator): + assert len(coord.backups()) == 0 + + time.advance(hours=1) + assert await reader.getjson("backup?custom_name=TestName&retain_drive=False&retain_ha=False¬e=ThisIsTheNote") == { + 'message': "Requested backup 'TestName'" + } + await coord.sync() + status = await reader.getjson('getstatus') + assert status['backups'][0]["note"] == "ThisIsTheNote" + + +@pytest.mark.asyncio +async def test_config(reader, ui_server, config: Config, supervisor: SimulatedSupervisor): + update = { + "config": { + "days_between_backups": 20, + "drive_ipv4": "" + }, + "backup_folder": "unused" + } + assert ui_server._starts == 1 + assert await reader.postjson("saveconfig", json=update) == {'message': 'Settings saved', "reload_page": False} + assert config.get(Setting.DAYS_BETWEEN_BACKUPS) == 20 + assert supervisor._options["days_between_backups"] == 20 + assert ui_server._starts == 1 + + +@pytest.mark.asyncio +async def test_auth_and_restart(reader, ui_server, config: Config, restarter, coord: Coordinator, supervisor: SimulatedSupervisor): + update = {"config": {"require_login": True, + "expose_extra_server": True}, "backup_folder": "unused"} + assert ui_server._starts == 1 + assert not config.get(Setting.REQUIRE_LOGIN) + assert await reader.postjson("saveconfig", json=update) == {'message': 'Settings saved', "reload_page": False} + await restarter.waitForRestart() + assert config.get(Setting.REQUIRE_LOGIN) + assert supervisor._options['require_login'] + assert ui_server._starts == 2 + + await reader.get("getstatus", status=401, ingress=False) + await reader.get("getstatus", auth=BasicAuth("user", "badpassword"), status=401, ingress=False) + await reader.get("getstatus", auth=BasicAuth("user", "pass"), ingress=False) + await coord.waitForSyncToFinish() + status = await reader.getjson("getstatus", auth=BasicAuth("user", "pass"), ingress=False) + + # verify a the sync succeeded (no errors) + assert status["last_error"] is None + + # The ingress server shouldn't require login, even though its turned on for the extra server + await reader.get("getstatus") + # even a bad user/pass should work + await reader.get("getstatus", auth=BasicAuth("baduser", "badpassword")) + + +@pytest.mark.asyncio +@pytest.mark.timeout(100) +async def test_expose_extra_server_option(reader, ui_server: UiServer, config: Config): + with pytest.raises(aiohttp.client_exceptions.ClientConnectionError): + await reader.getjson("sync", ingress=False) + config.override(Setting.EXPOSE_EXTRA_SERVER, True) + await ui_server.run() + await reader.getjson("sync", ingress=False) + await ui_server.run() + await reader.getjson("sync", ingress=False) + config.override(Setting.EXPOSE_EXTRA_SERVER, False) + await ui_server.run() + with pytest.raises(aiohttp.client_exceptions.ClientConnectionError): + await reader.getjson("sync", ingress=False) + await reader.getjson("sync") + + +@pytest.mark.asyncio +async def test_update_error_reports_true(reader, ui_server, config: Config, supervisor: SimulatedSupervisor): + assert config.get(Setting.SEND_ERROR_REPORTS) is False + assert not config.isExplicit(Setting.SEND_ERROR_REPORTS) + assert await reader.getjson("errorreports?send=true") == {'message': 'Configuration updated'} + assert config.get(Setting.SEND_ERROR_REPORTS) is True + assert config.isExplicit(Setting.SEND_ERROR_REPORTS) + assert supervisor._options["send_error_reports"] is True + + +@pytest.mark.asyncio +async def test_update_error_reports_false(reader, ui_server, config: Config, supervisor: SimulatedSupervisor): + assert config.get(Setting.SEND_ERROR_REPORTS) is False + assert not config.isExplicit(Setting.SEND_ERROR_REPORTS) + assert await reader.getjson("errorreports?send=false") == {'message': 'Configuration updated'} + assert config.get(Setting.SEND_ERROR_REPORTS) is False + assert config.isExplicit(Setting.SEND_ERROR_REPORTS) + assert supervisor._options["send_error_reports"] is False + + +@pytest.mark.asyncio +async def test_drive_cred_generation(reader: ReaderHelper, ui_server: UiServer, backup, config: Config, global_info: GlobalInfo, session: ClientSession, google): + status = await reader.getjson("getstatus") + assert len(status["backups"]) == 1 + assert global_info.credVersion == 0 + # Invalidate the drive creds, sync, then verify we see an error + google.expireCreds() + status = await reader.getjson("sync") + assert status["last_error"]["error_type"] == ERROR_CREDS_EXPIRED + + # simulate the user going through the Drive authentication workflow + auth_url = URL(status['authenticate_url']).with_query({ + "redirectbacktoken": reader.getUrl(True) + "token", + "version": VERSION, + "return": reader.getUrl(True) + }) + async with session.get(auth_url) as resp: + resp.raise_for_status() + html = await resp.text() + page = BeautifulSoup(html, 'html.parser') + area = page.find("textarea") + creds = str(area.getText()).strip() + + cred_url = URL(reader.getUrl(True) + "token").with_query({"creds": creds, "host": reader.getUrl(True)}) + async with session.get(cred_url) as resp: + resp.raise_for_status() + # verify we got redirected to the addon main page. + assert resp.url == URL(reader.getUrl(True)) + await ui_server.sync(None) + assert global_info._last_error is None + assert global_info.credVersion == 1 + + +@pytest.mark.asyncio +async def test_confirm_multiple_deletes(reader, ui_server, server, config: Config, time: FakeTime, ha: HaSource): + # reconfigure to only store 1 backup + config.override(Setting.MAX_BACKUPS_IN_GOOGLE_DRIVE, 1) + config.override(Setting.MAX_BACKUPS_IN_HA, 1) + + # create three backups + await ha.create(CreateOptions(time.now(), "Name1")) + await ha.create(CreateOptions(time.now(), "Name2")) + await ha.create(CreateOptions(time.now(), "Name3")) + + # verify we have 3 backups an the multiple delete error + status = await reader.getjson("sync") + assert len(status['backups']) == 3 + assert status["last_error"]["error_type"] == ERROR_MULTIPLE_DELETES + assert status["last_error"]["data"] == { + SOURCE_GOOGLE_DRIVE: 0, + SOURCE_HA: 2 + } + + # request that multiple deletes be allowed + assert await reader.getjson("confirmdelete?always=false") == { + 'message': 'Backups deleted this one time' + } + assert config.get(Setting.CONFIRM_MULTIPLE_DELETES) + + # backup, verify the deletes go through + status = await reader.getjson("sync") + assert status["last_error"] is None + assert len(status["backups"]) == 1 + + # create another backup, verify we delete the one + await ha.create(CreateOptions(time.now(), "Name1")) + status = await reader.getjson("sync") + assert len(status['backups']) == 1 + assert status["last_error"] is None + + # create two mroe backups, verify we see the error again + await ha.create(CreateOptions(time.now(), "Name1")) + await ha.create(CreateOptions(time.now(), "Name2")) + status = await reader.getjson("sync") + assert len(status['backups']) == 3 + assert status["last_error"]["error_type"] == ERROR_MULTIPLE_DELETES + assert status["last_error"]["data"] == { + SOURCE_GOOGLE_DRIVE: 0, + SOURCE_HA: 2 + } + + +@pytest.mark.asyncio +async def test_update_multiple_deletes_setting(reader, ui_server, server, config: Config, time: FakeTime, ha: HaSource, global_info: GlobalInfo): + assert await reader.getjson("confirmdelete?always=true") == { + 'message': 'Configuration updated, I\'ll never ask again' + } + assert not config.get(Setting.CONFIRM_MULTIPLE_DELETES) + + +@pytest.mark.asyncio +async def test_resolve_folder_reuse(reader, config: Config, backup, time, drive): + # Simulate an existing folder error + old_folder = await drive.getFolderId() + File.delete(config.get(Setting.FOLDER_FILE_PATH)) + time.advance(days=1) + status = await reader.getjson("sync") + assert status["last_error"]["error_type"] == ERROR_EXISTING_FOLDER + + assert (await reader.getjson("resolvefolder?use_existing=true")) == {'message': 'Done'} + status = await reader.getjson("sync") + assert status["last_error"] is None + assert old_folder == await drive.getFolderId() + + +@pytest.mark.asyncio +async def test_resolve_folder_new(reader, config: Config, backup, time, drive): + # Simulate an existing folder error + old_folder = await drive.getFolderId() + File.delete(config.get(Setting.FOLDER_FILE_PATH)) + time.advance(days=1) + status = await reader.getjson("sync") + assert status["last_error"]["error_type"] == ERROR_EXISTING_FOLDER + + assert (await reader.getjson("resolvefolder?use_existing=false")) == {'message': 'Done'} + status = await reader.getjson("sync") + assert status["last_error"] is None + assert old_folder != await drive.getFolderId() + + +@pytest.mark.asyncio +async def test_ssl_server(reader: ReaderHelper, ui_server: UiServer, config, server, cleandir, restarter): + ssl_dir = abspath(join(__file__, "..", "..", "dev", "ssl")) + copyfile(join(ssl_dir, "localhost.crt"), join(cleandir, "localhost.crt")) + copyfile(join(ssl_dir, "localhost.key"), join(cleandir, "localhost.key")) + update = { + "config": { + "use_ssl": True, + "expose_extra_server": True, + "certfile": join(cleandir, "localhost.crt"), + "keyfile": join(cleandir, "localhost.key") + }, + "backup_folder": "unused" + } + assert ui_server._starts == 1 + assert await reader.postjson("saveconfig", json=update) == {'message': 'Settings saved', "reload_page": False} + await restarter.waitForRestart() + assert ui_server._starts == 2 + + +@pytest.mark.asyncio +async def test_bad_ssl_config_missing_files(reader: ReaderHelper, ui_server: UiServer, config, server, cleandir, restarter): + update = { + "config": { + "use_ssl": True, + "expose_extra_server": True, + "certfile": join(cleandir, "localhost.crt"), + "keyfile": join(cleandir, "localhost.key") + }, + "backup_folder": "unused" + } + assert ui_server._starts == 1 + assert await reader.postjson("saveconfig", json=update) == {'message': 'Settings saved', "reload_page": False} + await restarter.waitForRestart() + assert ui_server._starts == 2 + + # Verify the ingress endpoint is still up, but not the SSL one + await reader.getjson("getstatus") + with pytest.raises(aiohttp.client_exceptions.ClientConnectionError): + await reader.getjson("getstatus", ingress=False, ssl=True, sslcontext=False) + + +@pytest.mark.asyncio +async def test_bad_ssl_config_wrong_files(reader: ReaderHelper, ui_server: UiServer, config, server, cleandir, restarter): + ssl_dir = abspath(join(__file__, "..", "..", "dev", "ssl")) + copyfile(join(ssl_dir, "localhost.crt"), join(cleandir, "localhost.crt")) + copyfile(join(ssl_dir, "localhost.key"), join(cleandir, "localhost.key")) + update = { + "config": { + "use_ssl": True, + "expose_extra_server": True, + "certfile": join(cleandir, "localhost.key"), + "keyfile": join(cleandir, "localhost.crt") + }, + "backup_folder": "unused" + } + assert ui_server._starts == 1 + assert await reader.postjson("saveconfig", json=update) == {'message': 'Settings saved', "reload_page": False} + await restarter.waitForRestart() + assert ui_server._starts == 2 + + # Verify the ingress endpoint is still up, but not the SSL one + await reader.getjson("getstatus") + with pytest.raises(aiohttp.client_exceptions.ClientConnectionError): + await reader.getjson("getstatus", ingress=False, ssl=True, sslcontext=False) + + +@pytest.mark.asyncio +async def test_download_drive(reader, ui_server, backup, drive: DriveSource, ha: HaSource, session, time): + await ha.delete(backup) + # download the item from Google Drive + from_drive = await drive.read(backup) + # Download rom the web server + from_server = AsyncHttpGetter( + reader.getUrl() + "download?slug=" + backup.slug(), {}, session, time=time) + await compareStreams(from_drive, from_server) + + +@pytest.mark.asyncio +async def test_download_home_assistant(reader: ReaderHelper, ui_server, backup, drive: DriveSource, ha: HaSource, session, time): + await drive.delete(backup) + # download the item from Google Drive + from_ha = await ha.read(backup) + # Download rom the web server + from_server = AsyncHttpGetter( + reader.getUrl() + "download?slug=" + backup.slug(), {}, session, time=time) + await compareStreams(from_ha, from_server) + + +@pytest.mark.asyncio +async def test_cancel_and_startsync(reader: ReaderHelper, coord: Coordinator): + coord._sync_wait.set() + status = await reader.getjson("startSync") + assert status["syncing"] + cancel = await reader.getjson('cancelSync') + assert not cancel["syncing"] + assert cancel["last_error"]["error_type"] == "cancelled" + + +@pytest.mark.asyncio +async def test_token(reader: ReaderHelper, coord: Coordinator, ha, drive: DriveSource): + creds = { + "client_id": "new_access_token", + "access_token": "new_access_token", + "refresh_token": "new_refresh_token", + "token_expiry": "2022-01-01T00:00:00" + } + serialized = str(base64.b64encode(json.dumps(creds).encode("utf-8")), "utf-8") + await reader.get("token?creds={0}&host={1}".format(quote(serialized), quote(reader.getUrl(True)))) + assert drive.drivebackend.creds.access_token == 'new_access_token' + assert drive.drivebackend.creds.refresh_token == 'new_refresh_token' + assert drive.drivebackend.creds.secret is None + + +@pytest.mark.asyncio +async def test_token_with_secret(reader: ReaderHelper, coord: Coordinator, ha, drive: DriveSource): + creds = { + "client_id": "new_access_token", + "client_secret": "new_client_secret", + "access_token": "new_access_token", + "refresh_token": "new_refresh_token", + "token_expiry": "2022-01-01T00:00:00" + } + serialized = str(base64.b64encode(json.dumps(creds).encode("utf-8")), "utf-8") + await reader.get("token?creds={0}&host={1}".format(quote(serialized), quote(reader.getUrl(True)))) + assert drive.drivebackend.creds.access_token == 'new_access_token' + assert drive.drivebackend.creds.refresh_token == 'new_refresh_token' + assert drive.drivebackend.creds.secret == 'new_client_secret' + + +@pytest.mark.asyncio +async def test_token_extra_server(reader: ReaderHelper, coord: Coordinator, ha, drive: DriveSource, restarter, time): + update = { + "config": { + "expose_extra_server": True + }, + "backup_folder": "unused" + } + assert await reader.postjson("saveconfig", json=update) == {'message': 'Settings saved', "reload_page": False} + await restarter.waitForRestart() + creds = Creds(time, "id", time.now(), "token", "refresh") + serialized = str(base64.b64encode(json.dumps(creds.serialize()).encode("utf-8")), "utf-8") + await reader.get("token?creds={0}&host={1}".format(quote(serialized), quote(reader.getUrl(False))), ingress=False) + assert drive.drivebackend.creds.access_token == 'token' + + +@pytest.mark.asyncio +async def test_changefolder(reader: ReaderHelper, coord: Coordinator, ha, ui_server, folder_finder: FolderFinder): + assert await reader.get("changefolder?id=12345") == '{}' + assert await folder_finder.get() == "12345" + + +@pytest.mark.asyncio +async def test_changefolder_extra_server(reader: ReaderHelper, coord: Coordinator, ha, drive: DriveSource, restarter, ui_server, folder_finder: FolderFinder): + update = { + "config": { + "expose_extra_server": True + }, + "backup_folder": "unused" + } + assert await reader.postjson("saveconfig", json=update) == {'message': 'Settings saved', "reload_page": False} + await restarter.waitForRestart() + + # create a folder + folder_metadata = { + 'name': "Other Folder", + 'mimeType': FOLDER_MIME_TYPE, + 'appProperties': { + "backup_folder": "true", + }, + } + + # create two folders at different times + id = (await drive.drivebackend.createFolder(folder_metadata))['id'] + + await reader.get("changefolder?id=" + str(id), ingress=False) + assert await folder_finder.get() == id + + +@pytest.mark.asyncio +async def test_update_sync_interval(reader, ui_server, config: Config, supervisor: SimulatedSupervisor): + # Make sure the default saves nothing + update = { + "config": { + "max_sync_interval_seconds": '3 hours', + }, + "backup_folder": "unused" + } + assert await reader.postjson("saveconfig", json=update) == {'message': 'Settings saved', "reload_page": False} + assert config.get(Setting.MAX_SYNC_INTERVAL_SECONDS) == 60 * 60 * 3 + assert "max_sync_interval_seconds" not in supervisor._options + + # Update custom + update = { + "config": { + "max_sync_interval_seconds": '2 hours', + }, + "backup_folder": "unused" + } + assert await reader.postjson("saveconfig", json=update) == {'message': 'Settings saved', "reload_page": False} + assert config.get(Setting.MAX_SYNC_INTERVAL_SECONDS) == 60 * 60 * 2 + assert supervisor._options["max_sync_interval_seconds"] == 60 * 60 * 2 + + +@pytest.mark.asyncio +async def test_manual_creds(reader: ReaderHelper, ui_server: UiServer, config: Config, server: SimulationServer, session, drive: DriveSource): + periodic_check = await reader.getjson("checkManualAuth") + assert periodic_check['message'] == "No request for authorization is in progress." + drive.saveCreds(None) + assert not drive.enabled() + + await setup_manual_creds(reader, server, drive, session) + + # Verify creds are saved and drive is enabled + assert drive.enabled() + assert drive.isCustomCreds() + + +@pytest.mark.asyncio +async def test_manual_creds_failure(reader: ReaderHelper, ui_server: UiServer, config: Config, server: SimulationServer, session, drive: DriveSource): + drive.saveCreds(None) + assert not drive.enabled() + + # Try with a bad client_id + req_path = URL("manualauth").with_query({ + "client_id": "wrong_id", + "client_secret": server.google._custom_drive_client_secret}) + data = await reader.getjson(str(req_path), status=500) + assert data["message"] == "Google responded with error status HTTP 401. Please verify your credentials are set up correctly." + + # Try with a bad client_secret + req_path = URL("manualauth").with_query({ + "client_id": server.google._custom_drive_client_id, + "client_secret": "wrong_secret"}) + data = await reader.getjson(str(req_path)) + + await asyncio.sleep(2) + + periodic_check = await reader.getjson("checkManualAuth", status=500) + assert periodic_check['message'] == "Failed unexpectedly while trying to reach Google. See the add-on logs for details." + + # verify creds are saved and drive is enabled + assert not drive.enabled() + + +@pytest.mark.asyncio +async def test_setting_cancels_and_resyncs(reader: ReaderHelper, ui_server: UiServer, config: Config, server, session, drive: DriveSource, coord: Coordinator): + # Create a blocking sync task + coord._sync_wait.set() + sync = asyncio.create_task(coord.sync(), name="Sync from saving settings") + await coord._sync_start.wait() + assert not sync.cancelled() + assert not sync.done() + + # Change some config + update = { + "config": { + "days_between_backups": 20, + "drive_ipv4": "" + }, + "backup_folder": "unused" + } + assert await reader.postjson("saveconfig", json=update) == {'message': 'Settings saved', "reload_page": False} + + # verify the previous sync is done and another one is running + assert sync.done() + assert coord.isSyncing() + + +@pytest.mark.asyncio +async def test_change_specify_folder_setting(reader: ReaderHelper, server, session, coord: Coordinator, folder_finder: FolderFinder): + await coord.sync() + assert folder_finder.getCachedFolder() is not None + + old_folder = folder_finder.getCachedFolder() + # Change some config + update = { + "config": { + "specify_backup_folder": True + }, + "backup_folder": "" + } + assert await reader.postjson("saveconfig", json=update) == {'message': 'Settings saved', "reload_page": False} + + # verify the backup folder was reset, which triggers the error dialog to find a new folder + assert folder_finder.getCachedFolder() == old_folder + + await coord.waitForSyncToFinish() + result = await reader.postjson("getstatus") + assert result["last_error"] is None + + +@pytest.mark.asyncio +async def test_change_specify_folder_setting_with_manual_creds(reader: ReaderHelper, google: SimulatedGoogle, session, coord: Coordinator, folder_finder: FolderFinder, drive: DriveSource, config): + google.resetDriveAuth() + drive.saveCreds(None) + assert not drive.enabled() + + # get the auth url + req_path = URL("manualauth").with_query({ + "client_id": google._custom_drive_client_id, + "client_secret": google._custom_drive_client_secret}) + data = await reader.getjson(str(req_path)) + + # Authorize the device using the url and device code provided + authorize_url = URL(data["auth_url"]).with_query({"code": data['code']}) + async with session.get(str(authorize_url), allow_redirects=False) as resp: + resp.raise_for_status() + + # TODO: wait for creds in a smarter way + await asyncio.sleep(2) + + assert drive.enabled() + assert drive.isCustomCreds() + + await coord.sync() + assert folder_finder.getCachedFolder() is not None + + # Specify the backup folder, which should cache the new one + update = { + "config": { + Setting.SPECIFY_BACKUP_FOLDER.value: True + }, + "backup_folder": "12345" + } + assert await reader.postjson("saveconfig", json=update) == {'message': 'Settings saved', "reload_page": False} + assert folder_finder.getCachedFolder() == "12345" + + # Un change the folder, which should keep the existing folder + update = { + "config": { + Setting.SPECIFY_BACKUP_FOLDER.value: False + }, + "backup_folder": "" + } + assert await reader.postjson("saveconfig", json=update) == {'message': 'Settings saved', "reload_page": False} + assert folder_finder.getCachedFolder() == "12345" + + +@pytest.mark.asyncio +async def test_update_non_ui_setting(reader: ReaderHelper, server, session, coord: Coordinator, folder_finder: FolderFinder, config: Config): + await coord.sync() + # Change some config + update = { + "config": { + Setting.NEW_BACKUP_TIMEOUT_SECONDS.value: 10 + }, + "backup_folder": "" + } + assert await reader.postjson("saveconfig", json=update) == {'message': 'Settings saved', "reload_page": False} + + assert config.get(Setting.NEW_BACKUP_TIMEOUT_SECONDS) == 10 + + update = { + "config": { + Setting.MAX_BACKUPS_IN_HA.value: 1 + }, + "backup_folder": "" + } + assert await reader.postjson("saveconfig", json=update) == {'message': 'Settings saved', "reload_page": False} + assert config.get(Setting.NEW_BACKUP_TIMEOUT_SECONDS) == 10 + + +@pytest.mark.asyncio +async def test_update_disable_drive(reader: ReaderHelper, server, coord: Coordinator, config: Config, drive_requests: DriveRequests): + # Disable drive + drive_requests.creds = None + os.remove(config.get(Setting.CREDENTIALS_FILE_PATH)) + assert not coord.enabled() + await coord.sync() + assert len(coord.backups()) == 0 + + # Disable Drive Upload + update = { + "config": { + Setting.ENABLE_DRIVE_UPLOAD.value: False + }, + "backup_folder": "" + } + assert await reader.postjson("saveconfig", json=update) == {'message': 'Settings saved', "reload_page": True} + assert config.get(Setting.ENABLE_DRIVE_UPLOAD) is False + + # Verify the app is working fine. + assert coord.enabled() + await coord.waitForSyncToFinish() + assert len(coord.backups()) == 1 + + +@pytest.mark.asyncio +async def test_update_ignore(reader: ReaderHelper, time: FakeTime, coord: Coordinator, config: Config, supervisor: SimulatedSupervisor, ha: HaSource, drive: DriveSource): + config.override(Setting.IGNORE_UPGRADE_BACKUPS, True) + config.override(Setting.DAYS_BETWEEN_BACKUPS, 0) + + # make an ignored_backup + slug = await supervisor.createBackup({'name': "Ignore_me", 'folders': ['homeassistant'], 'addons': []}, date=time.now()) + + await coord.sync() + assert len(await drive.get()) == 0 + assert len(await ha.get()) == 1 + assert len(coord.backups()) == 1 + + # Disable Drive Upload + update = { + "ignore": False, + "slug": slug, + } + await reader.postjson("ignore", json=update) + await coord.waitForSyncToFinish() + assert len(coord.backups()) == 1 + assert len(await drive.get()) == 1 + assert len(await ha.get()) == 1 + + +@pytest.mark.asyncio +async def test_check_ignored_backup_notification(reader: ReaderHelper, time: FakeTime, coord: Coordinator, config: Config, supervisor: SimulatedSupervisor, ha: HaSource, drive: DriveSource): + # Create an "ignored" backup after upgrade to the current version. + time.advance(days=1) + await supervisor.createBackup({'name': "test_name"}, date=time.now()) + + # cerate one that isn't ignored. + time.advance(days=1) + await ha.create(CreateOptions(time.now(), name_template=None)) + + update = { + "config": { + Setting.IGNORE_OTHER_BACKUPS.value: True + }, + "backup_folder": "" + } + await reader.postjson("saveconfig", json=update) + await coord.waitForSyncToFinish() + + status = await reader.getjson("getstatus") + assert status["backups"][0]["ignored"] + assert not status["backups"][1]["ignored"] + assert not status["notify_check_ignored"] + + # Create an ignored backup from "before" the addon was upgraded to v0.104.0 + await supervisor.createBackup({'name': "test_name"}, date=time.now() - timedelta(days=10)) + await coord.sync() + + # The UI should nofify about checking ignored backups + status = await reader.getjson("getstatus") + assert status["backups"][0]["ignored"] + assert status["backups"][1]["ignored"] + assert not status["backups"][2]["ignored"] + assert status["notify_check_ignored"] + + # Acknowledge the notification + await reader.postjson("ackignorecheck") == {'message': "Acknowledged."} + status = await reader.getjson("getstatus") + assert not status["notify_check_ignored"] + + +@pytest.mark.asyncio +async def test_snapshot_to_backup_upgrade_use_new_values(reader: ReaderHelper, time: FakeTime, coord: Coordinator, config: Config, supervisor: SimulatedSupervisor, ha: HaSource, drive: DriveSource, data_cache: DataCache, updater: HaUpdater): + """ Test the path where a user upgrades from the addon before the backup rename and then chooses to use the new names""" + status = await reader.getjson("getstatus") + assert not status["warn_backup_upgrade"] + + # simulate upgrading config + supervisor._options = { + Setting.DEPRECTAED_MAX_BACKUPS_IN_HA.value: 7 + } + await coord.sync() + assert Setting.CALL_BACKUP_SNAPSHOT.value in supervisor._options + assert Setting.DEPRECTAED_MAX_BACKUPS_IN_HA.value not in supervisor._options + assert config.get(Setting.CALL_BACKUP_SNAPSHOT) + + status = await reader.getjson("getstatus") + assert status["warn_backup_upgrade"] + assert not data_cache.checkFlag(UpgradeFlags.NOTIFIED_ABOUT_BACKUP_RENAME) + assert not updater._trigger_once + + # simulate user clicking the button to use new names + assert await reader.getjson("callbackupsnapshot?switch=true") == {'message': 'Configuration updated'} + assert data_cache.checkFlag(UpgradeFlags.NOTIFIED_ABOUT_BACKUP_RENAME) + assert not config.get(Setting.CALL_BACKUP_SNAPSHOT) + status = await reader.getjson("getstatus") + assert not status["warn_backup_upgrade"] + assert updater._trigger_once + + +@pytest.mark.asyncio +async def test_snapshot_to_backup_upgrade_use_old_values(reader: ReaderHelper, time: FakeTime, coord: Coordinator, config: Config, supervisor: SimulatedSupervisor, ha: HaSource, drive: DriveSource, data_cache: DataCache, updater: HaUpdater): + """ Test the path where a user upgrades from the addon before the backup rename and then chooses to use the old names""" + status = await reader.getjson("getstatus") + assert not status["warn_backup_upgrade"] + + # simulate upgrading config + supervisor._options = { + Setting.DEPRECTAED_MAX_BACKUPS_IN_HA.value: 7 + } + await coord.sync() + assert Setting.CALL_BACKUP_SNAPSHOT.value in supervisor._options + assert config.get(Setting.CALL_BACKUP_SNAPSHOT) + + status = await reader.getjson("getstatus") + assert status["warn_backup_upgrade"] + assert not data_cache.checkFlag(UpgradeFlags.NOTIFIED_ABOUT_BACKUP_RENAME) + assert not updater._trigger_once + + # simulate user clicking the button to use new names + assert await reader.getjson("callbackupsnapshot?switch=false") == {'message': 'Configuration updated'} + assert data_cache.checkFlag(UpgradeFlags.NOTIFIED_ABOUT_BACKUP_RENAME) + status = await reader.getjson("getstatus") + assert not status["warn_backup_upgrade"] + assert config.get(Setting.CALL_BACKUP_SNAPSHOT) + + +@pytest.mark.asyncio +async def test_snapshot_to_backup_upgrade_avoid_default_overwrite(reader: ReaderHelper, time: FakeTime, coord: Coordinator, config: Config, supervisor: SimulatedSupervisor, ha: HaSource, drive: DriveSource, data_cache: DataCache, updater: HaUpdater): + """ Test the path where a user upgrades from the addon but a new value with a default value gets overwritten""" + status = await reader.getjson("getstatus") + assert not status["warn_backup_upgrade"] + + # simulate upgrading config + supervisor._options = { + Setting.DEPRECTAED_MAX_BACKUPS_IN_HA.value: 7, + Setting.MAX_BACKUPS_IN_HA.value: 4 # defuault, should get overridden + } + await coord.sync() + assert Setting.CALL_BACKUP_SNAPSHOT.value in supervisor._options + assert config.get(Setting.CALL_BACKUP_SNAPSHOT) + assert config.get(Setting.MAX_BACKUPS_IN_HA) == 7 + + +@pytest.mark.asyncio +async def test_ha_upload(reader: ReaderHelper, backup_helper, ui_server: UiServer, drive: DriveSource, ha: HaSource, config: Config, model, time): + from_backup, data = await backup_helper.createFile() + backup = await drive.save(from_backup, data) + + config.override(Setting.DAYS_BETWEEN_BACKUPS, 0) + await model.sync(time.now()) + assert len(await ha.get()) == 0 + assert len(await drive.get()) == 1 + + reply = await reader.getjson(str(URL("upload").with_query({"slug": backup.slug()}))) + assert reply['message'] == "Uploading backup in the background" + await ui_server.waitForUpload() + assert len(await ha.get()) == 1 + + +async def setup_manual_creds(reader: ReaderHelper, server: SimulationServer, drive: DriveSource, session: ClientSession): + # get the auth url + req_path = URL("manualauth").with_query({ + "client_id": server.google._custom_drive_client_id, + "client_secret": server.google._custom_drive_client_secret}) + data = await reader.getjson(str(req_path)) + assert "auth_url" in data + assert "code" in data + assert "expires" in data + + periodic_check = await reader.getjson("checkManualAuth") + assert periodic_check['message'] == "Waiting for you to authorize the add-on." + + # Authorize the device using the url and device code provided + drive._cred_trigger.clear() + authorize_url = URL(data["auth_url"]).with_query({"code": data['code']}) + async with session.get(str(authorize_url), allow_redirects=False) as resp: + resp.raise_for_status() + + await drive.debug_wait_for_credentials() + + # verify creds are saved and drive is enabled + assert drive.enabled() + assert drive.isCustomCreds() + + +@pytest.mark.asyncio +async def test_oob_warning(reader: ReaderHelper, ui_server: UiServer, config: Config, server: SimulationServer, session, drive: DriveSource, data_cache: DataCache): + server.google._custom_drive_client_expiration = OOB_CRED_CUTOFF + timedelta(seconds=1) + assert not data_cache.checkFlag(UpgradeFlags.NOTIFIED_ABOUT_OOB_FLOW) + await setup_manual_creds(reader, server, drive, session) + assert data_cache.checkFlag(UpgradeFlags.NOTIFIED_ABOUT_OOB_FLOW) + data_cache.TESTS_ONLY_clearFlags() + status = await reader.getjson("getstatus") + assert status['warn_oob_oauth'] is False + + server.google._custom_drive_client_expiration = OOB_CRED_CUTOFF - timedelta(seconds=1) + await setup_manual_creds(reader, server, drive, session) + data_cache.TESTS_ONLY_clearFlags() + status = await reader.getjson("getstatus") + assert status['warn_oob_oauth'] is True + + data_cache.addFlag(UpgradeFlags.NOTIFIED_ABOUT_OOB_FLOW) + status = await reader.getjson("getstatus") + assert status['warn_oob_oauth'] is False diff --git a/hassio-google-drive-backup/tests/test_version.py b/hassio-google-drive-backup/tests/test_version.py new file mode 100644 index 0000000..5303dc8 --- /dev/null +++ b/hassio-google-drive-backup/tests/test_version.py @@ -0,0 +1,47 @@ +from backup.config import Version + + +def test_default(): + assert Version.default() == Version.default() + assert not Version.default() > Version.default() + assert not Version.default() < Version.default() + assert not Version.default() != Version.default() + assert Version.default() >= Version.default() + assert Version.default() <= Version.default() + + +def test_version(): + assert Version(1, 2, 3) == Version(1, 2, 3) + assert Version(1, 2, 3) >= Version(1, 2, 3) + assert Version(1, 2, 3) <= Version(1, 2, 3) + assert Version(1, 2, 3) > Version(1, 2) + assert Version(1) < Version(2) + assert Version(2) > Version(1) + assert Version(1) != Version(2) + assert Version(1, 2) > Version(1) + assert Version(1) < Version(1, 2) + + +def test_parse(): + assert Version.parse("1.0") == Version(1, 0) + assert Version.parse("1.2.3") == Version(1, 2, 3) + + +def test_parse_staging(): + assert Version.parse("1.0.staging.1") == Version(1, 0, 1) + assert Version.parse("1.0.staging.1").staging + assert Version.parse("1.0.staging.1") > Version(1.0) + assert Version.parse("1.2.3") == Version(1, 2, 3) + + +def test_junk_strings(): + assert Version.parse("1-.2.3.1") == Version(1, 2, 3, 1) + assert Version.parse("ignore-1.2.3.1") == Version(1, 2, 3, 1) + assert Version.parse("1.2.ignore.this.text.3.and...andhere.too.1") == Version(1, 2, 3, 1) + + +def test_broken_versions(): + assert Version.parse("") == Version.default() + assert Version.parse(".") == Version.default() + assert Version.parse("empty") == Version.default() + assert Version.parse("no.version.here") == Version.default() diff --git a/hassio-google-drive-backup/tests/test_watcher.py b/hassio-google-drive-backup/tests/test_watcher.py new file mode 100644 index 0000000..798b422 --- /dev/null +++ b/hassio-google-drive-backup/tests/test_watcher.py @@ -0,0 +1,119 @@ +from backup.watcher import Watcher +from backup.config import Config, Setting, CreateOptions +from backup.ha import HaSource +from os.path import join +from .faketime import FakeTime +from asyncio import sleep +import pytest +import os + +TEST_FILE_NAME = "test.tar" + + +@pytest.mark.asyncio +async def test_watcher_trigger_on_backup(server, watcher: Watcher, config: Config, time: FakeTime, ha: HaSource): + await watcher.start() + assert not await watcher.check() + watcher.noticed_change_signal.clear() + await simulateBackup(config, TEST_FILE_NAME, ha, time) + await watcher.noticed_change_signal.wait() + time.advance(minutes=11) + assert await watcher.check() + + +@pytest.mark.asyncio +async def test_disable_watching(server, watcher: Watcher, config: Config, time: FakeTime, ha: HaSource): + config.override(Setting.WATCH_BACKUP_DIRECTORY, False) + await watcher.start() + assert not await watcher.check() + await simulateBackup(config, TEST_FILE_NAME, ha, time) + await sleep(1) + time.advance(minutes=11) + assert not await watcher.check() + + +@pytest.mark.asyncio +async def test_watcher_doesnt_trigger_on_no_backup(server, watcher: Watcher, config: Config, time: FakeTime, ha: HaSource): + await watcher.start() + assert not await watcher.check() + file = join(config.get(Setting.BACKUP_DIRECTORY_PATH), TEST_FILE_NAME) + watcher.noticed_change_signal.clear() + with open(file, "w"): + pass + await watcher.noticed_change_signal.wait() + time.advance(minutes=11) + assert not await watcher.check() + + +@pytest.mark.asyncio +async def test_watcher_below_wait_threshold(server, watcher: Watcher, config: Config, time: FakeTime, ha: HaSource): + await watcher.start() + assert not await watcher.check() + for x in range(10): + watcher.noticed_change_signal.clear() + await simulateBackup(config, f"{TEST_FILE_NAME}.{x}", ha, time) + await watcher.noticed_change_signal.wait() + time.advance(seconds=9) + assert not await watcher.check() + time.advance(minutes=11) + assert await watcher.check() + + +@pytest.mark.asyncio +async def test_watcher_triggers_for_deletes(server, watcher: Watcher, config: Config, time: FakeTime, ha: HaSource): + await simulateBackup(config, TEST_FILE_NAME, ha, time) + + await watcher.start() + assert not await watcher.check() + watcher.noticed_change_signal.clear() + os.remove(join(config.get(Setting.BACKUP_DIRECTORY_PATH), TEST_FILE_NAME)) + await watcher.noticed_change_signal.wait() + + time.advance(seconds=30) + assert await watcher.check() + + +@pytest.mark.asyncio +async def test_moves_out_trigger(server, watcher: Watcher, config: Config, time: FakeTime, ha: HaSource): + await simulateBackup(config, TEST_FILE_NAME, ha, time) + await watcher.start() + watcher.noticed_change_signal.clear() + os.mkdir(join(config.get(Setting.BACKUP_DIRECTORY_PATH), "subdir")) + os.rename(join(config.get(Setting.BACKUP_DIRECTORY_PATH), TEST_FILE_NAME), join(config.get(Setting.BACKUP_DIRECTORY_PATH), "subdir", TEST_FILE_NAME)) + await watcher.noticed_change_signal.wait() + time.advance(minutes=11) + assert await watcher.check() + +# Check if move ins are really necessary +# @pytest.mark.asyncio +# async def test_moves_in_trigger(server, watcher: Watcher, config: Config, time: FakeTime, ha: HaSource): +# os.mkdir(join(config.get(Setting.BACKUP_DIRECTORY_PATH), "subdir")) +# await simulateBackup(config, "subdir/" + TEST_FILE_NAME, ha, time) +# await watcher.start() +# watcher.noticed_change_signal.clear() +# os.rename(join(config.get(Setting.BACKUP_DIRECTORY_PATH), "subdir", TEST_FILE_NAME), join(config.get(Setting.BACKUP_DIRECTORY_PATH), TEST_FILE_NAME)) +# await watcher.noticed_change_signal.wait() +# time.advance(minutes=11) +# assert await watcher.check() + + +@pytest.mark.asyncio +async def test_subdirs_dont_trigger(server, watcher: Watcher, config: Config, time: FakeTime, ha: HaSource): + await simulateBackup(config, TEST_FILE_NAME, ha, time) + await watcher.start() + watcher.noticed_change_signal.clear() + os.mkdir(join(config.get(Setting.BACKUP_DIRECTORY_PATH), "subdir")) + with open(join(config.get(Setting.BACKUP_DIRECTORY_PATH), "subdir", "ignored.txt"), "w"): + pass + assert not await watcher.check() + time.advance(minutes=11) + assert not await watcher.check() + + +async def simulateBackup(config, file_name, ha, time): + file = join(config.get(Setting.BACKUP_DIRECTORY_PATH), file_name) + with open(file, "w"): + pass + await ha.create(CreateOptions(time.now(), file_name)) + +# Verify that subdirectories get ignored diff --git a/hassio-google-drive-backup/tests/test_worker.py b/hassio-google-drive-backup/tests/test_worker.py new file mode 100644 index 0000000..98bfd1e --- /dev/null +++ b/hassio-google-drive-backup/tests/test_worker.py @@ -0,0 +1,46 @@ +import asyncio + +import pytest + +from backup.worker import StopWorkException, Worker +from .faketime import FakeTime + + +@pytest.mark.asyncio +async def test_worker(time: FakeTime): + data = {'count': 0} + + async def work(): + if data['count'] >= 5: + raise StopWorkException() + data['count'] += 1 + + worker = Worker("test", work, time, 1) + task = await worker.start() + await asyncio.wait([task]) + assert not worker.isRunning() + assert data['count'] == 5 + assert time.sleeps == [1, 1, 1, 1, 1] + # assert worker._task.name == "test" + assert worker.getLastError() is None + + +@pytest.mark.asyncio +async def test_worker_error(time: FakeTime): + data = {'count': 0} + + async def work(): + if data['count'] >= 5: + raise StopWorkException() + data['count'] += 1 + raise OSError() + + worker = Worker("test", work, time, 1) + task = await worker.start() + await asyncio.wait([task]) + assert not worker.isRunning() + assert data['count'] == 5 + assert time.sleeps == [1, 1, 1, 1, 1] + # assert worker.getName() == "test" + assert worker.getLastError() is not None + assert type(worker.getLastError()) is OSError