├── .gitattributes
├── tests
├── data
│ ├── jms.pub
│ ├── versions.gz
│ ├── pyu-wi-1.1.tar.gz
│ ├── pyu-win-0.0.2.xz
│ ├── pyu-win-1.tar.gz
│ ├── Acme-mac-4.1.tar.gz
│ ├── with spaces-nix-0.0.1b1.zip
│ ├── with spaces-win-0.0.1a2.zip
│ ├── keypack.pyu
│ ├── update_repo_extract
│ │ ├── .pyupdater
│ │ │ └── config.pyu
│ │ ├── client_config.py
│ │ ├── app_extract_02.py
│ │ ├── app_extract_01.py
│ │ ├── app_extract_onedir.py
│ │ ├── build_onedir_extract.py
│ │ └── build_onefile_extract.py
│ ├── update_repo_restart
│ │ ├── .pyupdater
│ │ │ └── config.pyu
│ │ ├── client_config.py
│ │ ├── app_restart_02.py
│ │ ├── app_restart_01.py
│ │ ├── app_restart_onedir.py
│ │ ├── build_onefile_restart.py
│ │ └── build_onedir_restart.py
│ ├── version.json
│ └── dont delete pyu test.txt
├── README.md
├── requirements.txt
├── test_keys.py
├── tconfig.py
├── test_version_file.py
├── test_builder.py
├── test_config.py
├── conftest.py
├── test_uploader.py
├── test_patcher.py
├── test_downloader.py
├── test_utils.py
├── test_package_handler.py
└── test_cli.py
├── pyupdater
├── hooks
│ ├── hook-nacl.py
│ ├── __init__.py
│ └── hook-dsdev_utils.py
├── utils
│ ├── encoding.py
│ ├── pyinstaller_compat.py
│ ├── storage.py
│ ├── exceptions.py
│ └── config.py
├── __main__.py
├── settings.py
├── __init__.py
├── cli
│ ├── __init__.py
│ └── helpers.py
└── core
│ ├── __init__.py
│ ├── key_handler
│ ├── keys.py
│ └── __init__.py
│ ├── package_handler
│ └── patch.py
│ └── uploader.py
├── MANIFEST.in
├── .github
├── FUNDING.yml
└── workflows
│ └── main.yaml
├── dev
├── requirements.txt
├── namespace-test.py
├── move.py
└── clean.py
├── CONTRIBUTING.md
├── docs
├── demo-wxpython.md
├── downloads.md
├── contributing.md
├── contributors.md
├── danger-zone.md
├── usage-cli-advanced.md
├── oss.md
├── migrate-from-esky.md
├── license.md
├── installation.md
├── data-folder-layout.md
├── upgrading.md
├── index.md
├── usage-cli-asset.md
├── create-upload-plugin.md
├── usage-client-asset.md
├── usage-client.md
├── usage-client-advanced.md
├── usage-cli.md
└── api.md
├── requirements.txt
├── setup.cfg
├── README.md
├── .coveragerc
├── Makefile
├── tox.ini
├── .gitignore
├── Makefile.bat
├── mkdocs.yml
└── setup.py
/.gitattributes:
--------------------------------------------------------------------------------
1 | pyupdater/_version.py export-subst
2 |
--------------------------------------------------------------------------------
/tests/data/jms.pub:
--------------------------------------------------------------------------------
1 | MEviIkxfr74822UoF97+RJ9GdS9eVtkKe4PKH2J3khA
--------------------------------------------------------------------------------
/pyupdater/hooks/hook-nacl.py:
--------------------------------------------------------------------------------
1 | hiddenimports = [u"_cffi_backend"]
2 |
--------------------------------------------------------------------------------
/tests/data/versions.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Digital-Sapphire/PyUpdater/HEAD/tests/data/versions.gz
--------------------------------------------------------------------------------
/tests/data/pyu-wi-1.1.tar.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Digital-Sapphire/PyUpdater/HEAD/tests/data/pyu-wi-1.1.tar.gz
--------------------------------------------------------------------------------
/tests/data/pyu-win-0.0.2.xz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Digital-Sapphire/PyUpdater/HEAD/tests/data/pyu-win-0.0.2.xz
--------------------------------------------------------------------------------
/tests/data/pyu-win-1.tar.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Digital-Sapphire/PyUpdater/HEAD/tests/data/pyu-win-1.tar.gz
--------------------------------------------------------------------------------
/tests/data/Acme-mac-4.1.tar.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Digital-Sapphire/PyUpdater/HEAD/tests/data/Acme-mac-4.1.tar.gz
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include requirements.txt
2 | include versioneer.py
3 | include docs/license.md
4 | include pyupdater/_version.py
5 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | patreon: JMSwag
4 | custom: https://cash.app/$jmswag
5 |
--------------------------------------------------------------------------------
/dev/requirements.txt:
--------------------------------------------------------------------------------
1 | memory_profiler
2 | mkdocs
3 | mkdocs-material
4 | PyUpdater-s3-Plugin
5 | tox
6 | versioneer==0.19
7 | vulture
8 |
--------------------------------------------------------------------------------
/tests/data/with spaces-nix-0.0.1b1.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Digital-Sapphire/PyUpdater/HEAD/tests/data/with spaces-nix-0.0.1b1.zip
--------------------------------------------------------------------------------
/tests/data/with spaces-win-0.0.1a2.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Digital-Sapphire/PyUpdater/HEAD/tests/data/with spaces-win-0.0.1a2.zip
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | To get started, sign the Contributor License Agreement.
2 |
--------------------------------------------------------------------------------
/docs/demo-wxpython.md:
--------------------------------------------------------------------------------
1 | # wxPython Demo
2 |
3 | Check out the [wxPython Demo](http://pyupdater-wx-demo.readthedocs.io) to see how easy it is to integrate PyUpdater with wxPython.
4 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | appdirs>=1.4.3
2 | bsdiff4
3 | certifi>=2019.3.9
4 | dsdev-utils>=1.0.4
5 | pyinstaller>=3.0
6 | pynacl>=1.4.0
7 | stevedore>=1.30.1, <4.0
8 | urllib3>=1.24.1, <2.0
9 |
--------------------------------------------------------------------------------
/tests/README.md:
--------------------------------------------------------------------------------
1 | ##Things to know about tests
2 |
3 | ####pytest-datadir:
4 | Used to easily access files in the data folder. [pytest-datadir](https://github.com/gabrielcnr/pytest-datadir)
--------------------------------------------------------------------------------
/docs/downloads.md:
--------------------------------------------------------------------------------
1 | # Downloads
2 |
3 | #### [Zip](https://github.com/Digital-Sapphire/PyUpdater/zipball/master)
4 |
5 | #### [Tarball](https://github.com/Digital-Sapphire/PyUpdater/tarball/master)
6 |
--------------------------------------------------------------------------------
/docs/contributing.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | - Stick to PEP8
4 | - Ensure to rebase from master
5 | - Squash commits into 1 commit
6 | - Check open issues for places to help
7 | - PR's are always welcome
8 |
--------------------------------------------------------------------------------
/tests/requirements.txt:
--------------------------------------------------------------------------------
1 | codecov>=1.4.0
2 | coverage==4.5.4
3 | filelock
4 | pytest>=4.4.0
5 | pytest-datadir>=1.3.0, <2.0
6 | pytest-cov
7 | pytest-ordering
8 | pytest-xdist[psutil]
9 | pyupdater-scp-plugin
10 | pyupdater-s3-plugin
11 | tox-pyenv
12 | wheel
13 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [metadata]
2 | description-file = README.md
3 |
4 | [bdist_wheel]
5 | universal=1
6 |
7 | [versioneer]
8 | VCS = git
9 | style = pep440
10 | versionfile_source=pyupdater/_version.py
11 | versionfile_build=pyupdater/_version.py
12 | tag_prefix=
13 | parentdir_prefix=
14 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # This is the end.
2 |
3 | I haven't made any desktop apps in a while. Mostly making PWA's. I also no longer use Python for my backends, in favor of Golang.
4 |
5 | # PyUpdater
6 | #### An auto-update framework for pyinstaller that enables simple, secure & efficient shipment of app updates.
7 |
--------------------------------------------------------------------------------
/docs/contributors.md:
--------------------------------------------------------------------------------
1 | # Contributors
2 |
3 | [@cbenhagen](https://github.com/cbenhagen)
4 |
5 | [@dlernstrom](https://github.com/dlernstrom)
6 |
7 | [@LucaBernstein](https://github.com/LucaBernstein)
8 |
9 | [@jameshilliard](https://github.com/jameshilliard)
10 |
11 | [@mayli](https://github.com/mayli)
12 |
13 | [@moscoquera](https://github.com/moscoquera)
14 |
--------------------------------------------------------------------------------
/tests/data/keypack.pyu:
--------------------------------------------------------------------------------
1 | {
2 | "client": {
3 | "offline_public": "a/Hr20EtHRSg13qWOFJGnDPFOH9KkyBLKWWhITlpThg"
4 | },
5 | "repo": {
6 | "app_private": "p433qLjrmZpH6CnFztBQh+NuJoqpzxemS36U7LKx86s"
7 | },
8 | "upload": {
9 | "app_public": "tPOGc6w2m+PG46mAdO7cgKHvy1G8I6ffELIwNchpI70",
10 | "signature": "a/nMf5UUNk6UeNU/vlwFsBVnfeAWhwoohu0mAyc8xMb4RUdmI1oh+sfyHbHA126qUUfR00U609XtisDe1cb2CQ"
11 | }
12 | }
--------------------------------------------------------------------------------
/.coveragerc:
--------------------------------------------------------------------------------
1 | [run]
2 | source = pyupdater
3 |
4 | [report]
5 | omit =
6 | pyupdater/_version.py
7 |
8 | exclude_lines =
9 | # Have to re-enable the standard pragma
10 | pragma: no cover
11 |
12 | # Don't complain if tests don't hit defensive assertion code:
13 | raise AssertionError
14 | raise NotImplementedError
15 |
16 | # Don't complain if non-runnable code isn't run:
17 | if 0:
18 | if __name__ == .__main__.:
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | clean:
2 | python dev/clean.py
3 |
4 | deps:
5 | pip install -r requirements.txt --upgrade
6 |
7 | deps-dev:
8 | pip install -r dev/requirements.txt --upgrade
9 |
10 | api-md:
11 | python dev/api_docs.py
12 |
13 | docs-deploy:
14 | mkdocs build --clean
15 | python dev/move.py
16 |
17 | register:
18 | python setup.py register -r pypi
19 |
20 | register-test:
21 | python setup.py register -r pypitest
22 |
23 | test: clean
24 | tox
25 |
--------------------------------------------------------------------------------
/docs/danger-zone.md:
--------------------------------------------------------------------------------
1 | # Danger Zone
2 |
3 |
4 | ### Spec File
5 |
6 | Do not change the name attribute in the spec file
7 |
8 |
9 | ### Version Numbers
10 |
11 | Version numbers should be in the format specified below. Supported release versions are alpha and beta. For shorthand you can use a and b respectively.
12 |
13 | 1.0
14 |
15 | 1.1alpha
16 |
17 | 1.1-alpha2
18 |
19 | 1.1-alpha-3
20 |
21 | 1.1.1
22 |
23 | 1.0.2beta
24 |
25 | 1.0.2-beta2
26 |
27 | 1.0.2-beta-3
28 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | envlist=py{36,37,38,39}
3 |
4 | [testenv]
5 | commands = pytest
6 | recreate = True
7 | setenv = TOX_ENV_NAME={envname}
8 | sitepackages = True
9 | usedevelop = True
10 |
11 | deps =
12 | -r tests/requirements.txt
13 | -r requirements.txt
14 | --upgrade
15 |
16 | [pytest]
17 | addopts =
18 | -n auto
19 | -vv
20 | --durations=20
21 | --cov
22 | --cov-config .coveragerc
23 | --cov-report xml
24 | --cov pyupdater
25 | --cov-append
26 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.egg-info
2 |
3 | *.log
4 |
5 | *.pkg
6 |
7 | *.pyc
8 |
9 | *.pyz
10 |
11 | *.toc
12 |
13 | .coverage
14 |
15 | .coverage.*
16 |
17 | .idea
18 |
19 | MANIFEST
20 |
21 | .DS_Store
22 |
23 | htmlcov
24 |
25 | site
26 |
27 | .coverage.gw*
28 |
29 | .tox
30 |
31 | .cache/v/cache/lastfailed
32 |
33 | tests/dev
34 |
35 | unused.txt
36 |
37 | keypack.pyu
38 |
39 | private
40 |
41 | test.py
42 |
43 | .python-version
44 |
45 | coverage.xml
46 |
47 | .pytest_cache
48 |
49 | Acme*
50 |
--------------------------------------------------------------------------------
/docs/usage-cli-advanced.md:
--------------------------------------------------------------------------------
1 | # Usage | CLI | Advanced
2 | PyUpdater supports 3 release channels. Release channels are specified when providing a version number to the --app-version flag. Patches are created for each channel. Examples below.
3 |
4 | ### Example - Setting channels
5 | Stable:
6 | ```
7 | $ pyupdater build --app-version=3.30.1
8 | $ pyupdater build --app-version=2.1
9 | $ pyupdater build --app-version=1.0
10 | ```
11 |
12 | Beta:
13 | ```
14 | $ pyupdater build --app-version=1.0.1b
15 | $ pyupdater build --app-version=3.1b2
16 | $ pyupdater build --app-version=11.3.1beta2
17 | ```
18 |
19 | Alpha:
20 | ```
21 | $ pyupdater build --app-version=1.0.1a
22 | $ pyupdater build --app-version=5.0alpha
23 | $ pyupdater build --app-version=1.1.1alpha1
24 | ```
--------------------------------------------------------------------------------
/Makefile.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 | CLS
3 | ECHO 1. Clean
4 | ECHO 2. Install Dev Dependencies
5 | ECHO 3. Run Tests
6 | ECHO 4. Generate docs
7 | ECHO.
8 |
9 | CHOICE /C 1234 /M "Enter your choice:"
10 |
11 | :: Note - list ERRORLEVELS in decreasing order
12 | IF ERRORLEVEL 4 GOTO Docs
13 | IF ERRORLEVEL 3 GOTO TEST
14 | IF ERRORLEVEL 2 GOTO Install
15 | IF ERRORLEVEL 1 GOTO Clean
16 |
17 | :Clean
18 | ECHO cleaning temp items
19 | python dev\clean.py
20 | GOTO End
21 |
22 | :Install
23 | ECHO Installing development dependencies
24 | pip install -r requirements --upgrade
25 | pip install -r dev\requirements.txt --upgrade
26 | GOTO End
27 |
28 | :Docs
29 | ECHO Creating Docs
30 | mkdocs build --clean
31 | GOTO End
32 |
33 | :Test
34 | ECHO Starting Test
35 | python dev\clean.py
36 | tox
37 | GOTO End
38 |
39 | :End
--------------------------------------------------------------------------------
/.github/workflows/main.yaml:
--------------------------------------------------------------------------------
1 | name: PyUpdater Tests
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - main
10 |
11 | jobs:
12 | build:
13 | runs-on: ${{ matrix.os }}
14 | strategy:
15 | fail-fast: false
16 | matrix:
17 | python: [3.6, 3.7, 3.8, 3.9]
18 | os: [windows-2016, ubuntu-16.04, macos-10.15]
19 |
20 | steps:
21 | - uses: actions/checkout@v2
22 | - name: Setup Python
23 | uses: actions/setup-python@v2
24 | with:
25 | python-version: ${{ matrix.python }}
26 | - name: Install Tox and any other packages
27 | run: pip install tox
28 | - name: Install self
29 | run: pip install -e .
30 | - name: Run Tox
31 | # Run tox using the version of Python in `PATH`
32 | run: tox -e py
33 | - uses: codecov/codecov-action@v1
34 |
--------------------------------------------------------------------------------
/docs/oss.md:
--------------------------------------------------------------------------------
1 | # O.S.S.
2 |
3 | PyUpdater uses the libraries listed below.
4 |
5 | ### appdirs
6 | https://github.com/ActiveState/appdirs/blob/master/LICENSE.txt
7 |
8 |
9 | ### bsdiff4
10 | https://github.com/ilanschnell/bsdiff4/blob/master/README.rst
11 |
12 |
13 | ### certifi
14 | https://github.com/certifi/python-certifi/blob/master/LICENSE
15 |
16 |
17 | ### dsdev-utils
18 | https://github.com/Digital-Sapphire/dsdev-utils/blob/master/LICENSE
19 |
20 |
21 | ### PyInstaller
22 | https://github.com/pyinstaller/pyinstaller/blob/develop/COPYING.txt
23 |
24 |
25 | ### PyNacl
26 | https://github.com/pyca/pynacl/blob/master/LICENSE
27 |
28 |
29 | ### six
30 | https://bitbucket.org/gutworth/six/src/986089f3d04e468458cc9f5b9b3be26fb3d75ade/LICENSE?at=default
31 |
32 |
33 | ### stevedore
34 | https://github.com/dreamhost/stevedore/blob/master/LICENSE
35 |
36 |
37 | ### urllib3
38 | https://github.com/shazow/urllib3/blob/master/LICENSE.txt
39 |
--------------------------------------------------------------------------------
/pyupdater/utils/encoding.py:
--------------------------------------------------------------------------------
1 | import base64
2 |
3 |
4 | class UnpaddedBase64Encoder(object):
5 | """
6 | A simple encoder class to encode/decode to base64 with the 'padding' (trailing equal characters) removed.
7 |
8 | This is needed for PyNaCL to be compatible with keys generated by the old ed25519 library.
9 | """
10 |
11 | @staticmethod
12 | def encode(data):
13 | return base64.b64encode(data).rstrip(b"=")
14 |
15 | @staticmethod
16 | def decode(data):
17 | # We need to add the padding back if it's been removed.
18 | # The correct type of the padding depends on what the input is,
19 | # as base64 (and the decoder in general) is happy to take in both bytes and unicode.
20 |
21 | if isinstance(data, bytes):
22 | padding = b"="
23 | else:
24 | padding = u"="
25 |
26 | data += padding * ((4 - len(data) % 4) % 4)
27 |
28 | return base64.b64decode(data)
29 |
--------------------------------------------------------------------------------
/docs/migrate-from-esky.md:
--------------------------------------------------------------------------------
1 | # Migrate from Esky
2 |
3 | Currently the best way to migrate from Esky is follow the [Getting Started](usage-cli.md) guide.
4 | End Users will have to download this, updated version of your app, directly from a download link.
5 |
6 | ### Migration Help
7 |
8 | ##### Configuration
9 |
10 | Esky:
11 |
12 | - Uses setup.py
13 |
14 | PyUpdater:
15 |
16 | - Set during repository initialization.
17 |
18 |
19 | ##### Parsing App Current Versions
20 |
21 | Esky:
22 |
23 | - Parses local repository.
24 |
25 | PyUpdater:
26 |
27 | - Version is set within application script.
28 |
29 |
30 | ##### Parsing App Update Versions
31 |
32 | Esky:
33 |
34 | - Parses update repository.
35 |
36 | PyUpdater:
37 |
38 | - Uses a dedicated version file which includes hashes for security and integrity.
39 |
40 |
41 | ##### App Update Process
42 |
43 | Esky:
44 |
45 | - Initialize Esky update client, then use update method.
46 |
47 | PyUpdater:
48 |
49 | - Initialize PyUpdater client, then use update method.
--------------------------------------------------------------------------------
/docs/license.md:
--------------------------------------------------------------------------------
1 | # License
2 |
3 | Copyright (c) 2015-2019 Digital Sapphire
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/tests/data/update_repo_extract/.pyupdater/config.pyu:
--------------------------------------------------------------------------------
1 | {
2 | "version_meta": {
3 | "signature": "SFZSIMCiVl1AdjRvIAq5Qyxtk3Yk83SX8AgSFASsJT0RQ+BsNUFuRZ0OVXbnBXoNHuAfhrb7bzZhMeaxQCVSDg",
4 | "updates": {
5 | },
6 | "latest": {
7 | }
8 | },
9 | "count": 0,
10 | "sync_threshold": 3,
11 | "py_repo_config": {
12 | "patches": {
13 | "Acme": 4
14 | },
15 | "package": {
16 | }
17 | },
18 | "app_config": {
19 | "APP_NAME": "Acme",
20 | "UPDATE_URLS": [
21 | "http://localhost:8000/"
22 | ],
23 | "PLUGIN_CONFIGS": {},
24 | "UPDATE_PATCHES": true,
25 | "COMPANY_NAME": "Digital",
26 | "CLIENT_CONFIG_PATH": [
27 | "client_config.py"
28 | ]
29 | },
30 | "keypack": {
31 | "repo": {
32 | "app_private": "M7o+RcxPuqKBHEka3LjbnjMQr+zYWm2t6cNk9giu7MM"
33 | },
34 | "client": {
35 | "offline_public": "rRp4eJzzsPxN1nLXBOuLCqI33HWTridHKJpNnDSUlbU"
36 | },
37 | "upload": {
38 | "app_public": "MEviIkxfr74822UoF97+RJ9GdS9eVtkKe4PKH2J3khA",
39 | "signature": "WtNNMd2AsDlOIti1sFRylWJb/jyBOlfiF7eJPbSl/FNjvnQ+0L/7ooaDYAzRWsHCs58JyB7XYMcyfKSwDjO9Cg"
40 | }
41 | },
42 | "config_dir": ".pyupdater"
43 | }
44 |
--------------------------------------------------------------------------------
/tests/data/update_repo_restart/.pyupdater/config.pyu:
--------------------------------------------------------------------------------
1 | {
2 | "version_meta": {
3 | "signature": "SFZSIMCiVl1AdjRvIAq5Qyxtk3Yk83SX8AgSFASsJT0RQ+BsNUFuRZ0OVXbnBXoNHuAfhrb7bzZhMeaxQCVSDg",
4 | "updates": {
5 | },
6 | "latest": {
7 | }
8 | },
9 | "count": 0,
10 | "sync_threshold": 3,
11 | "py_repo_config": {
12 | "patches": {
13 | "Acme": 4
14 | },
15 | "package": {
16 | }
17 | },
18 | "app_config": {
19 | "APP_NAME": "Acme",
20 | "UPDATE_URLS": [
21 | "http://localhost:8000/"
22 | ],
23 | "PLUGIN_CONFIGS": {},
24 | "UPDATE_PATCHES": true,
25 | "COMPANY_NAME": "Digital",
26 | "CLIENT_CONFIG_PATH": [
27 | "client_config.py"
28 | ]
29 | },
30 | "keypack": {
31 | "repo": {
32 | "app_private": "M7o+RcxPuqKBHEka3LjbnjMQr+zYWm2t6cNk9giu7MM"
33 | },
34 | "client": {
35 | "offline_public": "rRp4eJzzsPxN1nLXBOuLCqI33HWTridHKJpNnDSUlbU"
36 | },
37 | "upload": {
38 | "app_public": "MEviIkxfr74822UoF97+RJ9GdS9eVtkKe4PKH2J3khA",
39 | "signature": "WtNNMd2AsDlOIti1sFRylWJb/jyBOlfiF7eJPbSl/FNjvnQ+0L/7ooaDYAzRWsHCs58JyB7XYMcyfKSwDjO9Cg"
40 | }
41 | },
42 | "config_dir": ".pyupdater"
43 | }
44 |
--------------------------------------------------------------------------------
/docs/installation.md:
--------------------------------------------------------------------------------
1 | # Installation
2 | PyUpdater depends on a few external libraries:
3 | [appdirs](https://pypi.python.org/pypi/appdirs/), [bsdiff4](https://github.com/ilanschnell/bsdiff4), [certifi](https://pypi.python.org/pypi/certifi), [dsdev-utils](https://pypi.python.org/pypi/dsdev-utils), [pyinstaller](https://github.com/pyinstaller/pyinstaller), [pynacl](https://pypi.org/project/PyNaCl/), [stevedore](https://pypi.python.org/pypi/stevedore) & [urllib3](https://pypi.python.org/pypi/urllib3).
4 |
5 | #### Install Stable version
6 |
7 | $ pip install --upgrade PyUpdater
8 |
9 |
10 | #### Install w/ upload plugins
11 |
12 | $ pip install --upgrade PyUpdater[s3]
13 |
14 | $ pip install --upgrade PyUpdater[scp]
15 |
16 |
17 | #### For complete install
18 |
19 | $ pip install --upgrade PyUpdater[all]
20 |
21 |
22 | Be sure to check the plugins docs for setup & configuration options.
23 |
24 | [PyUpdater-S3-Plugin](https://github.com/Digital-Sapphire/pyupdater-s3-plugin)
25 |
26 | [PyUpdater-SCP-Plugin](https://github.com/Digital-Sapphire/pyupdater-scp-plugin)
27 |
28 |
29 | #### Install Development version
30 | We are not responsible for broken things but please report your findings ;)
31 |
32 | $ pip install --upgrade git+https://github.com/Digital-Sapphire/PyUpdater.git@master
33 |
--------------------------------------------------------------------------------
/mkdocs.yml:
--------------------------------------------------------------------------------
1 | site_name: PyUpdater
2 | site_url: http://www.pyupdater.org
3 | site_description: Pyinstaller Auto-Update Framework
4 | google_analytics: ['UA-76455631-6', 'www.pyupdater.org']
5 | nav:
6 | - Home: index.md
7 | - User Guide:
8 | - Installation: installation.md
9 | - Commands: commands.md
10 | - Usage | CLI: usage-cli.md
11 | - Usage | CLI | Assets: usage-cli-asset.md
12 | - Usage | CLI | Advanced: usage-cli-advanced.md
13 | - Usage | Client: usage-client.md
14 | - Usage | Client | Asset: usage-client-asset.md
15 | - Usage | Client | Advanced: usage-client-advanced.md
16 | - Upgrading PyUpdater: upgrading.md
17 | - Create Upload Plugins: create-upload-plugin.md
18 | - Demo | wxPython: demo-wxpython.md
19 | - Danger Zone: danger-zone.md
20 | - API: api.md
21 | - Change Log: changelog.md
22 | - Extras:
23 | - Data Folder Layout: data-folder-layout.md
24 | - Migrate From Esky: migrate-from-esky.md
25 | - Contributing: contributing.md
26 | - Contributors: contributors.md
27 | - Downloads: downloads.md
28 | - License: license.md
29 | - O.S.S.: oss.md
30 |
31 | theme: 'material'
32 | repo_name: 'GitHub'
33 | repo_url: 'https://github.com/Digital-Sapphire/PyUpdater'
34 | extra:
35 | palette:
36 | primary: 'blue'
37 | accent: 'light blue'
38 |
39 | dev_addr: 0.0.0.0:8080
40 |
--------------------------------------------------------------------------------
/docs/data-folder-layout.md:
--------------------------------------------------------------------------------
1 | # PyUpdater Local Repository Layout.
2 | ```
3 | .
4 | ├── SuperApp.py
5 | ├── client_config.py
6 | ├── pyu-data
7 | │ ├── deploy
8 | │ ├── files
9 | │ └── new
10 | └── requirements.txt
11 | ```
12 | - client_config.py: Written by pyupdater.
13 | - pyu-data
14 |
15 | - New: Where you place newly compiled programs ready for signing
16 |
17 | - Deploy: After updates have been signed, they'll be staged here. The version meta data and public keys will also be staged here for upload.
18 |
19 | - Files: Most recent update of each app/lib is place here. Will be used as a base to crate a patch on the next build.
20 |
21 |
22 | ```
23 | .pyupdater
24 | ├── config.pyu
25 | ├── spec
26 | │ └── mac.spec
27 | └── work
28 | └── mac
29 | ├── out00-Analysis.toc
30 | ├── out00-EXE.toc
31 | ├── out00-PKG.pkg
32 | ├── out00-PKG.toc
33 | ├── out00-PYZ.pyz
34 | ├── out00-PYZ.toc
35 | ├── out00-Tree.toc
36 | ├── out01-Tree.toc
37 | └── warnmac.txt
38 | ```
39 |
40 | - .pyupdater/
41 |
42 | - config.pyu: This apps configuration information.
43 |
44 | - spec: Spec files generated by pyinstaller
45 |
46 | - mac.spec
47 |
48 | - win.spec
49 |
50 | - nix.spec
51 |
52 | - nix64.spec
53 |
54 | - work - Build artifacts generated by pyinstaller
55 |
--------------------------------------------------------------------------------
/docs/upgrading.md:
--------------------------------------------------------------------------------
1 | # Upgrading PyUpdater
2 | Note: Major version will maintain API compatibility.
3 |
4 | ## To PyUpdater >= 3.0
5 |
6 | You need to change the download signature
7 | ```
8 | AppUpdate.download(async=True)
9 | # To
10 | AppUpdate.download(background=True)
11 |
12 | LibUpdate.download(async=True)
13 | # To
14 | LibUpdate.download(background=True)
15 | ```
16 |
17 |
18 | ## To PyUpdater >= 2.0.3
19 | Deprecated since bsdiff4 is installed by default.
20 |
21 | ```
22 | $ pip install pyupdater[patch]
23 | ```
24 |
25 | ## To PyUpdater >= 2.0
26 | Coming from PyUpdater >= 1.1
27 | 1. Run the command below
28 | 2. Press enter to use the default value.
29 |
30 | ```
31 | $ pyupdater settings --config-path
32 | ```
33 | Coming from PyUpdater < 1.1
34 | 1. You'll need to update to PyUpdater 1.1.15
35 | 2. If using progress hooks note that progress_hook changed to progress_hooks and only accepts lists
36 | 3. Release a new version of your application that uses PyUpdater 1.1.15
37 | 4. Ensure all end users are using the latest version of your application.
38 | 5. Once all of your end users have the latest version or your application, you can upgrade to PyUpdater 2.0
39 |
40 | This extra step is required because the schema of the version manifest file changed to support release channels. Version 1.1 was released about 7+ months ago. Future releases will document, in the changelog, when backwards compatibility code is marked for deprecation.
--------------------------------------------------------------------------------
/pyupdater/hooks/__init__.py:
--------------------------------------------------------------------------------
1 | # ------------------------------------------------------------------------------
2 | # Copyright (c) 2015-2020 Digital Sapphire
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining
5 | # a copy of this software and associated documentation files
6 | # (the "Software"), to deal in the Software without restriction, including
7 | # without limitation the rights to use, copy, modify, merge, publish,
8 | # distribute, sublicense, and/or sell copies of the Software, and to permit
9 | # persons to whom the Software is furnished to do so, subject to the
10 | # following conditions:
11 | #
12 | # The above copyright notice and this permission notice shall be
13 | # included in all copies or substantial portions of the Software.
14 | #
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
16 | # ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
17 | # TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
18 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
19 | # SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
20 | # ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
21 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
23 | # OR OTHER DEALINGS IN THE SOFTWARE.
24 | # ------------------------------------------------------------------------------
25 | import os
26 |
27 |
28 | def get_hook_dir():
29 | return os.path.abspath(os.path.dirname(__file__))
30 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | # Welcome to PyUpdater
2 |
3 | If you think PyUpdater is awesome, please give [this issue](https://github.com/vinta/awesome-python/pull/720) an up vote on [awesome-python](https://github.com/vinta/awesome-python).
4 |
5 |
6 | ## What is PyUpdater?
7 |
8 | An auto-update library and cli tool that enables simple, secure & efficient shipment of app updates.
9 |
10 | This project would not be possible without [Pyinstaller](https://github.com/pyinstaller/pyinstaller).
11 | ## Status
12 |
13 | [](http://badge.fury.io/py/PyUpdater)
14 | 
15 | [](https://codecov.io/gh/JMSwag/PyUpdater)
16 |
17 | ## Features
18 |
19 | - Easy Setup
20 | - CI/CD Support
21 | - Basic Auth support
22 | - Secured with EdDSA
23 | - Cryptographically secure off line update
24 | - Release channels
25 | - Automatic patch update support
26 | - Intelligent update workflow
27 | - Asynchronous downloads
28 | - Update versioned external assets
29 | - Dual key verification
30 | - An offline private key signs an application specific key pair.
31 | - The application specific key pair is used to sign and verify update meta data.
32 | - The client is shipped with the offline public key to bootstrap the verification process.
33 | - Download progress callback
34 | - Uploading to the cloud handled by plugins.
35 | - S3 and SCP plugins available
36 |
--------------------------------------------------------------------------------
/pyupdater/__main__.py:
--------------------------------------------------------------------------------
1 | # ------------------------------------------------------------------------------
2 | # Copyright (c) 2015-2019 Digital Sapphire
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining
5 | # a copy of this software and associated documentation files
6 | # (the "Software"), to deal in the Software without restriction, including
7 | # without limitation the rights to use, copy, modify, merge, publish,
8 | # distribute, sublicense, and/or sell copies of the Software, and to permit
9 | # persons to whom the Software is furnished to do so, subject to the
10 | # following conditions:
11 | #
12 | # The above copyright notice and this permission notice shall be
13 | # included in all copies or substantial portions of the Software.
14 | #
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
16 | # ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
17 | # TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
18 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
19 | # SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
20 | # ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
21 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
23 | # OR OTHER DEALINGS IN THE SOFTWARE.
24 | # ------------------------------------------------------------------------------
25 | if __name__ == "__main__":
26 | # Enable python -m pyupdater [...]
27 | from pyupdater.cli import main
28 | import sys
29 |
30 | arguments = sys.argv[1:]
31 | main(arguments)
32 |
--------------------------------------------------------------------------------
/pyupdater/hooks/hook-dsdev_utils.py:
--------------------------------------------------------------------------------
1 | # ------------------------------------------------------------------------------
2 | # Copyright (c) 2015-2020 Digital Sapphire
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining
5 | # a copy of this software and associated documentation files
6 | # (the "Software"), to deal in the Software without restriction, including
7 | # without limitation the rights to use, copy, modify, merge, publish,
8 | # distribute, sublicense, and/or sell copies of the Software, and to permit
9 | # persons to whom the Software is furnished to do so, subject to the
10 | # following conditions:
11 | #
12 | # The above copyright notice and this permission notice shall be
13 | # included in all copies or substantial portions of the Software.
14 | #
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
16 | # ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
17 | # TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
18 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
19 | # SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
20 | # ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
21 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
23 | # OR OTHER DEALINGS IN THE SOFTWARE.
24 | # ------------------------------------------------------------------------------
25 | hiddenimports = [
26 | u"dsdev_utils.app",
27 | u"dsdev_utils.logger",
28 | u"dsdev_utils.paths",
29 | u"dsdev_utils.system",
30 | u"dsdev_utils.terminal",
31 | ] # pragma: no cover
32 |
--------------------------------------------------------------------------------
/tests/data/update_repo_extract/client_config.py:
--------------------------------------------------------------------------------
1 | # ------------------------------------------------------------------------------
2 | # Copyright (c) 2015-2020 Digital Sapphire
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining
5 | # a copy of this software and associated documentation files
6 | # (the "Software"), to deal in the Software without restriction, including
7 | # without limitation the rights to use, copy, modify, merge, publish,
8 | # distribute, sublicense, and/or sell copies of the Software, and to permit
9 | # persons to whom the Software is furnished to do so, subject to the
10 | # following conditions:
11 | #
12 | # The above copyright notice and this permission notice shall be
13 | # included in all copies or substantial portions of the Software.
14 | #
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
16 | # ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
17 | # TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
18 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
19 | # SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
20 | # ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
21 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
23 | # OR OTHER DEALINGS IN THE SOFTWARE.
24 | # ------------------------------------------------------------------------------
25 |
26 |
27 | class ClientConfig(object):
28 | APP_NAME = "Acme"
29 | COMPANY_NAME = "Digital"
30 | UPDATE_URLS = ["http://localhost:8000/"]
31 | PUBLIC_KEY = "rRp4eJzzsPxN1nLXBOuLCqI33HWTridHKJpNnDSUlbU"
32 |
--------------------------------------------------------------------------------
/tests/data/update_repo_restart/client_config.py:
--------------------------------------------------------------------------------
1 | # ------------------------------------------------------------------------------
2 | # Copyright (c) 2015-2020 Digital Sapphire
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining
5 | # a copy of this software and associated documentation files
6 | # (the "Software"), to deal in the Software without restriction, including
7 | # without limitation the rights to use, copy, modify, merge, publish,
8 | # distribute, sublicense, and/or sell copies of the Software, and to permit
9 | # persons to whom the Software is furnished to do so, subject to the
10 | # following conditions:
11 | #
12 | # The above copyright notice and this permission notice shall be
13 | # included in all copies or substantial portions of the Software.
14 | #
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
16 | # ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
17 | # TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
18 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
19 | # SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
20 | # ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
21 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
23 | # OR OTHER DEALINGS IN THE SOFTWARE.
24 | # ------------------------------------------------------------------------------
25 |
26 |
27 | class ClientConfig(object):
28 | APP_NAME = "Acme"
29 | COMPANY_NAME = "Digital"
30 | UPDATE_URLS = ["http://localhost:8001/"]
31 | PUBLIC_KEY = "rRp4eJzzsPxN1nLXBOuLCqI33HWTridHKJpNnDSUlbU"
32 |
--------------------------------------------------------------------------------
/tests/data/update_repo_extract/app_extract_02.py:
--------------------------------------------------------------------------------
1 | # ------------------------------------------------------------------------------
2 | # Copyright (c) 2015-2020 Digital Sapphire
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining
5 | # a copy of this software and associated documentation files
6 | # (the "Software"), to deal in the Software without restriction, including
7 | # without limitation the rights to use, copy, modify, merge, publish,
8 | # distribute, sublicense, and/or sell copies of the Software, and to permit
9 | # persons to whom the Software is furnished to do so, subject to the
10 | # following conditions:
11 | #
12 | # The above copyright notice and this permission notice shall be
13 | # included in all copies or substantial portions of the Software.
14 | #
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
16 | # ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
17 | # TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
18 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
19 | # SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
20 | # ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
21 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
23 | # OR OTHER DEALINGS IN THE SOFTWARE.
24 | # ------------------------------------------------------------------------------
25 | from __future__ import print_function
26 |
27 | VERSION = "4.2"
28 |
29 |
30 | def main():
31 | print(VERSION)
32 | with open("version1.txt", "w") as f:
33 | f.write(VERSION)
34 | return VERSION
35 |
36 |
37 | if __name__ == "__main__":
38 | main()
39 |
--------------------------------------------------------------------------------
/docs/usage-cli-asset.md:
--------------------------------------------------------------------------------
1 | # Usage | CLI | Assets
2 |
3 | ### Step 1 - Prepare external asset
4 |
5 | Place asset in pyu-data/new directory
6 |
7 | ### Step 2 - Run archive command
8 | ```
9 | $ pyupdater archive --name example.so --version 0.1.0
10 | ```
11 |
12 | ### Step 3 - Create patches
13 |
14 | Time to create binary patches if enabled, add sha256 hashes to version manifest & copy files to deploy folder. You can combine --process with --sign.
15 | ```
16 | $ pyupdater pkg --process
17 | ```
18 |
19 | ### Step 4 - Cryptographically Sign
20 |
21 | Now lets sign our version manifest file, gzip our version manifest, gzip keyfile & place in the deploy directory. You can combine --sign with --process
22 | ```
23 | $ pyupdater pkg --sign
24 | ```
25 |
26 | ### Step 5 - List Installed Plugins
27 |
28 | Get a list of the plugins you have installed. You probably wont have to run this many times :)
29 | ```
30 | # You can install the official plugins with
31 | # pip install pyupdater[s3, scp]
32 | $ pyupdater plugins
33 |
34 | s3 by Digital Sapphire
35 | scp by Digital Sapphire
36 |
37 | ```
38 |
39 | ### Step 6 - Configure Plugin
40 |
41 | Your plugin may or may not need configuration. If you are not sure then go ahead and check. It won't hurt anything. If nothing happens then the coast is clear. Plugin authors may ask you to set env vars. Please consult their docs. Now off we go.
42 | ```
43 | $ pyupdater settings --plugin s3
44 | ```
45 |
46 | ### Step 7 - Upload
47 |
48 | We've made it. Time to upload our updates, patches & metadata. On the first run you will not have any patches. There's no src files yet. It'll happen on the next build.
49 | ```
50 | $ pyupdater upload --service s3
51 | ```
52 |
--------------------------------------------------------------------------------
/tests/data/update_repo_restart/app_restart_02.py:
--------------------------------------------------------------------------------
1 | # ------------------------------------------------------------------------------
2 | # Copyright (c) 2015-2020 Digital Sapphire
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining
5 | # a copy of this software and associated documentation files
6 | # (the "Software"), to deal in the Software without restriction, including
7 | # without limitation the rights to use, copy, modify, merge, publish,
8 | # distribute, sublicense, and/or sell copies of the Software, and to permit
9 | # persons to whom the Software is furnished to do so, subject to the
10 | # following conditions:
11 | #
12 | # The above copyright notice and this permission notice shall be
13 | # included in all copies or substantial portions of the Software.
14 | #
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
16 | # ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
17 | # TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
18 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
19 | # SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
20 | # ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
21 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
23 | # OR OTHER DEALINGS IN THE SOFTWARE.
24 | # ------------------------------------------------------------------------------
25 | from __future__ import print_function
26 |
27 | VERSION = "4.2"
28 |
29 |
30 | def main():
31 | print(VERSION)
32 | with open("version2.txt", "w") as f:
33 | print("Writing version file")
34 | f.write(VERSION)
35 |
36 | print("Leaving main()")
37 |
38 |
39 | if __name__ == "__main__":
40 | main()
41 |
--------------------------------------------------------------------------------
/docs/create-upload-plugin.md:
--------------------------------------------------------------------------------
1 | # Create Upload Plugins
2 |
3 | - PyUpdater finds plugins by setuptools entry points.
4 | - Base class provides helper methods to get and set configuration information.
5 |
6 | ## Simple Example
7 | my_uploader.py
8 | ```
9 |
10 | from pyupdater.core.uploader import BaseUploader
11 |
12 |
13 | class MyUploader(BaseUploader):
14 |
15 | name = 'my uploader'
16 | author = 'Jane Doe'
17 |
18 | def init_config(self, config):
19 | self.server_url = config["server_url"]
20 |
21 | def set_config(self, config):
22 | server_name = self.get_answer("Please enter server name\n--> ")
23 | config["server_url"] = server_name
24 |
25 | def upload_file(self, filename):
26 | # Make the magic happen
27 | files = {'file': open(filename, 'rb')}
28 | r = request.post(self.server_url, files=files)
29 |
30 | ```
31 |
32 |
33 | setup.py
34 | ```
35 | setup(
36 | provides=['pyupdater.plugins',],
37 | entry_points={
38 | 'pyupdater.plugins': [
39 | 'my_uploader = my_uploader:MyUploader',
40 | ]
41 | },
42 | ```
43 |
44 | ## Plugin Settings
45 | Plugin authors have 2 ways of getting and setting config information.
46 |
47 | The first way would be to request the information from the user. In your plugin you'd do something like this.
48 | ```
49 | # Saves the config to disk.
50 | def set_config(self, config):
51 | server_name = self.get_answer("Please enter server name\n--> ")
52 | config["server_url"] = server_name
53 |
54 |
55 | # Will be called after the class is initialized.
56 | def init_config(self, config):
57 | self.server_url = config["server_url"]
58 |
59 | ```
60 |
61 | The second way would be env var.
62 |
63 | ## Examples plugins
64 | [S3 Plugin](https://github.com/Digital-Sapphire/PyUpdater-S3-Plugin)
65 |
66 | [SCP Plugin](https://github.com/Digital-Sapphire/PyUpdater-SCP-Plugin)
67 |
68 |
69 |
--------------------------------------------------------------------------------
/tests/test_keys.py:
--------------------------------------------------------------------------------
1 | # ------------------------------------------------------------------------------
2 | # Copyright (c) 2015-2020 Digital Sapphire
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining
5 | # a copy of this software and associated documentation files
6 | # (the "Software"), to deal in the Software without restriction, including
7 | # without limitation the rights to use, copy, modify, merge, publish,
8 | # distribute, sublicense, and/or sell copies of the Software, and to permit
9 | # persons to whom the Software is furnished to do so, subject to the
10 | # following conditions:
11 | #
12 | # The above copyright notice and this permission notice shall be
13 | # included in all copies or substantial portions of the Software.
14 | #
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
16 | # ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
17 | # TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
18 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
19 | # SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
20 | # ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
21 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
23 | # OR OTHER DEALINGS IN THE SOFTWARE.
24 | # ------------------------------------------------------------------------------
25 | from __future__ import unicode_literals
26 | import os
27 |
28 | import pytest
29 |
30 | from pyupdater.core.key_handler.keys import KeyImporter, Keys
31 |
32 |
33 | @pytest.mark.usefixtures("cleandir")
34 | class TestKeys(object):
35 | def test_create_keypack(self):
36 | k = Keys(test=True)
37 | for name in ["one", "two", "three"]:
38 | assert k.make_keypack(name) is True
39 | assert os.path.exists(k.data_dir) is True
40 |
41 | def test_key_importer(self):
42 | k = Keys(test=True)
43 | k.make_keypack("one")
44 |
45 | ki = KeyImporter()
46 | assert ki.start() is True
47 |
48 | def test_key_importer_fail(self):
49 | ki = KeyImporter()
50 | assert ki.start() is False
51 |
--------------------------------------------------------------------------------
/tests/tconfig.py:
--------------------------------------------------------------------------------
1 | # ------------------------------------------------------------------------------
2 | # Copyright (c) 2015-2019 Digital Sapphire
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining
5 | # a copy of this software and associated documentation files
6 | # (the "Software"), to deal in the Software without restriction, including
7 | # without limitation the rights to use, copy, modify, merge, publish,
8 | # distribute, sublicense, and/or sell copies of the Software, and to permit
9 | # persons to whom the Software is furnished to do so, subject to the
10 | # following conditions:
11 | #
12 | # The above copyright notice and this permission notice shall be
13 | # included in all copies or substantial portions of the Software.
14 | #
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
16 | # ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
17 | # TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
18 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
19 | # SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
20 | # ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
21 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
23 | # OR OTHER DEALINGS IN THE SOFTWARE.
24 | # ------------------------------------------------------------------------------
25 | from __future__ import unicode_literals
26 |
27 |
28 | class TConfig(object):
29 | bad_attr = "bad attr"
30 | # If left None "Not_So_TUF" will be used
31 | APP_NAME = "Acme"
32 |
33 | COMPANY_NAME = "Digital"
34 |
35 | DATA_DIR = None
36 |
37 | # Public Key used by your app to verify update data
38 | # REQUIRED
39 | PUBLIC_KEY = "rRp4eJzzsPxN1nLXBOuLCqI33HWTridHKJpNnDSUlbU"
40 |
41 | # Online repository where you host your packages
42 | # and version file
43 | # REQUIRED
44 | UPDATE_URLS = ["https://s3-us-west-1.amazonaws.com/pyu-tester/"]
45 | UPDATE_PATCHES = True
46 |
47 | # Upload Setup
48 | REMOTE_DIR = None
49 | HOST = None
50 |
51 | # Tests seem to fail when this is True
52 | VERIFY_SERVER_CERT = True
53 |
--------------------------------------------------------------------------------
/tests/data/version.json:
--------------------------------------------------------------------------------
1 | {
2 | "latest": {
3 | "Acme": {
4 | "stable": {
5 | "mac": "4.4.0.2.0"
6 | }
7 | }
8 | },
9 | "signature": "xyRZyduSdlLS+9eCmfU/NJeCk9Ybr/bmKQ5GareDbf9/BIajHZevErcRsZi0oTKqKos4b/6fjcNbO9W112T+Ag",
10 | "updates": {
11 | "Acme": {
12 | "4.1.0.2.0": {
13 | "mac": {
14 | "file_hash": "7ade54611f1b632e8df7b6eb0a59f4eb5f4eaaf6d4d806a9b3b7cb7129214e6a",
15 | "file_size": 4997814,
16 | "filename": "Acme-mac-4.1.tar.gz"
17 | }
18 | },
19 | "4.2.0.2.0": {
20 | "mac": {
21 | "file_hash": "7872d3b61026048e6b114a15819742ad00afb2395bcd35106d1b8dc42df00b14",
22 | "file_size": 4997728,
23 | "filename": "Acme-mac-4.2.tar.gz",
24 | "patch_hash": "70f3529a891af3145782722a6f5d39f8ea3d87b5ffebbc9d2110d230a49f840d",
25 | "patch_name": "Acme-mac-2",
26 | "patch_size": 1276614
27 | }
28 | },
29 | "4.3.0.2.0": {
30 | "mac": {
31 | "file_hash": "7d3d3b110be99648d70e10ea63f6520e54071f241fb712d23b8f7abc3494c600",
32 | "file_size": 4997724,
33 | "filename": "Acme-mac-4.3.tar.gz",
34 | "patch_hash": "b74269e7433535aa0884eb69937c19bed133fdd7a112a853f66f5815a1bea803",
35 | "patch_name": "Acme-mac-3",
36 | "patch_size": 824443
37 | }
38 | },
39 | "4.4.0.2.0": {
40 | "mac": {
41 | "file_hash": "4f583cdc3ef72f6a4876bdaae690aa634a3d6507211dea7b9e9f771563c1d8d4",
42 | "file_size": 4997724,
43 | "filename": "Acme-mac-4.4.tar.gz",
44 | "patch_hash": "025f443c98b58714e56d7aa9dbb174a28d0d5926942f1005c1a7ae7c5d2cde08",
45 | "patch_name": "Acme-mac-4",
46 | "patch_size": 612937
47 | }
48 | }
49 | }
50 | }
51 | }
--------------------------------------------------------------------------------
/dev/namespace-test.py:
--------------------------------------------------------------------------------
1 | # ------------------------------------------------------------------------------
2 | # Copyright (c) 2015-2020 Digital Sapphire
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining
5 | # a copy of this software and associated documentation files
6 | # (the "Software"), to deal in the Software without restriction, including
7 | # without limitation the rights to use, copy, modify, merge, publish,
8 | # distribute, sublicense, and/or sell copies of the Software, and to permit
9 | # persons to whom the Software is furnished to do so, subject to the
10 | # following conditions:
11 | #
12 | # The above copyright notice and this permission notice shall be
13 | # included in all copies or substantial portions of the Software.
14 | #
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
16 | # ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
17 | # TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
18 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
19 | # SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
20 | # ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
21 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
23 | # OR OTHER DEALINGS IN THE SOFTWARE.
24 | # ------------------------------------------------------------------------------
25 | import logging
26 | import os
27 |
28 | from stevedore.extension import ExtensionManager
29 |
30 | log = logging.getLogger()
31 | log.setLevel(logging.DEBUG)
32 | sh = logging.StreamHandler()
33 | sh.setLevel(logging.DEBUG)
34 | log.addHandler(sh)
35 |
36 | namespace = "pyupdater.plugins"
37 |
38 |
39 | def upgrade_pip():
40 | command = "./update.sh"
41 | log.debug("command: %s", command)
42 | os.system(command)
43 |
44 |
45 | def main():
46 | # upgrade_pip()
47 | mgr = ExtensionManager(namespace)
48 | eps = mgr.ENTRY_POINT_CACHE
49 | log.debug("EP Cache: %s", eps)
50 |
51 | for ep in eps[namespace]:
52 | try:
53 | ep.load()
54 | log.info("Successful Plugin Load")
55 | except Exception as err:
56 | log.error(str(err), exc_info=True)
57 |
58 | log.info("Test Complete")
59 |
60 |
61 | main()
62 |
--------------------------------------------------------------------------------
/tests/test_version_file.py:
--------------------------------------------------------------------------------
1 | # ------------------------------------------------------------------------------
2 | # Copyright (c) 2015-2020 Digital Sapphire
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining
5 | # a copy of this software and associated documentation files
6 | # (the "Software"), to deal in the Software without restriction, including
7 | # without limitation the rights to use, copy, modify, merge, publish,
8 | # distribute, sublicense, and/or sell copies of the Software, and to permit
9 | # persons to whom the Software is furnished to do so, subject to the
10 | # following conditions:
11 | #
12 | # The above copyright notice and this permission notice shall be
13 | # included in all copies or substantial portions of the Software.
14 | #
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
16 | # ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
17 | # TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
18 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
19 | # SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
20 | # ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
21 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
23 | # OR OTHER DEALINGS IN THE SOFTWARE.
24 | # ------------------------------------------------------------------------------
25 | from __future__ import unicode_literals
26 |
27 | import json
28 |
29 | from nacl.signing import VerifyKey
30 | import pytest
31 |
32 | from pyupdater.utils.encoding import UnpaddedBase64Encoder
33 |
34 |
35 | @pytest.mark.usefixtures("cleandir")
36 | class TestVersionFile(object):
37 | def test_signature(self, shared_datadir):
38 | json_raw = (shared_datadir / "version.json").read_text()
39 | version_data = json.loads(json_raw)
40 |
41 | sig = version_data["signature"]
42 | del version_data["signature"]
43 |
44 | data = json.dumps(version_data, sort_keys=True)
45 |
46 | version_data = bytes(data, "utf-8")
47 |
48 | jms_pub = (shared_datadir / "jms.pub").read_text()
49 | public_key = VerifyKey(jms_pub, UnpaddedBase64Encoder())
50 |
51 | sig = UnpaddedBase64Encoder.decode(sig)
52 | public_key.verify(version_data, sig)
53 |
--------------------------------------------------------------------------------
/docs/usage-client-asset.md:
--------------------------------------------------------------------------------
1 | # Usage | Client | Asset
2 |
3 | ### Step 1 - Imports & Constants
4 |
5 | The client_config.py is written to the root of the repository by default.
6 | ```
7 | from pyupdater.client import Client
8 | from client_config import ClientConfig
9 |
10 | APP_NAME = 'Super App'
11 | APP_VERSION = '1.1.0'
12 |
13 | ASSET_NAME = 'ffmpeg'
14 | ASSET_VERSION = '2.3.2'
15 | ```
16 |
17 | ### Step 2 - Create callback
18 |
19 | This callback will print download progress.
20 | ```
21 | def print_status_info(info):
22 | total = info.get(u'total')
23 | downloaded = info.get(u'downloaded')
24 | status = info.get(u'status')
25 | print downloaded, total, status
26 | ```
27 |
28 | ### Step 3a - Initialize Client
29 |
30 | Initialize a client with ClientConfig & later call refresh to get latest update data. You can also add progress hooks later.
31 | ```
32 | client = Client(ClientConfig())
33 | client.refresh()
34 |
35 | client.add_progress_hook(print_status_info)
36 | ```
37 |
38 | ### Step 3b - Initialize Client Alt
39 |
40 | Initialize a client with ClientConfig, add progress hook & refresh during initialization.
41 | ```
42 | client = Client(ClientConfig(), refresh=True,
43 | progress_hooks=[print_status_info])
44 | ```
45 |
46 | ### Step 4a - Update Check
47 |
48 | The update_check method returns an LibUpdate object if there is an update available.
49 | ```
50 | lib_update = client.update_check(ASSET_NAME, ASSET_VERSION)
51 | ```
52 |
53 | ### Step 4b - Update Check Alt
54 |
55 | Checking for updates on the beta channel.
56 | ```
57 | lib_update = client.update_check(ASSET_NAME, ASSET_VERSION, channel='beta')
58 | ```
59 |
60 | ### Step 5a - Download Update
61 |
62 | If we get an update object we can proceed to download the update.
63 | ```
64 | if lib_update is not None:
65 | lib_update.download()
66 | ```
67 |
68 | ### Step 5b - Download Update Alt
69 |
70 | We can also download in a background thread.
71 | ```
72 | if lib_update is not None:
73 | lib_update.download(background=True)
74 | ```
75 |
76 | ### Step 6a - Extract
77 |
78 | Ensure file downloaded successfully, extract update.
79 |
80 | ```
81 | if lib_update.is_downloaded():
82 | lib_update.extract()
83 |
84 | # Update will be extracted in the update folder
85 | lib_update.update_folder
86 | ```
87 |
--------------------------------------------------------------------------------
/tests/data/dont delete pyu test.txt:
--------------------------------------------------------------------------------
1 | kdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;akdkda;fkdj;afk;afkd;akdfj;afj;a
--------------------------------------------------------------------------------
/dev/move.py:
--------------------------------------------------------------------------------
1 | # ------------------------------------------------------------------------------
2 | # Copyright (c) 2015-2020 Digital Sapphire
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining
5 | # a copy of this software and associated documentation files
6 | # (the "Software"), to deal in the Software without restriction, including
7 | # without limitation the rights to use, copy, modify, merge, publish,
8 | # distribute, sublicense, and/or sell copies of the Software, and to permit
9 | # persons to whom the Software is furnished to do so, subject to the
10 | # following conditions:
11 | #
12 | # The above copyright notice and this permission notice shall be
13 | # included in all copies or substantial portions of the Software.
14 | #
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
16 | # ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
17 | # TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
18 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
19 | # SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
20 | # ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
21 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
23 | # OR OTHER DEALINGS IN THE SOFTWARE.
24 | # ------------------------------------------------------------------------------
25 | from __future__ import print_function
26 |
27 | import os
28 | import shutil
29 |
30 | from dsdev_utils.paths import ChDir, remove_any
31 |
32 | HTML_DIR = os.path.join(os.getcwd(), "site")
33 | DEST_DIR = os.path.join(
34 | os.path.expanduser("~"), "src", "Web", "PyUpdater", "public_html"
35 | )
36 |
37 |
38 | def main():
39 | with ChDir(DEST_DIR):
40 | files = os.listdir(os.getcwd())
41 | for f in files:
42 | remove_any(f)
43 |
44 | with ChDir(HTML_DIR):
45 | files = os.listdir(os.getcwd())
46 | for f in files:
47 | if f.startswith(u"."):
48 | continue
49 | if f.startswith("__init__"):
50 | continue
51 | if os.path.isfile(f):
52 | shutil.copy(f, os.path.join(DEST_DIR, f))
53 | elif os.path.isdir(f):
54 | shutil.copytree(f, DEST_DIR + os.sep + f)
55 |
56 | shutil.rmtree(HTML_DIR)
57 |
58 |
59 | if __name__ == "__main__":
60 | main()
61 | print(u"Move complete")
62 |
--------------------------------------------------------------------------------
/tests/test_builder.py:
--------------------------------------------------------------------------------
1 | # ------------------------------------------------------------------------------
2 | # Copyright (c) 2015-2020 Digital Sapphire
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining
5 | # a copy of this software and associated documentation files
6 | # (the "Software"), to deal in the Software without restriction, including
7 | # without limitation the rights to use, copy, modify, merge, publish,
8 | # distribute, sublicense, and/or sell copies of the Software, and to permit
9 | # persons to whom the Software is furnished to do so, subject to the
10 | # following conditions:
11 | #
12 | # The above copyright notice and this permission notice shall be
13 | # included in all copies or substantial portions of the Software.
14 | #
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
16 | # ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
17 | # TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
18 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
19 | # SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
20 | # ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
21 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
23 | # OR OTHER DEALINGS IN THE SOFTWARE.
24 | # ------------------------------------------------------------------------------
25 | from __future__ import unicode_literals
26 | import io
27 | import os
28 | import sys
29 |
30 | from dsdev_utils.system import get_system
31 | import pytest
32 |
33 | from pyupdater.utils.builder import ExternalLib
34 | from pyupdater.utils.config import ConfigManager
35 |
36 |
37 | CONFIG = {"APP_NAME": "PyUpdater Test", "COMPANY_NAME": "ACME", "UPDATE_PATCHES": True}
38 |
39 | if sys.platform == "win32":
40 | EXT = ".zip"
41 | else:
42 | EXT = ".tar.gz"
43 |
44 |
45 | @pytest.mark.usefixtures("cleandir")
46 | class TestBuilder(object):
47 | def test_build(self):
48 | cm = ConfigManager()
49 | config = cm.load_config()
50 | config.update(CONFIG)
51 | cm.save_config(config)
52 |
53 |
54 | @pytest.mark.usefixtures("cleandir")
55 | class TestExternalLib(object):
56 | def test_archive(self):
57 | with io.open("test", "w", encoding="utf-8") as f:
58 | f.write("this is a test")
59 | ex = ExternalLib("test", "0.1")
60 | ex.archive()
61 | assert os.path.exists("test-{}-0.1{}".format(get_system(), EXT))
62 |
--------------------------------------------------------------------------------
/docs/usage-client.md:
--------------------------------------------------------------------------------
1 | # Usage | Client
2 |
3 | ### Step 1 - Imports & Constants
4 | The client_config.py is written to the root of the repository by default.
5 | ```
6 | from pyupdater.client import Client
7 | from client_config import ClientConfig
8 |
9 | APP_NAME = 'Super App'
10 | APP_VERSION = '1.1.0'
11 |
12 | ASSET_NAME = 'ffmpeg'
13 | ASSET_VERSION = '2.3.2'
14 | ```
15 |
16 | ### Step 2 - Create callback
17 | This callback will print download progress.
18 | ```
19 | def print_status_info(info):
20 | total = info.get(u'total')
21 | downloaded = info.get(u'downloaded')
22 | status = info.get(u'status')
23 | print downloaded, total, status
24 | ```
25 |
26 | ### Step 3a - Initialize Client
27 |
28 | Initialize a client with the ClientConfig & later call refresh to get latest update data. Progress hooks can also be added later.
29 | ```
30 | client = Client(ClientConfig())
31 | client.refresh()
32 |
33 | client.add_progress_hook(print_status_info)
34 | ```
35 |
36 | ### Step 3b - Initialize Client Alt
37 |
38 | Initialize a client with the ClientConfig, add progress hook & refresh during initialization.
39 | ```
40 | client = Client(
41 | ClientConfig(), refresh=True, progress_hooks=[print_status_info]
42 | )
43 | ```
44 |
45 | ### Step 4a - Update Check
46 |
47 | Method update_check returns an AppUpdate object if there is an update available else None
48 | ```
49 | app_update = client.update_check(APP_NAME, APP_VERSION)
50 | ```
51 |
52 | ### Step 4b - Update Check Alt
53 |
54 | Checking for updates on the beta channel
55 | ```
56 | app_update = client.update_check(APP_NAME, APP_VERSION, channel='beta')
57 | ```
58 |
59 | ### Step 5a - Download Update
60 |
61 | If an update object was returned, we can proceed to download the update.
62 | ```
63 | if app_update is not None:
64 | app_update.download()
65 | ```
66 |
67 | ### Step 5b - Download Update Alt
68 |
69 | We can also download in a background thread.
70 | ```
71 | if app_update is not None:
72 | app_update.download(background=True)
73 | ```
74 |
75 | ### Step 6a - Overwrite
76 |
77 | Ensure file downloaded successfully, extract update & overwrite current application. Note that is_downloaded verifies the files hash.
78 |
79 | ```
80 | if app_update.is_downloaded():
81 | app_update.extract_overwrite()
82 | ```
83 |
84 | ### Step 6b - Restart
85 |
86 | Ensure file downloaded successfully, extract update, overwrite current application & restart application with the updated binary.
87 |
88 | ```
89 | if app_update.is_downloaded():
90 | app_update.extract_restart()
91 | ```
92 |
--------------------------------------------------------------------------------
/pyupdater/settings.py:
--------------------------------------------------------------------------------
1 | # ------------------------------------------------------------------------------
2 | # Copyright (c) 2015-2019 Digital Sapphire
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining
5 | # a copy of this software and associated documentation files
6 | # (the "Software"), to deal in the Software without restriction, including
7 | # without limitation the rights to use, copy, modify, merge, publish,
8 | # distribute, sublicense, and/or sell copies of the Software, and to permit
9 | # persons to whom the Software is furnished to do so, subject to the
10 | # following conditions:
11 | #
12 | # The above copyright notice and this permission notice shall be
13 | # included in all copies or substantial portions of the Software.
14 | #
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
16 | # ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
17 | # TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
18 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
19 | # SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
20 | # ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
21 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
23 | # OR OTHER DEALINGS IN THE SOFTWARE.
24 | # ------------------------------------------------------------------------------
25 | from __future__ import unicode_literals
26 | import struct
27 | import sys
28 |
29 | from dsdev_utils import system
30 |
31 |
32 | APP_NAME = "PyUpdater"
33 | APP_AUTHOR = "Digital Sapphire"
34 |
35 | # Used to hold PyUpdater config info for repo
36 | CONFIG_DATA_FOLDER = ".pyupdater"
37 |
38 | # User config file
39 | CONFIG_FILE_USER = "config.pyu"
40 |
41 | CONFIG_DB_KEY_APP_CONFIG = "app_config"
42 | CONFIG_DB_KEY_KEYPACK = "keypack"
43 | CONFIG_DB_KEY_VERSION_META = "version_meta"
44 | CONFIG_DB_KEY_PY_REPO_CONFIG = "py_repo_config"
45 |
46 | DEFAULT_CLIENT_CONFIG = ["client_config.py"]
47 |
48 | GENERIC_APP_NAME = "PyUpdater App"
49 | GENERIC_COMPANY_NAME = "PyUpdater"
50 |
51 | # Log filename
52 | LOG_FILENAME_DEBUG = "pyu-debug.log"
53 |
54 | # KeyFile
55 | KEYPACK_FILENAME = "keypack.pyu"
56 |
57 | # Main user visible data folder
58 | USER_DATA_FOLDER = "pyu-data"
59 |
60 | # Key in version file where value are update meta data
61 | UPDATES_KEY = "updates"
62 |
63 | # Folder on client system where updates are stored
64 | UPDATE_FOLDER = "update"
65 |
66 | # Name of version file in online repo
67 | VERSION_FILE_FILENAME = "versions-{}.gz".format(system.get_system())
68 | VERSION_FILE_FILENAME_COMPAT = "versions.gz"
69 | KEY_FILE_FILENAME = "keys.gz"
70 |
--------------------------------------------------------------------------------
/pyupdater/__init__.py:
--------------------------------------------------------------------------------
1 | # ------------------------------------------------------------------------------
2 | # Copyright (c) 2015-2019 Digital Sapphire
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining
5 | # a copy of this software and associated documentation files
6 | # (the "Software"), to deal in the Software without restriction, including
7 | # without limitation the rights to use, copy, modify, merge, publish,
8 | # distribute, sublicense, and/or sell copies of the Software, and to permit
9 | # persons to whom the Software is furnished to do so, subject to the
10 | # following conditions:
11 | #
12 | # The above copyright notice and this permission notice shall be
13 | # included in all copies or substantial portions of the Software.
14 | #
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
16 | # ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
17 | # TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
18 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
19 | # SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
20 | # ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
21 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
23 | # OR OTHER DEALINGS IN THE SOFTWARE.
24 | # ------------------------------------------------------------------------------
25 | import logging
26 | from logging.handlers import RotatingFileHandler
27 | import os
28 |
29 | from appdirs import user_log_dir
30 | from dsdev_utils.logger import logging_formatter
31 |
32 | from pyupdater import settings
33 | from pyupdater.core import PyUpdater
34 |
35 | __all__ = ["PyUpdater"]
36 |
37 |
38 | log = logging.getLogger(__name__)
39 | log.setLevel(logging.DEBUG)
40 |
41 | # Console logger
42 | fmt = logging.Formatter("[%(levelname)s] %(message)s")
43 | sh = logging.StreamHandler()
44 | sh.setLevel(logging.INFO)
45 | sh.setFormatter(fmt)
46 | log.addHandler(sh)
47 |
48 | # Log to pyu.log if available
49 | local_debug_file_path = os.path.join(os.getcwd(), "pyu.log")
50 | if os.path.exists(local_debug_file_path): # pragma: no cover
51 | fh = logging.FileHandler(local_debug_file_path)
52 | fh.setLevel(logging.DEBUG)
53 | fh.setFormatter(logging_formatter)
54 | log.addHandler(fh)
55 |
56 | # Default log directory
57 | LOG_DIR = user_log_dir(settings.APP_NAME, settings.APP_AUTHOR)
58 | if not os.path.exists(LOG_DIR): # pragma: no cover
59 | os.makedirs(LOG_DIR)
60 |
61 | log_file = os.path.join(LOG_DIR, settings.LOG_FILENAME_DEBUG)
62 | rfh = RotatingFileHandler(log_file, maxBytes=1048576, backupCount=2)
63 | rfh.setLevel(logging.DEBUG)
64 | rfh.setFormatter(logging_formatter)
65 | log.addHandler(rfh)
66 |
67 | # noinspection PyPep8
68 | from ._version import get_versions
69 |
70 | __version__ = get_versions()["version"]
71 | del get_versions
72 | log.debug("Version - %s", __version__)
73 |
--------------------------------------------------------------------------------
/dev/clean.py:
--------------------------------------------------------------------------------
1 | # ------------------------------------------------------------------------------
2 | # Copyright (c) 2015-2020 Digital Sapphire
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining
5 | # a copy of this software and associated documentation files
6 | # (the "Software"), to deal in the Software without restriction, including
7 | # without limitation the rights to use, copy, modify, merge, publish,
8 | # distribute, sublicense, and/or sell copies of the Software, and to permit
9 | # persons to whom the Software is furnished to do so, subject to the
10 | # following conditions:
11 | #
12 | # The above copyright notice and this permission notice shall be
13 | # included in all copies or substantial portions of the Software.
14 | #
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
16 | # ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
17 | # TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
18 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
19 | # SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
20 | # ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
21 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
23 | # OR OTHER DEALINGS IN THE SOFTWARE.
24 | # ------------------------------------------------------------------------------
25 | import shutil
26 | import os
27 |
28 | # If clean.py is moved from dev dir please update
29 | HOME = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
30 |
31 |
32 | print("Start dir: {}".format(HOME))
33 |
34 |
35 | def check_x(x):
36 | def bad_file(f):
37 | ext = os.path.splitext(f)[1]
38 | if ext == ".pyc":
39 | return True
40 | basename = os.path.basename(f)
41 | if basename.startswith(".coverage."):
42 | return True
43 | return False
44 |
45 | def bad_dir(d):
46 | basename = os.path.basename(d)
47 | bad = ["__pycache__", "htmlcov", "build", "dist", "PyUpdater.egg-info"]
48 | if basename in bad:
49 | return True
50 | return False
51 |
52 | if os.path.isfile(x):
53 | if bad_file(x) is True:
54 | remove(x)
55 | if os.path.isdir(x):
56 | if bad_dir(x) is True:
57 | remove(x)
58 |
59 |
60 | def remove(x):
61 | removed = False
62 | if os.path.isfile(x):
63 | removed = True
64 | os.remove(x)
65 | if os.path.isdir(x):
66 | removed = True
67 | shutil.rmtree(x, ignore_errors=True)
68 | if removed is True:
69 | print("Removed {}".format(x))
70 |
71 |
72 | def main():
73 | for root, dirs, files in os.walk(HOME):
74 | for f in files:
75 | path = os.path.abspath(os.path.join(root, f))
76 | check_x(path)
77 | for d in dirs:
78 | path = os.path.abspath(os.path.join(root, d))
79 | check_x(path)
80 |
81 |
82 | if __name__ == "__main__":
83 | main()
84 |
--------------------------------------------------------------------------------
/tests/test_config.py:
--------------------------------------------------------------------------------
1 | # ------------------------------------------------------------------------------
2 | # Copyright (c) 2015-2020 Digital Sapphire
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining
5 | # a copy of this software and associated documentation files
6 | # (the "Software"), to deal in the Software without restriction, including
7 | # without limitation the rights to use, copy, modify, merge, publish,
8 | # distribute, sublicense, and/or sell copies of the Software, and to permit
9 | # persons to whom the Software is furnished to do so, subject to the
10 | # following conditions:
11 | #
12 | # The above copyright notice and this permission notice shall be
13 | # included in all copies or substantial portions of the Software.
14 | #
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
16 | # ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
17 | # TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
18 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
19 | # SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
20 | # ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
21 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
23 | # OR OTHER DEALINGS IN THE SOFTWARE.
24 | # ------------------------------------------------------------------------------
25 | from __future__ import unicode_literals
26 |
27 | import os
28 |
29 | from pyupdater.utils.config import Config, ConfigManager
30 |
31 |
32 | class DevConfig(object):
33 | TESTING = True
34 | TEST_LOVE = True
35 | MORE_INFO = "No Thanks"
36 | Bad_Attr = True
37 |
38 |
39 | class ProdConfig(object):
40 | TESTING = False
41 | DEBUG = False
42 | MORE_INFO = "Yes Please"
43 |
44 |
45 | class BasicCofig(object):
46 | APP_NAME = "Tester"
47 | COMPANY_NAME = "Test App LLC"
48 | UPDATE_URLS = "http://acme.com/updates"
49 | PUBLIC_KEYS = "838d88df8adkld8s9s"
50 |
51 |
52 | def test_dev_config():
53 | config = Config()
54 | test_config = DevConfig()
55 | config.from_object(test_config)
56 | assert config["TESTING"] is True
57 |
58 |
59 | def test_dev_config_bad_attr():
60 | config = Config()
61 | test_config = DevConfig()
62 | config.from_object(test_config)
63 | assert config.get("BAD_ATTR", None) is None
64 |
65 |
66 | def test_prod_config():
67 | config = Config()
68 | prod_config = ProdConfig()
69 | config.from_object(prod_config)
70 | assert config["MORE_INFO"] == "Yes Please"
71 |
72 |
73 | def test_prod_bad_atter():
74 | config = Config()
75 | prod_config = ProdConfig()
76 | config.from_object(prod_config)
77 | assert config.get("DEBUG", None) is not None
78 |
79 |
80 | def test_write_config(cleandir):
81 | config = Config()
82 | prod_config = ProdConfig()
83 | config.from_object(prod_config)
84 | cm = ConfigManager()
85 | cm.write_config_py(config)
86 | assert "client_config.py" in os.listdir(os.getcwd())
87 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | # ------------------------------------------------------------------------------
2 | # Copyright (c) 2015-2020 Digital Sapphire
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining
5 | # a copy of this software and associated documentation files
6 | # (the "Software"), to deal in the Software without restriction, including
7 | # without limitation the rights to use, copy, modify, merge, publish,
8 | # distribute, sublicense, and/or sell copies of the Software, and to permit
9 | # persons to whom the Software is furnished to do so, subject to the
10 | # following conditions:
11 | #
12 | # The above copyright notice and this permission notice shall be
13 | # included in all copies or substantial portions of the Software.
14 | #
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
16 | # ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
17 | # TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
18 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
19 | # SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
20 | # ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
21 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
23 | # OR OTHER DEALINGS IN THE SOFTWARE.
24 | # ------------------------------------------------------------------------------
25 | from setuptools import find_packages, setup
26 |
27 | import versioneer
28 |
29 | KEYWORDS = (
30 | "PyUpdater Pyinstaller Auto Update AutoUpdate Auto-Update Esky "
31 | "updater4pyi bbfreeze ccfreeze freeze cz_freeze pyupdate"
32 | )
33 |
34 |
35 | with open(u"requirements.txt", u"r") as f:
36 | required = f.read().splitlines()
37 |
38 |
39 | with open("README.md", "r") as f:
40 | readme = f.read()
41 |
42 |
43 | extra_s3 = "PyUpdater-s3-Plugin >= 4.0.5"
44 | extra_scp = "PyUpdater-scp-Plugin >= 4.0"
45 |
46 |
47 | setup(
48 | name="PyUpdater",
49 | version=versioneer.get_version(),
50 | description="Python Auto Update Library for Pyinstaller",
51 | long_description=readme,
52 | long_description_content_type="text/markdown",
53 | author="Digital Sapphire",
54 | author_email="oss@digitalsapphire.io",
55 | url="https://www.pyupdater.org",
56 | download_url=("https://github.com/Digital-Sapphire/PyUpdater/archive/master.zip"),
57 | license="MIT",
58 | keywords=KEYWORDS,
59 | extras_require={"s3": extra_s3, "scp": extra_scp, "all": [extra_s3, extra_scp]},
60 | zip_safe=False,
61 | include_package_data=True,
62 | tests_require=["pytest"],
63 | cmdclass=versioneer.get_cmdclass(),
64 | install_requires=required,
65 | packages=find_packages(),
66 | entry_points="""
67 | [console_scripts]
68 | pyupdater=pyupdater.cli:main
69 | """,
70 | classifiers=[
71 | "Development Status :: 5 - Production/Stable",
72 | "Environment :: Console",
73 | "Intended Audience :: Developers",
74 | "License :: OSI Approved :: MIT License",
75 | "Operating System :: OS Independent",
76 | "Programming Language :: Python :: 3 :: Only",
77 | ],
78 | )
79 |
--------------------------------------------------------------------------------
/tests/data/update_repo_extract/app_extract_01.py:
--------------------------------------------------------------------------------
1 | # ------------------------------------------------------------------------------
2 | # Copyright (c) 2015-2020 Digital Sapphire
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining
5 | # a copy of this software and associated documentation files
6 | # (the "Software"), to deal in the Software without restriction, including
7 | # without limitation the rights to use, copy, modify, merge, publish,
8 | # distribute, sublicense, and/or sell copies of the Software, and to permit
9 | # persons to whom the Software is furnished to do so, subject to the
10 | # following conditions:
11 | #
12 | # The above copyright notice and this permission notice shall be
13 | # included in all copies or substantial portions of the Software.
14 | #
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
16 | # ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
17 | # TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
18 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
19 | # SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
20 | # ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
21 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
23 | # OR OTHER DEALINGS IN THE SOFTWARE.
24 | # ------------------------------------------------------------------------------
25 | from __future__ import print_function
26 | import os
27 | import sys
28 |
29 | from pyupdater.client import Client
30 | from dsdev_utils.paths import get_mac_dot_app_dir
31 |
32 | import client_config
33 |
34 | APPNAME = "Acme"
35 | VERSION = "4.1"
36 |
37 |
38 | def cb(status):
39 | out = ""
40 | percent = status.get("percent_complete")
41 | time = status.get("time")
42 | if percent is not None:
43 | out += "{}%".format(percent)
44 | if time is not None:
45 | out += " - {}".format(time)
46 | if len(out) == 0:
47 | out = "Nothing yet..."
48 | sys.stdout.write("\r{}".format(out))
49 | sys.stdout.flush()
50 |
51 |
52 | def main():
53 | print(VERSION)
54 | data_dir = None
55 | config = client_config.ClientConfig()
56 | if getattr(config, "USE_CUSTOM_DIR", False):
57 | if sys.platform == "darwin" and os.path.dirname(sys.executable).endswith(
58 | "MacOS"
59 | ):
60 | data_dir = os.path.join(
61 | get_mac_dot_app_dir(os.path.dirname(sys.executable)), ".update"
62 | )
63 | else:
64 | data_dir = os.path.join(
65 | os.path.dirname(os.path.dirname(sys.executable)), ".update"
66 | )
67 | client = Client(config, refresh=True, progress_hooks=[cb], data_dir=data_dir)
68 | update = client.update_check(APPNAME, VERSION)
69 | if update is not None:
70 | success = update.download()
71 | print("")
72 | if success is True:
73 | print("Update download successful")
74 | print("Extracting & overwriting")
75 | update.extract_overwrite()
76 | else:
77 | print("Failed to download update")
78 | return VERSION
79 |
80 |
81 | if __name__ == "__main__":
82 | main()
83 |
--------------------------------------------------------------------------------
/tests/data/update_repo_extract/app_extract_onedir.py:
--------------------------------------------------------------------------------
1 | # ------------------------------------------------------------------------------
2 | # Copyright (c) 2015-2020 Digital Sapphire
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining
5 | # a copy of this software and associated documentation files
6 | # (the "Software"), to deal in the Software without restriction, including
7 | # without limitation the rights to use, copy, modify, merge, publish,
8 | # distribute, sublicense, and/or sell copies of the Software, and to permit
9 | # persons to whom the Software is furnished to do so, subject to the
10 | # following conditions:
11 | #
12 | # The above copyright notice and this permission notice shall be
13 | # included in all copies or substantial portions of the Software.
14 | #
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
16 | # ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
17 | # TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
18 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
19 | # SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
20 | # ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
21 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
23 | # OR OTHER DEALINGS IN THE SOFTWARE.
24 | # ------------------------------------------------------------------------------
25 | from __future__ import print_function
26 | import os
27 | import sys
28 |
29 | from pyupdater.client import Client
30 | from dsdev_utils.paths import get_mac_dot_app_dir
31 |
32 | import client_config
33 |
34 | APPNAME = "Acme"
35 | VERSION = "4.1"
36 |
37 |
38 | def cb(status):
39 | out = ""
40 | percent = status.get("percent_complete")
41 | time = status.get("time")
42 | if percent is not None:
43 | out += "{}%".format(percent)
44 | if time is not None:
45 | out += " - {}".format(time)
46 | if len(out) == 0:
47 | out = "Nothing yet..."
48 | sys.stdout.write("\r{}".format(out))
49 | sys.stdout.flush()
50 |
51 |
52 | def main():
53 | print(VERSION)
54 | data_dir = None
55 | config = client_config.ClientConfig()
56 | if getattr(config, "USE_CUSTOM_DIR", False):
57 | if sys.platform == "darwin" and os.path.dirname(sys.executable).endswith(
58 | "MacOS"
59 | ):
60 | data_dir = os.path.join(
61 | get_mac_dot_app_dir(os.path.dirname(sys.executable)), ".update"
62 | )
63 | else:
64 | data_dir = os.path.join(
65 | os.path.dirname(os.path.dirname(sys.executable)), ".update"
66 | )
67 | client = Client(config, refresh=True, progress_hooks=[cb], data_dir=data_dir)
68 | update = client.update_check(APPNAME, VERSION)
69 | if update is not None:
70 | success = update.download()
71 | print("")
72 | if success is True:
73 | print("Update download successful")
74 | print("Extracting & overwriting")
75 | update.extract_overwrite()
76 | else:
77 | print("Failed to download update")
78 | return VERSION
79 |
80 |
81 | if __name__ == "__main__":
82 | main()
83 |
--------------------------------------------------------------------------------
/tests/data/update_repo_restart/app_restart_01.py:
--------------------------------------------------------------------------------
1 | # ------------------------------------------------------------------------------
2 | # Copyright (c) 2015-2020 Digital Sapphire
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining
5 | # a copy of this software and associated documentation files
6 | # (the "Software"), to deal in the Software without restriction, including
7 | # without limitation the rights to use, copy, modify, merge, publish,
8 | # distribute, sublicense, and/or sell copies of the Software, and to permit
9 | # persons to whom the Software is furnished to do so, subject to the
10 | # following conditions:
11 | #
12 | # The above copyright notice and this permission notice shall be
13 | # included in all copies or substantial portions of the Software.
14 | #
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
16 | # ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
17 | # TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
18 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
19 | # SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
20 | # ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
21 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
23 | # OR OTHER DEALINGS IN THE SOFTWARE.
24 | # ------------------------------------------------------------------------------
25 | from __future__ import print_function
26 | import os
27 | import sys
28 |
29 | from pyupdater.client import Client
30 | from dsdev_utils.paths import get_mac_dot_app_dir
31 |
32 | import client_config
33 |
34 | APPNAME = "Acme"
35 | VERSION = "4.1"
36 |
37 |
38 | def cb(status):
39 | out = ""
40 | percent = status.get("percent_complete")
41 | time = status.get("time")
42 | if percent is not None:
43 | out += "{}%".format(percent)
44 | if time is not None:
45 | out += " - {}".format(time)
46 | if len(out) == 0:
47 | out = "Nothing yet..."
48 | sys.stdout.write("\r{}".format(out))
49 | sys.stdout.flush()
50 |
51 |
52 | def main():
53 | print(VERSION)
54 | data_dir = None
55 | config = client_config.ClientConfig()
56 | if getattr(config, "USE_CUSTOM_DIR", False):
57 | print("Using custom directory")
58 | if sys.platform == "darwin" and os.path.dirname(sys.executable).endswith(
59 | "MacOS"
60 | ):
61 | data_dir = os.path.join(
62 | get_mac_dot_app_dir(os.path.dirname(sys.executable)), ".update"
63 | )
64 | else:
65 | data_dir = os.path.join(os.path.dirname(sys.executable), ".update")
66 | client = Client(config, refresh=True, progress_hooks=[cb], data_dir=data_dir)
67 | update = client.update_check(APPNAME, VERSION)
68 | if update is not None:
69 | print("We have an update")
70 | retry_count = 0
71 | while retry_count < 5:
72 | success = update.download()
73 | if success is True:
74 | break
75 | print("Retry Download. Count {}".format(retry_count + 1))
76 | retry_count += 1
77 |
78 | if success:
79 | print("Update download successful")
80 | print("Restarting")
81 | update.extract_restart()
82 | else:
83 | print("Failed to download update")
84 |
85 | print("Leaving main()")
86 |
87 |
88 | if __name__ == "__main__":
89 | main()
90 |
--------------------------------------------------------------------------------
/pyupdater/utils/pyinstaller_compat.py:
--------------------------------------------------------------------------------
1 | # ------------------------------------------------------------------------------
2 | # Copyright (c) 2015-2020 Digital Sapphire
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining
5 | # a copy of this software and associated documentation files
6 | # (the "Software"), to deal in the Software without restriction, including
7 | # without limitation the rights to use, copy, modify, merge, publish,
8 | # distribute, sublicense, and/or sell copies of the Software, and to permit
9 | # persons to whom the Software is furnished to do so, subject to the
10 | # following conditions:
11 | #
12 | # The above copyright notice and this permission notice shall be
13 | # included in all copies or substantial portions of the Software.
14 | #
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
16 | # ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
17 | # TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
18 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
19 | # SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
20 | # ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
21 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
23 | # OR OTHER DEALINGS IN THE SOFTWARE.
24 | # ------------------------------------------------------------------------------
25 | import argparse
26 | import logging
27 | import os
28 |
29 | try:
30 | from PyInstaller import __version__ as pyi_version
31 | except ImportError:
32 | pyi_version = "0.0"
33 | from PyInstaller.building import makespec as _pyi_makespec
34 | from PyInstaller import compat as _pyi_compat
35 | from PyInstaller import log as _pyi_log
36 |
37 |
38 | log = logging.getLogger(__name__)
39 |
40 |
41 | def pyi_makespec(pyi_args): # pragma: no cover
42 | """Wrapper to configure make_spec for multipule pyinstaller versions"""
43 |
44 | def run_makespec(_args):
45 | """Setup args & run make_spec command"""
46 | # Split pathex by using the path separator
47 | temppaths = _args.pathex[:]
48 | _args.pathex = []
49 | for p in temppaths:
50 | _args.pathex.extend(p.split(os.pathsep))
51 |
52 | # Fix for issue #4 - Not searching cwd for modules
53 | _args.pathex.insert(0, os.getcwd())
54 |
55 | spec_file = _pyi_makespec.main(_args.scriptname, **vars(_args))
56 | log.debug("wrote %s", spec_file)
57 |
58 | parser = argparse.ArgumentParser()
59 | # We are hacking into pyinstaller here & are aware of the risks
60 | # using noqa below so landscape.io will ignore it
61 | _pyi_makespec.__add_options(parser) # noqa
62 | _pyi_log.__add_options(parser) # noqa
63 | if hasattr(_pyi_compat, "__add_obsolete_options"):
64 | _pyi_compat.__add_obsolete_options(parser) # noqa
65 | # End hacking
66 | parser.add_argument("scriptname", nargs="+")
67 |
68 | args = parser.parse_args(pyi_args)
69 |
70 | # We call init because it loads logger into the global
71 | # namespace of the Pyinstaller.log module. logger is used
72 | # in the Pyinstaller.log.__process_options call
73 | if hasattr(_pyi_log, "init"):
74 | _pyi_log.init()
75 | # We are hacking into pyinstaller here & are aware of the risks
76 | # using noqa below so landscape.io will ignore it
77 | _pyi_log.__process_options(parser, args) # noqa
78 | # End hacking
79 |
80 | run_makespec(args)
81 |
82 | return True
83 |
--------------------------------------------------------------------------------
/tests/data/update_repo_restart/app_restart_onedir.py:
--------------------------------------------------------------------------------
1 | # ------------------------------------------------------------------------------
2 | # Copyright (c) 2015-2020 Digital Sapphire
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining
5 | # a copy of this software and associated documentation files
6 | # (the "Software"), to deal in the Software without restriction, including
7 | # without limitation the rights to use, copy, modify, merge, publish,
8 | # distribute, sublicense, and/or sell copies of the Software, and to permit
9 | # persons to whom the Software is furnished to do so, subject to the
10 | # following conditions:
11 | #
12 | # The above copyright notice and this permission notice shall be
13 | # included in all copies or substantial portions of the Software.
14 | #
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
16 | # ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
17 | # TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
18 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
19 | # SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
20 | # ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
21 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
23 | # OR OTHER DEALINGS IN THE SOFTWARE.
24 | # ------------------------------------------------------------------------------
25 | from __future__ import print_function
26 | import os
27 | import sys
28 |
29 | from pyupdater.client import Client
30 | from dsdev_utils.paths import get_mac_dot_app_dir
31 |
32 | import client_config
33 |
34 | APPNAME = "Acme"
35 | VERSION = "4.1"
36 |
37 |
38 | def cb(status):
39 | out = ""
40 | percent = status.get("percent_complete")
41 | time = status.get("time")
42 | if percent is not None:
43 | out += "{}%".format(percent)
44 | if time is not None:
45 | out += " - {}".format(time)
46 | if len(out) == 0:
47 | out = "Nothing yet..."
48 | sys.stdout.write("\r{}".format(out))
49 | sys.stdout.flush()
50 |
51 |
52 | def main():
53 | print(VERSION)
54 | data_dir = None
55 | config = client_config.ClientConfig()
56 | if getattr(config, "USE_CUSTOM_DIR", False):
57 | print("Using custom directory")
58 | if sys.platform == "darwin" and os.path.dirname(sys.executable).endswith(
59 | "MacOS"
60 | ):
61 | data_dir = os.path.join(
62 | get_mac_dot_app_dir(os.path.dirname(sys.executable)), ".update"
63 | )
64 | else:
65 | data_dir = os.path.join(
66 | os.path.dirname(os.path.dirname(sys.executable)), ".update"
67 | )
68 | client = Client(config, refresh=True, progress_hooks=[cb], data_dir=data_dir)
69 | update = client.update_check(APPNAME, VERSION)
70 | if update is not None:
71 | print("We have an update")
72 | retry_count = 0
73 | while retry_count < 5:
74 | success = update.download()
75 | if success is True:
76 | break
77 | print("Retry Download. Count {}".format(retry_count + 1))
78 | retry_count += 1
79 |
80 | if success:
81 | print("Update download successful")
82 | print("Restarting")
83 | update.extract_restart()
84 | else:
85 | print("Failed to download update")
86 |
87 | print("Leaving main()")
88 |
89 |
90 | if __name__ == "__main__":
91 | main()
92 |
--------------------------------------------------------------------------------
/pyupdater/cli/__init__.py:
--------------------------------------------------------------------------------
1 | # ------------------------------------------------------------------------------
2 | # Copyright (c) 2015-2020 Digital Sapphire
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining
5 | # a copy of this software and associated documentation files
6 | # (the "Software"), to deal in the Software without restriction, including
7 | # without limitation the rights to use, copy, modify, merge, publish,
8 | # distribute, sublicense, and/or sell copies of the Software, and to permit
9 | # persons to whom the Software is furnished to do so, subject to the
10 | # following conditions:
11 | #
12 | # The above copyright notice and this permission notice shall be
13 | # included in all copies or substantial portions of the Software.
14 | #
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
16 | # ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
17 | # TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
18 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
19 | # SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
20 | # ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
21 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
23 | # OR OTHER DEALINGS IN THE SOFTWARE.
24 | # ------------------------------------------------------------------------------
25 | from __future__ import unicode_literals
26 | import logging
27 | import sys
28 |
29 | from appdirs import user_log_dir
30 |
31 | from pyupdater import __version__, settings
32 | from pyupdater.cli import commands
33 | from pyupdater.cli.options import get_parser
34 |
35 |
36 | logging.getLogger("dsdev_utils").setLevel(logging.ERROR)
37 |
38 | log = logging.getLogger(__name__)
39 |
40 | # The collect_debug_info command will use this
41 | commands.LOG_DIR = user_log_dir(settings.APP_NAME, settings.APP_AUTHOR)
42 |
43 |
44 | def _real_main(args, namespace_test_helper=None): # pragma: no cover
45 | pyi_args = None
46 | if args is None:
47 | args = sys.argv[1:]
48 |
49 | if namespace_test_helper is None:
50 | parser = get_parser()
51 | args, pyi_args = parser.parse_known_args(args)
52 | else:
53 | # Used for tests
54 | args = namespace_test_helper
55 | dispatch_command(args, pyi_args)
56 |
57 |
58 | # Dispatch the passed in command to its respective function in
59 | # pyupdater.cli.commands
60 | def dispatch_command(args, pyi_args=None, test=False):
61 | # Turns collect-debug-info into collect_debug_info
62 | cmd_str = "_cmd_" + args.command.replace("-", "_")
63 | if hasattr(commands, cmd_str):
64 | cmd = getattr(commands, cmd_str)
65 | # We are just making sure we can load the function
66 | if test:
67 | return True
68 | cmd(args, pyi_args)
69 | else:
70 | # This should only get hit by misconfigured tests.
71 | # "Should" being the key word here :)
72 | log.error("Unknown Command: %s", cmd_str)
73 | return False
74 |
75 |
76 | def main(args=None): # pragma: no cover
77 | log.info("PyUpdater %s", __version__)
78 | try:
79 | _real_main(args)
80 | except KeyboardInterrupt:
81 | # Someones quick on the draw
82 | print("\n")
83 | msg = "Exited by user"
84 | log.warning(msg)
85 | except Exception as err:
86 | print(err)
87 | log.error(err)
88 | log.debug(err, exc_info=True)
89 |
90 |
91 | if __name__ == "__main__": # pragma: no cover
92 | main(sys.argv[1:])
93 |
--------------------------------------------------------------------------------
/docs/usage-client-advanced.md:
--------------------------------------------------------------------------------
1 | # Usage | Client | Advanced
2 | PyUpdater supports 3 release channels. Release channels are specified when providing a version number to the --app-version flag. Patches are only created for the stable channel. Examples below.
3 |
4 | ### Example - Update check
5 |
6 | Examples below of specifying channels when checking for updates:
7 | ```
8 | # Requesting updates from the beta channel
9 | app_update = client.update_check(APP_NAME, APP_VERSION, channel='beta')
10 |
11 | # If no channel is specified, stable will be used
12 | app_update = client.update_check(APP_NAME, APP_VERSION)
13 | ```
14 |
15 | Example of background download:
16 | ```
17 | app_update = client.update_check(APP_NAME, APP_VERSION)
18 | if app_update:
19 | app_update.download(background=True)
20 |
21 | # To check the status of the download
22 | # Returns a boolean
23 | app_update.is_downloaded()
24 | ```
25 |
26 | Examples of setting one or more callbacks. PyUpdater calls set() on the list of plugins to ensure duplicates are not added.
27 | ```
28 | # Progress hooks get passed a dict with the below keys.
29 | # total: total file size
30 | # downloaded: data received so far
31 | # status: will show either downloading or finished
32 | # percent_complete: Percentage of file downloaded so far
33 | # time: Time left to complete download
34 |
35 | def progress(data):
36 | print('Time remaining'.format(data['time']))
37 |
38 | def log_progress(data):
39 | log.debug('Total file size %s', data['total'])
40 |
41 |
42 | # You can initialize the client with a callbacks
43 | client = Client(ClientConfig(), progress_hooks=[progress, log_progress])
44 |
45 | # Or you can add them later.
46 | client = Client(ClientConfig())
47 | client.add_progress_hook(log_progress)
48 | client.add_progress_hook(progress)
49 | ```
50 |
51 | ### Using basic authentication
52 |
53 | Basic authentication is an easy way to prevent unauthorized people from downloading your app from your update server.
54 |
55 | Once you've configured your web server to require basic authentication from clients accessing your update repository, modify your update client code like below.
56 |
57 | Other headers can be sent in the same way.
58 | ```
59 | headers = {'basic_auth': 'user:pass'}
60 | client = Client(ClientConfig(), headers=headers)
61 | ```
62 |
63 |
64 | ### Use your own file downloader
65 | PyUpdater was originally written to work with servers that provide a directory listing.
66 | For services which expose an API to retrieve files, you can use a custom downloader.
67 | Your custom downloader must have the same signature as the MyDownloader class below.
68 |
69 | Do note that your file downloader will not always be given a hexdigest kwarg. In those
70 | cases skip hex verification.
71 |
72 | ```python
73 | from pyupdater.client import Client, DefaultClientConfig
74 |
75 |
76 | class MyDownloader:
77 |
78 | def __init__(self, filename, urls, **kwargs):
79 | self.filename = filename
80 | self.urls = urls
81 | self.hexdigest = kwargs.get("hexdigest")
82 |
83 | self._data = None
84 |
85 | def download_verify_return(self):
86 | # Download the data from the endpoint and return
87 | return self._data
88 |
89 | def download_verify_write(self):
90 | # Write the downloaded data to the current dir
91 | try:
92 | with open(self.filename, 'wb') as f:
93 | f.write(self._data)
94 | return True
95 | except:
96 | return False
97 |
98 | client = Client(DefaultClientConfig(), downloader=MyDownloader)
99 |
100 | ```
101 |
--------------------------------------------------------------------------------
/pyupdater/core/__init__.py:
--------------------------------------------------------------------------------
1 | # ------------------------------------------------------------------------------
2 | # Copyright (c) 2015-2020 Digital Sapphire
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining
5 | # a copy of this software and associated documentation files
6 | # (the "Software"), to deal in the Software without restriction, including
7 | # without limitation the rights to use, copy, modify, merge, publish,
8 | # distribute, sublicense, and/or sell copies of the Software, and to permit
9 | # persons to whom the Software is furnished to do so, subject to the
10 | # following conditions:
11 | #
12 | # The above copyright notice and this permission notice shall be
13 | # included in all copies or substantial portions of the Software.
14 | #
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
16 | # ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
17 | # TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
18 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
19 | # SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
20 | # ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
21 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
23 | # OR OTHER DEALINGS IN THE SOFTWARE.
24 | # ------------------------------------------------------------------------------
25 | from __future__ import unicode_literals
26 | import os
27 |
28 | from .key_handler import KeyHandler
29 | from .key_handler.keys import KeyImporter
30 | from .package_handler import PackageHandler
31 | from .uploader import Uploader
32 | from pyupdater.utils.config import Config
33 |
34 |
35 | class PyUpdater(object):
36 | """Processes, signs & uploads updates
37 |
38 | Kwargs:
39 |
40 | config (obj): config object
41 | """
42 |
43 | def __init__(self, config=None):
44 | self.config = Config()
45 | # Important to keep this before updating config
46 | if config is not None:
47 | self.update_config(config)
48 |
49 | def update_config(self, config):
50 | """Updates internal config
51 |
52 | Args:
53 |
54 | config (obj): config object
55 | """
56 | if not hasattr(config, "DATA_DIR"):
57 | config.DATA_DIR = None
58 | if config.DATA_DIR is None:
59 | config.DATA_DIR = os.getcwd()
60 | self.config.from_object(config)
61 | self._update(self.config)
62 |
63 | def _update(self, config):
64 | self.kh = KeyHandler()
65 | self.key_importer = KeyImporter()
66 | self.ph = PackageHandler(config)
67 | self.up = Uploader(config)
68 |
69 | def setup(self):
70 | """Sets up root dir with required PyUpdater folders"""
71 | self.ph.setup()
72 |
73 | def process_packages(self, report_errors=False):
74 | """Creates hash for updates & adds information about update to
75 | version file
76 | """
77 | self.ph.process_packages(report_errors)
78 |
79 | def set_uploader(self, requested_uploader, keep=False):
80 | """Sets upload destination
81 |
82 | Args:
83 |
84 | requested_uploader (str): upload service. i.e. s3, scp
85 |
86 | keep (bool): False to delete files after upload.
87 | True to keep files. Default False.
88 |
89 | """
90 | self.up.set_uploader(requested_uploader, keep)
91 |
92 | def upload(self):
93 | """Uploads files in deploy folder"""
94 | return self.up.upload()
95 |
96 | def get_plugin_names(self):
97 | return self.up.get_plugin_names()
98 |
99 | def import_keypack(self):
100 | """Creates signing keys"""
101 | return self.key_importer.start()
102 |
103 | def sign_update(self, split_version):
104 | """Signs version file with signing key"""
105 | self.kh.sign_update(split_version)
106 |
--------------------------------------------------------------------------------
/pyupdater/utils/storage.py:
--------------------------------------------------------------------------------
1 | # ------------------------------------------------------------------------------
2 | # Copyright (c) 2015-2020 Digital Sapphire
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining
5 | # a copy of this software and associated documentation files
6 | # (the "Software"), to deal in the Software without restriction, including
7 | # without limitation the rights to use, copy, modify, merge, publish,
8 | # distribute, sublicense, and/or sell copies of the Software, and to permit
9 | # persons to whom the Software is furnished to do so, subject to the
10 | # following conditions:
11 | #
12 | # The above copyright notice and this permission notice shall be
13 | # included in all copies or substantial portions of the Software.
14 | #
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
16 | # ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
17 | # TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
18 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
19 | # SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
20 | # ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
21 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
23 | # OR OTHER DEALINGS IN THE SOFTWARE.
24 | # ------------------------------------------------------------------------------
25 | from __future__ import print_function, unicode_literals
26 | import logging
27 | import os
28 |
29 | from pyupdater import settings
30 | from pyupdater.utils import JSONStore
31 |
32 | log = logging.getLogger(__name__)
33 |
34 |
35 | # Used by KeyHandler, PackageHandler & Config to
36 | # store data in a json file
37 | class Storage(object):
38 | def __init__(self):
39 | """Loads & saves config file to file-system."""
40 | self.config_dir = os.path.join(os.getcwd(), settings.CONFIG_DATA_FOLDER)
41 | if not os.path.exists(self.config_dir):
42 | log.debug("Creating config dir")
43 | os.mkdir(self.config_dir)
44 | log.debug("Config Dir: %s", self.config_dir)
45 | self.filename = os.path.join(self.config_dir, settings.CONFIG_FILE_USER)
46 | log.debug("Config DB: %s", self.filename)
47 | self.db = JSONStore(self.filename)
48 | self.count = 0
49 | self._load_db()
50 |
51 | def __getattr__(self, name):
52 | return self.__class__.__dict__.get(name)
53 |
54 | def __setattr__(self, name, value):
55 | setattr(self.__class__, name, value)
56 |
57 | def __delattr__(self, name):
58 | raise AttributeError("Cannot delete attributes!")
59 |
60 | def __getitem__(self, name):
61 | try:
62 | return self.__class__.__dict__[name]
63 | except KeyError:
64 | return self.__dict__[name]
65 |
66 | def __setitem__(self, name, value):
67 | setattr(Storage, name, value)
68 |
69 | def _load_db(self):
70 | """Loads database into memory."""
71 | for k, v in self.db:
72 | setattr(Storage, k, v)
73 |
74 | def save(self, key, value):
75 | """Saves key & value to database
76 |
77 | Args:
78 |
79 | key (str): used to retrieve value from database
80 |
81 | value (obj): python object to store in database
82 |
83 | """
84 | setattr(Storage, key, value)
85 | for k, v in Storage.__dict__.items():
86 | self.db[k] = v
87 | log.debug("Syncing db to filesystem")
88 | self.db.sync()
89 |
90 | def load(self, key):
91 | """Loads value for given key
92 |
93 | Args:
94 |
95 | key (str): The key associated with the value you want
96 | form the database.
97 |
98 | Returns:
99 |
100 | Object if exists or else None
101 | """
102 | return self.__class__.__dict__.get(key)
103 |
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | # ------------------------------------------------------------------------------
2 | # Copyright (c) 2015-2020 Digital Sapphire
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining
5 | # a copy of this software and associated documentation files
6 | # (the "Software"), to deal in the Software without restriction, including
7 | # without limitation the rights to use, copy, modify, merge, publish,
8 | # distribute, sublicense, and/or sell copies of the Software, and to permit
9 | # persons to whom the Software is furnished to do so, subject to the
10 | # following conditions:
11 | #
12 | # The above copyright notice and this permission notice shall be
13 | # included in all copies or substantial portions of the Software.
14 | #
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
16 | # ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
17 | # TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
18 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
19 | # SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
20 | # ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
21 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
23 | # OR OTHER DEALINGS IN THE SOFTWARE.
24 | # ------------------------------------------------------------------------------
25 | import os
26 | import tempfile
27 | import threading
28 |
29 | from http.server import SimpleHTTPRequestHandler as RequestHandler
30 |
31 | import socketserver as SocketServer
32 |
33 | import pytest
34 |
35 | from pyupdater import PyUpdater
36 | from pyupdater.cli.options import make_parser
37 | from pyupdater.client import Client
38 | from pyupdater.core.key_handler.keys import Keys
39 | from pyupdater.utils.config import ConfigManager
40 | from pyupdater.utils.storage import Storage
41 | from tconfig import TConfig
42 |
43 |
44 | @pytest.fixture
45 | def cleandir():
46 | new_path = tempfile.mkdtemp()
47 | os.chdir(new_path)
48 |
49 |
50 | @pytest.fixture
51 | def client():
52 | t_config = TConfig()
53 | t_config.DATA_DIR = os.getcwd()
54 | client = Client(t_config, refresh=True, test=True)
55 | client.FROZEN = True
56 | return client
57 |
58 |
59 | @pytest.fixture
60 | def create_keypack():
61 | keys = Keys(test=True)
62 | keys.make_keypack("test")
63 |
64 |
65 | @pytest.fixture
66 | def db():
67 | db = Storage()
68 | return db
69 |
70 |
71 | @pytest.fixture
72 | def loader():
73 | default_config = {
74 | "APP_NAME": "PyUpdater Test",
75 | "COMPANY_NAME": "ACME",
76 | "UPDATE_PATCHES": True,
77 | }
78 |
79 | cm = ConfigManager()
80 | config = cm.load_config()
81 | config.update(default_config)
82 | cm.save_config(config)
83 | return config
84 |
85 |
86 | @pytest.fixture
87 | def parser():
88 | parser = make_parser()
89 | return parser
90 |
91 |
92 | @pytest.fixture
93 | def pyu():
94 | t_config = TConfig()
95 | t_config.DATA_DIR = os.getcwd()
96 | pyu = PyUpdater(t_config)
97 | return pyu
98 |
99 |
100 | @pytest.fixture
101 | def simpleserver():
102 | class Server(object):
103 | def __init__(self):
104 | self.count = 0
105 | self._server = None
106 |
107 | def start(self, port=None):
108 | if port is None:
109 | raise ValueError("Port cannot be None.")
110 | self.count += 1
111 | if self._server is not None:
112 | return
113 | SocketServer.TCPServer.allow_reuse_address = True
114 | httpd = SocketServer.TCPServer(("", port), RequestHandler)
115 |
116 | self._server = threading.Thread(target=httpd.serve_forever)
117 | self._server.daemon = True
118 | self._server.start()
119 |
120 | def stop(self):
121 | self.count -= 1
122 | if self._server is not None and self.count == 0:
123 | self._server.alive = False
124 | self._server = None
125 |
126 | return Server()
127 |
--------------------------------------------------------------------------------
/docs/usage-cli.md:
--------------------------------------------------------------------------------
1 | # Usage | CLI
2 |
3 | ### Step 1 - Create Keypack
4 |
5 | Create a keypack on an air-gapped computer, not your dev machine, for best security. You'll be asked for the name of the application. It's important that you use the same name when creating a new keypack for the same application. If you don't your application will not be able to verify update meta-data and will not auto-update!
6 |
7 | ```
8 | $ pyupdater keys -c
9 | ```
10 |
11 | ### Step 2 - Copy Keypack
12 |
13 | Copy the keypack to the dev machine & place in the root of the code repository.
14 |
15 | ```bash
16 | $ scp dev-machine:keypack.pyu .
17 | ```
18 |
19 | ### Step 3 - Init Repo
20 |
21 | In the root of your repo run the init command. You'll be asked a few questions about your application then 2 directories & a config file will be created. The first directory is pyu-data which is used as a working directory for PyUpdater. The other is a hidden directory for repository configuration and data files.
22 |
23 | ```
24 | $ pyupdater init
25 | ```
26 |
27 | ### Step 4 - Import Keypack
28 |
29 | Make sure the keypack file is in the root the repo. After you've successfully imported the keypack it's safe to delete.
30 |
31 | ```
32 | $ pyupdater keys -i
33 | ```
34 |
35 | ### Step 5 - Integrate PyUpdater
36 |
37 | [See Usage | Client](usage-client.md)
38 |
39 | ### Step 6 - Make Spec
40 |
41 | Is your app more of the demanding type? Generate your spec file with PyUpdater then make edits as necessary. If you do not need a custom spec file skip to the next step. Please see the spec-file section in the [Danger Zone](danger-zone.md).
42 |
43 | ```
44 | $ pyupdater make-spec -w main.py
45 |
46 | # or
47 |
48 | $ pyupdater make-spec -F -w main.py
49 |
50 | # To show pyinstaller build info use --pyinstaller-log-info
51 | ```
52 |
53 | ### Step 7 - Build
54 |
55 | You have two options when building. You can specify a spec file or a main script. Please see the version numbers section in the [Danger Zone](danger-zone.md).
56 |
57 | ```
58 | # Build from a spec file.
59 | $ pyupdater build --app-version=1.0.0 main.spec
60 |
61 | # Build from a script.
62 | $ pyupdater build --app-version=1.0.0 main.py
63 |
64 | # To show pyinstaller build info use --pyinstaller-log-info
65 | ```
66 |
67 | ### Step 8 - Create patches
68 |
69 | Gets sha256 hashes, file sizes, adds meta-data to version manifest and copies files to deploy folder. If a source files is available, patches will also be created. You can combine --process with --sign.
70 |
71 | ```
72 | $ pyupdater pkg --process
73 | ```
74 |
75 | ### Step 9 - Cryptographically Sign
76 |
77 | Now lets sign & gzip our version manifest, gzip our keyfile & place both in the deploy directory. Note that the signing process works without any user intervention. You can combine --sign with --process
78 |
79 | ```
80 | $ pyupdater pkg --sign
81 |
82 | # For CI/CD
83 | $ pyupdater pkg --sign --split-version
84 | ```
85 |
86 | ### Step 10 - List Installed Plugins
87 |
88 | Get a list of the plugins you have installed. You probably wont have to run this many times :)
89 |
90 | ```
91 | # You can install the official plugins with
92 | # pip install pyupdater[s3, scp]
93 | $ pyupdater plugins
94 |
95 | s3 by Digital Sapphire
96 | scp by Digital Sapphire
97 |
98 | ```
99 |
100 | ### Step 11 - Configure Plugin
101 |
102 | Your plugin may or may not need configuration. If you are not sure then please check the documentation for that plugin.
103 |
104 | ```
105 | $ pyupdater settings --plugin s3
106 | ```
107 |
108 | ### Step 12 - Upload
109 |
110 | We've made it. Time to upload our updates, patches & metadata. You won't have any patches to upload on the first run. Don't worry they'll be there next time.
111 |
112 | ```
113 | $ pyupdater upload --service s3
114 | ```
115 |
116 | ### Debugging Issues
117 |
118 | Sometimes issues arise like misconfigured upload URLs or keys. To debug issues that involve PyUpdater, create the file `pyu.log` in the root of your app.
119 |
120 | PyUpdater will log debug statements to this file and hopefully provide you with information to assist with your debugging.
121 |
--------------------------------------------------------------------------------
/pyupdater/utils/exceptions.py:
--------------------------------------------------------------------------------
1 | # ------------------------------------------------------------------------------
2 | # Copyright (c) 2015-2020 Digital Sapphire
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining
5 | # a copy of this software and associated documentation files
6 | # (the "Software"), to deal in the Software without restriction, including
7 | # without limitation the rights to use, copy, modify, merge, publish,
8 | # distribute, sublicense, and/or sell copies of the Software, and to permit
9 | # persons to whom the Software is furnished to do so, subject to the
10 | # following conditions:
11 | #
12 | # The above copyright notice and this permission notice shall be
13 | # included in all copies or substantial portions of the Software.
14 | #
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
16 | # ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
17 | # TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
18 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
19 | # SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
20 | # ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
21 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
23 | # OR OTHER DEALINGS IN THE SOFTWARE.
24 | # ------------------------------------------------------------------------------
25 | from __future__ import unicode_literals
26 | import sys
27 | import traceback
28 |
29 |
30 | class STDError(Exception):
31 | """Extends exceptions to show added message if error isn't expected.
32 |
33 | Args:
34 |
35 | msg (str): error message
36 |
37 | Kwargs:
38 |
39 | tb (obj): is the original traceback so that it can be printed.
40 |
41 | expected (bool):
42 |
43 | Meaning:
44 |
45 | True - Report issue msg not shown
46 |
47 | False - Report issue msg shown
48 | """
49 |
50 | def __init__(self, msg, tb=None, expected=False):
51 | if expected is False:
52 | msg = msg + (
53 | "; please report this issue on https://github.com"
54 | "/Digital-Sapphire/PyUpdater/issues"
55 | )
56 | super(STDError, self).__init__(msg)
57 |
58 | self.traceback = tb
59 | self.exc_info = sys.exc_info() # preserve original exception
60 |
61 | def format_traceback(self): # pragma: no cover
62 | if self.traceback is None:
63 | return None
64 | return "".join(traceback.format_tb(self.traceback))
65 |
66 |
67 | class ClientError(STDError):
68 | """Raised for Client exceptions"""
69 |
70 | def __init__(self, *args, **kwargs):
71 | super(ClientError, self).__init__(*args, **kwargs)
72 |
73 |
74 | class FileDownloaderError(STDError):
75 | def __init__(self, *args, **kwargs):
76 | super(FileDownloaderError, self).__init__(*args, **kwargs)
77 |
78 |
79 | class KeyHandlerError(STDError):
80 | def __init__(self, *args, **kwargs):
81 | super(KeyHandlerError, self).__init__(*args, **kwargs)
82 |
83 |
84 | class PackageHandlerError(STDError):
85 | """Raised for PackageHandler exceptions"""
86 |
87 | def __init__(self, *args, **kwargs):
88 | super(PackageHandlerError, self).__init__(*args, **kwargs)
89 |
90 |
91 | class PatcherError(STDError):
92 | """Raised for Patcher exceptions"""
93 |
94 | def __init__(self, *args, **kwargs):
95 | super(PatcherError, self).__init__(*args, **kwargs)
96 |
97 |
98 | class UploaderError(STDError):
99 | """Raised for Uploader exceptions"""
100 |
101 | def __init__(self, *args, **kwargs):
102 | super(UploaderError, self).__init__(*args, **kwargs)
103 |
104 |
105 | class UploaderPluginError(STDError):
106 | """Raised for Uploader exceptions"""
107 |
108 | def __init__(self, *args, **kwargs):
109 | super(UploaderPluginError, self).__init__(*args, **kwargs)
110 |
111 |
112 | class UtilsError(STDError):
113 | """Raised for Utils exceptions"""
114 |
115 | def __init__(self, *args, **kwargs):
116 | super(UtilsError, self).__init__(*args, **kwargs)
117 |
--------------------------------------------------------------------------------
/tests/test_uploader.py:
--------------------------------------------------------------------------------
1 | # ------------------------------------------------------------------------------
2 | # Copyright (c) 2015-2020 Digital Sapphire
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining
5 | # a copy of this software and associated documentation files
6 | # (the "Software"), to deal in the Software without restriction, including
7 | # without limitation the rights to use, copy, modify, merge, publish,
8 | # distribute, sublicense, and/or sell copies of the Software, and to permit
9 | # persons to whom the Software is furnished to do so, subject to the
10 | # following conditions:
11 | #
12 | # The above copyright notice and this permission notice shall be
13 | # included in all copies or substantial portions of the Software.
14 | #
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
16 | # ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
17 | # TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
18 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
19 | # SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
20 | # ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
21 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
23 | # OR OTHER DEALINGS IN THE SOFTWARE.
24 | # ------------------------------------------------------------------------------
25 | import os
26 | import sys
27 |
28 | from dsdev_utils.paths import ChDir
29 | import pytest
30 |
31 | from pyupdater.core.uploader import BaseUploader, Uploader
32 | from pyupdater.utils.config import Config
33 |
34 |
35 | class BaseTestUploader(BaseUploader):
36 | def init_config(self, config):
37 | pass
38 |
39 | def set_config(self, config):
40 | pass
41 |
42 |
43 | class TestUploaderPass(BaseTestUploader):
44 |
45 | name = "success"
46 | author = "Digital Sapphire"
47 |
48 | def upload_file(self, filename):
49 | print(filename)
50 | return True
51 |
52 |
53 | class TestUploaderFail(BaseTestUploader):
54 |
55 | name = "fail"
56 | author = "Digital Sapphire"
57 |
58 | def upload_file(self, filename):
59 | print(filename)
60 | return False
61 |
62 |
63 | class TestUploaderRetryPass(BaseTestUploader):
64 |
65 | name = "retry-success"
66 | author = "Digital Sapphire"
67 | first_pass = False
68 |
69 | def upload_file(self, filename):
70 | print(filename)
71 | out = self.first_pass
72 | self.first_pass = True
73 | return out
74 |
75 |
76 | class TestUploaderRetryFail(BaseTestUploader):
77 |
78 | name = "retry-fail"
79 | author = "Digital Sapphire"
80 |
81 | def upload_file(self, filename):
82 | print(filename)
83 | return False
84 |
85 |
86 | class TestUploader(object):
87 | @pytest.mark.parametrize(
88 | "upload_plugin_type", ["success", "fail", "retry-success", "retry-fail"]
89 | )
90 | def test_uploader(self, cleandir, shared_datadir, upload_plugin_type):
91 |
92 | uploaders = [
93 | TestUploaderPass(),
94 | TestUploaderFail(),
95 | TestUploaderRetryPass(),
96 | TestUploaderRetryFail(),
97 | ]
98 |
99 | # Don't mind the name, should only contain 1 plugin
100 | upload_plugins = [u for u in uploaders if u.name == upload_plugin_type]
101 |
102 | if sys.version_info[1] == 5:
103 | data_dir = str(shared_datadir)
104 | else:
105 | data_dir = shared_datadir
106 |
107 | if not os.path.exists(data_dir):
108 | os.makedirs(data_dir)
109 |
110 | with ChDir(data_dir):
111 | uploader = Uploader(config=Config(), plugins=upload_plugins)
112 |
113 | upload_plugin = upload_plugins[0]
114 |
115 | uploader.set_uploader(upload_plugin_type)
116 | assert uploader.uploader.name == upload_plugin_type
117 |
118 | if "success" in upload_plugin.name:
119 | assert uploader.upload(["/file1"]) is True
120 | elif "fail" in upload_plugin.name:
121 | assert uploader.upload(["/file2"]) is False
122 |
--------------------------------------------------------------------------------
/tests/data/update_repo_extract/build_onedir_extract.py:
--------------------------------------------------------------------------------
1 | # ------------------------------------------------------------------------------
2 | # Copyright (c) 2015-2020 Digital Sapphire
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining
5 | # a copy of this software and associated documentation files
6 | # (the "Software"), to deal in the Software without restriction, including
7 | # without limitation the rights to use, copy, modify, merge, publish,
8 | # distribute, sublicense, and/or sell copies of the Software, and to permit
9 | # persons to whom the Software is furnished to do so, subject to the
10 | # following conditions:
11 | #
12 | # The above copyright notice and this permission notice shall be
13 | # included in all copies or substantial portions of the Software.
14 | #
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
16 | # ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
17 | # TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
18 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
19 | # SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
20 | # ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
21 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
23 | # OR OTHER DEALINGS IN THE SOFTWARE.
24 | # ------------------------------------------------------------------------------
25 | import logging
26 | import os
27 | import re
28 | import sys
29 | import tarfile
30 | import zipfile
31 |
32 | from dsdev_utils.system import get_system
33 |
34 | log = logging.getLogger()
35 |
36 |
37 | home_dir = os.path.dirname(os.path.abspath(__file__))
38 |
39 |
40 | def build(app):
41 | os.environ["PYINSTALLER_CONFIG_DIR"] = os.path.join(home_dir, ".cache")
42 | cmd = "pyupdater build -D --clean {} --path={} " "--app-version={} {}".format(
43 | app[2], home_dir, app[1], app[0]
44 | )
45 | os.system(cmd)
46 |
47 |
48 | def extract(filename):
49 | ext = os.path.splitext(filename)[1]
50 | if ext == ".zip":
51 | archive = zipfile.ZipFile(filename, "r")
52 | else:
53 | archive = tarfile.open(filename, "r:gz")
54 |
55 | archive.extractall()
56 |
57 |
58 | def main(use_custom_dir, port, windowed, split_version):
59 | cmd1 = "pyupdater pkg -P"
60 | cmd2 = "pyupdater pkg -S"
61 |
62 | if split_version:
63 | cmd2 += " --split-version"
64 |
65 | scripts = [
66 | ("app_extract_onedir.py", "4.1", "--windowed" if windowed else ""),
67 | ("app_extract_02.py", "4.2", "--windowed" if windowed else ""),
68 | ]
69 |
70 | # We use this flag to untar & move our binary to the
71 | # current working directory
72 | first = True
73 | # patch config_file for custom port number
74 | config_file = open("client_config.py", "rt").read()
75 | config_file = re.sub(r"localhost:\d+", "localhost:%s" % port, config_file)
76 |
77 | # patch config_file for use_custom_dir
78 | if use_custom_dir:
79 | config_file += "\n USE_CUSTOM_DIR = True\n"
80 | open("client_config.py", "wt").write(config_file)
81 | for s in scripts:
82 | build(s)
83 | if first:
84 | if sys.platform == "win32":
85 | ext = ".zip"
86 | else:
87 | ext = ".tar.gz"
88 |
89 | # Build path to archive
90 | archive_path = os.path.join(
91 | "pyu-data", "new", "Acme-{}-4.1{}".format(get_system(), ext)
92 | )
93 |
94 | if not os.path.exists(archive_path):
95 | print("Archive did not build!")
96 | sys.exit(1)
97 |
98 | # We extract the Acme binary here. When we call pyupdater pkg -P
99 | # the Acme binary will be moved to the deploy folder. In our test
100 | # (test_pyupdater.TestExecution.test_execution_update_*) we
101 | # move all of the files from the deploy directory to the cwd
102 | # of the test runner.
103 | extract(archive_path)
104 |
105 | first = False
106 |
107 | os.system(cmd1)
108 |
109 | os.system(cmd2)
110 |
111 |
112 | if __name__ == "__main__":
113 | if len(sys.argv) != 5:
114 | print(
115 | "usage: %s " % sys.argv[0]
116 | )
117 | else:
118 | main(
119 | sys.argv[1] == "True",
120 | sys.argv[2],
121 | sys.argv[3] == "True",
122 | sys.argv[4] == "True",
123 | )
124 |
--------------------------------------------------------------------------------
/tests/data/update_repo_extract/build_onefile_extract.py:
--------------------------------------------------------------------------------
1 | # ------------------------------------------------------------------------------
2 | # Copyright (c) 2015-2020 Digital Sapphire
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining
5 | # a copy of this software and associated documentation files
6 | # (the "Software"), to deal in the Software without restriction, including
7 | # without limitation the rights to use, copy, modify, merge, publish,
8 | # distribute, sublicense, and/or sell copies of the Software, and to permit
9 | # persons to whom the Software is furnished to do so, subject to the
10 | # following conditions:
11 | #
12 | # The above copyright notice and this permission notice shall be
13 | # included in all copies or substantial portions of the Software.
14 | #
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
16 | # ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
17 | # TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
18 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
19 | # SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
20 | # ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
21 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
23 | # OR OTHER DEALINGS IN THE SOFTWARE.
24 | # ------------------------------------------------------------------------------
25 | import logging
26 | import os
27 | import re
28 | import sys
29 | import tarfile
30 | import zipfile
31 |
32 | from dsdev_utils.system import get_system
33 |
34 | log = logging.getLogger()
35 |
36 |
37 | home_dir = os.path.dirname(os.path.abspath(__file__))
38 |
39 |
40 | def build(app):
41 | os.environ["PYINSTALLER_CONFIG_DIR"] = os.path.join(home_dir, ".cache")
42 | cmd = "pyupdater build -F {} --clean --path={} " "--app-version={} {}".format(
43 | app[2], home_dir, app[1], app[0]
44 | )
45 | os.system(cmd)
46 |
47 |
48 | def extract(filename):
49 | ext = os.path.splitext(filename)[1]
50 | if ext == ".zip":
51 | archive = zipfile.ZipFile(filename, "r")
52 | else:
53 | archive = tarfile.open(filename, "r:gz")
54 |
55 | archive.extractall()
56 |
57 |
58 | def main(use_custom_dir, port, windowed, split_version):
59 | cmd1 = "pyupdater pkg -P"
60 | cmd2 = "pyupdater pkg -S"
61 |
62 | if split_version:
63 | cmd2 += " --split-version"
64 |
65 | scripts = [
66 | ("app_extract_01.py", "4.1", "--windowed" if windowed else ""),
67 | ("app_extract_02.py", "4.2", "--windowed" if windowed else ""),
68 | ]
69 |
70 | # We use this flag to untar & move our binary to the
71 | # current working directory
72 | first = True
73 | # patch config_file for custom port number
74 | config_file = open("client_config.py", "rt").read()
75 | config_file = re.sub(r"localhost:\d+", "localhost:%s" % port, config_file)
76 |
77 | # patch config_file for use_custom_dir
78 | if use_custom_dir:
79 | config_file += "\n USE_CUSTOM_DIR = True\n"
80 | open("client_config.py", "wt").write(config_file)
81 | for s in scripts:
82 | build(s)
83 | if first:
84 | if sys.platform == "win32":
85 | ext = ".zip"
86 | else:
87 | ext = ".tar.gz"
88 |
89 | # Build path to archive
90 | archive_path = os.path.join(
91 | "pyu-data", "new", "Acme-{}-4.1{}".format(get_system(), ext)
92 | )
93 |
94 | if not os.path.exists(archive_path):
95 | print("Archive did not build!")
96 | sys.exit(1)
97 |
98 | # We extract the Acme binary here. When we call pyupdater pkg -P
99 | # the Acme binary will be moved to the deploy folder. In our test
100 | # (test_pyupdater.TestExecution.test_execution_update_*) we
101 | # move all of the files from the deploy directory to the cwd
102 | # of the test runner.
103 | extract(archive_path)
104 |
105 | first = False
106 |
107 | os.system(cmd1)
108 |
109 | os.system(cmd2)
110 |
111 |
112 | if __name__ == "__main__":
113 | if len(sys.argv) != 5:
114 | print(
115 | "usage: %s " % sys.argv[0]
116 | )
117 | else:
118 | main(
119 | sys.argv[1] == "True",
120 | sys.argv[2],
121 | sys.argv[3] == "True",
122 | sys.argv[4] == "True",
123 | )
124 |
--------------------------------------------------------------------------------
/tests/data/update_repo_restart/build_onefile_restart.py:
--------------------------------------------------------------------------------
1 | # ------------------------------------------------------------------------------
2 | # Copyright (c) 2015-2020 Digital Sapphire
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining
5 | # a copy of this software and associated documentation files
6 | # (the "Software"), to deal in the Software without restriction, including
7 | # without limitation the rights to use, copy, modify, merge, publish,
8 | # distribute, sublicense, and/or sell copies of the Software, and to permit
9 | # persons to whom the Software is furnished to do so, subject to the
10 | # following conditions:
11 | #
12 | # The above copyright notice and this permission notice shall be
13 | # included in all copies or substantial portions of the Software.
14 | #
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
16 | # ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
17 | # TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
18 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
19 | # SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
20 | # ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
21 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
23 | # OR OTHER DEALINGS IN THE SOFTWARE.
24 | # ------------------------------------------------------------------------------
25 | import logging
26 | import os
27 | import re
28 | import sys
29 | import tarfile
30 | import zipfile
31 |
32 | from dsdev_utils.system import get_system
33 |
34 | log = logging.getLogger()
35 |
36 |
37 | home_dir = os.path.dirname(os.path.abspath(__file__))
38 |
39 |
40 | def build(app):
41 | # Pyinstaller's --clean is not 'multiprocessing safe',
42 | # let's use our own cache
43 | os.environ["PYINSTALLER_CONFIG_DIR"] = os.path.join(home_dir, ".cache")
44 | cmd = "pyupdater build -F {} --clean --path={} " "--app-version={} {}".format(
45 | app[2], home_dir, app[1], app[0]
46 | )
47 | os.system(cmd)
48 |
49 |
50 | def extract(filename):
51 | ext = os.path.splitext(filename)[1]
52 | if ext == ".zip":
53 | archive = zipfile.ZipFile(filename, "r")
54 | else:
55 | archive = tarfile.open(filename, "r:gz")
56 |
57 | archive.extractall()
58 |
59 |
60 | def main(use_custom_dir, port, windowed, split_version):
61 | cmd1 = "pyupdater pkg -P"
62 | cmd2 = "pyupdater pkg -S"
63 |
64 | if split_version:
65 | cmd2 += " --split-version"
66 |
67 | scripts = [
68 | ("app_restart_01.py", "4.1", "--windowed" if windowed else ""),
69 | ("app_restart_02.py", "4.2", "--windowed" if windowed else ""),
70 | ]
71 |
72 | # We use this flag to untar & move our binary to the
73 | # current working directory
74 | first = True
75 | # patch config_file for custom port number
76 | config_file = open("client_config.py", "rt").read()
77 | config_file = re.sub(r"localhost:\d+", "localhost:%s" % port, config_file)
78 | # patch config_file for use_custom_dir
79 | if use_custom_dir:
80 | config_file += "\n USE_CUSTOM_DIR = True\n"
81 | open("client_config.py", "wt").write(config_file)
82 | for s in scripts:
83 | build(s)
84 | if first:
85 | if sys.platform == "win32":
86 | ext = ".zip"
87 | else:
88 | ext = ".tar.gz"
89 |
90 | # Build path to archive
91 | archive_path = os.path.join(
92 | "pyu-data", "new", "Acme-{}-4.1{}".format(get_system(), ext)
93 | )
94 |
95 | if not os.path.exists(archive_path):
96 | print("Archive did not build!")
97 | sys.exit(1)
98 |
99 | # We extract the Acme binary here. When we call pyupdater pkg -P
100 | # the Acme binary will be moved to the deploy folder. In our test
101 | # (test_pyupdater.TestExecution.test_execution_update_*) we
102 | # move all of the files from the deploy directory to the cwd
103 | # of the test runner.
104 | extract(archive_path)
105 |
106 | first = False
107 |
108 | os.system(cmd1)
109 |
110 | os.system(cmd2)
111 |
112 |
113 | if __name__ == "__main__":
114 | if len(sys.argv) != 5:
115 | print(
116 | "usage: %s " % sys.argv[0]
117 | )
118 | else:
119 | main(
120 | sys.argv[1] == "True",
121 | sys.argv[2],
122 | sys.argv[3] == "True",
123 | sys.argv[4] == "True",
124 | )
125 |
--------------------------------------------------------------------------------
/tests/data/update_repo_restart/build_onedir_restart.py:
--------------------------------------------------------------------------------
1 | # ------------------------------------------------------------------------------
2 | # Copyright (c) 2015-2020 Digital Sapphire
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining
5 | # a copy of this software and associated documentation files
6 | # (the "Software"), to deal in the Software without restriction, including
7 | # without limitation the rights to use, copy, modify, merge, publish,
8 | # distribute, sublicense, and/or sell copies of the Software, and to permit
9 | # persons to whom the Software is furnished to do so, subject to the
10 | # following conditions:
11 | #
12 | # The above copyright notice and this permission notice shall be
13 | # included in all copies or substantial portions of the Software.
14 | #
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
16 | # ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
17 | # TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
18 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
19 | # SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
20 | # ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
21 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
23 | # OR OTHER DEALINGS IN THE SOFTWARE.
24 | # ------------------------------------------------------------------------------
25 | import logging
26 | import os
27 | import re
28 | import sys
29 | import tarfile
30 | import zipfile
31 |
32 | from dsdev_utils.system import get_system
33 |
34 |
35 | log = logging.getLogger()
36 |
37 |
38 | home_dir = os.path.dirname(os.path.abspath(__file__))
39 |
40 |
41 | def build(app):
42 | # Pyinstaller's --clean is not 'multiprocessing safe',
43 | # let's use our own cache
44 | os.environ["PYINSTALLER_CONFIG_DIR"] = os.path.join(home_dir, ".cache")
45 | cmd = "pyupdater build -D {} --clean --path={} " "--app-version={} {}".format(
46 | app[2], home_dir, app[1], app[0]
47 | )
48 | os.system(cmd)
49 |
50 |
51 | def extract(filename):
52 | ext = os.path.splitext(filename)[1]
53 | if ext == ".zip":
54 | archive = zipfile.ZipFile(filename, "r")
55 | else:
56 | archive = tarfile.open(filename, "r:gz")
57 |
58 | archive.extractall()
59 |
60 |
61 | def main(use_custom_dir, port, windowed, split_version):
62 | cmd1 = "pyupdater pkg -P"
63 | cmd2 = "pyupdater pkg -S"
64 |
65 | if split_version:
66 | cmd2 += " --split-version"
67 |
68 | scripts = [
69 | ("app_restart_onedir.py", "4.1", "--windowed" if windowed else ""),
70 | ("app_restart_02.py", "4.2", "--windowed" if windowed else ""),
71 | ]
72 |
73 | # We use this flag to untar & move our binary to the
74 | # current working directory
75 | first = True
76 | # patch config_file for custom port number
77 | config_file = open("client_config.py", "rt").read()
78 | config_file = re.sub(r"localhost:\d+", "localhost:%s" % port, config_file)
79 |
80 | # patch config_file for use_custom_dir
81 | if use_custom_dir:
82 | config_file += "\n USE_CUSTOM_DIR = True\n"
83 | open("client_config.py", "wt").write(config_file)
84 | for s in scripts:
85 | build(s)
86 | if first:
87 | if sys.platform == "win32":
88 | ext = ".zip"
89 | else:
90 | ext = ".tar.gz"
91 |
92 | # Build path to archive
93 | archive_path = os.path.join(
94 | "pyu-data", "new", "Acme-{}-4.1{}".format(get_system(), ext)
95 | )
96 |
97 | if not os.path.exists(archive_path):
98 | print("Archive did not build!")
99 | sys.exit(1)
100 |
101 | # We extract the Acme binary here. When we call pyupdater pkg -P
102 | # the Acme binary will be moved to the deploy folder. In our test
103 | # (test_pyupdater.TestExecution.test_execution_update_*) we
104 | # move all of the files from the deploy directory to the cwd
105 | # of the test runner.
106 | extract(archive_path)
107 |
108 | first = False
109 |
110 | os.system(cmd1)
111 |
112 | os.system(cmd2)
113 |
114 |
115 | if __name__ == "__main__":
116 | if len(sys.argv) != 5:
117 | print(
118 | "usage: %s " % sys.argv[0]
119 | )
120 | else:
121 | main(
122 | sys.argv[1] == "True",
123 | sys.argv[2],
124 | sys.argv[3] == "True",
125 | sys.argv[4] == "True",
126 | )
127 |
--------------------------------------------------------------------------------
/tests/test_patcher.py:
--------------------------------------------------------------------------------
1 | # ------------------------------------------------------------------------------
2 | # Copyright (c) 2015-2020 Digital Sapphire
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining
5 | # a copy of this software and associated documentation files
6 | # (the "Software"), to deal in the Software without restriction, including
7 | # without limitation the rights to use, copy, modify, merge, publish,
8 | # distribute, sublicense, and/or sell copies of the Software, and to permit
9 | # persons to whom the Software is furnished to do so, subject to the
10 | # following conditions:
11 | #
12 | # The above copyright notice and this permission notice shall be
13 | # included in all copies or substantial portions of the Software.
14 | #
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
16 | # ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
17 | # TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
18 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
19 | # SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
20 | # ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
21 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
23 | # OR OTHER DEALINGS IN THE SOFTWARE.
24 | # ------------------------------------------------------------------------------
25 | from __future__ import unicode_literals, print_function
26 | import json
27 | import os
28 |
29 | import pytest
30 |
31 | from pyupdater.client.patcher import Patcher
32 |
33 |
34 | def cb1(status):
35 | pass
36 |
37 |
38 | # Just throwing a monkey wrench into.
39 | def cb2(status):
40 | raise IndexError
41 |
42 |
43 | update_data = {
44 | "name": "Acme",
45 | "current_filename": "Acme-mac-4.1.tar.gz",
46 | "current_version": "4.1.0.2.0",
47 | "latest_version": "4.4.0.2.0",
48 | "update_folder": None,
49 | "update_urls": ["https://pyu-tester.s3.amazonaws.com/"],
50 | "platform": "mac",
51 | "progress_hooks": [cb1, cb2],
52 | }
53 |
54 |
55 | # noinspection PyStatementEffect,PyStatementEffect
56 | @pytest.mark.usefixtures("cleandir")
57 | class TestFails(object):
58 |
59 | base_binary = "Acme-mac-4.1.tar.gz"
60 |
61 | @pytest.fixture
62 | def json_data(self, shared_datadir):
63 | version_data_str = (shared_datadir / "version.json").read_text()
64 | return json.loads(version_data_str)
65 |
66 | def test_no_base_binary(self, json_data):
67 | assert os.listdir(os.getcwd()) == []
68 | data = update_data.copy()
69 | data["update_folder"] = os.getcwd()
70 | data["json_data"] = json_data
71 | p = Patcher(**data)
72 | assert p.start() is False
73 |
74 | def test_bad_hash_current_version(self, shared_datadir, json_data):
75 | data = update_data.copy()
76 | data["update_folder"] = str(shared_datadir)
77 | data["json_data"] = json_data
78 | data["current_file_hash"] = "Thisisabadhash"
79 | p = Patcher(**data)
80 | assert p.start() is False
81 |
82 | @pytest.mark.run(order=8)
83 | def test_missing_version(self, shared_datadir, json_data):
84 | data = update_data.copy()
85 | data["update_folder"] = str(shared_datadir)
86 | data["json_data"] = json_data
87 | data["latest_version"] = "0.0.4.2.0"
88 | p = Patcher(**data)
89 | assert p.start() is False
90 |
91 |
92 | # noinspection PyStatementEffect,PyStatementEffect
93 | @pytest.mark.usefixtures("cleandir")
94 | class TestExecution(object):
95 |
96 | base_binary = "Acme-mac-4.1.tar.gz"
97 |
98 | @pytest.fixture
99 | def json_data(self, shared_datadir):
100 | version_data_str = (shared_datadir / "version.json").read_text()
101 | return json.loads(version_data_str)
102 |
103 | @pytest.mark.run(order=7)
104 | def test_execution(self, shared_datadir, json_data):
105 | data = update_data.copy()
106 | data["update_folder"] = str(shared_datadir)
107 | data["json_data"] = json_data
108 | data["channel"] = "stable"
109 | p = Patcher(**data)
110 | assert p.start() is True
111 |
112 | def test_execution_callback(self, shared_datadir, json_data):
113 | def cb(status):
114 | assert "downloaded" in status.keys()
115 | assert "total" in status.keys()
116 | assert "status" in status.keys()
117 | assert "percent_complete" in status.keys()
118 |
119 | data = update_data.copy()
120 | data["update_folder"] = str(shared_datadir)
121 | data["json_data"] = json_data
122 | data["channel"] = "stable"
123 | data["progress_hooks"] = [cb]
124 | p = Patcher(**data)
125 | assert p.start() is True
126 |
--------------------------------------------------------------------------------
/tests/test_downloader.py:
--------------------------------------------------------------------------------
1 | # ------------------------------------------------------------------------------
2 | # Copyright (c) 2015-2020 Digital Sapphire
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining
5 | # a copy of this software and associated documentation files
6 | # (the "Software"), to deal in the Software without restriction, including
7 | # without limitation the rights to use, copy, modify, merge, publish,
8 | # distribute, sublicense, and/or sell copies of the Software, and to permit
9 | # persons to whom the Software is furnished to do so, subject to the
10 | # following conditions:
11 | #
12 | # The above copyright notice and this permission notice shall be
13 | # included in all copies or substantial portions of the Software.
14 | #
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
16 | # ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
17 | # TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
18 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
19 | # SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
20 | # ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
21 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
23 | # OR OTHER DEALINGS IN THE SOFTWARE.
24 | # ------------------------------------------------------------------------------
25 | from __future__ import unicode_literals
26 |
27 | import pytest
28 |
29 | from pyupdater.client.downloader import FileDownloader, get_hash
30 | from pyupdater.utils.exceptions import FileDownloaderError
31 |
32 |
33 | FILENAME = "dont delete pyu test.txt"
34 | FILE_HASH = "82719546b992ef81f4544fb2690c6a05b300a0216eeaa8f3616b3b107a311629"
35 | URLS = ["https://pyu-tester.s3.amazonaws.com/"]
36 |
37 |
38 | @pytest.mark.usefixtures("cleandir")
39 | @pytest.mark.parametrize("download_max_size", [0, 4 * 1024 * 1024])
40 | class TestData(object):
41 | def test_return(self, download_max_size):
42 | fd = FileDownloader(FILENAME, URLS, FILE_HASH, verify=True)
43 | fd.download_max_size = download_max_size
44 | binary_data = fd.download_verify_return()
45 | assert binary_data is not None
46 |
47 | def test_cb(self, download_max_size):
48 | def cb(status):
49 | pass
50 |
51 | fd = FileDownloader(
52 | FILENAME, URLS, hexdigest=FILE_HASH, progress_hooks=[cb], verify=True
53 | )
54 | fd.download_max_size = download_max_size
55 | binary_data = fd.download_verify_return()
56 | assert binary_data is not None
57 |
58 | def test_return_fail(self, download_max_size):
59 | fd = FileDownloader(FILENAME, URLS, "JKFEIFJILEFJ983NKFNKL", verify=True)
60 | fd.download_max_size = download_max_size
61 | binary_data = fd.download_verify_return()
62 | assert binary_data is None
63 |
64 |
65 | @pytest.mark.usefixtures("cleandir")
66 | class TestBasicAuthUrlLib(object):
67 | def test_basic_auth(self):
68 | headers = {"basic_auth": "user:pass"}
69 | fd = FileDownloader("test", ["test"], headers=headers)
70 | http = fd._get_http_pool(secure=True)
71 | sc = http.request("GET", "https://httpbin.org/basic-auth/user/pass").status
72 | assert sc == 200
73 |
74 |
75 | @pytest.mark.usefixtures("cleandir")
76 | class TestAuthorizationHeader(object):
77 | def test_auth_header(self):
78 | headers = {"Authorization": "Basic dXNlcjpwYXNz"}
79 | fd = FileDownloader("test", ["test"], headers=headers)
80 | http = fd._get_http_pool(secure=True)
81 | sc = http.request("GET", "https://httpbin.org/basic-auth/user/pass").status
82 | assert sc == 200
83 |
84 |
85 | @pytest.mark.usefixtures("cleandir")
86 | class TestUrl(object):
87 | def test_bad_url(self):
88 | fd = FileDownloader(FILENAME, ["bad url"], hexdigest="bad hash", verify=True)
89 | binary_data = fd.download_verify_return()
90 | assert binary_data is None
91 |
92 | def test_url_as_string(self):
93 | with pytest.raises(FileDownloaderError):
94 | FileDownloader(FILENAME, URLS[0])
95 |
96 |
97 | @pytest.mark.usefixtures("cleandir")
98 | class TestContentLength(object):
99 | def test_bad_content_length(self):
100 | class FakeHeaders(object):
101 | headers = {}
102 |
103 | fd = FileDownloader(FILENAME, URLS, hexdigest=FILE_HASH, verify=True)
104 | data = FakeHeaders()
105 | assert fd._get_content_length(data) is None
106 |
107 | def test_good_content_length(self):
108 | fd = FileDownloader(FILENAME, URLS, hexdigest=FILE_HASH, verify=True)
109 | fd.download_verify_return()
110 | assert fd.content_length == 2387
111 |
112 |
113 | @pytest.mark.usefixtures("cleandir")
114 | class TestGetHash(object):
115 | def test_get_hash(self):
116 | digest = "380fd2bf3d78bb411e4c1801ce3ce7804bf5a22d79" "405d950e5d5c8f3169fca0"
117 | assert digest == get_hash("Get this hash please")
118 |
--------------------------------------------------------------------------------
/tests/test_utils.py:
--------------------------------------------------------------------------------
1 | # ------------------------------------------------------------------------------
2 | # Copyright (c) 2015-2020 Digital Sapphire
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining
5 | # a copy of this software and associated documentation files
6 | # (the "Software"), to deal in the Software without restriction, including
7 | # without limitation the rights to use, copy, modify, merge, publish,
8 | # distribute, sublicense, and/or sell copies of the Software, and to permit
9 | # persons to whom the Software is furnished to do so, subject to the
10 | # following conditions:
11 | #
12 | # The above copyright notice and this permission notice shall be
13 | # included in all copies or substantial portions of the Software.
14 | #
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
16 | # ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
17 | # TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
18 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
19 | # SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
20 | # ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
21 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
23 | # OR OTHER DEALINGS IN THE SOFTWARE.
24 | # ------------------------------------------------------------------------------
25 | from __future__ import unicode_literals
26 | import io
27 | import os
28 |
29 | import pytest
30 |
31 | from pyupdater.utils.config import Config
32 | from pyupdater.utils import (
33 | check_repo,
34 | create_asset_archive,
35 | make_archive,
36 | PluginManager,
37 | remove_dot_files,
38 | run,
39 | )
40 |
41 |
42 | @pytest.mark.usefixtures("cleandir")
43 | class TestUtils(object):
44 | def test_make_archive(self):
45 | with io.open("hash-test1.txt", "w", encoding="utf-8") as f:
46 | f.write("I should find some lorem text" * 11)
47 |
48 | with io.open("hash-test2.txt", "w", encoding="utf-8") as f:
49 | f.write("I should find some lorem text" * 5)
50 |
51 | filename1 = make_archive("hash", "hash-test1.txt", "0.2", "default")
52 | filename2 = make_archive("hash", "hash-test2.txt", "0.3", "default")
53 |
54 | assert os.path.exists(filename1)
55 | assert os.path.exists(filename2)
56 |
57 | def test_create_asset_archive(self):
58 | with io.open("hash-test1.dll", "w", encoding="utf-8") as f:
59 | f.write("I should find some lorem text" * 20)
60 |
61 | with io.open("hash-test2.so", "w", encoding="utf-8") as f:
62 | f.write("I should find some lorem text" * 11)
63 |
64 | with io.open("binary", "w", encoding="utf-8") as f:
65 | f.write("I should find some lorem text" * 5)
66 |
67 | filename = create_asset_archive("hash-test1.dll", "0.1")
68 | filename1 = create_asset_archive("hash-test2.so", "0.2")
69 | filename2 = create_asset_archive("binary", "0.3")
70 |
71 | assert os.path.exists(filename)
72 | assert os.path.exists(filename1)
73 | assert os.path.exists(filename2)
74 |
75 | def test_check_repo_fail(self):
76 | assert check_repo() is False
77 |
78 | def test_remove_dot_files(self):
79 | bad_list = [".DS_Store", "test", "stuff", ".trash"]
80 | good_list = ["test", "stuff"]
81 | for n in remove_dot_files(bad_list):
82 | assert n in good_list
83 |
84 | def test_run(self):
85 | assert run('echo "hello world!"') == 0
86 |
87 | @pytest.mark.parametrize(
88 | "n, a",
89 | [("test", None), (None, "test"), (1, "test"), ("test", 1), ("test", "test")],
90 | )
91 | def test_plugin_manager_load(self, n, a):
92 | class Plugin(object):
93 | name = n
94 | author = a
95 |
96 | pm = PluginManager(Config(), plugins=[Plugin()])
97 |
98 | if isinstance(n, str) and isinstance(a, str):
99 | assert len(pm.plugins) == 1
100 | else:
101 | assert len(pm.plugins) == 0
102 |
103 | def test_default_plugin_detection_s3(self):
104 | pm = PluginManager(Config())
105 |
106 | plugin_names = [n["name"] for n in pm.plugins]
107 | assert "s3" in plugin_names
108 |
109 | def test_default_plugin_detection_scp(self):
110 | pm = PluginManager(Config())
111 |
112 | plugin_names = [n["name"] for n in pm.plugins]
113 | assert "scp" in plugin_names
114 |
115 | def test_plugin_unique_names(self):
116 | class Plugin1(object):
117 | name = "test"
118 | author = "test1"
119 |
120 | class Plugin2(object):
121 | name = "test"
122 | author = "test2"
123 |
124 | pm = PluginManager(Config(), plugins=[Plugin1(), Plugin2()])
125 |
126 | plugin_names = [n["name"] for n in pm.plugins]
127 |
128 | assert "test" in plugin_names
129 | assert "test2" in plugin_names
130 |
131 | def test_plugin_config(self):
132 | class TPlugin(object):
133 | name = "test"
134 | author = "test1"
135 | bucket = ""
136 |
137 | def init_config(self, config):
138 | self.bucket = config.get("bucket", "bad")
139 |
140 | master_config = Config()
141 |
142 | master_config["PLUGIN_CONFIGS"] = {"test-test1": {"bucket": "test_bucket"}}
143 |
144 | pm = PluginManager(master_config, plugins=[TPlugin()])
145 |
146 | p = pm.get_plugin("test", False)
147 | assert p.bucket == ""
148 |
149 | p = pm.get_plugin("test", True)
150 | assert p.bucket == "test_bucket"
151 |
--------------------------------------------------------------------------------
/pyupdater/cli/helpers.py:
--------------------------------------------------------------------------------
1 | # ------------------------------------------------------------------------------
2 | # Copyright (c) 2015-2020 Digital Sapphire
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining
5 | # a copy of this software and associated documentation files
6 | # (the "Software"), to deal in the Software without restriction, including
7 | # without limitation the rights to use, copy, modify, merge, publish,
8 | # distribute, sublicense, and/or sell copies of the Software, and to permit
9 | # persons to whom the Software is furnished to do so, subject to the
10 | # following conditions:
11 | #
12 | # The above copyright notice and this permission notice shall be
13 | # included in all copies or substantial portions of the Software.
14 | #
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
16 | # ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
17 | # TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
18 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
19 | # SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
20 | # ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
21 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
23 | # OR OTHER DEALINGS IN THE SOFTWARE.
24 | # ------------------------------------------------------------------------------
25 | import logging
26 | import os
27 | import sys
28 |
29 | from dsdev_utils import terminal
30 |
31 | from pyupdater import settings
32 | from pyupdater.utils import PluginManager
33 |
34 | log = logging.getLevelName(__name__)
35 |
36 |
37 | def print_plugin_settings(plugin_name, config): # pragma: no cover
38 | pm = PluginManager(config)
39 | config = pm.get_plugin_settings(plugin_name)
40 | if len(config.keys()) == 0:
41 | print("No config found for {}".format(plugin_name))
42 | else:
43 | print(plugin_name)
44 | print(config)
45 |
46 |
47 | def setup_appname(config): # pragma: no cover
48 | if config.APP_NAME is not None:
49 | default = config.APP_NAME
50 | else:
51 | default = None
52 | config.APP_NAME = terminal.get_correct_answer(
53 | "Please enter app name", required=True, default=default
54 | )
55 |
56 |
57 | def setup_client_config_path(config): # pragma: no cover
58 | _default_dir = os.path.basename(os.path.abspath(os.getcwd()))
59 | question = (
60 | "Please enter the path to where pyupdater "
61 | "will write the client_config.py file. "
62 | "You'll need to import this file to "
63 | "initialize the update process. \nExamples:\n\n"
64 | "lib/utils, src/lib, src. \n\nLeave blank to use "
65 | "the current directory"
66 | )
67 | answer = terminal.get_correct_answer(question, default=_default_dir)
68 |
69 | if answer == _default_dir:
70 | config.CLIENT_CONFIG_PATH = settings.DEFAULT_CLIENT_CONFIG
71 | else:
72 | answer = answer.split(os.sep)
73 | answer.append(settings.DEFAULT_CLIENT_CONFIG[0])
74 |
75 | config.CLIENT_CONFIG_PATH = answer
76 |
77 |
78 | def setup_company(config): # pragma: no cover
79 | if config.COMPANY_NAME is not None:
80 | default = config.COMPANY_NAME
81 | else:
82 | default = None
83 | temp = terminal.get_correct_answer(
84 | "Please enter your name or company name", required=True, default=default
85 | )
86 | config.COMPANY_NAME = temp
87 |
88 |
89 | def setup_max_download_retries(config): # pragma: no cover
90 | default = config.MAX_DOWNLOAD_RETRIES
91 | while 1:
92 | temp = terminal.get_correct_answer(
93 | "Enter max download retries", required=True, default=str(default)
94 | )
95 | try:
96 | temp = int(temp)
97 | except Exception as err:
98 | log.error(err)
99 | log.debug(err, exc_info=True)
100 | continue
101 |
102 | if temp > 10 or temp < 1:
103 | log.error("Max retries can only be from 1 to 10")
104 | continue
105 | break
106 |
107 | config.MAX_DOWNLOAD_RETRIES = temp
108 |
109 |
110 | def setup_http_timeout(config): # pragma: no cover
111 | default = config.HTTP_TIMEOUT
112 | while 1:
113 | temp = terminal.get_correct_answer(
114 | "Enter HTTP timeout in seconds", required=True, default=str(default)
115 | )
116 | try:
117 | temp = int(temp)
118 | except Exception as err:
119 | log.error(err)
120 | continue
121 |
122 | if temp < 1:
123 | log.error("HTTP timeout has to be >= 1")
124 | continue
125 | break
126 |
127 | config.HTTP_TIMEOUT = temp
128 |
129 |
130 | def setup_patches(config): # pragma: no cover
131 | question = "Would you like to enable patch updates?"
132 | config.UPDATE_PATCHES = terminal.ask_yes_no(question, default="yes")
133 |
134 |
135 | def setup_plugin(name, config): # pragma: no cover
136 | pgm = PluginManager(config)
137 | plugin = pgm.get_plugin(name)
138 | if plugin is None:
139 | sys.exit("Invalid plugin name...")
140 |
141 | pgm.config_plugin(name, config)
142 |
143 |
144 | def setup_urls(config): # pragma: no cover
145 | url = terminal.get_correct_answer("Enter a url to ping for updates.", required=True)
146 | config.UPDATE_URLS = [url]
147 | while 1:
148 | answer = terminal.ask_yes_no(
149 | "Would you like to add " "another url for backup?", default="no"
150 | )
151 | if answer is True:
152 | url = terminal.get_correct_answer("Enter another url.", required=True)
153 | config.UPDATE_URLS.append(url)
154 | else:
155 | break
156 |
157 |
158 | def initial_setup(config): # pragma: no cover
159 | setup_appname(config)
160 | setup_company(config)
161 | setup_urls(config)
162 | setup_patches(config)
163 | setup_client_config_path(config)
164 | return config
165 |
--------------------------------------------------------------------------------
/pyupdater/utils/config.py:
--------------------------------------------------------------------------------
1 | # ------------------------------------------------------------------------------
2 | # Copyright (c) 2015-2020 Digital Sapphire
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining
5 | # a copy of this software and associated documentation files
6 | # (the "Software"), to deal in the Software without restriction, including
7 | # without limitation the rights to use, copy, modify, merge, publish,
8 | # distribute, sublicense, and/or sell copies of the Software, and to permit
9 | # persons to whom the Software is furnished to do so, subject to the
10 | # following conditions:
11 | #
12 | # The above copyright notice and this permission notice shall be
13 | # included in all copies or substantial portions of the Software.
14 | #
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
16 | # ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
17 | # TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
18 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
19 | # SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
20 | # ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
21 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
23 | # OR OTHER DEALINGS IN THE SOFTWARE.
24 | # ------------------------------------------------------------------------------
25 | from __future__ import unicode_literals
26 | import logging
27 | import os
28 |
29 | from pyupdater import settings
30 | from pyupdater.utils.storage import Storage
31 |
32 | log = logging.getLogger(__name__)
33 |
34 |
35 | class Config(dict):
36 | def __init__(self, *args, **kwargs):
37 | super(Config, self).__init__(*args, **kwargs)
38 | self.__dict__ = self
39 | self._set_default()
40 |
41 | def _set_default(self):
42 | config_template = {
43 | # If left None "PyUpdater App" will be used
44 | "APP_NAME": settings.GENERIC_APP_NAME,
45 | # path to place client config
46 | "CLIENT_CONFIG_PATH": settings.DEFAULT_CLIENT_CONFIG,
47 | # Company/Your name
48 | "COMPANY_NAME": settings.GENERIC_APP_NAME,
49 | "PLUGIN_CONFIGS": {},
50 | # Support for patch updates
51 | "UPDATE_PATCHES": True,
52 | # Max retries for downloads
53 | "MAX_DOWNLOAD_RETRIES": 3,
54 | # HTTP TIMEOUT
55 | "HTTP_TIMEOUT": 30,
56 | }
57 | self.update(config_template)
58 |
59 | def from_object(self, obj):
60 | # Updates the values from the given object
61 |
62 | # Args:
63 |
64 | # obj (instance): Object with config attributes
65 |
66 | # Objects are classes.
67 |
68 | # Just the uppercase variables in that object are stored in the config.
69 | # Example usage::
70 |
71 | # from yourapplication import default_config
72 | # app.config.from_object(default_config())
73 | for key in dir(obj):
74 | if key.isupper():
75 | self[key] = getattr(obj, key)
76 |
77 |
78 | # Loads & saves config file
79 | class ConfigManager(object):
80 | def __init__(self):
81 | self.cwd = os.getcwd()
82 | self.db = Storage()
83 | self.config_key = settings.CONFIG_DB_KEY_APP_CONFIG
84 |
85 | # Loads config from database (json file)
86 | def load_config(self):
87 | config_data = self.db.load(self.config_key)
88 | if config_data is None:
89 | config_data = {}
90 | config = Config()
91 | for k, v in config_data.items():
92 | config[k] = v
93 | config.DATA_DIR = os.getcwd()
94 | return config
95 |
96 | def get_app_name(self):
97 | config = self.load_config()
98 | return config.APP_NAME
99 |
100 | # Saves config to database (json file)
101 | def save_config(self, obj):
102 | log.debug("Saving Config")
103 | self.db.save(self.config_key, obj)
104 | log.debug("Config saved")
105 | self.write_config_py(obj)
106 | log.debug("Wrote client config")
107 |
108 | # Writes client config to client_config.py
109 | def write_config_py(self, obj):
110 | keypack_data = self.db.load(settings.CONFIG_DB_KEY_KEYPACK)
111 | if keypack_data is None:
112 | log.debug("*** Keypack data is None ***")
113 | public_key = None
114 | else:
115 | public_key = keypack_data["client"]["offline_public"]
116 |
117 | filename = os.path.join(self.cwd, *obj.CLIENT_CONFIG_PATH)
118 | attr_str_format = " {} = '{}'\n"
119 | attr_format = " {} = {}\n"
120 |
121 | log.debug("Writing client_config.py")
122 | with open(filename, "w") as f:
123 | f.write("class ClientConfig(object):\n")
124 |
125 | log.debug("Adding PUBLIC_KEY to client_config.py")
126 | f.write(attr_str_format.format("PUBLIC_KEY", public_key))
127 |
128 | if hasattr(obj, "APP_NAME"):
129 | log.debug("Adding APP_NAME to client_config.py")
130 | f.write(attr_str_format.format("APP_NAME", obj.APP_NAME))
131 |
132 | if hasattr(obj, "COMPANY_NAME"):
133 | log.debug("Adding COMPANY_NAME to client_config.py")
134 | f.write(attr_str_format.format("COMPANY_NAME", obj.COMPANY_NAME))
135 |
136 | if hasattr(obj, "HTTP_TIMEOUT"):
137 | log.debug("Adding HTTP_TIMEOUT to cilent_config.py")
138 | f.write(attr_format.format("HTTP_TIMEOUT", obj.HTTP_TIMEOUT))
139 |
140 | if hasattr(obj, "MAX_DOWNLOAD_RETRIES"):
141 | log.debug("Adding MAX_DOWNLOAD_RETRIES to client_config.py")
142 | f.write(
143 | attr_format.format("MAX_DOWNLOAD_RETRIES", obj.MAX_DOWNLOAD_RETRIES)
144 | )
145 |
146 | if hasattr(obj, "UPDATE_URLS"):
147 | log.debug("Adding UPDATE_URLS to client_config.py")
148 | f.write(attr_format.format("UPDATE_URLS", obj.UPDATE_URLS))
149 |
--------------------------------------------------------------------------------
/pyupdater/core/key_handler/keys.py:
--------------------------------------------------------------------------------
1 | # ------------------------------------------------------------------------------
2 | # Copyright (c) 2015-2020 Digital Sapphire
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining
5 | # a copy of this software and associated documentation files
6 | # (the "Software"), to deal in the Software without restriction, including
7 | # without limitation the rights to use, copy, modify, merge, publish,
8 | # distribute, sublicense, and/or sell copies of the Software, and to permit
9 | # persons to whom the Software is furnished to do so, subject to the
10 | # following conditions:
11 | #
12 | # The above copyright notice and this permission notice shall be
13 | # included in all copies or substantial portions of the Software.
14 | #
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
16 | # ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
17 | # TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
18 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
19 | # SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
20 | # ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
21 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
23 | # OR OTHER DEALINGS IN THE SOFTWARE.
24 | # ------------------------------------------------------------------------------
25 | from __future__ import unicode_literals
26 | import io
27 | import logging
28 | import json
29 | import os
30 |
31 | from appdirs import user_data_dir
32 | from nacl.signing import SigningKey
33 |
34 | from pyupdater import settings
35 | from pyupdater.utils.exceptions import KeyHandlerError
36 | from pyupdater.utils.encoding import UnpaddedBase64Encoder
37 | from pyupdater.utils.storage import Storage
38 |
39 |
40 | log = logging.getLogger(__name__)
41 |
42 |
43 | class Keys(object):
44 | def __init__(self, test=False):
45 | # We use base64 encoding for easy human consumption
46 | self.key_encoder = UnpaddedBase64Encoder()
47 | self.key_data = {}
48 |
49 | if test:
50 | self.data_dir = os.path.join("private", "data")
51 | else:
52 | self.data_dir = user_data_dir("PyUpdater", "Digital Sapphire")
53 |
54 | if not os.path.exists(self.data_dir):
55 | os.makedirs(self.data_dir)
56 |
57 | # The name of the offline key database
58 | self.keypack_filename = os.path.join(self.data_dir, "offline_keys.db")
59 | self._load()
60 |
61 | def make_keypack(self, name):
62 | # Will generate keypack specific to app name provided
63 | try:
64 | keypack = self._gen_keypack(name)
65 | except AssertionError:
66 | log.debug("Failed to generate keypack")
67 | return False
68 | except KeyHandlerError as err:
69 | log.error(err)
70 | return False
71 |
72 | # Write keypack to cwd
73 | with io.open(settings.KEYPACK_FILENAME, "w", encoding="utf-8") as f:
74 | out = json.dumps(keypack, indent=2, sort_keys=True)
75 | f.write(out)
76 | return True
77 |
78 | def _load(self):
79 | if not os.path.exists(self.keypack_filename):
80 | self._save()
81 | else:
82 | with io.open(self.keypack_filename, "r", encoding="utf-8") as f:
83 | self.key_data = json.loads(f.read())
84 |
85 | def _save(self):
86 | with io.open(self.keypack_filename, "w", encoding="utf-8") as f:
87 | out = json.dumps(self.key_data, indent=2, sort_keys=True)
88 | f.write(out)
89 |
90 | def _gen_keypack(self, name):
91 | # Create new public & private key for app signing
92 | try:
93 | app_pri, app_pub = self._make_keys()
94 | except Exception as err:
95 | log.error(err)
96 | log.debug(err, exc_info=True)
97 | raise KeyHandlerError("Failed to create keypair")
98 |
99 | # Load app specific private & public key
100 | off_pri, off_pub = self._load_offline_keys(name)
101 |
102 | log.debug("off_pri type: %s", type(off_pri))
103 | off_pri = off_pri.encode()
104 |
105 | signing_key = SigningKey(off_pri, self.key_encoder)
106 |
107 | # Create signature from app signing public key
108 | signature = signing_key.sign(app_pub)[:64]
109 | signature = self.key_encoder.encode(signature).decode()
110 |
111 | app_pri = app_pri.decode()
112 | app_pub = app_pub.decode()
113 |
114 | keypack = {
115 | "upload": {"app_public": app_pub, "signature": signature},
116 | "client": {"offline_public": off_pub},
117 | "repo": {"app_private": app_pri},
118 | }
119 | return keypack
120 |
121 | def _load_offline_keys(self, name):
122 | if name not in self.key_data.keys():
123 | # We create new offline keys for each app
124 | pri, pub = self._make_keys()
125 | pri = pri.decode()
126 | pub = pub.decode()
127 | self.key_data[name] = {"private": pri, "public": pub}
128 | self._save()
129 | return self.key_data[name]["private"], self.key_data[name]["public"]
130 |
131 | def _make_keys(self):
132 | # Makes a set of private and public keys
133 | privkey = SigningKey.generate()
134 | pubkey = privkey.verify_key
135 |
136 | pri = privkey.encode(self.key_encoder)
137 | pub = pubkey.encode(self.key_encoder)
138 | return pri, pub
139 |
140 |
141 | class KeyImporter(object):
142 | def __init__(self):
143 | self.db = Storage()
144 |
145 | @staticmethod
146 | def _look_for_keypack():
147 | files = os.listdir(os.getcwd())
148 | if settings.KEYPACK_FILENAME not in files:
149 | return False
150 | return True
151 |
152 | @staticmethod
153 | def _load_keypack():
154 | json_data = None
155 | try:
156 | with io.open(settings.KEYPACK_FILENAME, "r", encoding="utf-8") as f:
157 | data = f.read()
158 | except Exception as err:
159 | log.debug(err, exc_info=True)
160 | else:
161 | try:
162 | json_data = json.loads(data)
163 | except Exception as err:
164 | log.debug(err, exc_info=True)
165 | return json_data
166 |
167 | def start(self):
168 | found = KeyImporter._look_for_keypack()
169 | if found is False:
170 | return False
171 | keypack = KeyImporter._load_keypack()
172 | if keypack is None:
173 | return False
174 | self.db.save(settings.CONFIG_DB_KEY_KEYPACK, keypack)
175 | return True
176 |
--------------------------------------------------------------------------------
/pyupdater/core/key_handler/__init__.py:
--------------------------------------------------------------------------------
1 | # ------------------------------------------------------------------------------
2 | # Copyright (c) 2015-2020 Digital Sapphire
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining
5 | # a copy of this software and associated documentation files
6 | # (the "Software"), to deal in the Software without restriction, including
7 | # without limitation the rights to use, copy, modify, merge, publish,
8 | # distribute, sublicense, and/or sell copies of the Software, and to permit
9 | # persons to whom the Software is furnished to do so, subject to the
10 | # following conditions:
11 | #
12 | # The above copyright notice and this permission notice shall be
13 | # included in all copies or substantial portions of the Software.
14 | #
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
16 | # ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
17 | # TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
18 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
19 | # SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
20 | # ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
21 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
23 | # OR OTHER DEALINGS IN THE SOFTWARE.
24 | # ------------------------------------------------------------------------------
25 | from __future__ import print_function, unicode_literals
26 | import copy
27 | import gzip
28 | import json
29 | import logging
30 | import os
31 |
32 | from nacl.signing import SigningKey
33 |
34 | from pyupdater import settings
35 | from pyupdater.utils.storage import Storage
36 | from pyupdater.utils.encoding import UnpaddedBase64Encoder
37 |
38 |
39 | log = logging.getLogger(__name__)
40 |
41 |
42 | class KeyHandler(object):
43 | """KeyHandler object is used to manage keys used for signing updates
44 |
45 | Kwargs:
46 |
47 | app (obj): Config object to get config values from
48 | """
49 |
50 | def __init__(self):
51 | self.db = Storage()
52 |
53 | self.key_encoder = UnpaddedBase64Encoder()
54 | data_dir = os.getcwd()
55 | self.data_dir = os.path.join(data_dir, settings.USER_DATA_FOLDER)
56 | self.deploy_dir = os.path.join(self.data_dir, "deploy")
57 |
58 | # Name of the keypack to import. It should be placed
59 | # in the root of the repo
60 | self.keypack_filename = os.path.join(
61 | data_dir, settings.CONFIG_DATA_FOLDER, settings.KEYPACK_FILENAME
62 | )
63 |
64 | # The name of the gzipped version file in
65 | # the pyu-data/deploy directory
66 | self.version_file = os.path.join(
67 | self.deploy_dir, settings.VERSION_FILE_FILENAME
68 | )
69 |
70 | self.version_file_compat = os.path.join(
71 | self.deploy_dir, settings.VERSION_FILE_FILENAME_COMPAT
72 | )
73 |
74 | # The name of the gzipped key file in
75 | # the pyu-data/deploy directory
76 | self.key_file = os.path.join(self.deploy_dir, settings.KEY_FILE_FILENAME)
77 |
78 | def sign_update(self, split_version):
79 | """Signs version file with private key
80 |
81 | Proxy method for :meth:`_add_sig`
82 | """
83 | # Loads private key
84 | # Loads version file to memory
85 | # Signs Version file
86 | # Writes version file back to disk
87 | self._add_sig(split_version)
88 |
89 | def _load_private_keys(self):
90 | # Loads private key
91 | log.debug("Loading private key")
92 |
93 | # Loading keypack data from .pyupdater/config.pyu
94 | keypack_data = self.db.load(settings.CONFIG_DB_KEY_KEYPACK)
95 | private_key = None
96 | if keypack_data is not None:
97 | try:
98 | private_key = keypack_data["repo"]["app_private"]
99 | except KeyError:
100 | # We will exit in _add_sig if private_key is None
101 | pass
102 | return private_key
103 |
104 | def _add_sig(self, split_version):
105 | # Adding new signature to version file
106 | # Raw private key will need to be converted into
107 | # a signing key object
108 | private_key_raw = self._load_private_keys()
109 | if private_key_raw is None:
110 | log.error("Private Key not found. Please " "import a keypack & try again")
111 | return
112 |
113 | # Load update manifest
114 | update_data = self._load_update_data()
115 |
116 | # We don't want to verify the signature
117 | if "signature" in update_data:
118 | log.debug("Removing signatures from version file")
119 | del update_data["signature"]
120 |
121 | # We create a signature from the string
122 | update_data_str = json.dumps(update_data, sort_keys=True)
123 |
124 | # Creating signing key object
125 | private_key = SigningKey(private_key_raw, self.key_encoder)
126 | log.debug("Signing update data")
127 | # Signs update data with private key
128 | signature = private_key.sign(bytes(update_data_str, "latin-1"))
129 |
130 | signature = self.key_encoder.encode(signature[:64]).decode()
131 |
132 | log.debug("Sig: %s", signature)
133 |
134 | # Create new dict from json string
135 | update_data = json.loads(update_data_str)
136 |
137 | # Add signatures to update data
138 | update_data["signature"] = signature
139 | log.debug("Adding signature to update data")
140 |
141 | # Write updated version file to .pyupdater/config.pyu
142 | self._write_update_data(update_data, split_version)
143 |
144 | # Write gzipped key file
145 | self._write_key_file()
146 |
147 | def _write_update_data(self, data, split_version):
148 | log.debug("Saved version meta data")
149 |
150 | if split_version:
151 | version_file = self.version_file
152 | else:
153 | version_file = self.version_file_compat
154 |
155 | # Gzip update date
156 | with gzip.open(version_file, "wb") as f:
157 | new_data = json.dumps(data)
158 | f.write(bytes(new_data, "utf-8"))
159 |
160 | log.debug("Created gzipped version manifest in deploy dir")
161 |
162 | def _write_key_file(self):
163 | keypack_data = self.db.load(settings.CONFIG_DB_KEY_KEYPACK)
164 | if keypack_data is None:
165 | log.error("Private Key not found. Please import a keypack & try again")
166 | return
167 |
168 | upload_data = keypack_data["upload"]
169 | with gzip.open(self.key_file, "wb") as f:
170 | new_data = json.dumps(upload_data)
171 | f.write(bytes(new_data, "utf-8"))
172 | log.debug("Created gzipped key file in deploy dir")
173 |
174 | def _load_update_data(self):
175 | log.debug("Loading version data")
176 | update_data = self.db.load(settings.CONFIG_DB_KEY_VERSION_META)
177 | # If update_data is None, create a new one
178 | if update_data is None:
179 | update_data = {}
180 | log.error("Version meta data not found")
181 | self.db.save(settings.CONFIG_DB_KEY_VERSION_META, update_data)
182 | log.debug("Created new version meta data")
183 | log.debug("Version file loaded")
184 |
185 | return copy.deepcopy(update_data)
186 |
--------------------------------------------------------------------------------
/pyupdater/core/package_handler/patch.py:
--------------------------------------------------------------------------------
1 | # ------------------------------------------------------------------------------
2 | # Copyright (c) 2015-2020 Digital Sapphire
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining
5 | # a copy of this software and associated documentation files
6 | # (the "Software"), to deal in the Software without restriction, including
7 | # without limitation the rights to use, copy, modify, merge, publish,
8 | # distribute, sublicense, and/or sell copies of the Software, and to permit
9 | # persons to whom the Software is furnished to do so, subject to the
10 | # following conditions:
11 | #
12 | # The above copyright notice and this permission notice shall be
13 | # included in all copies or substantial portions of the Software.
14 | #
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
16 | # ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
17 | # TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
18 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
19 | # SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
20 | # ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
21 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
23 | # OR OTHER DEALINGS IN THE SOFTWARE.
24 | # ------------------------------------------------------------------------------
25 | from __future__ import unicode_literals
26 | import json
27 | import logging
28 | import os
29 |
30 | try: # pragma: no cover
31 | import bsdiff4
32 | except ImportError: # pragma: no cover
33 | bsdiff4 = None
34 | from dsdev_utils.paths import ChDir
35 |
36 | from pyupdater import settings
37 | from pyupdater.utils import remove_dot_files
38 |
39 |
40 | log = logging.getLogger(__name__)
41 |
42 |
43 | def make_patch(patch):
44 | log.debug("Patch source path: %s", patch.src)
45 | log.debug("Patch destination path: %s", patch.dst)
46 | log.info("Creating patch... %s", patch.basename)
47 |
48 | bsdiff4.file_diff(patch.src, patch.dst, patch.patch_name)
49 |
50 | log.info("Done creating patch... %s", patch.basename)
51 |
52 | return patch
53 |
54 |
55 | class Patch(object):
56 | def __init__(self, **kwargs):
57 | self._pkg_info = kwargs.get("pkg_info")
58 | self._filename = kwargs.get("filename")
59 | self._files_dir = kwargs.get("files_dir")
60 | self._new_dir = kwargs.get("new_dir")
61 | self._json_data = kwargs.get("json_data")
62 | self._config = kwargs.get("config")
63 | self._test = kwargs.get("test", False)
64 |
65 | self.ok = False
66 | self.patch_num = None
67 | self.src = None
68 | self.dst = None
69 | self.patch_name = None
70 | self.basename = None
71 | self.hash = None
72 | self.size = None
73 |
74 | self._check_make_patch()
75 |
76 | if self.ok:
77 | self.dst = os.path.abspath(self._filename)
78 | _patch_name = "{}-{}-{}-{}".format(
79 | self._pkg_info.name,
80 | self._pkg_info.platform,
81 | self._pkg_info.channel,
82 | self.patch_num,
83 | )
84 | self.patch_name = os.path.join(self._new_dir, _patch_name)
85 | self.basename = os.path.basename(self.patch_name)
86 | self.dst_filename = self._pkg_info.filename
87 | self.channel = self._pkg_info.channel
88 |
89 | def __str__(self):
90 | tmpl = "Patch(ok={ok}, patch_name={patch_name}, basename={basename})"
91 | return tmpl.format(
92 | ok=self.ok, patch_name=self.patch_name, basename=self.basename
93 | )
94 |
95 | def _check_make_patch(self):
96 | # Check to see if previous version is available to
97 | # make patch updates. Also calculates patch number
98 | if self._json_data.get("latest") is not None:
99 | log.debug(json.dumps(self._json_data["latest"], indent=2))
100 | log.debug("Checking if patch creation is possible")
101 | if bsdiff4 is None:
102 | log.warning("Bsdiff is missing. Cannot create patches")
103 | return
104 |
105 | if os.path.exists(self._files_dir):
106 | with ChDir(self._files_dir):
107 | files = os.listdir(os.getcwd())
108 | log.debug("Found %s files in files dir", len(files))
109 |
110 | files = remove_dot_files(files)
111 | # No src files to patch from. Exit quickly
112 | if len(files) == 0:
113 | log.debug("No src file to patch from")
114 | return
115 |
116 | _name = self._pkg_info.name
117 | _plat = self._pkg_info.platform
118 | _channel = self._pkg_info.channel
119 | if self._test is False:
120 | # If latest not available in version file. Exit
121 | try:
122 | log.debug("Looking for %s on %s", _name, _plat)
123 | latest = self._json_data["latest"][_name][_channel][_plat]
124 | log.debug("Found latest version for patches: %s", latest)
125 | except KeyError:
126 | log.debug("Cannot find latest version in version meta")
127 | return
128 | try:
129 | u_key = settings.UPDATES_KEY
130 | latest_platform = self._json_data[u_key][_name][latest]
131 | log.debug("Found latest platform for patches")
132 | try:
133 | filename = latest_platform[_plat]["filename"]
134 | log.debug("Found filename for patches")
135 | except KeyError:
136 | log.error(
137 | "Found old version file. Please read "
138 | "the upgrade section in the docs."
139 | )
140 | log.debug("Found old verison file")
141 | return
142 | except Exception as err:
143 | log.debug(err, exc_info=True)
144 | return
145 | else:
146 | filename = self._filename
147 |
148 | log.debug("Generating src file path")
149 | self.src = os.path.join(self._files_dir, filename)
150 | log.debug("Source path: %s", self.src)
151 | if not os.path.exists(self.src):
152 | log.warning("Source path does not exist: %s", filename)
153 | return
154 |
155 | try:
156 | patch_num = self._config["patches"][_name]
157 | log.debug("Found patch number")
158 | self._config["patches"][_name] += 1
159 | except KeyError:
160 | log.debug("Cannot find patch number")
161 | # If no patch number we will start at 1
162 | patch_num = 1
163 | if "patches" not in self._config.keys():
164 | log.debug("Adding patches to version meta")
165 | self._config["patches"] = {}
166 | if _name not in self._config["patches"].keys():
167 | log.debug("Adding %s to patches version meta", _name)
168 | self._config["patches"][_name] = patch_num + 1
169 | self.patch_num = patch_num + 1
170 | log.debug("Patch Number: %s", self.patch_num)
171 | self.ok = True
172 |
--------------------------------------------------------------------------------
/tests/test_package_handler.py:
--------------------------------------------------------------------------------
1 | # ------------------------------------------------------------------------------
2 | # Copyright (c) 2015-2020 Digital Sapphire
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining
5 | # a copy of this software and associated documentation files
6 | # (the "Software"), to deal in the Software without restriction, including
7 | # without limitation the rights to use, copy, modify, merge, publish,
8 | # distribute, sublicense, and/or sell copies of the Software, and to permit
9 | # persons to whom the Software is furnished to do so, subject to the
10 | # following conditions:
11 | #
12 | # The above copyright notice and this permission notice shall be
13 | # included in all copies or substantial portions of the Software.
14 | #
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
16 | # ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
17 | # TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
18 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
19 | # SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
20 | # ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
21 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
23 | # OR OTHER DEALINGS IN THE SOFTWARE.
24 | # ------------------------------------------------------------------------------
25 | from __future__ import unicode_literals
26 |
27 | import io
28 | import os
29 |
30 | from dsdev_utils.paths import ChDir
31 | import pytest
32 |
33 | from pyupdater import settings
34 | from pyupdater.core.package_handler import PackageHandler
35 | from pyupdater.core.package_handler.package import Package, parse_platform
36 | from pyupdater.core.package_handler.patch import Patch
37 | from pyupdater.utils.config import Config
38 | from pyupdater.utils.exceptions import PackageHandlerError
39 |
40 | from tconfig import TConfig
41 |
42 | user_data_dir = settings.USER_DATA_FOLDER
43 |
44 |
45 | @pytest.mark.usefixtures("cleandir", "pyu")
46 | class TestUtils(object):
47 | def test_init(self):
48 | data_dir = os.getcwd()
49 | t_config = TConfig()
50 | t_config.DATA_DIR = data_dir
51 | config = Config()
52 | config.from_object(t_config)
53 | p = PackageHandler(config)
54 | assert p.files_dir == os.path.join(data_dir, user_data_dir, "files")
55 | assert p.deploy_dir == os.path.join(data_dir, user_data_dir, "deploy")
56 |
57 | def test_no_patch_support(self):
58 | data_dir = os.getcwd()
59 | t_config = TConfig()
60 | t_config.DATA_DIR = data_dir
61 | t_config.UPDATE_PATCHES = False
62 | config = Config()
63 | config.from_object(t_config)
64 | p = PackageHandler(config)
65 | p.process_packages()
66 |
67 | def test_parse_platform(self):
68 | assert parse_platform("app-mac-0.1.0.tar.gz") == "mac"
69 | assert parse_platform("app-win-1.0.0.zip") == "win"
70 | assert parse_platform("Email Parser-mac-0.2.0.tar.gz") == "mac"
71 | assert parse_platform("Hangman-nix-0.0.1b1.zip") == "nix"
72 |
73 | def test_parse_platform_fail(self):
74 | with pytest.raises(PackageHandlerError):
75 | parse_platform("app-nex-1.0.0.tar.gz")
76 |
77 |
78 | @pytest.mark.usefixtures("cleandir", "pyu")
79 | class TestExecution(object):
80 | def test_process_packages(self):
81 | data_dir = os.getcwd()
82 | t_config = TConfig()
83 | t_config.DATA_DIR = data_dir
84 | t_config.UPDATE_PATCHES = False
85 | config = Config()
86 | config.from_object(t_config)
87 | p = PackageHandler(config)
88 | p.process_packages()
89 |
90 |
91 | @pytest.mark.usefixtures("cleandir")
92 | class TestPackage(object):
93 | def test_package_1(self, shared_datadir):
94 | test_file = "Acme-mac-4.1.tar.gz"
95 | p1 = Package(shared_datadir / test_file)
96 |
97 | assert p1.name == "Acme"
98 | assert p1.version == "4.1.0.2.0"
99 | assert p1.filename == test_file
100 | assert p1.platform == "mac"
101 | assert p1.channel == "stable"
102 | assert p1.info["status"] is True
103 |
104 | def test_package_name_with_spaces(self, shared_datadir):
105 | test_file = "with spaces-nix-0.0.1b1.zip"
106 | p1 = Package(shared_datadir / test_file)
107 |
108 | assert p1.name == "with spaces"
109 | assert p1.version == "0.0.1.1.1"
110 | assert p1.filename == test_file
111 | assert p1.platform == "nix"
112 | assert p1.channel == "beta"
113 | assert p1.info["status"] is True
114 |
115 | def test_package_alpha(self, shared_datadir):
116 | test_file = "with spaces-win-0.0.1a2.zip"
117 | p1 = Package(shared_datadir / test_file)
118 |
119 | assert p1.name == "with spaces"
120 | assert p1.version == "0.0.1.0.2"
121 | assert p1.filename == test_file
122 | assert p1.platform == "win"
123 | assert p1.channel == "alpha"
124 | assert p1.info["status"] is True
125 |
126 | def test_package_ignored_file(self):
127 | with io.open(".DS_Store", "w", encoding="utf-8") as f:
128 | f.write("")
129 | p = Package(".DS_Store")
130 | assert p.info["status"] is False
131 |
132 | def test_package_bad_extension(self, shared_datadir):
133 | test_file_2 = "pyu-win-0.0.2.xz"
134 | p2 = Package(shared_datadir / test_file_2)
135 |
136 | assert p2.filename == test_file_2
137 | assert p2.name is None
138 | assert p2.version is None
139 | assert p2.info["status"] is False
140 | assert p2.info["reason"] == (
141 | "Not a supported archive format: " "{}".format(test_file_2)
142 | )
143 |
144 | def test_package_bad_version(self, shared_datadir):
145 | filename = "pyu-win-1.tar.gz"
146 | p = Package(shared_datadir / filename)
147 | out = "Package version not formatted correctly: {}"
148 | assert p.info["reason"] == out.format(filename)
149 |
150 | def test_package_bad_platform(self, shared_datadir):
151 | filename = "pyu-wi-1.1.tar.gz"
152 | p = Package(shared_datadir / filename)
153 | out = "Package platform not formatted correctly"
154 | assert p.info["reason"] == out
155 |
156 |
157 | @pytest.mark.usefixtures("cleandir")
158 | class TestPatch(object):
159 | new_dir = "pyu-data/new"
160 | files_dir = "pyu-data/files"
161 |
162 | @pytest.fixture
163 | def patch_setup(self):
164 | os.makedirs(self.new_dir)
165 | os.makedirs(self.files_dir)
166 |
167 | with open(os.path.join(self.new_dir, "Acme-mac-4.2.tar.gz"), "w") as f:
168 | f.write("v2")
169 |
170 | with open(os.path.join(self.files_dir, "Acme-mac-4.1.tar.gz"), "w") as f:
171 | f.write("v1")
172 |
173 | def test_patch(self, patch_setup):
174 | filename = "Acme-mac-4.2.tar.gz"
175 |
176 | with ChDir(self.new_dir):
177 | full_path = os.path.abspath(filename)
178 | pkg = Package(full_path)
179 |
180 | config = {}
181 | version_data = {}
182 | data = {
183 | "filename": full_path,
184 | "files_dir": self.files_dir,
185 | "new_dir": self.new_dir,
186 | "json_data": version_data,
187 | "pkg_info": pkg,
188 | "config": config,
189 | "test": True,
190 | }
191 |
192 | patch = Patch(**data)
193 |
194 | assert patch.ok
195 | assert config["patches"][pkg.name]
196 |
197 | def test_patch_fail(self):
198 | pass
199 |
--------------------------------------------------------------------------------
/docs/api.md:
--------------------------------------------------------------------------------
1 | # API
2 |
3 |
4 |
5 | ## Table of contents
6 |
7 | ### [pyupdater.client](#pyupdaterclient)
8 |
9 | * [pyupdater.client.get_highest_version](#pyupdaterclientget_highest_versionname-plat-channel-easy_data-strict)
10 | * [pyupdater.client.AppUpdate](#pyupdaterclientappupdate)
11 | * [pyupdater.client.Client](#pyupdaterclientclient)
12 | * [pyupdater.client.ClientError](#pyupdaterclientclienterror)
13 | * [pyupdater.client.DefaultClientConfig](#pyupdaterclientdefaultclientconfig)
14 | * [pyupdater.client.FileDownloader](#pyupdaterclientfiledownloader)
15 | * [pyupdater.client.LibUpdate](#pyupdaterclientlibupdate)
16 | * [pyupdater.client.UnpaddedBase64Encoder](#pyupdaterclientunpaddedbase64encoder)
17 | * [pyupdater.client.UpdateStrategy](#pyupdaterclientupdatestrategy)
18 | * [pyupdater.client.VerifyKey](#pyupdaterclientverifykey)
19 |
20 |
21 |
22 |
23 | ## pyupdater.client
24 |
25 |
26 |
27 | ##### pyupdater.client.get_highest_version(name, plat, channel, easy_data, strict)
28 |
29 |
30 |
31 | ### pyupdater.client.AppUpdate
32 |
33 | Used to update an application. This object is returned by
34 | pyupdater.client.Client.update_check
35 |
36 | Args:
37 |
38 | data (dict): Info dict
39 |
40 | #### Methods
41 |
42 | ##### AppUpdate.cleanup()
43 |
44 | Cleans up old update archives for this app or asset
45 |
46 | ##### AppUpdate.download(background=False)
47 |
48 | Downloads update
49 |
50 | ######Args:
51 |
52 | background (bool): Perform download in background thread
53 |
54 | ##### AppUpdate.extract()
55 |
56 | Will extract the update from its archive to the update folder.
57 | If updating a lib you can take over from there. If updating
58 | an app this call should be followed by method "restart" to
59 | complete update.
60 |
61 | ######Returns:
62 |
63 | (bool) True - Extract successful. False - Extract failed.
64 |
65 | ##### AppUpdate.extract_overwrite()
66 |
67 | Will extract the update then overwrite the current binary
68 |
69 | ##### AppUpdate.extract_restart()
70 |
71 | Will extract the update, overwrite the current binary,
72 | then restart the application using the updated binary.
73 |
74 | ##### AppUpdate.is_downloaded()
75 |
76 | Used to check if update has been downloaded.
77 |
78 | ######Returns (bool):
79 |
80 | True - File is already downloaded.
81 |
82 | False - File has not been downloaded.
83 |
84 | ### pyupdater.client.Client
85 |
86 | Used to check for updates & returns an updateobject if there
87 | is an update.
88 |
89 | ######Args:
90 |
91 | obj (instance): config object
92 |
93 | ######Kwargs:
94 |
95 | refresh (bool): True - Refresh update manifest on init
96 | False - Don't refresh update manifest on init
97 |
98 | progress_hooks (list): List of callbacks
99 |
100 | data_dir (str): Path to custom update folder
101 |
102 | headers (dict): A dictionary of generic and/or urllib3.utils.make_headers compatible headers
103 |
104 | strategy (str): The update strategy to use (default: overwrite). See the UpdateStrategy enum for options.
105 |
106 | test (bool): Used to initialize a test client
107 |
108 | #### Methods
109 |
110 | ##### Client.add_progress_hook(cb)
111 |
112 | Add a download progress callback function to the list of progress
113 | hooks.
114 |
115 | total: Total size of the file to download
116 |
117 | downloaded: The amount of bytes that have been downloaded so far.
118 |
119 | percent_complete: The percentage downloaded so far
120 |
121 | status: Status of download
122 |
123 | Args:
124 |
125 | cb (function): Function which takes a dict as its first argument
126 |
127 | ##### Client.refresh()
128 |
129 | Will download and verify the version manifest.
130 |
131 | ##### Client.update_check(name, version, channel='stable', strict=True)
132 |
133 | Checks for available updates
134 |
135 | ######Args:
136 |
137 | name (str): Name of file to update
138 |
139 | version (str): Current version number of file to update
140 |
141 | channel (str): Release channel
142 |
143 | strict (bool):
144 | True - Only look for updates on specified channel.
145 | False - Look for updates on all channels
146 |
147 | ######Returns:
148 |
149 | (updateobject):
150 |
151 | AppUpdate - Used to update current binary
152 |
153 | LibUpdate - Used to update external assets
154 |
155 | None - No Updates available
156 |
157 | ### pyupdater.client.ClientError
158 |
159 | Raised for Client exceptions
160 |
161 | #### Methods
162 |
163 | ##### ClientError.format_traceback()
164 |
165 |
166 |
167 | ### pyupdater.client.DefaultClientConfig
168 |
169 |
170 |
171 | #### Methods
172 |
173 | ### pyupdater.client.FileDownloader
174 |
175 | The FileDownloader object downloads files to memory and
176 | verifies their hash. If hash is verified data is either
177 | written to disk to returned to calling object
178 |
179 | ######Args:
180 |
181 | filename (str): The name of file to download
182 |
183 | urls (list): List of urls to use for file download
184 |
185 | hexdigest (str): The hash of the file to download
186 |
187 | ######Kwargs:
188 |
189 | headers (str):
190 |
191 | hexdigest (str): The hash of the file to download
192 |
193 | verify (bool):
194 |
195 | True: Verify https connection
196 |
197 | False: Do not verify https connection
198 |
199 | #### Methods
200 |
201 | ##### FileDownloader.download_verify_return()
202 |
203 | Downloads file to memory, checks against provided hash
204 | If matched returns binary data
205 |
206 | Returns:
207 |
208 | (data):
209 |
210 | Binary data - If hashes match or no hash was given during
211 | initialization.
212 |
213 | None - If any verification didn't pass
214 |
215 | ##### FileDownloader.download_verify_write()
216 |
217 | Downloads file then verifies against provided hash
218 | If hash verfies then writes data to disk
219 |
220 | Returns:
221 |
222 | (bool):
223 |
224 | True - Hashes match or no hash was given during initialization.
225 |
226 | False - Hashes don't match
227 |
228 | ### pyupdater.client.LibUpdate
229 |
230 | Used to update library files used by an application. This object is
231 | returned by pyupdater.client.Client.update_check
232 |
233 | ######Args:
234 |
235 | data (dict): Info dict
236 |
237 | #### Methods
238 |
239 | ##### LibUpdate.cleanup()
240 |
241 | Cleans up old update archives for this app or asset
242 |
243 | ##### LibUpdate.download(background=False)
244 |
245 | Downloads update
246 |
247 | ######Args:
248 |
249 | background (bool): Perform download in background thread
250 |
251 | ##### LibUpdate.extract()
252 |
253 | Will extract the update from its archive to the update folder.
254 | If updating a lib you can take over from there. If updating
255 | an app this call should be followed by method "restart" to
256 | complete update.
257 |
258 | ######Returns:
259 |
260 | (bool) True - Extract successful. False - Extract failed.
261 |
262 | ##### LibUpdate.is_downloaded()
263 |
264 | Used to check if update has been downloaded.
265 |
266 | ######Returns (bool):
267 |
268 | True - File is already downloaded.
269 |
270 | False - File has not been downloaded.
271 |
272 | ### pyupdater.client.UnpaddedBase64Encoder
273 |
274 | A simple encoder class to encode/decode to base64 with the 'padding' (trailing equal characters) removed.
275 |
276 | This is needed for PyNaCL to be compatible with keys generated by the old ed25519 library.
277 |
278 | #### Methods
279 |
280 | ##### UnpaddedBase64Encoder.decode(data)
281 |
282 |
283 |
284 | ##### UnpaddedBase64Encoder.encode(data)
285 |
286 |
287 |
288 | ### pyupdater.client.UpdateStrategy
289 |
290 | Enum representing the update strategies available
291 |
292 | #### Methods
293 |
294 | ### pyupdater.client.VerifyKey
295 |
296 | The public key counterpart to an Ed25519 SigningKey for producing digital
297 | signatures.
298 |
299 | :param key: [:class:`bytes`] Serialized Ed25519 public key
300 | :param encoder: A class that is able to decode the `key`
301 |
302 | #### Methods
303 |
304 |
--------------------------------------------------------------------------------
/tests/test_cli.py:
--------------------------------------------------------------------------------
1 | # ------------------------------------------------------------------------------
2 | # Copyright (c) 2015-2020 Digital Sapphire
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining
5 | # a copy of this software and associated documentation files
6 | # (the "Software"), to deal in the Software without restriction, including
7 | # without limitation the rights to use, copy, modify, merge, publish,
8 | # distribute, sublicense, and/or sell copies of the Software, and to permit
9 | # persons to whom the Software is furnished to do so, subject to the
10 | # following conditions:
11 | #
12 | # The above copyright notice and this permission notice shall be
13 | # included in all copies or substantial portions of the Software.
14 | #
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
16 | # ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
17 | # TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
18 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
19 | # SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
20 | # ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
21 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
23 | # OR OTHER DEALINGS IN THE SOFTWARE.
24 | # ------------------------------------------------------------------------------
25 | from __future__ import unicode_literals
26 |
27 | import io
28 | import os
29 |
30 | import pytest
31 |
32 | from pyupdater.cli import commands, dispatch_command
33 | from pyupdater.cli.options import (
34 | add_build_parser,
35 | add_clean_parser,
36 | add_keys_parser,
37 | add_make_spec_parser,
38 | add_package_parser,
39 | add_upload_parser,
40 | make_subparser,
41 | )
42 |
43 | commands.TEST = True
44 |
45 |
46 | class NamespaceHelper(object):
47 | def __init__(self, **kwargs):
48 | self.reload(**kwargs)
49 |
50 | def _clear(self):
51 | self.__dict__.clear()
52 |
53 | def _pre_update(self):
54 | self.__dict__.update(test=True)
55 |
56 | def reload(self, **kwargs):
57 | self._clear()
58 | self._pre_update()
59 | self.__dict__.update(**kwargs)
60 |
61 | def __getattribute__(self, name):
62 | try:
63 | return object.__getattribute__(self, name)
64 | except AttributeError:
65 | self.__dict__[name] = None
66 | return None
67 |
68 |
69 | @pytest.mark.usefixtures("cleandir")
70 | class TestCommandDispatch(object):
71 | def test_build(self):
72 | assert dispatch_command(NamespaceHelper(command="build"), test=True) is True
73 |
74 | def test_clean(self):
75 | assert dispatch_command(NamespaceHelper(command="clean"), test=True) is True
76 |
77 | def test_settings(self):
78 | assert dispatch_command(NamespaceHelper(command="settings"), test=True) is True
79 |
80 | def test_init(self):
81 | assert dispatch_command(NamespaceHelper(command="init"), test=True) is True
82 |
83 | def test_keys(self):
84 | assert dispatch_command(NamespaceHelper(command="keys"), test=True) is True
85 |
86 | def test_make_spec(self):
87 | assert dispatch_command(NamespaceHelper(command="make-spec"), test=True) is True
88 |
89 | def test_pkg(self):
90 | assert dispatch_command(NamespaceHelper(command="pkg"), test=True) is True
91 |
92 | def test_collect_debug_info(self):
93 | assert (
94 | dispatch_command(NamespaceHelper(command="collect-debug-info"), test=True)
95 | is True
96 | )
97 |
98 | def test_upload(self):
99 | assert dispatch_command(NamespaceHelper(command="upload"), test=True) is True
100 |
101 | def test_version(self):
102 | assert dispatch_command(NamespaceHelper(command="version"), test=True) is True
103 |
104 |
105 | @pytest.mark.usefixtures("cleandir")
106 | class TestBuilder(object):
107 | def test_build_no_options(self, parser):
108 | subparser = make_subparser(parser)
109 | add_build_parser(subparser)
110 | with pytest.raises(SystemExit):
111 | parser.parse_known_args(["build"])
112 |
113 | def test_build_no_arguments(self, parser, pyu):
114 | pyu.setup()
115 | subparser = make_subparser(parser)
116 | add_build_parser(subparser)
117 | with pytest.raises(SystemExit):
118 | with io.open("app.py", "w", encoding="utf-8") as f:
119 | f.write("from __future__ import print_function\n")
120 | f.write('print("Hello, World!")')
121 | opts, other = parser.parse_known_args(["build", "app.py"])
122 | commands._cmd_build(opts, other)
123 |
124 |
125 | @pytest.mark.usefixtures("cleandir", "parser")
126 | class TestClean(object):
127 | def test_no_args(self, parser):
128 | subparser = make_subparser(parser)
129 | add_clean_parser(subparser)
130 | assert parser.parse_known_args(["clean"])
131 |
132 | def test_execution(self, parser):
133 | update_folder = "pyu-data"
134 | data_folder = ".pyupdater"
135 | subparser = make_subparser(parser)
136 | add_clean_parser(subparser)
137 | os.mkdir(update_folder)
138 | os.mkdir(data_folder)
139 | args, other = parser.parse_known_args(["clean", "-y"])
140 | commands._cmd_clean(args)
141 | assert not os.path.exists(update_folder)
142 | assert not os.path.exists(data_folder)
143 |
144 | def test_execution_no_clean(self, parser):
145 | update_folder = "pyu-data"
146 | data_folder = ".pyupdater"
147 | subparser = make_subparser(parser)
148 | add_clean_parser(subparser)
149 | args, other = parser.parse_known_args(["clean", "-y"])
150 | commands._cmd_clean(args)
151 | assert not os.path.exists(update_folder)
152 | assert not os.path.exists(data_folder)
153 |
154 |
155 | @pytest.mark.usefixtures("cleandir", "parser")
156 | class TestKeys(object):
157 | def test_no_options(self, parser):
158 | subparser = make_subparser(parser)
159 | add_keys_parser(subparser)
160 | assert parser.parse_known_args(["keys"])
161 |
162 | def test_create_keys(self):
163 | commands._cmd_keys(NamespaceHelper(command="keys", create_keys=True))
164 | assert os.path.exists("keypack.pyu")
165 |
166 |
167 | @pytest.mark.usefixtures("cleandir", "parser", "pyu")
168 | class TestMakeSpec(object):
169 | def test_deprecated_opts(self, parser, pyu):
170 | pyu.setup()
171 | subparser = make_subparser(parser)
172 | add_make_spec_parser(subparser)
173 | with io.open("app.py", "w", encoding="utf-8") as f:
174 | f.write('print "Hello World"')
175 | opts, other = parser.parse_known_args(["make-spec", "-F", "app.py"])
176 | commands._cmd_make_spec(opts, other)
177 |
178 | def test_execution(self, parser, pyu):
179 | pyu.setup()
180 | subparser = make_subparser(parser)
181 | add_make_spec_parser(subparser)
182 | with io.open("app.py", "w", encoding="utf-8") as f:
183 | f.write('print "Hello World"')
184 | opts, other = parser.parse_known_args(["make-spec", "-F", "app.py"])
185 | commands._cmd_make_spec(opts, other)
186 |
187 |
188 | @pytest.mark.usefixtures("cleandir")
189 | class TestPkg(object):
190 | def test_pkg_execution(self, parser, pyu):
191 | subparser = make_subparser(parser)
192 | add_package_parser(subparser)
193 | pyu.update_config(pyu.config)
194 | pyu.setup()
195 | cmd = ["pkg", "-P", "-S"]
196 | opts, other = parser.parse_known_args(cmd)
197 | commands._cmd_pkg(opts)
198 |
199 | def test_pkg_execution_no_opts(self, parser, pyu):
200 | subparser = make_subparser(parser)
201 | add_package_parser(subparser)
202 |
203 | pyu.update_config(pyu.config)
204 | pyu.setup()
205 | cmd = ["pkg"]
206 |
207 | opts, other = parser.parse_known_args(cmd)
208 | commands._cmd_pkg(opts)
209 |
--------------------------------------------------------------------------------
/pyupdater/core/uploader.py:
--------------------------------------------------------------------------------
1 | # ------------------------------------------------------------------------------
2 | # Copyright (c) 2015-2020 Digital Sapphire
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining
5 | # a copy of this software and associated documentation files
6 | # (the "Software"), to deal in the Software without restriction, including
7 | # without limitation the rights to use, copy, modify, merge, publish,
8 | # distribute, sublicense, and/or sell copies of the Software, and to permit
9 | # persons to whom the Software is furnished to do so, subject to the
10 | # following conditions:
11 | #
12 | # The above copyright notice and this permission notice shall be
13 | # included in all copies or substantial portions of the Software.
14 | #
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
16 | # ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
17 | # TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
18 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
19 | # SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
20 | # ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
21 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
23 | # OR OTHER DEALINGS IN THE SOFTWARE.
24 | # ------------------------------------------------------------------------------
25 | from __future__ import print_function, unicode_literals
26 | import logging
27 | import os
28 |
29 | from dsdev_utils.paths import remove_any
30 | from dsdev_utils.terminal import get_correct_answer
31 |
32 | from pyupdater import settings
33 | from pyupdater.utils import remove_dot_files, PluginManager
34 | from pyupdater.utils.exceptions import UploaderError, UploaderPluginError
35 |
36 | log = logging.getLogger(__name__)
37 |
38 |
39 | class Uploader(object):
40 | """Uploads updates to configured servers. SSH, SFTP, S3
41 | Will automatically pick the correct uploader depending on
42 | what is configured thorough the config object
43 |
44 | Sets up client with config values from obj
45 |
46 | Args:
47 |
48 | config (instance): config object
49 | """
50 |
51 | def __init__(self, config, plugins=None):
52 | # Specifies whether to keep a file after uploading
53 | self.keep = False
54 |
55 | data_dir = os.path.join(os.getcwd(), settings.USER_DATA_FOLDER)
56 | self.deploy_dir = os.path.join(data_dir, "deploy")
57 |
58 | # The upload plugin that'll be used to upload our files
59 | self.uploader = None
60 |
61 | # Files to be uploaded
62 | self.files = []
63 |
64 | # Extension Manager
65 | self.plg_mgr = PluginManager(config, plugins)
66 |
67 | def _get_files_to_upload(self, files=None):
68 | if files:
69 | self.files = files
70 | else:
71 | try:
72 | _files = os.listdir(self.deploy_dir)
73 | except OSError:
74 | _files = []
75 |
76 | files = []
77 | for f in _files:
78 | files.append(os.path.join(self.deploy_dir, f))
79 |
80 | self.files = remove_dot_files(files)
81 |
82 | def get_plugin_names(self):
83 | return self.plg_mgr.get_plugin_names()
84 |
85 | def set_uploader(self, requested_uploader, keep=False):
86 | """Sets the named upload plugin.
87 |
88 | Args:
89 |
90 | requested_uploader (string): Either s3 or scp
91 |
92 | keep (bool): False to delete files after upload.
93 | True to keep files. Default False.
94 |
95 | """
96 | self.keep = keep
97 | if isinstance(requested_uploader, str) is False:
98 | raise UploaderError("Must pass str to set_uploader", expected=True)
99 |
100 | self.uploader = self.plg_mgr.get_plugin(requested_uploader, init=True)
101 | if self.uploader is None:
102 | log.debug("PLUGIN_NAMESPACE: %s", self.plg_mgr.PLUGIN_NAMESPACE)
103 | raise UploaderPluginError(
104 | "Requested uploader is not installed", expected=True
105 | )
106 |
107 | msg = "Requested uploader: {}".format(requested_uploader)
108 | log.debug(msg)
109 |
110 | def upload(self, files=None):
111 | """Uploads all files in file_list"""
112 | self._get_files_to_upload(files)
113 |
114 | failed_uploads = []
115 | files_completed = 1
116 | file_count = len(self.files)
117 | log.info("Plugin: %s", self.uploader.name)
118 | log.info("Author: %s", self.uploader.author)
119 |
120 | for f in self.files:
121 | basename = os.path.basename(f)
122 | msg = "\n\nUploading: {}".format(basename)
123 | msg2 = " - File {} of {}\n".format(files_completed, file_count)
124 | print(msg + msg2)
125 | complete = self.uploader.upload_file(f)
126 |
127 | if complete:
128 | log.debug("%s uploaded successfully", basename)
129 | if self.keep is False:
130 | remove_any(f)
131 | files_completed += 1
132 | else:
133 | log.debug("%s failed to upload. will retry", basename)
134 | failed_uploads.append(f)
135 |
136 | if len(failed_uploads) > 0:
137 | failed_uploads = self._retry_upload(failed_uploads)
138 |
139 | if len(failed_uploads) < 1:
140 | return True
141 | else:
142 | log.error("The following files were not uploaded")
143 | for i in failed_uploads:
144 | log.error("%s failed to upload", os.path.basename(i))
145 | return False
146 |
147 | def _retry_upload(self, failed_uploads):
148 | # Takes list of failed downloads and tries to re upload them
149 | retry = failed_uploads[:]
150 | failed_uploads = []
151 | failed_count = len(retry)
152 | count = 1
153 | for f in retry:
154 | msg = "Retyring: {} - File {} of {}".format(f, count, failed_count)
155 | log.info(msg)
156 | complete = self.uploader.upload_file(f)
157 | if complete:
158 | log.debug("%s uploaded on retry", f)
159 | if self.keep is False:
160 | remove_any(f)
161 | count += 1
162 | else:
163 | failed_uploads.append(f)
164 |
165 | return failed_uploads
166 |
167 |
168 | # noinspection PyProtectedMember
169 | class AbstractBaseUploaderMeta(type):
170 | def __call__(cls, *args, **kwargs):
171 | obj = type.__call__(cls, *args, **kwargs)
172 | # We are using this meta class to ensure plugin authors have
173 | # a plugin name & author name as class attributes.
174 | obj._check_attributes() # noqa
175 | # End ignore
176 | return obj
177 |
178 |
179 | class BaseUploader(object, metaclass=AbstractBaseUploaderMeta):
180 | name = None
181 | author = None
182 | """Base Uploader. All uploaders should subclass
183 | this base class
184 | """
185 |
186 | def _check_attributes(self):
187 | if self.name is None or self.author is None:
188 | raise NotImplementedError
189 |
190 | @staticmethod
191 | def get_answer(question, default=None):
192 | return get_correct_answer(question, default=default)
193 |
194 | def init_config(self, config):
195 | """Used to initialize plugin with saved config.
196 |
197 | Args:
198 |
199 | config (dict): config dict for plugin
200 | """
201 | raise NotImplementedError(
202 | "{} by {} must implemented in " "subclass.".format(self.name, self.author)
203 | )
204 |
205 | def set_config(self, config):
206 | """Used to ask user questions and return config
207 | for saving
208 |
209 | Args:
210 |
211 | config (dict): config dict that can be used to query
212 | already set values
213 |
214 | """
215 | raise NotImplementedError(
216 | "{} by {} must implemented in " "subclass.".format(self.name, self.author)
217 | )
218 |
219 | def upload_file(self, filename):
220 | """Uploads file to remote repository
221 |
222 | Args:
223 | filename (str): file to upload
224 |
225 | Returns:
226 | (bool):
227 |
228 | True - Upload Successful
229 |
230 | False - Upload Failed
231 | """
232 | raise NotImplementedError(
233 | "{} by {} must implemented in " "subclass.".format(self.name, self.author)
234 | )
235 |
--------------------------------------------------------------------------------