├── .clang-format ├── .editorconfig ├── .github └── workflows │ ├── linux.yml │ ├── macos.yml │ └── windows.yml ├── .gitignore ├── CHANGELOG.md ├── CMakeLists.txt ├── CMakePresets.json ├── LICENSE ├── README.md ├── cmake └── DeployQt.cmake ├── examples ├── basic │ ├── CMakeLists.txt │ └── main.cpp ├── dev_server │ ├── .gitignore │ ├── main.py │ ├── public │ │ ├── .gitignore │ │ ├── v1.1.0.exe │ │ ├── v1.1.0.json │ │ └── v1.1.0.md │ └── server.py └── qtwidgets │ ├── CMakeLists.txt │ ├── QtUpdateWidget.cpp │ ├── QtUpdateWidget.hpp │ ├── icon.ico │ ├── main.cpp │ └── resources.qrc ├── logo.svg ├── src ├── CMakeLists.txt ├── include │ └── oclero │ │ ├── QtDownloader.hpp │ │ ├── QtUpdateController.hpp │ │ └── QtUpdater.hpp └── source │ └── oclero │ ├── QtDownloader.cpp │ ├── QtUpdateController.cpp │ └── QtUpdater.cpp └── tests ├── CMakeLists.txt └── src ├── QtUpdaterTests.cpp ├── QtUpdaterTests.hpp └── main.cpp /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | Standard: Cpp11 3 | ColumnLimit: 120 4 | TabWidth: 2 5 | UseTab: Never 6 | AccessModifierOffset: -2 7 | ConstructorInitializerIndentWidth: 2 8 | ContinuationIndentWidth: 2 9 | ObjCBlockIndentWidth: 2 10 | IndentWidth: 2 11 | AlignAfterOpenBracket: DontAlign 12 | AlignConsecutiveAssignments: false 13 | AlignConsecutiveDeclarations: false 14 | AlignEscapedNewlines: DontAlign 15 | AlignOperands: true 16 | AlignTrailingComments: false 17 | AllowShortBlocksOnASingleLine: false 18 | AllowShortCaseLabelsOnASingleLine: false 19 | AllowShortFunctionsOnASingleLine: Empty 20 | AllowShortLambdasOnASingleLine: Empty 21 | AllowShortIfStatementsOnASingleLine: false 22 | AllowShortLoopsOnASingleLine: false 23 | AllowAllParametersOfDeclarationOnNextLine: true 24 | AlwaysBreakAfterReturnType: None 25 | AlwaysBreakBeforeMultilineStrings: false 26 | AlwaysBreakTemplateDeclarations: true 27 | BinPackArguments: true 28 | BinPackParameters: true 29 | BreakBeforeBraces: Custom 30 | BraceWrapping: 31 | AfterClass: false 32 | AfterEnum: false 33 | AfterFunction: false 34 | AfterNamespace: false 35 | AfterObjCDeclaration: false 36 | AfterStruct: false 37 | AfterUnion: false 38 | AfterExternBlock: false 39 | BeforeCatch: false 40 | BeforeElse: false 41 | BeforeLambdaBody: false 42 | IndentBraces: false 43 | SplitEmptyFunction: false 44 | SplitEmptyRecord: false 45 | SplitEmptyNamespace: false 46 | BreakBeforeBinaryOperators: NonAssignment 47 | BreakBeforeTernaryOperators: true 48 | BreakConstructorInitializers: BeforeComma 49 | ConstructorInitializerAllOnOneLineOrOnePerLine: false 50 | BreakInheritanceList: BeforeComma 51 | BreakStringLiterals: false 52 | CompactNamespaces: false 53 | Cpp11BracedListStyle: false 54 | IndentCaseLabels: true 55 | FixNamespaceComments: true 56 | IndentPPDirectives: AfterHash 57 | IndentWrappedFunctionNames: false 58 | KeepEmptyLinesAtTheStartOfBlocks: false 59 | MaxEmptyLinesToKeep: 2 60 | NamespaceIndentation: None 61 | ReflowComments: false 62 | SortUsingDeclarations: false 63 | SpaceAfterCStyleCast: true 64 | SpaceAfterTemplateKeyword: false 65 | SpaceBeforeAssignmentOperators: true 66 | SpaceBeforeCpp11BracedList: false 67 | SpaceBeforeCtorInitializerColon: true 68 | SpaceBeforeInheritanceColon: true 69 | SpaceBeforeParens: ControlStatements 70 | SpaceBeforeRangeBasedForLoopColon: true 71 | SpaceInEmptyParentheses: false 72 | SpacesBeforeTrailingComments: 1 73 | SpacesInAngles: false 74 | SpacesInCStyleCastParentheses: false 75 | SpacesInContainerLiterals: false 76 | SpacesInParentheses: false 77 | SpacesInSquareBrackets: false 78 | DerivePointerAlignment: false 79 | PointerAlignment: Left 80 | IncludeBlocks: Regroup 81 | SortIncludes: false 82 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | tab_width = 2 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.github/workflows/linux.yml: -------------------------------------------------------------------------------- 1 | name: Linux 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - dev 8 | - setup-ci 9 | pull_request: 10 | branches: 11 | - master 12 | - dev 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - name: Check Out 20 | uses: actions/checkout@v4 21 | 22 | - name: Install Qt 23 | uses: jurplel/install-qt-action@v3 24 | with: 25 | version: "5.15.2" 26 | host: linux 27 | target: desktop 28 | 29 | - name: Configure CMake 30 | run: cmake --preset linux 31 | 32 | - name: Build library 33 | run: cmake --build --preset linux 34 | 35 | - name: Build tests 36 | run: cmake --build --preset linux-test 37 | 38 | - name: Run tests 39 | run: ctest --preset linux 40 | -------------------------------------------------------------------------------- /.github/workflows/macos.yml: -------------------------------------------------------------------------------- 1 | name: MacOS 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - dev 8 | - setup-ci 9 | pull_request: 10 | branches: 11 | - master 12 | - dev 13 | 14 | jobs: 15 | build: 16 | runs-on: macos-latest 17 | 18 | steps: 19 | - name: Check Out 20 | uses: actions/checkout@v4 21 | 22 | - name: Install Qt 23 | uses: jurplel/install-qt-action@v3 24 | with: 25 | version: "5.15.2" 26 | host: mac 27 | target: desktop 28 | 29 | - name: Configure CMake 30 | run: cmake --preset macos -DCMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED=OFF -DCMAKE_XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY="-" 31 | 32 | - name: Build library 33 | run: cmake --build --preset macos 34 | 35 | - name: Build tests 36 | run: cmake --build --preset macos-test 37 | 38 | - name: Run tests 39 | run: ctest --preset macos 40 | -------------------------------------------------------------------------------- /.github/workflows/windows.yml: -------------------------------------------------------------------------------- 1 | name: Windows 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - dev 8 | - setup-ci 9 | pull_request: 10 | branches: 11 | - master 12 | - dev 13 | 14 | jobs: 15 | build: 16 | runs-on: windows-latest 17 | 18 | steps: 19 | - name: Check Out 20 | uses: actions/checkout@v4 21 | 22 | - name: Install Qt 23 | uses: jurplel/install-qt-action@v3 24 | with: 25 | version: "5.15.2" 26 | host: windows 27 | target: desktop 28 | 29 | - name: Configure CMake 30 | run: cmake --preset windows 31 | 32 | - name: Build library 33 | run: cmake --build --preset windows 34 | 35 | - name: Build tests 36 | run: cmake --build --preset windows-test 37 | 38 | - name: Run tests 39 | run: ctest --preset windows 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS Generated files 2 | .DS_Store 3 | .Spotlight-V100 4 | .Trashes 5 | Thumbs.db 6 | ehthumbs.db 7 | desktop.ini 8 | *~ 9 | *.swp 10 | ~`~* 11 | 12 | # Build files. 13 | *.d 14 | *.slo 15 | *.lo 16 | *.o 17 | *.obj 18 | *.gch 19 | *.pch 20 | *.so 21 | *.dylib 22 | *.dll 23 | *.lai 24 | *.la 25 | *.a 26 | *.lib 27 | *.exe 28 | *.out 29 | *.app 30 | build/ 31 | _build/ 32 | Testing/ 33 | 34 | # VS Code files. 35 | .cproject 36 | .project 37 | .settings 38 | .csettings 39 | .vscode/ 40 | .qt_for_python/ 41 | 42 | # Qt Creator files. 43 | CMakeLists.txt.user 44 | *creator.user* 45 | *.qml.autosave 46 | debug.log 47 | 48 | # Temporary files. 49 | tmp/ 50 | temp/ 51 | 52 | # Python files. 53 | __pycache__/ 54 | *.py[cod] 55 | *$py.class 56 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v1.5.0 4 | 5 | - Rename `lib` folder into `src`, and `src` subfolder into `source`. 6 | - Add CI jobs to build and test on Windows, Linux and MacOS. 7 | - Follow redirects when downloading. 8 | - Add CMake presets. 9 | - Use CMake FetchContent instead of a Git Submodule for dependency QtUtils. 10 | 11 | ## v1.4.1 12 | 13 | - Update submodule QtUtils to v1.1.0 14 | 15 | ## v1.4.0 16 | 17 | - Make `installMode` and `installerDestinationDir` properties instead of function args. 18 | - Fix compilation and build on Linux. 19 | 20 | ## v1.3.0 21 | 22 | - Unify the method names. 23 | 24 | ## v1.2.1 25 | 26 | - Fix QWidget example. 27 | 28 | ## v1.2.0 29 | 30 | - Move `QWidget` to examples instead of core lib. 31 | - The core lib doesn't depend on `Qt5::Widgets` anymore (useful for CLI or QtQuick apps). 32 | 33 | ## v1.1.0 34 | 35 | - Improve core API 36 | - Fix some bugs 37 | - Add controller and widget 38 | - Add exemple for controller/widget usage. 39 | - The lib nows depends on `QtWidgets`. 40 | 41 | ## v1.0.12 42 | 43 | - Update submodule QtUtils to v1.0.6 44 | 45 | ## v1.0.11 46 | 47 | - Update submodule QtUtils to v1.0.5 48 | - Ensure the lib works on Linux. 49 | 50 | ## v1.0.10 51 | 52 | - Update submodule QtUtils to v1.0.4 53 | 54 | ## v1.0.9 55 | 56 | - Update submodule QtUtils to v1.0.3 57 | 58 | ## v1.0.8 59 | 60 | - Fix ambiguous constructors 61 | 62 | ## v1.0.7 63 | 64 | - Update submodule QtUtils to v1.0.2 65 | 66 | ## v1.0.6 67 | 68 | - Allow for custom QSettings parameters. 69 | 70 | ## v1.0.5 71 | 72 | - Fix some warnings. 73 | - Add support for timeout when making HTTP requests. 74 | 75 | ## v1.0.4 76 | 77 | - Set oclero::QtUtils library as PUBLIC in CMake. 78 | 79 | ## v1.0.3 80 | 81 | - Use oclero::QtUtils library for utilities. 82 | 83 | ## v1.0.2 84 | 85 | - Add ability to cancel downloads. 86 | 87 | ## v1.0.1 88 | 89 | - Fix CMake for usage as a lib. 90 | 91 | ## v1.0.0 92 | 93 | - Basic updater. 94 | - Download utilities. 95 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.21.0) 2 | enable_testing() 3 | 4 | set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") 5 | include(DeployQt) 6 | 7 | # Set project information. 8 | project("QtUpdater" 9 | LANGUAGES CXX 10 | VERSION 1.5.0.0 11 | ) 12 | set(PROJECT_NAMESPACE "oclero") 13 | 14 | # Global flags. 15 | set(CMAKE_CXX_EXTENSIONS OFF) 16 | set(CMAKE_CXX_STANDARD 17) 17 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 18 | set_property(GLOBAL PROPERTY USE_FOLDERS ON) 19 | 20 | # Library dependencies. 21 | include(FetchContent) 22 | FetchContent_Declare(httplib 23 | GIT_REPOSITORY https://github.com/yhirose/cpp-httplib.git 24 | GIT_TAG v0.15.3 25 | GIT_SHALLOW TRUE 26 | ) 27 | FetchContent_Declare(qtutils 28 | GIT_REPOSITORY https://github.com/oclero/qtutils.git 29 | ) 30 | FetchContent_MakeAvailable(httplib qtutils) 31 | 32 | # The library. 33 | add_subdirectory(src) 34 | 35 | if(${CMAKE_PROJECT_NAME} STREQUAL ${PROJECT_NAME}) 36 | # Tests. 37 | add_subdirectory(tests) 38 | 39 | # Examples. 40 | add_subdirectory(examples/basic) 41 | add_subdirectory(examples/qtwidgets) 42 | endif() 43 | -------------------------------------------------------------------------------- /CMakePresets.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "cmakeMinimumRequired": { 4 | "major": 3, 5 | "minor": 21, 6 | "patch": 0 7 | }, 8 | "configurePresets": [ 9 | { 10 | "name": "macos", 11 | "displayName": "macOS", 12 | "description": "Xcode project for macOS", 13 | "generator": "Xcode", 14 | "binaryDir": "${sourceDir}/_build", 15 | "cacheVariables": { 16 | "Qt5_DIR": "/opt/homebrew/opt/qt/lib/cmake/Qt5" 17 | }, 18 | "condition": { 19 | "type": "equals", 20 | "lhs": "${hostSystemName}", 21 | "rhs": "Darwin" 22 | } 23 | }, 24 | { 25 | "name": "windows", 26 | "displayName": "Windows", 27 | "description": "Visual Studio project for Windows", 28 | "generator": "Visual Studio 17 2022", 29 | "binaryDir": "${sourceDir}/_build", 30 | "cacheVariables": { 31 | "Qt5_DIR": "C:/Qt/5.15.2/msvc2019_64/lib/cmake/Qt5" 32 | }, 33 | "condition": { 34 | "type": "equals", 35 | "lhs": "${hostSystemName}", 36 | "rhs": "Windows" 37 | } 38 | }, 39 | { 40 | "name": "linux", 41 | "displayName": "Linux", 42 | "description": "Makefile for Linux", 43 | "generator": "Unix Makefiles", 44 | "binaryDir": "${sourceDir}/_build", 45 | "condition": { 46 | "type": "equals", 47 | "lhs": "${hostSystemName}", 48 | "rhs": "Linux" 49 | } 50 | } 51 | ], 52 | "buildPresets": [ 53 | { 54 | "name": "macos", 55 | "displayName": "macOS", 56 | "configurePreset": "macos", 57 | "description": "Release build with Xcode for macOS", 58 | "targets": ["QtUpdater"], 59 | "configuration": "Release", 60 | "condition": { 61 | "type": "equals", 62 | "lhs": "${hostSystemName}", 63 | "rhs": "Darwin" 64 | } 65 | }, 66 | { 67 | "name": "macos-test", 68 | "displayName": "Tests for macOS", 69 | "configurePreset": "macos", 70 | "description": "Tests release build with Xcode for macOS", 71 | "targets": ["QtUpdaterTests"], 72 | "configuration": "Release", 73 | "condition": { 74 | "type": "equals", 75 | "lhs": "${hostSystemName}", 76 | "rhs": "Darwin" 77 | } 78 | }, 79 | { 80 | "name": "windows", 81 | "displayName": "Windows", 82 | "configurePreset": "windows", 83 | "description": "Release build with Visual Studio for Windows", 84 | "targets": ["QtUpdater"], 85 | "configuration": "Release", 86 | "condition": { 87 | "type": "equals", 88 | "lhs": "${hostSystemName}", 89 | "rhs": "Windows" 90 | } 91 | }, 92 | { 93 | "name": "windows-test", 94 | "displayName": "Tests for Windows", 95 | "configurePreset": "windows", 96 | "description": "Tests release build with Visual Studio for Windows", 97 | "targets": ["QtUpdaterTests"], 98 | "configuration": "Release", 99 | "condition": { 100 | "type": "equals", 101 | "lhs": "${hostSystemName}", 102 | "rhs": "Windows" 103 | } 104 | }, 105 | { 106 | "name": "linux", 107 | "displayName": "Linux", 108 | "configurePreset": "linux", 109 | "description": "Release build for Linux", 110 | "targets": ["QtUpdater"], 111 | "configuration": "Release", 112 | "condition": { 113 | "type": "equals", 114 | "lhs": "${hostSystemName}", 115 | "rhs": "Linux" 116 | } 117 | }, 118 | { 119 | "name": "linux-test", 120 | "displayName": "Tests for Linux", 121 | "configurePreset": "linux", 122 | "description": "Tests release build for Linux", 123 | "targets": ["QtUpdaterTests"], 124 | "configuration": "Release", 125 | "condition": { 126 | "type": "equals", 127 | "lhs": "${hostSystemName}", 128 | "rhs": "Linux" 129 | } 130 | } 131 | ], 132 | "testPresets": [ 133 | { 134 | "name": "macos", 135 | "configurePreset": "macos", 136 | "configuration": "Release", 137 | "output": { 138 | "outputOnFailure": true 139 | }, 140 | "execution": { 141 | "noTestsAction": "error", 142 | "stopOnFailure": false, 143 | "rerun-failed": true 144 | }, 145 | "condition": { 146 | "type": "equals", 147 | "lhs": "${hostSystemName}", 148 | "rhs": "Darwin" 149 | } 150 | }, 151 | { 152 | "name": "linux", 153 | "configurePreset": "linux", 154 | "configuration": "Release", 155 | "output": { 156 | "outputOnFailure": true 157 | }, 158 | "execution": { 159 | "noTestsAction": "error", 160 | "stopOnFailure": false, 161 | "rerun-failed": true 162 | }, 163 | "condition": { 164 | "type": "equals", 165 | "lhs": "${hostSystemName}", 166 | "rhs": "Linux" 167 | } 168 | }, 169 | { 170 | "name": "windows", 171 | "configurePreset": "windows", 172 | "configuration": "Release", 173 | "output": { 174 | "outputOnFailure": true 175 | }, 176 | "execution": { 177 | "noTestsAction": "error", 178 | "stopOnFailure": false, 179 | "rerun-failed": true 180 | }, 181 | "condition": { 182 | "type": "equals", 183 | "lhs": "${hostSystemName}", 184 | "rhs": "Windows" 185 | } 186 | } 187 | ] 188 | } 189 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Olivier Cléro 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | # QtUpdater 6 | 7 | [![License: MIT](https://img.shields.io/badge/license-MIT-green)](https://mit-license.org/) 8 | [![CMake version](https://img.shields.io/badge/CMake-3.19+-064F8C?logo=cmake)](https://www.qt.io) 9 | [![C++ version](https://img.shields.io/badge/C++-17-00599C?logo=++)](https://www.qt.io) 10 | [![Qt version](https://img.shields.io/badge/Qt-5.15.2+-41CD52?logo=qt)](https://www.qt.io) 11 | 12 | Updater for Qt5 (auto-updates). 13 | 14 | --- 15 | 16 | ### Table of Contents 17 | 18 | - [Requirements](#requirements) 19 | - [Features](#features) 20 | - [Usage](#usage) 21 | - [Server Specifications](#server-specifications) 22 | - [Example](#example) 23 | - [Author](#author) 24 | - [License](#license) 25 | 26 | --- 27 | 28 | ## Requirements 29 | 30 | - Platform: Windows, MacOS, Linux (except for installer auto-start). 31 | - [CMake 3.19+](https://cmake.org/download/) 32 | - [Qt 5.15+](https://www.qt.io/download-qt-installer) 33 | - [cpphttplib](https://github.com/yhirose/cpp-httplib) (Only for unit tests) 34 | 35 | ## Features 36 | 37 | This library contains: 38 | 39 | - A core: `QtUpdater` 40 | - A controller: `QtUpdateController`, that may be use with QtWidgets or QtQuick/QML. 41 | - A widget: `QtUpdateWidget`, that may be used as a `QWidget` or inside a `QDialog`. 42 | 43 | It provides these features: 44 | 45 | - Get latest update information. 46 | - Get changelog. 47 | - Get installer. 48 | - Execute installer. 49 | - Temporarly stores the update data in the `temp` folder. 50 | - Verify checksum after downloading and before executing installer. 51 | 52 | ## Usage 53 | 54 | 1. Add the library as a dependency with CMake FetchContent. 55 | 56 | ```cmake 57 | include(FetchContent) 58 | FetchContent_Declare(QtUpdater 59 | GIT_REPOSITORY "https://github.com/oclero/qtupdater.git" 60 | ) 61 | FetchContent_MakeAvailable(QtUpdater) 62 | ``` 63 | 64 | 2. Link with the library in CMake. 65 | 66 | ```cmake 67 | target_link_libraries(your_project oclero::QtUpdater) 68 | ``` 69 | 70 | 3. Include the only necessary header in your C++ file. 71 | 72 | ```c++ 73 | #include 74 | ``` 75 | 76 | ## Server Specifications 77 | 78 | ### Protocol 79 | 80 | The protocol is the following: 81 | 82 | 1. The client sends a request to the endpoint URL of your choice. Example (with curl): 83 | 84 | ```bash 85 | curl http://server/endpoint?version=latest 86 | ``` 87 | 88 | 2. The server answers by sending back an _appcast_: a JSON file containing the necessary information. The _appcast_ must look like the following: 89 | 90 | ```json 91 | { 92 | "version": "x.y.z", 93 | "date": "dd/MM/YYYY", 94 | "checksum": "418397de9ef332cd0e477ff5e8ca38d4", 95 | "checksumType": "md5", 96 | "installerUrl": "http://server/endpoint/package-name.exe", 97 | "changelogUrl": "http://server/endpoint/changelog-name.md" 98 | } 99 | ``` 100 | 101 | 3. The client downloads the changelog from `changelogUrl`, if any provided (facultative step). 102 | 103 | 4. The client downloads the installer from `installerUrl`, if any provided. 104 | 105 | 5. The client installs the installer: 106 | - The client may start the installer and quit, if necessary. 107 | - It may also move the downloaded file to some location. 108 | 109 | ## Example 110 | 111 | ### Server 112 | 113 | A _very basic_ server written in Python is included as testing purposes. Don't use in production environment! 114 | 115 | ```bash 116 | # Start with default config. 117 | python examples/dev_server/main.py 118 | 119 | # ... Or set your own config. 120 | python examples/dev_server/main.py --dir /some-directory --port 8000 --address 127.0.0.1 121 | ``` 122 | 123 | Some examples of valid requests for this server: 124 | 125 | ```bash 126 | # The client must be able to retrieve the latest version. 127 | curl http://localhost:8000?version=latest 128 | 129 | # This is equivalent to getting the latest version. 130 | curl http://localhost:8000 131 | 132 | # If the following version exist, the request is valid. 133 | curl http://localhost:8000?version=1.2.3 134 | 135 | # If the file exist, the request is valid. 136 | curl http://localhost:8000/v1.1.0.exe 137 | ``` 138 | 139 | ### Client 140 | 141 | ```c++ 142 | // Create an updater. 143 | oclero::QtUpdater updater("https://server/endpoint"); 144 | 145 | // Subscribe to all necessary signals. See documentation for complete list. 146 | QObject::connect(&updater, &oclero::QtUpdater::updateAvailabilityChanged, 147 | &updater, [&updater]() { 148 | if (updater.updateAvailability() == oclero::QtUpdater::UpdateAvailable::Available) { 149 | qDebug() << "Update available! You have: " 150 | << qPrintable(updater.currentVersion()) 151 | << " - Latest is: " 152 | << qPrintable(updater.latestVersion()); 153 | } else if (updater.updateAvailability() == oclero::QtUpdater::UpdateAvailable::UpToDate) { 154 | qDebug() << "You have the latest version."; 155 | } else { 156 | qDebug() << "Error."; 157 | } 158 | }); 159 | 160 | // Start checking. 161 | updater.checkForUpdate(); 162 | ``` 163 | 164 | ## Author 165 | 166 | **Olivier Cléro** | [email](mailto:oclero@pm.me) | [website](https://www.olivierclero.com) | [github](https://www.github.com/oclero) | [gitlab](https://www.gitlab.com/oclero) 167 | 168 | ## License 169 | 170 | **QtUpdater** is available under the MIT license. See the [LICENSE](LICENSE) file for more info. 171 | -------------------------------------------------------------------------------- /cmake/DeployQt.cmake: -------------------------------------------------------------------------------- 1 | # Deploys Qt libraries besides the target executable. 2 | function(target_deploy_qt TARGET_NAME) 3 | if(WIN32) 4 | set(DEPLOYQT_NAME "windeployqt") 5 | elseif(APPLE) 6 | set(DEPLOYQT_NAME "macdeployqt") 7 | else() 8 | set(DEPLOYQT_NAME "") 9 | endif() 10 | 11 | if (DEPLOYQT_NAME) 12 | get_target_property(QMAKE_LOCATION Qt5::qmake IMPORTED_LOCATION) 13 | get_filename_component(QT_BINARY_DIR ${QMAKE_LOCATION} DIRECTORY) 14 | find_program(DEPLOYQT_EXE "${DEPLOYQT_NAME}" HINTS "${QT_BINARY_DIR}" REQUIRED) 15 | set(QTDEPLOY_TARGET_NAME Qt5::deploy) 16 | add_executable(${QTDEPLOY_TARGET_NAME} IMPORTED) 17 | set_property(TARGET ${QTDEPLOY_TARGET_NAME} PROPERTY IMPORTED_LOCATION ${DEPLOYQT_EXE}) 18 | 19 | # Deploy Qt dependencies to help launch individual instances. 20 | if(TARGET ${QTDEPLOY_TARGET_NAME}) 21 | add_custom_command(TARGET ${TARGET_NAME} POST_BUILD 22 | COMMAND ${CMAKE_COMMAND} -E echo "Deploying Qt..." 23 | COMMAND ${QTDEPLOY_TARGET_NAME} --verbose 0 --no-patchqt --no-compiler-runtime --no-webkit2 --no-system-d3d-compiler --no-translations --no-angle --no-opengl-sw --dir "$" "$" 24 | ) 25 | endif() 26 | endif() 27 | endfunction() 28 | -------------------------------------------------------------------------------- /examples/basic/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | find_package(Qt5 REQUIRED Core) 2 | 3 | add_executable(BasicUpdaterExample) 4 | target_sources(BasicUpdaterExample PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp) 5 | target_link_libraries(BasicUpdaterExample PRIVATE oclero::QtUpdater Qt5::Core) 6 | 7 | set_target_properties(BasicUpdaterExample PROPERTIES 8 | INTERNAL_CONSOLE ON 9 | EXCLUDE_FROM_ALL ON 10 | FOLDER examples 11 | ) 12 | 13 | ############# Minimal example ends here ############# 14 | target_deploy_qt(BasicUpdaterExample) 15 | -------------------------------------------------------------------------------- /examples/basic/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include 6 | 7 | int main(int argc, char* argv[]) { 8 | QCoreApplication::setApplicationName("BasicUpdaterExample"); 9 | QCoreApplication::setApplicationVersion("1.0.0"); 10 | QCoreApplication::setOrganizationName("example"); 11 | QCoreApplication app(argc, argv); 12 | 13 | oclero::QtUpdater updater; 14 | updater.setServerUrl("http://localhost:8000/"); 15 | updater.setInstallerDestinationDir(QStandardPaths::standardLocations(QStandardPaths::DownloadLocation).first()); 16 | updater.setInstallMode(oclero::QtUpdater::InstallMode::MoveFileToDir); 17 | 18 | QObject::connect(&updater, &oclero::QtUpdater::updateAvailabilityChanged, &updater, [&updater]() { 19 | if (updater.updateAvailability() == oclero::QtUpdater::UpdateAvailability::Available) { 20 | qDebug() << "Update available! You have " << qPrintable(updater.currentVersion()) << " - Latest is " 21 | << qPrintable(updater.latestVersion()); 22 | 23 | qDebug() << "Downloading changelog..."; 24 | updater.downloadChangelog(); 25 | } 26 | }); 27 | 28 | QObject::connect(&updater, &oclero::QtUpdater::changelogAvailableChanged, &updater, [&updater]() { 29 | if (updater.changelogAvailable()) { 30 | qDebug() << "Changelog downloaded!\nHere's what's new:"; 31 | qDebug() << updater.latestChangelog(); 32 | 33 | qDebug() << "Downloading installer..."; 34 | updater.downloadInstaller(); 35 | } 36 | }); 37 | 38 | QObject::connect(&updater, &oclero::QtUpdater::installerAvailableChanged, &updater, [&updater]() { 39 | if (updater.installerAvailable()) { 40 | qDebug() << "Installer downloaded!"; 41 | 42 | qDebug() << "Starting installation..."; 43 | 44 | updater.installUpdate(/* dry */ false); 45 | } 46 | }); 47 | 48 | QObject::connect(&updater, &oclero::QtUpdater::installationFinished, &updater, []() { 49 | qDebug() << "Installation done!"; 50 | }); 51 | 52 | updater.checkForUpdate(); 53 | 54 | return app.exec(); 55 | } 56 | -------------------------------------------------------------------------------- /examples/dev_server/.gitignore: -------------------------------------------------------------------------------- 1 | server.log 2 | -------------------------------------------------------------------------------- /examples/dev_server/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from http.server import HTTPServer 4 | from server import Server 5 | import logging 6 | import argparse 7 | import os 8 | import sys 9 | 10 | DEFAULT_HOST_NAME = 'localhost' 11 | DEFAULT_PORT_NUMBER = 8000 12 | DEFAULT_ROOT_DIR = os.path.realpath(os.path.dirname(os.path.realpath(__file__)) + '/public/') 13 | 14 | if __name__ == '__main__': 15 | # Configure logging. 16 | log_filepath = os.path.dirname(os.path.realpath(__file__)) + '/server.log' 17 | if os.path.exists(log_filepath): 18 | os.remove(log_filepath) 19 | logging.basicConfig( 20 | level=logging.DEBUG, 21 | format="[%(asctime)s] [%(levelname)s] %(message)s", 22 | handlers=[ 23 | logging.FileHandler(log_filepath), 24 | logging.StreamHandler() 25 | ] 26 | ) 27 | 28 | # Parse arguments. 29 | parser = argparse.ArgumentParser(description='Basic auto-update server for development purposes') 30 | parser.add_argument('--dir', type=str, help='Directory where the update files are.') 31 | parser.add_argument('--port', type=int, help='Port number.') 32 | parser.add_argument('--address', type=str, help='Address.') 33 | args = parser.parse_args() 34 | 35 | if args.dir is not None and len(args.dir) > 0 and not os.path.isdir(args.dir): 36 | logging.debug('Root directory does not exist') 37 | sys.exit() 38 | 39 | host_name = DEFAULT_HOST_NAME 40 | port_number = DEFAULT_PORT_NUMBER 41 | root_dir = DEFAULT_ROOT_DIR 42 | if args.port is not None: 43 | port_number = args.port 44 | if args.address is not None: 45 | host_name = args.address 46 | if args.dir is not None and len(args.dir) > 0: 47 | root_dir = os.path.realpath(args.dir) 48 | 49 | # Start server. 50 | Server.root_dir = root_dir 51 | httpd = HTTPServer((host_name, port_number), Server) 52 | logging.debug('Server started @ \'%s:%s\' \'%s\'' % (host_name, port_number, Server.root_dir)) 53 | 54 | try: 55 | httpd.serve_forever() 56 | except KeyboardInterrupt: 57 | print('') 58 | 59 | httpd.server_close() 60 | logging.debug('Server stopped') 61 | -------------------------------------------------------------------------------- /examples/dev_server/public/.gitignore: -------------------------------------------------------------------------------- 1 | !*.exe 2 | -------------------------------------------------------------------------------- /examples/dev_server/public/v1.1.0.exe: -------------------------------------------------------------------------------- 1 | Dummy 2 | -------------------------------------------------------------------------------- /examples/dev_server/public/v1.1.0.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.1.0", 3 | "date": "01/01/2022", 4 | "checksum": "f7dd0b5215adbb605c146b934a7e17f5", 5 | "checksumType": "md5" 6 | } 7 | -------------------------------------------------------------------------------- /examples/dev_server/public/v1.1.0.md: -------------------------------------------------------------------------------- 1 | ### v1.1.0 2 | 3 | - Some new feature 4 | - Some fix 5 | -------------------------------------------------------------------------------- /examples/dev_server/server.py: -------------------------------------------------------------------------------- 1 | import os 2 | from urllib import parse 3 | from distutils import version 4 | import glob 5 | import json 6 | from typing import Dict, List 7 | from pathlib import Path 8 | from http.server import BaseHTTPRequestHandler 9 | import logging 10 | 11 | ################################################################################################### 12 | # Constants. 13 | 14 | ALIAS_VERSION_LATEST = 'latest' 15 | ALIAS_BRANCH_MAIN = 'release' 16 | PACKAGE_FILE_EXTENSION = 'exe' 17 | CHANGELOG_FILE_EXTENSION = 'md' 18 | MIMETYPES = { 19 | '.exe': 'application/vnd.microsoft.portable-executable', 20 | '.dmg': 'application/vnd.apple.diskimage', 21 | '.md': 'text/markdown', 22 | } 23 | 24 | ################################################################################################### 25 | # Classes. 26 | 27 | class VersionInformation(object): 28 | version = version.StrictVersion('0.0.0') 29 | json_filepath = '' 30 | installer_filepath = '' 31 | changelog_filepath = '' 32 | json_data = None 33 | 34 | def __init__(self, version, json_filepath = '', installer_filepath = '', changelog_filepath = '', json_data = None): 35 | self.version = version 36 | self.json_filepath = json_filepath 37 | self.installer_filepath = installer_filepath 38 | self.changelog_filepath = changelog_filepath 39 | self.json_data = json_data 40 | 41 | def __repr__(self): 42 | return str(self.__dict__) 43 | 44 | class RequestResult(object): 45 | success = False 46 | message = '' # Error message. 47 | filepath = '' # Path on the server. 48 | content = bytes(0) # Request data. 49 | size = 0 # Data size. 50 | content_type = '' # MIME type. 51 | 52 | def __init__(self, success, message, filepath = '', content = bytes(0), size = 0, content_type = ''): 53 | self.success = success 54 | self.message = message 55 | self.filepath = filepath 56 | self.content = content 57 | self.size = size 58 | self.content_type = content_type 59 | 60 | def __repr__(self): 61 | return str(self.__dict__) 62 | 63 | ################################################################################################### 64 | # Internal functions. 65 | 66 | def get_available_versions(root_dir, server_address) -> List[VersionInformation]: 67 | available_versions = [] 68 | 69 | # Find all available versions. 70 | json_files = glob.glob(os.path.abspath(root_dir + '/*.json')) 71 | for json_file in json_files: 72 | try: 73 | with open(json_file, 'rb') as f: 74 | print(json_file) 75 | data = json.load(f) 76 | data_version = version.StrictVersion(data['version']) 77 | file_name = Path(json_file).stem 78 | 79 | # Add field 'url' that contains a public link to the installer file. 80 | installer_url = f'http://{server_address}/{file_name}.{PACKAGE_FILE_EXTENSION}' 81 | if os.path.isfile(os.path.abspath(f'{root_dir}/{file_name}.{PACKAGE_FILE_EXTENSION}')): 82 | data['installerUrl'] = installer_url 83 | else: 84 | raise Exception() 85 | 86 | # Add fild 'changelog' that contains a public link to the changelog file. 87 | changelog_url = f'http://{server_address}/{file_name}.{CHANGELOG_FILE_EXTENSION}' 88 | if os.path.isfile(os.path.abspath(f'{root_dir}/{file_name}.{CHANGELOG_FILE_EXTENSION}')): 89 | data['changelogUrl'] = changelog_url 90 | else: 91 | raise Exception() 92 | 93 | available_versions.append(VersionInformation(data_version, json_file, installer_url, changelog_url, data)) 94 | except: 95 | # Silently ignore IO errors. 96 | pass 97 | 98 | # Sort them from older to newer. 99 | available_versions.sort(key=lambda item: item.version) 100 | 101 | return available_versions 102 | 103 | def get_latest_version(available_versions) -> version.StrictVersion: 104 | if len(available_versions) > 0: 105 | return available_versions[-1] 106 | else: 107 | return None 108 | 109 | def get_params(query) -> Dict[str, str]: 110 | # Parse query. 111 | query_params = parse.parse_qs(query) 112 | 113 | # Flatten lists by keeping only the first element. 114 | for key in query_params.keys(): 115 | if isinstance(query_params[key], list): 116 | query_params[key] = query_params[key][0] 117 | 118 | return query_params 119 | 120 | def get_extension(path) -> str: 121 | return os.path.splitext(path)[1] 122 | 123 | def get_version(query_params, latest_version) -> version.StrictVersion: 124 | if latest_version is None: 125 | return None 126 | 127 | version_str = query_params.get('version', ALIAS_VERSION_LATEST) 128 | 129 | # If it is alias keyword, get actual latest version. 130 | if version_str == ALIAS_VERSION_LATEST: 131 | return latest_version 132 | 133 | # Check if version is valid. 134 | query_version = None 135 | try: 136 | query_version = version.StrictVersion(version_str) 137 | except: 138 | query_version = None 139 | 140 | return query_version 141 | 142 | def handle_appcast_request(request_url, root_dir, server_address) -> RequestResult: 143 | # Check path validity. 144 | request_path_elements = request_url.path.split('/') 145 | if len(request_path_elements) != 2: 146 | return RequestResult(False, 'Invalid URL') 147 | 148 | # Check query parameters. 149 | request_query_params = get_params(request_url.query) 150 | 151 | # Check if version is valid. 152 | available_versions = get_available_versions(root_dir, server_address) 153 | latest_version = get_latest_version(available_versions) 154 | 155 | if latest_version is None: 156 | return RequestResult(False, 'No latest version available') 157 | 158 | request_version = get_version(request_query_params, latest_version.version) 159 | if request_version is None: 160 | return RequestResult(False, 'Invalid version: %s' % (request_query_params['version'])) 161 | 162 | if request_version > latest_version.version: 163 | return RequestResult(False, 'Version not available (too high): %s' % (request_version)) 164 | 165 | # Check if version is available. 166 | matching_versions = [x for x in available_versions if x.version == request_version] 167 | if len(matching_versions) == 0: 168 | return RequestResult(False, 'Version not available') 169 | 170 | # Get JSON content. 171 | matching_version = matching_versions[0] 172 | result_filepath = matching_version.json_filepath 173 | result_content = json.dumps(matching_version.json_data).encode('utf-8') 174 | result_size = len(result_content) 175 | result_content_type = 'application/json' 176 | 177 | return RequestResult(True, '', result_filepath, result_content, result_size, result_content_type) 178 | 179 | def handle_file_request(request_url, root_dir) -> RequestResult: 180 | # Check path validity. 181 | request_path_elements = request_url.path.split('/') 182 | if len(request_path_elements) != 2: 183 | return RequestResult(False, 'Invalid URL') 184 | 185 | request_file = request_path_elements[1] 186 | result_filepath = f'{root_dir}/{request_file}' 187 | 188 | if not os.path.isfile(result_filepath): 189 | return RequestResult(False, 'File does not exist') 190 | 191 | # Get file content. 192 | result_content = bytes(0) 193 | result_size = 0 194 | result_content_type = '' 195 | 196 | try: 197 | result_size = os.path.getsize(result_filepath) 198 | with open(result_filepath, 'rb') as f: 199 | result_content = f.read() 200 | result_size = len(result_content) 201 | except: 202 | return RequestResult(False, 'Cannot read file content') 203 | 204 | # Get file content-type. 205 | file_extension = get_extension(request_file) 206 | result_content_type = MIMETYPES[file_extension] 207 | 208 | return RequestResult(True, '', result_filepath, result_content, result_size, result_content_type) 209 | 210 | ################################################################################################### 211 | 212 | class Server(BaseHTTPRequestHandler): 213 | root_dir = '.' 214 | 215 | def handle_request(self, url, root_dir, server_address) -> RequestResult: 216 | request_url = parse.urlsplit(url) 217 | request_extension = get_extension(request_url.path) 218 | 219 | # Check if the URL is valid. 220 | if '..' in request_url.path or '/.' in request_url.path or './' in request_url.path: 221 | return RequestResult(False, 'Invalid URL') 222 | 223 | # Handle request. 224 | if request_extension == '': 225 | # Case: request for JSON file. 226 | return handle_appcast_request(request_url, root_dir, server_address) 227 | elif request_extension == '.exe' or request_extension == '.dmg' or request_extension == '.md': 228 | # Case: request to download file. 229 | return handle_file_request(request_url, root_dir) 230 | else: 231 | return RequestResult(False, 'Invalid URL') 232 | 233 | def do_GET(self): 234 | logging.debug('Request received: \'%s\'' % (self.path)) 235 | request_result = self.handle_request(self.path, self.root_dir, "%s:%s" % self.server.server_address) 236 | 237 | if request_result.success: 238 | logging.debug('Request succeeded (\'%s\')' % (os.path.basename(request_result.filepath))) 239 | else: 240 | logging.debug('Request failed (%s)' % (request_result.message)) 241 | 242 | if request_result.success: 243 | self.send_response(200) 244 | self.send_header('Accept', request_result.content_type) 245 | self.send_header('Content-Type', request_result.content_type) 246 | self.send_header('Content-Length', str(request_result.size)) 247 | self.end_headers() 248 | self.wfile.write(request_result.content) 249 | 250 | else: 251 | self.send_response(404) 252 | self.send_header('Content-type', 'text/html') 253 | self.end_headers() 254 | -------------------------------------------------------------------------------- /examples/qtwidgets/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | find_package(Qt5 2 | REQUIRED 3 | Core 4 | Network 5 | Widgets 6 | ) 7 | 8 | add_executable(QtWidgetsUpdaterExample WIN32 MACOSX_BUNDLE) 9 | target_sources(QtWidgetsUpdaterExample PRIVATE 10 | ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp 11 | ${CMAKE_CURRENT_SOURCE_DIR}/QtUpdateWidget.cpp 12 | ${CMAKE_CURRENT_SOURCE_DIR}/QtUpdateWidget.hpp 13 | ${CMAKE_CURRENT_SOURCE_DIR}/resources.qrc 14 | ) 15 | target_link_libraries(QtWidgetsUpdaterExample 16 | PRIVATE 17 | oclero::QtUpdater 18 | Qt5::Core 19 | Qt5::Network 20 | Qt5::Widgets 21 | ) 22 | 23 | set_target_properties(QtWidgetsUpdaterExample PROPERTIES 24 | INTERNAL_CONSOLE OFF 25 | EXCLUDE_FROM_ALL ON 26 | FOLDER examples 27 | AUTOMOC ON 28 | AUTORCC ON 29 | ) 30 | 31 | ############# Minimal example ends here ############# 32 | target_deploy_qt(QtWidgetsUpdaterExample) 33 | -------------------------------------------------------------------------------- /examples/qtwidgets/QtUpdateWidget.cpp: -------------------------------------------------------------------------------- 1 | #include "QtUpdateWidget.hpp" 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | namespace oclero { 19 | namespace { 20 | QString toBold(const QString& str) { 21 | return QString("%1").arg(str); 22 | } 23 | 24 | QString dateToString(const QDateTime& date) { 25 | QLocale locale; 26 | return locale.toString(date.date(), QLocale::FormatType::ShortFormat); 27 | } 28 | 29 | class StartPage : public QWidget { 30 | public: 31 | StartPage(QtUpdateController& controller, QWidget* parent = nullptr) 32 | : QWidget(parent) { 33 | auto* layout = new QVBoxLayout(this); 34 | setLayout(layout); 35 | 36 | // Title. 37 | _title = new QLabel(this); 38 | _title->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); 39 | auto titleFont = QFont(_title->font()); 40 | titleFont.setPointSize(titleFont.pointSize() * 1.25); 41 | _title->setFont(titleFont); 42 | layout->addWidget(_title, 0, Qt::AlignTop); 43 | updateTitle(window()->windowTitle()); 44 | QObject::connect(this->window(), &QWidget::windowTitleChanged, this, [this](const QString& text) { 45 | updateTitle(text); 46 | }); 47 | 48 | // Text. 49 | auto* label = new QLabel(this); 50 | label->setText(tr("This wizard will guide you during the update.")); 51 | label->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); 52 | label->setWordWrap(true); 53 | layout->addWidget(label, 0, Qt::AlignTop); 54 | 55 | // Space. 56 | const auto space = style()->pixelMetric(QStyle::PM_LayoutVerticalSpacing); 57 | layout->addItem(new QSpacerItem(0, space * 2, QSizePolicy::Fixed, QSizePolicy::MinimumExpanding)); 58 | 59 | // Buttons 60 | auto* btnBox = new QDialogButtonBox(this); 61 | btnBox->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed); 62 | btnBox->setStandardButtons(QDialogButtonBox::StandardButton::Cancel | QDialogButtonBox::StandardButton::Apply); 63 | layout->addWidget(btnBox, 0, Qt::AlignBottom); 64 | 65 | auto* cancelBtn = btnBox->button(QDialogButtonBox::StandardButton::Cancel); 66 | cancelBtn->setAutoDefault(false); 67 | QObject::connect(cancelBtn, &QPushButton::clicked, this, [&controller]() { 68 | controller.cancel(); 69 | }); 70 | 71 | auto* checkForUpdateBtn = btnBox->button(QDialogButtonBox::StandardButton::Apply); 72 | checkForUpdateBtn->setText(tr("Check For Updates")); 73 | checkForUpdateBtn->setDefault(true); 74 | QObject::connect(checkForUpdateBtn, &QPushButton::clicked, this, [&controller]() { 75 | controller.checkForUpdate(); 76 | }); 77 | } 78 | 79 | private: 80 | void updateTitle(const QString& text) { 81 | if (text.isEmpty()) { 82 | _title->setText(toBold(tr("Update"))); 83 | } else { 84 | _title->setText(toBold(text)); 85 | } 86 | } 87 | 88 | QLabel* _title; 89 | }; 90 | 91 | class CheckingPage : public QWidget { 92 | public: 93 | CheckingPage(QtUpdateController& controller, QWidget* parent = nullptr) 94 | : QWidget(parent) { 95 | auto* layout = new QVBoxLayout(this); 96 | setLayout(layout); 97 | 98 | // Title. 99 | auto* title = new QLabel(this); 100 | title->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); 101 | auto titleFont = QFont(title->font()); 102 | titleFont.setPointSize(titleFont.pointSize() * 1.25); 103 | title->setFont(titleFont); 104 | title->setText(toBold(tr("Checking For Updates"))); 105 | layout->addWidget(title, 0, Qt::AlignTop); 106 | 107 | // Text. 108 | auto* label = new QLabel(this); 109 | label->setText(tr("Please wait while the application is looking for update…")); 110 | label->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); 111 | label->setWordWrap(true); 112 | layout->addWidget(label, 0, Qt::AlignTop); 113 | 114 | // Space. 115 | const auto space = style()->pixelMetric(QStyle::PM_LayoutVerticalSpacing); 116 | layout->addItem(new QSpacerItem(0, space * 2, QSizePolicy::Fixed, QSizePolicy::Fixed)); 117 | 118 | // ProgressBar. 119 | auto* progressBar = new QProgressBar(this); 120 | progressBar->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); 121 | progressBar->setRange(0, 0); 122 | progressBar->setTextVisible(false); 123 | layout->addWidget(progressBar, 0); 124 | 125 | // Space. 126 | layout->addItem(new QSpacerItem(0, space * 3, QSizePolicy::Fixed, QSizePolicy::MinimumExpanding)); 127 | layout->setStretch(2, 1); 128 | 129 | // Buttons 130 | auto* btnBox = new QDialogButtonBox(this); 131 | btnBox->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed); 132 | btnBox->setStandardButtons(QDialogButtonBox::StandardButton::Cancel); 133 | layout->addWidget(btnBox, 0, Qt::AlignBottom); 134 | 135 | auto* cancelBtn = btnBox->button(QDialogButtonBox::StandardButton::Cancel); 136 | cancelBtn->setAutoDefault(false); 137 | cancelBtn->setDefault(false); 138 | QObject::connect(cancelBtn, &QPushButton::clicked, this, [&controller]() { 139 | controller.cancel(); 140 | }); 141 | } 142 | }; 143 | 144 | class CheckingFailPage : public QWidget { 145 | public: 146 | CheckingFailPage(QtUpdateController& controller, QWidget* parent = nullptr) 147 | : QWidget(parent) { 148 | auto* layout = new QVBoxLayout(this); 149 | setLayout(layout); 150 | 151 | // Title. 152 | auto* title = new QLabel(this); 153 | title->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); 154 | auto titleFont = QFont(title->font()); 155 | titleFont.setPointSize(titleFont.pointSize() * 1.25); 156 | title->setFont(titleFont); 157 | title->setText(toBold(tr("Checking for Updates Failed"))); 158 | layout->addWidget(title, 0, Qt::AlignTop); 159 | 160 | // Text. 161 | auto* label = new QLabel(this); 162 | label->setText(tr("Sorry, an error happened. We can't find updates.")); 163 | label->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); 164 | label->setWordWrap(true); 165 | layout->addWidget(label, 0, Qt::AlignTop); 166 | 167 | // Error code. 168 | auto* errorLabel = new QLabel(this); 169 | errorLabel->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); 170 | layout->addWidget(errorLabel, 0, Qt::AlignTop); 171 | 172 | // Space. 173 | const auto space = style()->pixelMetric(QStyle::PM_LayoutVerticalSpacing); 174 | layout->addItem(new QSpacerItem(0, space * 2, QSizePolicy::Fixed, QSizePolicy::MinimumExpanding)); 175 | 176 | // Buttons 177 | auto* btnBox = new QDialogButtonBox(this); 178 | btnBox->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed); 179 | btnBox->setStandardButtons(QDialogButtonBox::StandardButton::Ok); 180 | layout->addWidget(btnBox, 0, Qt::AlignBottom); 181 | 182 | auto* okBtn = btnBox->button(QDialogButtonBox::StandardButton::Ok); 183 | okBtn->setDefault(true); 184 | QObject::connect(okBtn, &QPushButton::clicked, this, [&controller]() { 185 | controller.cancel(); 186 | }); 187 | 188 | // Connections to controller. 189 | QObject::connect( 190 | &controller, &QtUpdateController::checkForUpdateErrorChanged, this, [errorLabel](QtUpdater::ErrorCode error) { 191 | const auto errorStr = QString::number(static_cast(error)); 192 | const auto text = tr("Error code: %1").arg(errorStr); 193 | errorLabel->setText(text); 194 | }); 195 | } 196 | }; 197 | 198 | class CheckingUpToDatePage : public QWidget { 199 | public: 200 | CheckingUpToDatePage(QtUpdateController& controller, QWidget* parent = nullptr) 201 | : QWidget(parent) { 202 | auto* layout = new QVBoxLayout(this); 203 | setLayout(layout); 204 | 205 | // Title. 206 | auto* title = new QLabel(this); 207 | title->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); 208 | auto titleFont = QFont(title->font()); 209 | titleFont.setPointSize(titleFont.pointSize() * 1.25); 210 | title->setFont(titleFont); 211 | title->setText(toBold(tr("You are up-to-date!"))); 212 | layout->addWidget(title, 0, Qt::AlignTop); 213 | 214 | // Text. 215 | auto* label = new QLabel(this); 216 | label->setText(tr("You are running the latest version of the application.")); 217 | label->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); 218 | label->setWordWrap(true); 219 | layout->addWidget(label, 0, Qt::AlignTop); 220 | 221 | // Space. 222 | const auto space = style()->pixelMetric(QStyle::PM_LayoutVerticalSpacing); 223 | layout->addItem(new QSpacerItem(0, space * 2, QSizePolicy::Fixed, QSizePolicy::MinimumExpanding)); 224 | 225 | // Buttons 226 | auto* btnBox = new QDialogButtonBox(this); 227 | btnBox->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed); 228 | btnBox->setStandardButtons(QDialogButtonBox::StandardButton::Ok); 229 | layout->addWidget(btnBox, 0, Qt::AlignBottom); 230 | 231 | auto* okBtn = btnBox->button(QDialogButtonBox::StandardButton::Ok); 232 | okBtn->setDefault(true); 233 | QObject::connect(okBtn, &QPushButton::clicked, this, [&controller]() { 234 | controller.cancel(); 235 | }); 236 | } 237 | }; 238 | 239 | class CheckingSuccessPage : public QWidget { 240 | public: 241 | CheckingSuccessPage(QtUpdateController& controller, QWidget* parent = nullptr) 242 | : QWidget(parent) 243 | , _controller(controller) { 244 | auto* layout = new QVBoxLayout(this); 245 | setLayout(layout); 246 | 247 | // Title. 248 | auto* title = new QLabel(this); 249 | title->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); 250 | auto titleFont = QFont(title->font()); 251 | titleFont.setPointSize(titleFont.pointSize() * 1.25); 252 | title->setFont(titleFont); 253 | title->setText(toBold(tr("A New Version is Available!"))); 254 | layout->addWidget(title, 0, Qt::AlignTop); 255 | 256 | // Text. 257 | _label = new QLabel(this); 258 | _label->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); 259 | _label->setWordWrap(true); 260 | layout->addWidget(_label, 0, Qt::AlignTop); 261 | updateLabelText(); 262 | 263 | // Changelog. 264 | auto* textEdit = new QTextEdit(this); 265 | textEdit->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); 266 | textEdit->setTextInteractionFlags(Qt::TextInteractionFlag::TextBrowserInteraction); 267 | textEdit->setTabStopDistance(0); 268 | layout->addWidget(textEdit, 1); 269 | 270 | // Space. 271 | const auto space = style()->pixelMetric(QStyle::PM_LayoutVerticalSpacing); 272 | layout->addItem(new QSpacerItem(0, space * 2, QSizePolicy::Fixed, QSizePolicy::Fixed)); 273 | 274 | // Buttons 275 | auto* btnBox = new QDialogButtonBox(this); 276 | btnBox->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed); 277 | btnBox->setStandardButtons(QDialogButtonBox::StandardButton::Apply | QDialogButtonBox::StandardButton::Cancel); 278 | layout->addWidget(btnBox, 0, Qt::AlignBottom); 279 | 280 | auto* downloadButton = btnBox->button(QDialogButtonBox::StandardButton::Apply); 281 | downloadButton->setDefault(true); 282 | downloadButton->setText(tr("Download")); 283 | QObject::connect(downloadButton, &QPushButton::clicked, this, [&controller]() { 284 | controller.downloadUpdate(); 285 | }); 286 | auto* cancelBtn = btnBox->button(QDialogButtonBox::StandardButton::Cancel); 287 | cancelBtn->setAutoDefault(false); 288 | QObject::connect(cancelBtn, &QPushButton::clicked, this, [&controller]() { 289 | controller.cancel(); 290 | }); 291 | 292 | // Connections to controller. 293 | QObject::connect(&controller, &QtUpdateController::latestVersionChanged, this, [this]() { 294 | updateLabelText(); 295 | }); 296 | QObject::connect(&controller, &QtUpdateController::latestVersionDateChanged, this, [this]() { 297 | updateLabelText(); 298 | }); 299 | QObject::connect(&controller, &QtUpdateController::latestVersionChangelogChanged, this, [this, textEdit]() { 300 | textEdit->setMarkdown(_controller.latestVersionChangelog()); 301 | }); 302 | 303 | // Connections to controller. 304 | QObject::connect( 305 | &controller, &QtUpdateController::changelogDownloadErrorChanged, this, [textEdit](QtUpdater::ErrorCode error) { 306 | const auto firstLine = tr("Can't download changelog."); 307 | const auto errorStr = QString::number(static_cast(error)); 308 | const auto text = tr("Error code: %1").arg(errorStr); 309 | textEdit->setText(firstLine + '\n' + text); 310 | }); 311 | } 312 | 313 | private: 314 | void updateLabelText() { 315 | const auto currentVersion = _controller.currentVersion(); 316 | const auto currentVersionDate = dateToString(_controller.currentVersionDate()); 317 | const auto latestVersion = _controller.latestVersion(); 318 | const auto latestVersionDate = dateToString(_controller.latestVersionDate()); 319 | const auto text = 320 | QString("You have version %1 (released on %2).
Version %3 is available (released on %4).") 321 | .arg(currentVersion) 322 | .arg(currentVersionDate) 323 | .arg(latestVersion) 324 | .arg(latestVersionDate); 325 | _label->setText(text); 326 | } 327 | 328 | QLabel* _label; 329 | QtUpdateController& _controller; 330 | }; 331 | 332 | class DownloadingPage : public QWidget { 333 | public: 334 | DownloadingPage(QtUpdateController& controller, QWidget* parent = nullptr) 335 | : QWidget(parent) { 336 | auto* layout = new QVBoxLayout(this); 337 | setLayout(layout); 338 | 339 | // Title. 340 | auto* title = new QLabel(this); 341 | title->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); 342 | auto titleFont = QFont(title->font()); 343 | titleFont.setPointSize(titleFont.pointSize() * 1.25); 344 | title->setFont(titleFont); 345 | title->setText(toBold(tr("Downloading the Update"))); 346 | layout->addWidget(title, 0, Qt::AlignTop); 347 | 348 | // Text. 349 | auto* label = new QLabel(this); 350 | label->setText(tr("Please wait while the application is downloading the update...")); 351 | label->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); 352 | label->setWordWrap(true); 353 | layout->addWidget(label, 0, Qt::AlignTop); 354 | 355 | // Space. 356 | const auto space = style()->pixelMetric(QStyle::PM_LayoutVerticalSpacing); 357 | layout->addItem(new QSpacerItem(0, space * 2, QSizePolicy::Fixed, QSizePolicy::Fixed)); 358 | 359 | // ProgressBar. 360 | auto* progressBar = new QProgressBar(this); 361 | progressBar->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); 362 | progressBar->setTextVisible(false); 363 | progressBar->setRange(0, 100); 364 | layout->addWidget(progressBar, 0); 365 | 366 | // Progress label. 367 | auto* progressLabel = new QLabel(this); 368 | progressLabel->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); 369 | layout->addWidget(progressLabel, 0, Qt::AlignHCenter); 370 | 371 | // Space. 372 | layout->addItem(new QSpacerItem(0, space * 3, QSizePolicy::Fixed, QSizePolicy::MinimumExpanding)); 373 | layout->setStretch(2, 1); 374 | 375 | // Buttons 376 | auto* btnBox = new QDialogButtonBox(this); 377 | btnBox->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed); 378 | btnBox->setStandardButtons(QDialogButtonBox::StandardButton::Cancel); 379 | layout->addWidget(btnBox, 0, Qt::AlignBottom); 380 | 381 | auto* cancelBtn = btnBox->button(QDialogButtonBox::StandardButton::Cancel); 382 | cancelBtn->setAutoDefault(false); 383 | cancelBtn->setDefault(false); 384 | QObject::connect(cancelBtn, &QPushButton::clicked, this, [&controller]() { 385 | controller.cancel(); 386 | }); 387 | 388 | // Connections to controller. 389 | QObject::connect( 390 | &controller, &QtUpdateController::downloadProgressChanged, this, [progressBar, progressLabel](int value) { 391 | progressBar->setValue(value); 392 | progressLabel->setText(QString("%1%").arg(value)); 393 | }); 394 | } 395 | }; 396 | 397 | class DownloadingFailPage : public QWidget { 398 | public: 399 | DownloadingFailPage(QtUpdateController& controller, QWidget* parent = nullptr) 400 | : QWidget(parent) { 401 | auto* layout = new QVBoxLayout(this); 402 | setLayout(layout); 403 | 404 | // Title. 405 | auto* title = new QLabel(this); 406 | title->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); 407 | auto titleFont = QFont(title->font()); 408 | titleFont.setPointSize(titleFont.pointSize() * 1.25); 409 | title->setFont(titleFont); 410 | title->setText(toBold(tr("Update Download Failed"))); 411 | layout->addWidget(title, 0, Qt::AlignTop); 412 | 413 | // Text. 414 | auto* label = new QLabel(this); 415 | label->setText(tr("Sorry, an error happened. We can't download the update.")); 416 | label->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); 417 | label->setWordWrap(true); 418 | layout->addWidget(label, 0, Qt::AlignTop); 419 | 420 | // Error code. 421 | auto* errorLabel = new QLabel(this); 422 | errorLabel->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); 423 | layout->addWidget(errorLabel, 0, Qt::AlignTop); 424 | 425 | // Space. 426 | const auto space = style()->pixelMetric(QStyle::PM_LayoutVerticalSpacing); 427 | layout->addItem(new QSpacerItem(0, space * 2, QSizePolicy::Fixed, QSizePolicy::MinimumExpanding)); 428 | 429 | // Buttons 430 | auto* btnBox = new QDialogButtonBox(this); 431 | btnBox->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed); 432 | btnBox->setStandardButtons(QDialogButtonBox::StandardButton::Ok); 433 | layout->addWidget(btnBox, 0, Qt::AlignBottom); 434 | 435 | auto* okBtn = btnBox->button(QDialogButtonBox::StandardButton::Ok); 436 | okBtn->setDefault(true); 437 | QObject::connect(okBtn, &QPushButton::clicked, this, [&controller]() { 438 | controller.cancel(); 439 | }); 440 | 441 | // Connections to controller. 442 | QObject::connect( 443 | &controller, &QtUpdateController::updateDownloadErrorChanged, this, [errorLabel](QtUpdater::ErrorCode error) { 444 | const auto errorStr = QString::number(static_cast(error)); 445 | const auto text = tr("Error code: %1").arg(errorStr); 446 | errorLabel->setText(text); 447 | }); 448 | } 449 | }; 450 | 451 | class DownloadingSuccessPage : public QWidget { 452 | public: 453 | DownloadingSuccessPage(QtUpdateController& controller, QWidget* parent = nullptr) 454 | : QWidget(parent) { 455 | auto* layout = new QVBoxLayout(this); 456 | setLayout(layout); 457 | 458 | // Title. 459 | auto* title = new QLabel(this); 460 | title->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); 461 | auto titleFont = QFont(title->font()); 462 | titleFont.setPointSize(titleFont.pointSize() * 1.25); 463 | title->setFont(titleFont); 464 | title->setText(toBold(tr("Download successful!"))); 465 | layout->addWidget(title, 0, Qt::AlignTop); 466 | 467 | // Text. 468 | auto* label = new QLabel(this); 469 | label->setText(tr("Do you want to install the update?")); 470 | label->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); 471 | label->setWordWrap(true); 472 | layout->addWidget(label, 0, Qt::AlignTop); 473 | 474 | // Space. 475 | const auto space = style()->pixelMetric(QStyle::PM_LayoutVerticalSpacing); 476 | layout->addItem(new QSpacerItem(0, space * 2, QSizePolicy::Fixed, QSizePolicy::MinimumExpanding)); 477 | 478 | // Buttons 479 | auto* btnBox = new QDialogButtonBox(this); 480 | btnBox->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed); 481 | btnBox->setStandardButtons(QDialogButtonBox::StandardButton::Apply | QDialogButtonBox::StandardButton::No); 482 | layout->addWidget(btnBox, 0, Qt::AlignBottom); 483 | 484 | auto* installButton = btnBox->button(QDialogButtonBox::StandardButton::Apply); 485 | installButton->setAutoDefault(false); 486 | installButton->setDefault(true); 487 | installButton->setText(tr("Install")); 488 | QObject::connect(installButton, &QPushButton::clicked, this, [&controller]() { 489 | controller.installUpdate(); 490 | }); 491 | auto* cancelBtn = btnBox->button(QDialogButtonBox::StandardButton::No); 492 | cancelBtn->setAutoDefault(false); 493 | QObject::connect(cancelBtn, &QPushButton::clicked, this, [&controller]() { 494 | controller.cancel(); 495 | }); 496 | } 497 | }; 498 | 499 | class InstallingPage : public QWidget { 500 | public: 501 | InstallingPage(QtUpdateController& controller, QWidget* parent = nullptr) 502 | : QWidget(parent) { 503 | auto* layout = new QVBoxLayout(this); 504 | setLayout(layout); 505 | 506 | // Title. 507 | auto* title = new QLabel(this); 508 | title->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); 509 | auto titleFont = QFont(title->font()); 510 | titleFont.setPointSize(titleFont.pointSize() * 1.25); 511 | title->setFont(titleFont); 512 | title->setText(toBold(tr("Installation"))); 513 | layout->addWidget(title, 0, Qt::AlignTop); 514 | 515 | // Text. 516 | auto* label = new QLabel(this); 517 | label->setText(tr("Please wait while the application is installing the update...")); 518 | label->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); 519 | label->setWordWrap(true); 520 | layout->addWidget(label, 0, Qt::AlignTop); 521 | 522 | // Space. 523 | const auto space = style()->pixelMetric(QStyle::PM_LayoutVerticalSpacing); 524 | layout->addItem(new QSpacerItem(0, space * 2, QSizePolicy::Fixed, QSizePolicy::Fixed)); 525 | 526 | // ProgressBar. 527 | auto* progressBar = new QProgressBar(this); 528 | progressBar->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); 529 | progressBar->setRange(0, 0); 530 | progressBar->setTextVisible(false); 531 | layout->addWidget(progressBar, 0); 532 | 533 | // Space. 534 | layout->addItem(new QSpacerItem(0, space * 3, QSizePolicy::Fixed, QSizePolicy::MinimumExpanding)); 535 | layout->setStretch(2, 1); 536 | 537 | // Buttons 538 | auto* btnBox = new QDialogButtonBox(this); 539 | btnBox->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed); 540 | btnBox->setStandardButtons(QDialogButtonBox::StandardButton::Cancel); 541 | layout->addWidget(btnBox, 0, Qt::AlignBottom); 542 | 543 | auto* cancelBtn = btnBox->button(QDialogButtonBox::StandardButton::Cancel); 544 | cancelBtn->setAutoDefault(false); 545 | cancelBtn->setDefault(false); 546 | QObject::connect(cancelBtn, &QPushButton::clicked, this, [&controller]() { 547 | controller.cancel(); 548 | }); 549 | } 550 | }; 551 | 552 | class InstallingFailPage : public QWidget { 553 | public: 554 | InstallingFailPage(QtUpdateController& controller, QWidget* parent = nullptr) 555 | : QWidget(parent) { 556 | auto* layout = new QVBoxLayout(this); 557 | setLayout(layout); 558 | 559 | // Title. 560 | auto* title = new QLabel(this); 561 | title->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); 562 | auto titleFont = QFont(title->font()); 563 | titleFont.setPointSize(titleFont.pointSize() * 1.25); 564 | title->setFont(titleFont); 565 | title->setText(toBold(tr("Installation Error"))); 566 | layout->addWidget(title, 0, Qt::AlignTop); 567 | 568 | // Text. 569 | auto* label = new QLabel(this); 570 | label->setText(tr("Sorry, an error happened. We can't install the update.")); 571 | label->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); 572 | label->setWordWrap(true); 573 | layout->addWidget(label, 0, Qt::AlignTop); 574 | 575 | // Error code. 576 | auto* errorLabel = new QLabel(this); 577 | errorLabel->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); 578 | layout->addWidget(errorLabel, 0, Qt::AlignTop); 579 | 580 | // Space. 581 | const auto space = style()->pixelMetric(QStyle::PM_LayoutVerticalSpacing); 582 | layout->addItem(new QSpacerItem(0, space * 2, QSizePolicy::Fixed, QSizePolicy::MinimumExpanding)); 583 | 584 | // Buttons 585 | auto* btnBox = new QDialogButtonBox(this); 586 | btnBox->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed); 587 | btnBox->setStandardButtons(QDialogButtonBox::StandardButton::Ok); 588 | layout->addWidget(btnBox, 0, Qt::AlignBottom); 589 | 590 | auto* okBtn = btnBox->button(QDialogButtonBox::StandardButton::Ok); 591 | okBtn->setDefault(true); 592 | QObject::connect(okBtn, &QPushButton::clicked, this, [&controller]() { 593 | controller.cancel(); 594 | }); 595 | 596 | // Connections to controller. 597 | QObject::connect( 598 | &controller, &QtUpdateController::updateInstallationErrorChanged, this, [errorLabel](QtUpdater::ErrorCode error) { 599 | const auto errorStr = QString::number(static_cast(error)); 600 | const auto text = tr("Error code: %1").arg(errorStr); 601 | errorLabel->setText(text); 602 | }); 603 | } 604 | }; 605 | 606 | class InstallingSuccessPage : public QWidget { 607 | public: 608 | InstallingSuccessPage(QtUpdateController& controller, QWidget* parent = nullptr) 609 | : QWidget(parent) 610 | , _controller(controller) { 611 | auto* layout = new QVBoxLayout(this); 612 | setLayout(layout); 613 | 614 | // Title. 615 | auto* title = new QLabel(this); 616 | title->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); 617 | auto titleFont = QFont(title->font()); 618 | titleFont.setPointSize(titleFont.pointSize() * 1.25); 619 | title->setFont(titleFont); 620 | title->setText(toBold(tr("Installation Successful!"))); 621 | layout->addWidget(title, 0, Qt::AlignTop); 622 | 623 | // Text. 624 | _label = new QLabel(this); 625 | _label->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); 626 | _label->setWordWrap(true); 627 | layout->addWidget(_label, 0, Qt::AlignTop); 628 | 629 | // Space. 630 | const auto space = style()->pixelMetric(QStyle::PM_LayoutVerticalSpacing); 631 | layout->addItem(new QSpacerItem(0, space * 2, QSizePolicy::Fixed, QSizePolicy::MinimumExpanding)); 632 | 633 | // Buttons 634 | auto* btnBox = new QDialogButtonBox(this); 635 | btnBox->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed); 636 | btnBox->setStandardButtons(QDialogButtonBox::StandardButton::Ok); 637 | layout->addWidget(btnBox, 0, Qt::AlignBottom); 638 | 639 | auto* okButton = btnBox->button(QDialogButtonBox::StandardButton::Ok); 640 | QObject::connect(okButton, &QPushButton::clicked, this, [&controller]() { 641 | controller.cancel(); 642 | }); 643 | 644 | // Connections to controller. 645 | QObject::connect(&controller, &QtUpdateController::latestVersionChanged, this, [this]() { 646 | const auto latestVersion = _controller.latestVersion(); 647 | _label->setText(tr("The installation of version %1 succeeded.").arg(latestVersion)); 648 | }); 649 | } 650 | 651 | private: 652 | void updateLabelText() { 653 | const auto currentVersion = _controller.currentVersion(); 654 | const auto currentVersionDate = dateToString(_controller.currentVersionDate()); 655 | const auto latestVersion = _controller.latestVersion(); 656 | const auto latestVersionDate = dateToString(_controller.latestVersionDate()); 657 | const auto text = 658 | tr("You have version %1 (released on %2).
Version %3 is available (released on %4).") 659 | .arg(currentVersion) 660 | .arg(currentVersionDate) 661 | .arg(latestVersion) 662 | .arg(latestVersionDate); 663 | _label->setText(text); 664 | } 665 | 666 | QLabel* _label; 667 | QtUpdateController& _controller; 668 | }; 669 | } // namespace 670 | 671 | QtUpdateWidget::QtUpdateWidget(QtUpdateController& controller, QWidget* parent) 672 | : QWidget(parent) 673 | , _controller(controller) { 674 | setupUi(); 675 | 676 | // Connections to controller. 677 | QObject::connect(&_controller, &QtUpdateController::stateChanged, this, [this]() { 678 | const auto state = _controller.state(); 679 | const auto widget = _pages.contains(state) ? _pages.value(state) : nullptr; 680 | assert(widget != nullptr); 681 | if (widget) { 682 | _stackedWidget->setCurrentWidget(widget); 683 | } 684 | }); 685 | 686 | QObject::connect(&_controller, &QtUpdateController::closeDialogRequested, this, [this]() { 687 | close(); 688 | }); 689 | } 690 | 691 | QtUpdateWidget::~QtUpdateWidget() {} 692 | 693 | void QtUpdateWidget::setupUi() { 694 | auto* layout = new QHBoxLayout(this); 695 | setLayout(layout); 696 | 697 | // Icon on the left. 698 | const auto windowIcon = this->windowIcon(); 699 | if (!windowIcon.isNull()) { 700 | const auto iconExtent = style()->pixelMetric(QStyle::PM_MessageBoxIconSize) * 2; 701 | const auto pixmap = windowIcon.pixmap(iconExtent); 702 | if (!pixmap.isNull()) { 703 | auto* labelContainer = new QWidget(this); 704 | labelContainer->setAttribute(Qt::WidgetAttribute::WA_TransparentForMouseEvents, true); 705 | auto* labelContainerLayout = new QVBoxLayout(labelContainer); 706 | labelContainer->setLayout(labelContainerLayout); 707 | auto* iconLabel = new QLabel(labelContainer); 708 | iconLabel->setFixedSize(iconExtent, iconExtent); 709 | iconLabel->setPixmap(pixmap); 710 | labelContainerLayout->addWidget(iconLabel, 0, Qt::Alignment{ Qt::AlignTop | Qt::AlignHCenter }); 711 | layout->addWidget(labelContainer, 0, Qt::Alignment{ Qt::AlignTop | Qt::AlignHCenter }); 712 | } 713 | } 714 | 715 | // Pages. 716 | _stackedWidget = new QStackedWidget(this); 717 | layout->addWidget(_stackedWidget); 718 | 719 | { 720 | auto* page = new StartPage(_controller, _stackedWidget); 721 | _stackedWidget->addWidget(page); 722 | _pages.insert(QtUpdateController::State::None, page); 723 | } 724 | { 725 | auto* page = new CheckingPage(_controller, _stackedWidget); 726 | _stackedWidget->addWidget(page); 727 | _pages.insert(QtUpdateController::State::Checking, page); 728 | } 729 | { 730 | auto* page = new CheckingFailPage(_controller, _stackedWidget); 731 | _stackedWidget->addWidget(page); 732 | _pages.insert(QtUpdateController::State::CheckingFail, page); 733 | } 734 | { 735 | auto* page = new CheckingSuccessPage(_controller, _stackedWidget); 736 | _stackedWidget->addWidget(page); 737 | _pages.insert(QtUpdateController::State::CheckingSuccess, page); 738 | } 739 | { 740 | auto* page = new CheckingUpToDatePage(_controller, _stackedWidget); 741 | _stackedWidget->addWidget(page); 742 | _pages.insert(QtUpdateController::State::CheckingUpToDate, page); 743 | } 744 | { 745 | auto* page = new DownloadingPage(_controller, _stackedWidget); 746 | _stackedWidget->addWidget(page); 747 | _pages.insert(QtUpdateController::State::Downloading, page); 748 | } 749 | { 750 | auto* page = new DownloadingFailPage(_controller, _stackedWidget); 751 | _stackedWidget->addWidget(page); 752 | _pages.insert(QtUpdateController::State::DownloadingFail, page); 753 | } 754 | { 755 | auto* page = new DownloadingSuccessPage(_controller, _stackedWidget); 756 | _stackedWidget->addWidget(page); 757 | _pages.insert(QtUpdateController::State::DownloadingSuccess, page); 758 | } 759 | { 760 | auto* page = new InstallingPage(_controller, _stackedWidget); 761 | _stackedWidget->addWidget(page); 762 | _pages.insert(QtUpdateController::State::Installing, page); 763 | } 764 | { 765 | auto* page = new InstallingFailPage(_controller, _stackedWidget); 766 | _stackedWidget->addWidget(page); 767 | _pages.insert(QtUpdateController::State::InstallingFail, page); 768 | } 769 | { 770 | auto* page = new InstallingSuccessPage(_controller, _stackedWidget); 771 | _stackedWidget->addWidget(page); 772 | _pages.insert(QtUpdateController::State::InstallingSuccess, page); 773 | } 774 | 775 | // Fix the size. 776 | setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); 777 | const auto sizeHint = this->sizeHint(); 778 | const auto w = std::max(sizeHint.width(), 450); 779 | const auto h = std::max(sizeHint.height(), 280); 780 | setFixedSize(w, h); 781 | 782 | // Close with Escape key. 783 | auto* closeAction = new QAction(this); 784 | closeAction->setShortcut(Qt::Key_Escape); 785 | closeAction->setShortcutContext(Qt::WindowShortcut); 786 | QObject::connect(closeAction, &QAction::triggered, this, [this]() { 787 | _controller.cancel(); 788 | }); 789 | addAction(closeAction); 790 | } 791 | } // namespace oclero 792 | -------------------------------------------------------------------------------- /examples/qtwidgets/QtUpdateWidget.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | 8 | class QStackedWidget; 9 | 10 | namespace oclero { 11 | class QtUpdateWidget : public QWidget { 12 | public: 13 | QtUpdateWidget(QtUpdateController& controller, QWidget* parent = nullptr); 14 | ~QtUpdateWidget(); 15 | 16 | private: 17 | void setupUi(); 18 | 19 | private: 20 | QtUpdateController& _controller; 21 | QStackedWidget* _stackedWidget{ nullptr }; 22 | QMap _pages; 23 | }; 24 | } // namespace oclero 25 | -------------------------------------------------------------------------------- /examples/qtwidgets/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oclero/qtupdater/431628a883f4e9d7bc373e5be2f7574b085939c2/examples/qtwidgets/icon.ico -------------------------------------------------------------------------------- /examples/qtwidgets/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | 9 | #include "QtUpdateWidget.hpp" 10 | 11 | int main(int argc, char* argv[]) { 12 | Q_INIT_RESOURCE(resources); 13 | 14 | QCoreApplication::setApplicationName("QtWidgetsUpdaterExample"); 15 | QCoreApplication::setApplicationVersion("1.0.0"); 16 | QCoreApplication::setOrganizationName("example"); 17 | QApplication app(argc, argv); 18 | QApplication::setWindowIcon(QIcon(":/example/icon.ico")); 19 | 20 | // 1. Create updater backend. 21 | oclero::QtUpdater updater; 22 | updater.setServerUrl("http://localhost:8000/"); 23 | updater.setFrequency(oclero::QtUpdater::Frequency::Never); 24 | 25 | // 2. Create update dialog controller. 26 | oclero::QtUpdateController updateCtrl(updater); 27 | 28 | // 3. Create and show dialog. 29 | auto* widget = new oclero::QtUpdateWidget(updateCtrl); 30 | widget->show(); 31 | 32 | return app.exec(); 33 | } 34 | -------------------------------------------------------------------------------- /examples/qtwidgets/resources.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | icon.ico 4 | 5 | 6 | -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | set(LIB_TARGET_NAME ${PROJECT_NAME}) 2 | 3 | find_package(Qt5 4 | REQUIRED 5 | Core 6 | Network 7 | ) 8 | 9 | set(HEADERS 10 | ${CMAKE_CURRENT_SOURCE_DIR}/include/oclero/QtUpdater.hpp 11 | ${CMAKE_CURRENT_SOURCE_DIR}/include/oclero/QtDownloader.hpp 12 | ${CMAKE_CURRENT_SOURCE_DIR}/include/oclero/QtUpdateController.hpp 13 | ) 14 | 15 | set(SOURCES 16 | ${CMAKE_CURRENT_SOURCE_DIR}/source/oclero/QtUpdater.cpp 17 | ${CMAKE_CURRENT_SOURCE_DIR}/source/oclero/QtDownloader.cpp 18 | ${CMAKE_CURRENT_SOURCE_DIR}/source/oclero/QtUpdateController.cpp 19 | ) 20 | 21 | # Configure target. 22 | add_library(${LIB_TARGET_NAME} STATIC) 23 | add_library(${PROJECT_NAMESPACE}::${LIB_TARGET_NAME} ALIAS ${LIB_TARGET_NAME}) 24 | 25 | target_sources(${LIB_TARGET_NAME} 26 | PRIVATE 27 | ${HEADERS} 28 | ${SOURCES} 29 | ) 30 | 31 | target_include_directories(${LIB_TARGET_NAME} 32 | PUBLIC 33 | $ 34 | PRIVATE 35 | $ 36 | ) 37 | 38 | target_link_libraries(${LIB_TARGET_NAME} 39 | PRIVATE 40 | Qt5::Core 41 | Qt5::Network 42 | PUBLIC 43 | oclero::QtUtils 44 | ) 45 | 46 | set_target_properties(${LIB_TARGET_NAME} 47 | PROPERTIES 48 | AUTOMOC ON 49 | AUTORCC ON 50 | OUTPUT_NAME ${LIB_TARGET_NAME} 51 | PROJECT_LABEL ${LIB_TARGET_NAME} 52 | FOLDER lib 53 | SOVERSION ${PROJECT_VERSION_MAJOR} 54 | VERSION ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR} 55 | DEBUG_POSTFIX _debug 56 | ) 57 | 58 | target_compile_options(${LIB_TARGET_NAME} PRIVATE $<$:/MP>) 59 | 60 | # Create source groups. 61 | source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES 62 | ${HEADERS} 63 | ${SOURCES} 64 | ) 65 | 66 | # Select correct startup project in Visual Studio. 67 | if(WIN32) 68 | set_property(DIRECTORY PROPERTY VS_STARTUP_PROJECT ${LIB_TARGET_NAME}) 69 | endif() 70 | -------------------------------------------------------------------------------- /src/include/oclero/QtDownloader.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | 11 | namespace oclero { 12 | /** 13 | * @brief Utility class to download a file or a data buffer. 14 | */ 15 | class QtDownloader : QObject { 16 | Q_OBJECT 17 | 18 | public: 19 | enum class ErrorCode { 20 | NoError, 21 | AlreadyDownloading, 22 | UrlIsInvalid, 23 | LocalDirIsInvalid, 24 | CannotCreateLocalDir, 25 | CannotRemoveFile, 26 | NotAllowedToWriteFile, 27 | NetworkError, 28 | FileDoesNotExistOrIsCorrupted, 29 | FileDoesNotEndWithSuffix, 30 | CannotRenameFile, 31 | Cancelled, 32 | }; 33 | Q_ENUM(ErrorCode) 34 | 35 | enum class ChecksumType { 36 | NoChecksum, 37 | MD5, 38 | SHA1, 39 | }; 40 | Q_ENUM(ChecksumType) 41 | 42 | enum class InvalidChecksumBehavior { 43 | RemoveFile, 44 | KeepFile, 45 | }; 46 | Q_ENUM(InvalidChecksumBehavior) 47 | 48 | using FileFinishedCallback = std::function; 49 | using DataFinishedCallback = std::function; 50 | using ProgressCallback = std::function; 51 | 52 | static inline const int DefaultTimeout = 30000; 53 | 54 | public: 55 | QtDownloader(QObject* parent = nullptr); 56 | ~QtDownloader(); 57 | 58 | void downloadFile(const QUrl& url, const QString& localDir, const FileFinishedCallback&& onFinished, 59 | const ProgressCallback&& onProgress = nullptr, const int timeout = DefaultTimeout); 60 | 61 | void downloadData(const QUrl& url, const DataFinishedCallback&& onFinished, 62 | const ProgressCallback&& onProgress = nullptr, const int timeout = DefaultTimeout); 63 | 64 | void cancel(); 65 | 66 | bool isDownloading() const; 67 | 68 | static bool verifyFileChecksum(const QString& filePath, const QString& checksum, ChecksumType const checksumType, 69 | InvalidChecksumBehavior const behavior = InvalidChecksumBehavior::RemoveFile); 70 | 71 | private: 72 | struct Impl; 73 | std::unique_ptr _impl; 74 | }; 75 | } // namespace oclero 76 | -------------------------------------------------------------------------------- /src/include/oclero/QtUpdateController.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | 8 | namespace oclero { 9 | /** 10 | * @brief Controller for the Update dialog. Might be used with a QtWidgets-based or QtQuick-based dialog. 11 | */ 12 | class QtUpdateController : public QObject { 13 | Q_OBJECT 14 | 15 | Q_PROPERTY(State state READ state NOTIFY stateChanged) 16 | Q_PROPERTY(QString currentVersion READ currentVersion NOTIFY currentVersionChanged) 17 | Q_PROPERTY(QDateTime currentVersionDate READ currentVersionDate NOTIFY currentVersionDateChanged) 18 | Q_PROPERTY(QString latestVersion READ latestVersion NOTIFY latestVersionChanged) 19 | Q_PROPERTY(QDateTime latestVersionDate READ latestVersionDate NOTIFY latestVersionDateChanged) 20 | Q_PROPERTY(int downloadProgress READ downloadProgress NOTIFY downloadProgressChanged) 21 | Q_PROPERTY(QString latestVersionChangelog READ latestVersionChangelog NOTIFY latestVersionChangelogChanged) 22 | 23 | public: 24 | enum class State { 25 | None, 26 | Checking, 27 | CheckingFail, 28 | CheckingSuccess, 29 | CheckingUpToDate, 30 | Downloading, 31 | DownloadingFail, 32 | DownloadingSuccess, 33 | Installing, 34 | InstallingFail, 35 | InstallingSuccess, 36 | }; 37 | Q_ENUM(State) 38 | 39 | public: 40 | explicit QtUpdateController(oclero::QtUpdater& updater, QObject* parent = nullptr); 41 | ~QtUpdateController() = default; 42 | 43 | State state() const; 44 | QString currentVersion() const; 45 | QDateTime currentVersionDate() const; 46 | QString latestVersion() const; 47 | QDateTime latestVersionDate() const; 48 | QString latestVersionChangelog() const; 49 | int downloadProgress() const; 50 | 51 | private: 52 | void setState(State state); 53 | void setDownloadProgress(int); 54 | 55 | public slots: 56 | void cancel(); 57 | void checkForUpdate(); 58 | void forceCheckForUpdate(); 59 | void downloadUpdate(); 60 | void installUpdate(); 61 | 62 | signals: 63 | void stateChanged(); 64 | void currentVersionChanged(); 65 | void currentVersionDateChanged(); 66 | void latestVersionChanged(); 67 | void latestVersionDateChanged(); 68 | void latestVersionChangelogChanged(); 69 | void downloadProgressChanged(int); 70 | void manualCheckingRequested(); 71 | void closeDialogRequested(); 72 | void checkForUpdateErrorChanged(QtUpdater::ErrorCode code); 73 | void changelogDownloadErrorChanged(QtUpdater::ErrorCode code); 74 | void updateDownloadErrorChanged(QtUpdater::ErrorCode code); 75 | void updateInstallationErrorChanged(QtUpdater::ErrorCode code); 76 | 77 | void linuxDownloadUpdateRequested(); 78 | 79 | private: 80 | oclero::QtUpdater& _updater; 81 | State _state{ State::None }; 82 | int _downloadProgress{ 0 }; 83 | }; 84 | } // namespace oclero 85 | -------------------------------------------------------------------------------- /src/include/oclero/QtUpdater.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | namespace oclero { 11 | /** 12 | * @brief Updater that checks for updates and download installer (Windows-only feature). 13 | * The Updater expects a JSON response like this from the server. 14 | * { 15 | * "version": "x.y.z", 16 | * "date": "dd/MM/YYYY", 17 | * "checksum": "418397de9ef332cd0e477ff5e8ca38d4", 18 | * "checksumType": "md5", 19 | * "installerUrl": "http://server/endpoint/package-name.exe", 20 | * "changelogUrl": "http://server/endpoint/changelog-name.md" 21 | * } 22 | */ 23 | class QtUpdater : public QObject { 24 | Q_OBJECT 25 | 26 | Q_PROPERTY(QString temporaryDirectoryPath READ temporaryDirectoryPath WRITE setTemporaryDirectoryPath NOTIFY 27 | temporaryDirectoryPathChanged) 28 | Q_PROPERTY(UpdateAvailability updateAvailability READ updateAvailability NOTIFY updateAvailabilityChanged) 29 | Q_PROPERTY(bool installerAvailable READ installerAvailable NOTIFY installerAvailableChanged) 30 | Q_PROPERTY(QString currentVersion READ currentVersion CONSTANT) 31 | Q_PROPERTY(QDateTime currentVersionDate READ currentVersionDate CONSTANT) 32 | Q_PROPERTY(QString latestVersion READ latestVersion NOTIFY latestVersionChanged) 33 | Q_PROPERTY(QDateTime latestVersionDate READ latestVersionDate NOTIFY latestVersionDateChanged) 34 | Q_PROPERTY(QString latestChangelog READ latestChangelog NOTIFY latestChangelogChanged) 35 | Q_PROPERTY(State state READ state NOTIFY stateChanged) 36 | Q_PROPERTY(QString serverUrl READ serverUrl WRITE setServerUrl NOTIFY serverUrlChanged) 37 | Q_PROPERTY(Frequency frequency READ frequency WRITE setFrequency NOTIFY frequencyChanged) 38 | Q_PROPERTY(QDateTime lastCheckTime READ lastCheckTime NOTIFY lastCheckTimeChanged) 39 | Q_PROPERTY(InstallMode installMode READ installMode WRITE setInstallMode NOTIFY installModeChanged) 40 | Q_PROPERTY(QString installerDestinationDir READ installerDestinationDir WRITE setInstallerDestinationDir NOTIFY installerDestinationDirChanged) 41 | 42 | public: 43 | enum class State { 44 | Idle, 45 | CheckingForUpdate, 46 | DownloadingChangelog, 47 | DownloadingInstaller, 48 | InstallingUpdate, 49 | }; 50 | Q_ENUM(State) 51 | 52 | enum class UpdateAvailability { 53 | Unknown, 54 | UpToDate, 55 | Available, 56 | }; 57 | Q_ENUM(UpdateAvailability) 58 | 59 | enum class Frequency { 60 | Never, 61 | EveryStart, 62 | EveryHour, 63 | EveryDay, 64 | EveryWeek, 65 | EveryTwoWeeks, 66 | EveryMonth, 67 | }; 68 | Q_ENUM(Frequency) 69 | 70 | enum class InstallMode { 71 | ExecuteFile, 72 | MoveFileToDir, 73 | }; 74 | Q_ENUM(InstallMode) 75 | 76 | struct SettingsParameters { 77 | QSettings::Format format; 78 | QSettings::Scope scope; 79 | QString organization; 80 | QString application; 81 | }; 82 | 83 | enum class ErrorCode { 84 | NoError, 85 | UrlError, 86 | NetworkError, 87 | DiskError, 88 | ChecksumError, 89 | InstallerExecutionError, 90 | UnknownError, 91 | }; 92 | Q_ENUM(ErrorCode) 93 | 94 | public: 95 | explicit QtUpdater(QObject* parent = nullptr); 96 | QtUpdater(const QString& serverUrl, QObject* parent = nullptr); 97 | QtUpdater(const QString& serverUrl, const SettingsParameters& settingsParameters, QObject* parent = nullptr); 98 | ~QtUpdater(); 99 | 100 | public: 101 | const QString& temporaryDirectoryPath() const; 102 | UpdateAvailability updateAvailability() const; 103 | bool changelogAvailable() const; 104 | bool installerAvailable() const; 105 | const QString& currentVersion() const; 106 | const QDateTime& currentVersionDate() const; 107 | QString latestVersion() const; 108 | QDateTime latestVersionDate() const; 109 | const QString& latestChangelog() const; 110 | State state() const; 111 | const QString& serverUrl() const; 112 | Frequency frequency() const; 113 | QDateTime lastCheckTime() const; 114 | int checkTimeout() const; 115 | InstallMode installMode() const; 116 | const QString& installerDestinationDir() const; 117 | 118 | public slots: 119 | void setTemporaryDirectoryPath(const QString& path); 120 | void setServerUrl(const QString& serverUrl); 121 | void setFrequency(Frequency frequency); 122 | void checkForUpdate(); 123 | void forceCheckForUpdate(); 124 | void downloadChangelog(); 125 | void downloadInstaller(); 126 | // Set dry to true if you don't want to quit the application. 127 | void installUpdate(const bool dry = false); 128 | void setCheckTimeout(int timeout); 129 | void setInstallMode(InstallMode mode); 130 | void setInstallerDestinationDir(const QString& path); 131 | void cancel(); 132 | 133 | signals: 134 | void temporaryDirectoryPathChanged(); 135 | void latestVersionChanged(); 136 | void latestVersionDateChanged(); 137 | void latestChangelogChanged(); 138 | void stateChanged(); 139 | void serverUrlChanged(); 140 | void frequencyChanged(); 141 | void lastCheckTimeChanged(); 142 | void installModeChanged(); 143 | void installerDestinationDirChanged(); 144 | void checkTimeoutChanged(); 145 | 146 | void checkForUpdateForced(); 147 | void checkForUpdateStarted(); 148 | void checkForUpdateProgressChanged(int percentage); 149 | void checkForUpdateFinished(); 150 | void checkForUpdateOnlineFailed(); 151 | void checkForUpdateFailed(ErrorCode error); 152 | void checkForUpdateCancelled(); 153 | void updateAvailabilityChanged(); 154 | 155 | void changelogDownloadStarted(); 156 | void changelogDownloadProgressChanged(int percentage); 157 | void changelogDownloadFinished(); 158 | void changelogDownloadFailed(ErrorCode error); 159 | void changelogDownloadCancelled(); 160 | void changelogAvailableChanged(); 161 | 162 | void installerDownloadStarted(); 163 | void installerDownloadProgressChanged(int percentage); 164 | void installerDownloadFinished(); 165 | void installerDownloadFailed(ErrorCode error); 166 | void installerDownloadCancelled(); 167 | void installerAvailableChanged(); 168 | 169 | void installationStarted(); 170 | void installationFailed(ErrorCode error); 171 | // Emitted only when run in dry mode. 172 | void installationFinished(); 173 | 174 | private: 175 | struct Impl; 176 | std::unique_ptr _impl; 177 | }; 178 | } // namespace oclero 179 | -------------------------------------------------------------------------------- /src/source/oclero/QtDownloader.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #include 15 | #include 16 | 17 | namespace oclero { 18 | static const QString PARTIAL_DOWNLOAD_SUFFIX = ".part"; 19 | static const int PARTIAL_DOWNLOAD_SUFFIX_LENGTH = PARTIAL_DOWNLOAD_SUFFIX.length(); 20 | 21 | struct QtDownloader::Impl { 22 | QtDownloader& owner; 23 | QNetworkAccessManager manager; 24 | QUrl url; 25 | QFileInfo fileInfo; 26 | QScopedPointer fileStream{ nullptr }; 27 | bool isDownloading{ false }; 28 | bool cancelled{ false }; 29 | QPointer reply{ nullptr }; 30 | QMetaObject::Connection progressConnection; 31 | QMetaObject::Connection readyReadConnection; 32 | QMetaObject::Connection finishedConnection; 33 | FileFinishedCallback onFileFinished; 34 | DataFinishedCallback onDataFinished; 35 | ProgressCallback onProgress; 36 | QString localDir; 37 | QString downloadedFilepath; 38 | QByteArray downloadedData; 39 | int timeout{ DefaultTimeout }; 40 | 41 | Impl(QtDownloader& o) 42 | : owner(o) { 43 | manager.setAutoDeleteReplies(false); 44 | } 45 | 46 | ~Impl() { 47 | QObject::disconnect(progressConnection); 48 | QObject::disconnect(readyReadConnection); 49 | QObject::disconnect(finishedConnection); 50 | } 51 | 52 | void startFileDownload() { 53 | isDownloading = true; 54 | 55 | // Check url validity. 56 | if (url.isEmpty() || !url.isValid()) { 57 | onFileDownloadFinished(ErrorCode::UrlIsInvalid); 58 | return; 59 | } 60 | 61 | // Check directory. 62 | if (localDir.isEmpty()) { 63 | onFileDownloadFinished(ErrorCode::LocalDirIsInvalid); 64 | return; 65 | } 66 | 67 | // Create directory if it does not exist. 68 | QDir dir(localDir); 69 | if (!dir.exists()) { 70 | if (!dir.mkpath(".")) { 71 | onFileDownloadFinished(ErrorCode::CannotCreateLocalDir); 72 | return; 73 | } 74 | } 75 | 76 | // Get output file name. 77 | const auto urlFileName = url.fileName(); 78 | const auto finalFilePath = dir.absolutePath() + '/' + urlFileName; 79 | const auto partialFilePath = finalFilePath + PARTIAL_DOWNLOAD_SUFFIX; 80 | 81 | // Remove file if it was previously downloaded. 82 | for (const auto& previousPath : { partialFilePath, finalFilePath }) { 83 | QFile previousFile{ previousPath }; 84 | if (previousFile.exists()) { 85 | if (!previousFile.remove()) { 86 | onFileDownloadFinished(ErrorCode::CannotRemoveFile); 87 | return; 88 | } 89 | } 90 | } 91 | 92 | // Create file to write to. 93 | fileInfo = QFileInfo(partialFilePath); 94 | fileStream.reset(new QFile(partialFilePath)); 95 | if (fileStream->exists()) { 96 | if (!fileStream->remove()) { 97 | onFileDownloadFinished(ErrorCode::CannotRemoveFile); 98 | return; 99 | } 100 | } 101 | 102 | if (!fileStream->open(QIODevice::WriteOnly | QIODevice::NewOnly)) { 103 | onFileDownloadFinished(ErrorCode::NotAllowedToWriteFile); 104 | return; 105 | } 106 | 107 | auto request = QNetworkRequest(url); 108 | request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::SameOriginRedirectPolicy); 109 | request.setTransferTimeout(timeout); 110 | reply = manager.get(request); 111 | if (onProgress) { 112 | onProgress(0); 113 | progressConnection = QObject::connect( 114 | reply, &QNetworkReply::downloadProgress, &owner, [this](qint64 bytesReceived, qint64 bytesTotal) { 115 | if (bytesTotal >= bytesReceived) { 116 | onDownloadProgress(bytesReceived, bytesTotal); 117 | } 118 | }); 119 | } 120 | 121 | readyReadConnection = QObject::connect(reply, &QNetworkReply::readyRead, &owner, [this]() { 122 | if (reply->bytesAvailable()) { 123 | fileStream->write(reply->readAll()); 124 | } 125 | }); 126 | 127 | finishedConnection = QObject::connect(reply, &QNetworkReply::finished, &owner, [this]() { 128 | if (onProgress) { 129 | onProgress(100); 130 | } 131 | 132 | QObject::disconnect(progressConnection); 133 | QObject::disconnect(readyReadConnection); 134 | QObject::disconnect(finishedConnection); 135 | const auto errorCode = handleFileReply(reply, cancelled); 136 | onFileDownloadFinished(errorCode); 137 | }); 138 | } 139 | 140 | void startDataDownload() { 141 | isDownloading = true; 142 | downloadedData.clear(); 143 | 144 | if (url.isEmpty() || !url.isValid()) { 145 | onDataDownloadFinished(ErrorCode::UrlIsInvalid); 146 | return; 147 | } 148 | 149 | auto request = QNetworkRequest(url); 150 | request.setTransferTimeout(timeout); 151 | reply = manager.get(request); 152 | 153 | const auto error = reply->error(); 154 | if (error != QNetworkReply::NoError) { 155 | onDataDownloadFinished(ErrorCode::NetworkError); 156 | } 157 | 158 | if (onProgress) { 159 | onProgress(0); 160 | 161 | progressConnection = QObject::connect( 162 | reply, &QNetworkReply::downloadProgress, &owner, [this](qint64 bytesReceived, qint64 bytesTotal) { 163 | onDownloadProgress(bytesReceived, bytesTotal); 164 | }); 165 | } 166 | 167 | readyReadConnection = QObject::connect(reply, &QNetworkReply::readyRead, &owner, [this]() { 168 | if (reply->bytesAvailable()) { 169 | downloadedData.append(reply->readAll()); 170 | } 171 | }); 172 | 173 | finishedConnection = QObject::connect(reply, &QNetworkReply::finished, &owner, [this]() { 174 | if (onProgress) { 175 | onProgress(100); 176 | } 177 | 178 | QObject::disconnect(progressConnection); 179 | QObject::disconnect(readyReadConnection); 180 | QObject::disconnect(finishedConnection); 181 | const auto errorCode = handleDataReply(reply, cancelled); 182 | onDataDownloadFinished(errorCode); 183 | }); 184 | } 185 | 186 | void onFileDownloadFinished(ErrorCode const errorCode) { 187 | isDownloading = false; 188 | cancelled = false; 189 | if (onFileFinished) { 190 | downloadedFilepath = errorCode != ErrorCode::NoError ? QString{} : fileInfo.absoluteFilePath(); 191 | onFileFinished(errorCode, downloadedFilepath); 192 | } 193 | } 194 | 195 | void onDataDownloadFinished(ErrorCode const errorCode) { 196 | isDownloading = false; 197 | cancelled = false; 198 | if (onDataFinished) { 199 | onDataFinished(errorCode, downloadedData); 200 | } 201 | } 202 | 203 | void onDownloadProgress(qint64 bytesReceived, qint64 bytesTotal) { 204 | // Arbitrary minimum size above which we consider we are actually downloading a real file (>= 1KB), 205 | // and not just a reply from the server. 206 | if (bytesTotal >= 1000) { 207 | const auto percentage = bytesTotal == 0 ? 0. : (bytesReceived * 100.) / bytesTotal; 208 | const auto percentageInt = static_cast(std::round(percentage)); 209 | if (onProgress) { 210 | onProgress(percentageInt); 211 | } 212 | } 213 | } 214 | 215 | ErrorCode handleFileReply(QNetworkReply* reply, bool cancelled) { 216 | assert(reply); 217 | assert(fileStream.get()); 218 | 219 | if (!reply) 220 | return ErrorCode::NetworkError; 221 | 222 | QtDeleteLaterScopedPointer replyRAII(reply); 223 | 224 | const auto closeFilestream = [this, reply](bool const removeFile) { 225 | isDownloading = false; 226 | if (removeFile) { 227 | fileStream->remove(); 228 | } 229 | fileStream.reset(nullptr); 230 | }; 231 | 232 | // Cancelled by user. 233 | if (cancelled) { 234 | closeFilestream(true); 235 | return ErrorCode::Cancelled; 236 | } 237 | 238 | // Network error. 239 | if (reply->error() != QNetworkReply::NoError) { 240 | closeFilestream(true); 241 | return ErrorCode::NetworkError; 242 | } 243 | 244 | // IO error. 245 | if (!fileInfo.exists()) { 246 | closeFilestream(true); 247 | return ErrorCode::FileDoesNotExistOrIsCorrupted; 248 | } 249 | 250 | // Filename should end with a certain suffix as we are still writing to disk. 251 | if (!fileInfo.fileName().endsWith(PARTIAL_DOWNLOAD_SUFFIX)) { 252 | closeFilestream(true); 253 | return ErrorCode::FileDoesNotEndWithSuffix; 254 | } 255 | 256 | // Rename file as it is fully downloaded 257 | auto actualFileName = fileInfo.absoluteFilePath().chopped(PARTIAL_DOWNLOAD_SUFFIX_LENGTH); 258 | fileInfo.setFile(actualFileName); 259 | if (!fileStream->rename(actualFileName)) { 260 | closeFilestream(true); 261 | return ErrorCode::CannotRenameFile; 262 | } 263 | 264 | // File is ready. 265 | closeFilestream(false); 266 | return ErrorCode::NoError; 267 | } 268 | 269 | ErrorCode handleDataReply(QNetworkReply* reply, bool cancelled) { 270 | assert(reply); 271 | 272 | if (!reply) 273 | return ErrorCode::NetworkError; 274 | 275 | QtDeleteLaterScopedPointer replyRAII(reply); 276 | 277 | // Cancelled by user. 278 | if (cancelled) { 279 | return ErrorCode::Cancelled; 280 | } 281 | 282 | return reply->error() != QNetworkReply::NoError ? ErrorCode::NetworkError : ErrorCode::NoError; 283 | } 284 | 285 | static std::optional getQtAlgorithm(ChecksumType const checksumType) { 286 | auto result = std::optional(); 287 | switch (checksumType) { 288 | case ChecksumType::MD5: 289 | result = QCryptographicHash::Algorithm::Md5; 290 | break; 291 | case ChecksumType::SHA1: 292 | result = QCryptographicHash::Algorithm::Sha1; 293 | break; 294 | default: 295 | break; 296 | } 297 | return result; 298 | } 299 | }; 300 | 301 | QtDownloader::QtDownloader(QObject* parent) 302 | : QObject(parent) 303 | , _impl(new Impl(*this)) {} 304 | 305 | QtDownloader::~QtDownloader() = default; 306 | 307 | void QtDownloader::downloadFile(const QUrl& url, const QString& localDir, const FileFinishedCallback&& onFinished, 308 | const ProgressCallback&& onProgress, const int timeout) { 309 | if (_impl->isDownloading) { 310 | if (onFinished) { 311 | onFinished(ErrorCode::AlreadyDownloading, {}); 312 | } 313 | return; 314 | } 315 | 316 | _impl->url = url; 317 | _impl->localDir = localDir; 318 | _impl->onFileFinished = onFinished; 319 | _impl->onDataFinished = nullptr; 320 | _impl->onProgress = onProgress; 321 | _impl->timeout = timeout; 322 | _impl->reply.clear(); 323 | _impl->cancelled = false; 324 | 325 | _impl->startFileDownload(); 326 | } 327 | 328 | void QtDownloader::downloadData( 329 | const QUrl& url, const DataFinishedCallback&& onFinished, const ProgressCallback&& onProgress, const int timeout) { 330 | if (_impl->isDownloading) { 331 | if (onFinished) { 332 | onFinished(ErrorCode::AlreadyDownloading, {}); 333 | } 334 | return; 335 | } 336 | 337 | _impl->url = url; 338 | _impl->localDir.clear(); 339 | _impl->onFileFinished = nullptr; 340 | _impl->onDataFinished = onFinished; 341 | _impl->onProgress = onProgress; 342 | _impl->timeout = timeout; 343 | _impl->reply.clear(); 344 | _impl->cancelled = false; 345 | 346 | _impl->startDataDownload(); 347 | } 348 | 349 | 350 | void QtDownloader::cancel() { 351 | if (isDownloading()) { 352 | _impl->cancelled = true; 353 | 354 | if (_impl->reply) { 355 | // Finished signal will be emitted, and the reply will be deleted at this moment. 356 | _impl->reply->abort(); 357 | } 358 | } 359 | } 360 | 361 | bool QtDownloader::isDownloading() const { 362 | return _impl->isDownloading; 363 | } 364 | 365 | bool QtDownloader::verifyFileChecksum(const QString& filePath, const QString& checksumStr, 366 | ChecksumType const checksumType, InvalidChecksumBehavior const behavior) { 367 | if (checksumType == ChecksumType::NoChecksum) { 368 | return true; 369 | } 370 | 371 | if (checksumStr.isEmpty() || filePath.isEmpty()) { 372 | return false; 373 | } 374 | 375 | const auto qtAlgorithm = Impl::getQtAlgorithm(checksumType); 376 | if (!qtAlgorithm) { 377 | return false; 378 | } 379 | 380 | auto result = false; 381 | QFile file(filePath); 382 | if (file.open(QFile::ReadOnly)) { 383 | QCryptographicHash hash(qtAlgorithm.value()); 384 | if (hash.addData(&file)) { 385 | const auto fileHash = hash.result().toHex(); 386 | const auto checksum = checksumStr.toLower().toUtf8(); 387 | result = checksum == fileHash; 388 | } 389 | } 390 | file.close(); 391 | 392 | if (!result && behavior == InvalidChecksumBehavior::RemoveFile) { 393 | file.remove(); 394 | } 395 | 396 | return result; 397 | } 398 | } // namespace oclero 399 | -------------------------------------------------------------------------------- /src/source/oclero/QtUpdateController.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | namespace oclero { 6 | QtUpdateController::QtUpdateController(oclero::QtUpdater& updater, QObject* parent) 7 | : QObject(parent) 8 | , _updater(updater) { 9 | const auto updaterState = _updater.state(); 10 | switch (updaterState) { 11 | case QtUpdater::State::CheckingForUpdate: 12 | setState(State::Checking); 13 | break; 14 | case QtUpdater::State::DownloadingInstaller: 15 | setState(State::Downloading); 16 | break; 17 | case QtUpdater::State::InstallingUpdate: 18 | setState(State::Installing); 19 | break; 20 | default: 21 | break; 22 | } 23 | 24 | // Checking. 25 | QObject::connect(&_updater, &QtUpdater::checkForUpdateForced, this, &QtUpdateController::manualCheckingRequested); 26 | QObject::connect(&_updater, &QtUpdater::checkForUpdateStarted, this, [this]() { 27 | setState(State::Checking); 28 | }); 29 | QObject::connect(&_updater, &QtUpdater::checkForUpdateProgressChanged, this, [this](int percentage) { 30 | setDownloadProgress(percentage); 31 | }); 32 | QObject::connect(&_updater, &QtUpdater::checkForUpdateFailed, this, [this](QtUpdater::ErrorCode code) { 33 | setState(State::CheckingFail); 34 | emit checkForUpdateErrorChanged(code); 35 | }); 36 | QObject::connect(&_updater, &QtUpdater::checkForUpdateFinished, this, [this]() { 37 | const auto availability = _updater.updateAvailability(); 38 | switch (availability) { 39 | case oclero::QtUpdater::UpdateAvailability::Available: 40 | _updater.downloadChangelog(); 41 | break; 42 | case oclero::QtUpdater::UpdateAvailability::UpToDate: 43 | setState(State::CheckingUpToDate); 44 | break; 45 | default: 46 | setState(State::CheckingFail); 47 | break; 48 | } 49 | }); 50 | QObject::connect(&_updater, &QtUpdater::changelogDownloadFinished, this, [this]() { 51 | const auto available = _updater.changelogAvailable(); 52 | if (available) { 53 | setState(State::CheckingSuccess); 54 | } 55 | }); 56 | QObject::connect(&_updater, &QtUpdater::changelogDownloadFailed, this, [this](QtUpdater::ErrorCode code) { 57 | emit changelogDownloadErrorChanged(code); 58 | }); 59 | 60 | // Downloading. 61 | QObject::connect(&_updater, &QtUpdater::installerDownloadStarted, this, [this]() { 62 | setState(State::Downloading); 63 | }); 64 | QObject::connect(&_updater, &QtUpdater::installerDownloadProgressChanged, this, [this](int percentage) { 65 | setDownloadProgress(percentage); 66 | }); 67 | QObject::connect(&_updater, &QtUpdater::installerDownloadFailed, this, [this](QtUpdater::ErrorCode code) { 68 | setState(State::DownloadingFail); 69 | emit updateDownloadErrorChanged(code); 70 | }); 71 | QObject::connect(&_updater, &QtUpdater::installerDownloadFinished, this, [this]() { 72 | const auto available = _updater.installerAvailable(); 73 | setState(available ? State::DownloadingSuccess : State::DownloadingFail); 74 | }); 75 | 76 | // Installing. 77 | QObject::connect(&_updater, &QtUpdater::installationStarted, this, [this]() { 78 | setState(State::Installing); 79 | }); 80 | QObject::connect(&_updater, &QtUpdater::installationFailed, this, [this](QtUpdater::ErrorCode code) { 81 | setState(State::InstallingFail); 82 | emit updateInstallationErrorChanged(code); 83 | }); 84 | QObject::connect(&_updater, &QtUpdater::installationFinished, this, [this]() { 85 | setState(State::InstallingSuccess); 86 | }); 87 | 88 | // Metadata. 89 | QObject::connect(&_updater, &QtUpdater::latestVersionChanged, this, &QtUpdateController::latestVersionChanged); 90 | QObject::connect( 91 | &_updater, &QtUpdater::latestVersionDateChanged, this, &QtUpdateController::latestVersionDateChanged); 92 | QObject::connect( 93 | &_updater, &QtUpdater::latestChangelogChanged, this, &QtUpdateController::latestVersionChangelogChanged); 94 | } 95 | 96 | QtUpdateController::State QtUpdateController::state() const { 97 | return _state; 98 | } 99 | 100 | void QtUpdateController::setState(State state) { 101 | if (state != _state) { 102 | _state = state; 103 | emit stateChanged(); 104 | } 105 | } 106 | 107 | void QtUpdateController::setDownloadProgress(int value) { 108 | if (value != _downloadProgress) { 109 | _downloadProgress = value; 110 | emit downloadProgressChanged(value); 111 | } 112 | } 113 | 114 | QString QtUpdateController::currentVersion() const { 115 | return _updater.currentVersion(); 116 | } 117 | 118 | QDateTime QtUpdateController::currentVersionDate() const { 119 | return _updater.currentVersionDate(); 120 | } 121 | 122 | QString QtUpdateController::latestVersion() const { 123 | return _updater.latestVersion(); 124 | } 125 | 126 | QDateTime QtUpdateController::latestVersionDate() const { 127 | return _updater.latestVersionDate(); 128 | } 129 | 130 | QString QtUpdateController::latestVersionChangelog() const { 131 | return _updater.latestChangelog(); 132 | } 133 | 134 | int QtUpdateController::downloadProgress() const { 135 | return _downloadProgress; 136 | } 137 | 138 | void QtUpdateController::cancel() { 139 | _updater.cancel(); 140 | setState(State::None); 141 | emit closeDialogRequested(); 142 | } 143 | 144 | void QtUpdateController::checkForUpdate() { 145 | _updater.checkForUpdate(); 146 | } 147 | 148 | void QtUpdateController::forceCheckForUpdate() { 149 | _updater.forceCheckForUpdate(); 150 | } 151 | 152 | void QtUpdateController::downloadUpdate() { 153 | if (_updater.updateAvailability() == oclero::QtUpdater::UpdateAvailability::Available) { 154 | #ifdef Q_OS_LINUX 155 | emit linuxDownloadUpdateRequested(); 156 | emit closeDialogRequested(); 157 | #else 158 | _updater.downloadInstaller(); 159 | #endif 160 | } 161 | } 162 | 163 | void QtUpdateController::installUpdate() { 164 | if (_updater.installerAvailable()) { 165 | _updater.installUpdate(); 166 | } 167 | } 168 | } // namespace oclero 169 | -------------------------------------------------------------------------------- /src/source/oclero/QtUpdater.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | 22 | #include 23 | 24 | Q_LOGGING_CATEGORY(CATEGORY_UPDATER, "oclero.qtupdater") 25 | 26 | #if !defined UPDATER_ENABLE_DEBUG 27 | # define UPDATER_ENABLE_DEBUG 0 28 | #endif 29 | 30 | namespace utils { 31 | QString getDefaultTemporaryDirectoryPath() { 32 | QString result; 33 | 34 | const auto dirs = QStandardPaths::standardLocations(QStandardPaths::StandardLocation::TempLocation); 35 | if (!dirs.isEmpty()) { 36 | result = dirs.first(); 37 | 38 | const auto subDirectories = { 39 | QCoreApplication::organizationName(), 40 | QCoreApplication::applicationName(), 41 | }; 42 | 43 | QStringList subDirectoriesList; 44 | for (const auto& subDirectory : subDirectories) { 45 | if (!subDirectory.isEmpty()) { 46 | subDirectoriesList << subDirectory; 47 | } 48 | } 49 | 50 | result += '/' + subDirectoriesList.join('/') + "/Update"; 51 | } 52 | 53 | return result; 54 | } 55 | } // namespace utils 56 | 57 | namespace oclero { 58 | constexpr auto JSON_DATETIME_FORMAT = "dd/MM/yyyy"; 59 | constexpr auto JSON_TAG_CHECKSUM = "checksum"; 60 | constexpr auto JSON_TAG_CHECKSUM_TYPE = "checksumType"; 61 | constexpr auto JSON_TAG_DATE = "date"; 62 | constexpr auto JSON_TAG_INSTALLER_URL = "installerUrl"; 63 | constexpr auto JSON_TAG_CHANGELOG_URL = "changelogUrl"; 64 | constexpr auto JSON_TAG_VERSION = "version"; 65 | 66 | constexpr auto SETTINGS_KEY_LASTCHECKTIME = "Update/LastCheckTime"; 67 | constexpr auto SETTINGS_KEY_FREQUENCY = "Update/CheckFrequency"; 68 | constexpr auto SETTINGS_KEY_LASTUPDATEJSON = "Update/LastUpdateJSON"; 69 | 70 | class LazyFileContent { 71 | public: 72 | LazyFileContent(const QString& path = {}) 73 | : _path(path) {} 74 | 75 | void setPath(const QString& path) { 76 | if (path != _path) { 77 | _path = path; 78 | _content.reset(); 79 | } 80 | } 81 | 82 | const QString& getContent() { 83 | if (!_content.has_value()) { 84 | if (!_path.isEmpty()) { 85 | QFile file(_path); 86 | if (file.open(QIODevice::ReadOnly)) { 87 | _content = QString::fromUtf8(file.readAll()); 88 | } else { 89 | _content = QString(); // Mark as read. 90 | } 91 | file.close(); 92 | } else { 93 | _content = QString(); // Mark as read. 94 | } 95 | } 96 | return _content.value(); 97 | } 98 | 99 | private: 100 | QString _path; 101 | std::optional _content; 102 | }; 103 | 104 | struct UpdateJSON { 105 | QVersionNumber version; 106 | QUrl installerUrl; 107 | QUrl changelogUrl; 108 | QByteArray checksum; 109 | QtDownloader::ChecksumType checksumType{ QtDownloader::ChecksumType::NoChecksum }; 110 | QDateTime date; 111 | 112 | UpdateJSON() = default; 113 | 114 | UpdateJSON(const QByteArray& data) { 115 | const auto jsonDocument = QJsonDocument::fromJson(data); 116 | if (!jsonDocument.isNull() && jsonDocument.isObject()) { 117 | const auto jsonObject = jsonDocument.object(); 118 | if (!jsonObject.isEmpty()) { 119 | if (jsonObject.contains(JSON_TAG_VERSION)) { 120 | version = QVersionNumber::fromString(jsonObject[JSON_TAG_VERSION].toString()); 121 | } 122 | 123 | if (jsonObject.contains(JSON_TAG_CHANGELOG_URL)) { 124 | changelogUrl = QUrl(jsonObject[JSON_TAG_CHANGELOG_URL].toString()); 125 | } 126 | 127 | if (jsonObject.contains(JSON_TAG_INSTALLER_URL)) { 128 | installerUrl = QUrl(jsonObject[JSON_TAG_INSTALLER_URL].toString()); 129 | } 130 | 131 | if (jsonObject.contains(JSON_TAG_CHECKSUM)) { 132 | checksum = jsonObject[JSON_TAG_CHECKSUM].toString().toUtf8(); 133 | } 134 | 135 | if (jsonObject.contains(JSON_TAG_CHECKSUM_TYPE)) { 136 | checksumType = 137 | enumFromString(jsonObject[JSON_TAG_CHECKSUM_TYPE].toString().toUpper()); 138 | } 139 | 140 | if (jsonObject.contains(JSON_TAG_DATE)) { 141 | date = QDateTime::fromString(jsonObject[JSON_TAG_DATE].toString(), JSON_DATETIME_FORMAT); 142 | } 143 | } 144 | } 145 | } 146 | 147 | bool isValid() const { 148 | const auto validVersionNumber = !version.isNull(); 149 | if (!validVersionNumber) 150 | return false; 151 | 152 | const auto validInstallerUrl = installerUrl.isEmpty() || installerUrl.isValid(); 153 | if (!validInstallerUrl) 154 | return false; 155 | 156 | const auto validChangelogUrl = changelogUrl.isEmpty() || changelogUrl.isValid(); 157 | if (!validChangelogUrl) 158 | return false; 159 | 160 | const auto validDate = date.isValid(); 161 | if (!validDate) 162 | return false; 163 | 164 | auto validChecksum = true; 165 | if (checksumType != QtDownloader::ChecksumType::NoChecksum) { 166 | auto qtAlgorithm = QCryptographicHash::Md5; 167 | switch (checksumType) { 168 | case QtDownloader::ChecksumType::MD5: 169 | qtAlgorithm = QCryptographicHash::Algorithm::Md5; 170 | break; 171 | case QtDownloader::ChecksumType::SHA1: 172 | qtAlgorithm = QCryptographicHash::Algorithm::Sha1; 173 | break; 174 | default: 175 | break; 176 | } 177 | 178 | validChecksum = !checksum.isEmpty() && checksum.size() == 2 * QCryptographicHash::hashLength(qtAlgorithm); 179 | } 180 | if (!validChecksum) 181 | return false; 182 | 183 | return true; 184 | } 185 | 186 | QByteArray toJSON() const { 187 | if (!isValid()) { 188 | return QByteArray(); 189 | } 190 | 191 | // Create JSON object. 192 | QJsonObject jsonObject({ 193 | { JSON_TAG_VERSION, version.toString() }, 194 | { JSON_TAG_INSTALLER_URL, installerUrl.toString() }, 195 | { JSON_TAG_CHANGELOG_URL, changelogUrl.toString() }, 196 | { JSON_TAG_CHECKSUM, checksum.constData() }, 197 | { JSON_TAG_CHECKSUM_TYPE, enumToString(checksumType).toLower() }, 198 | { JSON_TAG_DATE, date.toString(JSON_DATETIME_FORMAT) }, 199 | }); 200 | 201 | return QJsonDocument(jsonObject).toJson(QJsonDocument::JsonFormat::Compact); 202 | } 203 | 204 | std::tuple saveToFile(const QString& dirPath) const { 205 | if (!isValid()) { 206 | return { false, {} }; 207 | } 208 | 209 | const auto filename = QFileInfo(installerUrl.fileName()).completeBaseName(); 210 | const auto filePath = dirPath + '/' + filename + ".json"; 211 | QFile file(filePath); 212 | 213 | // Remove existing JSON file, if one. 214 | if (file.exists()) { 215 | if (!file.remove()) { 216 | return { false, {} }; 217 | } 218 | } 219 | 220 | // Create directory if not existing yet. 221 | QDir const dir(dirPath); 222 | if (!dir.exists()) { 223 | if (!dir.mkpath(".")) { 224 | return { false, {} }; 225 | } 226 | } 227 | 228 | // Write file. 229 | if (!file.open(QIODevice::WriteOnly)) { 230 | return { false, {} }; 231 | } 232 | 233 | const auto data = toJSON(); 234 | if (data.isEmpty()) { 235 | return { false, {} }; 236 | } 237 | 238 | file.write(data); 239 | file.close(); 240 | 241 | return { true, filePath }; 242 | } 243 | }; 244 | 245 | struct UpdateInfo { 246 | UpdateJSON json; 247 | QFileInfo installer; 248 | QFileInfo changelog; 249 | LazyFileContent changelogContent; 250 | 251 | bool isValid() const { 252 | return json.isValid(); 253 | } 254 | 255 | bool readyToDisplayChangelog() const { 256 | return isValid() && changelog.exists() && changelog.isFile(); 257 | } 258 | 259 | bool readyToInstall() const { 260 | #if defined(Q_OS_WIN) 261 | const auto isOSInstaller = installer.isExecutable(); 262 | #elif defined(Q_OS_MAC) 263 | const auto isOSInstaller = true; 264 | #else 265 | const auto isOSInstaller = true; 266 | #endif 267 | 268 | return isValid() && installer.exists() && installer.isFile() && isOSInstaller; 269 | } 270 | 271 | const QString& getChangelogContent() { 272 | changelogContent.setPath(changelog.absoluteFilePath()); 273 | return changelogContent.getContent(); 274 | } 275 | }; 276 | 277 | struct QtUpdater::Impl { 278 | QtUpdater& owner; 279 | SettingsParameters settingsParameters; 280 | QString serverUrl; 281 | bool serverUrlInitialized{ false }; 282 | State state{ State::Idle }; 283 | QtDownloader downloader; 284 | UpdateInfo localUpdateInfo; 285 | UpdateInfo onlineUpdateInfo; 286 | Frequency frequency{ Frequency::EveryDay }; 287 | QDateTime lastCheckTime; 288 | int checkTimeout{ QtDownloader::DefaultTimeout }; 289 | QTimer timer; 290 | QString downloadsDir{ utils::getDefaultTemporaryDirectoryPath() }; 291 | QString currentVersion{ QCoreApplication::applicationVersion() }; 292 | QDateTime currentVersionDate; 293 | InstallMode installMode{ InstallMode::ExecuteFile }; 294 | QString installerDestinationDir; 295 | 296 | Impl(QtUpdater& o, const SettingsParameters& p = {}) 297 | : owner(o) 298 | , settingsParameters(p) { 299 | // Load settings. 300 | QSettings settings(settingsParameters.format, settingsParameters.scope, settingsParameters.organization, 301 | settingsParameters.application); 302 | const auto lastCheckTimeInSettings = loadSetting(settings, SETTINGS_KEY_LASTCHECKTIME); 303 | lastCheckTime = QDateTime::fromString(lastCheckTimeInSettings, Qt::DateFormat::ISODate); 304 | 305 | const auto freq = tryLoadSetting(settings, SETTINGS_KEY_FREQUENCY); 306 | if (freq) { 307 | frequency = freq.value(); 308 | } else { 309 | saveSetting(settings, SETTINGS_KEY_FREQUENCY, frequency); 310 | } 311 | 312 | // Setup timer, for hourly checking for updates. 313 | timer.setInterval(std::chrono::seconds(3600)); 314 | timer.setTimerType(Qt::TimerType::VeryCoarseTimer); // No need for precision. 315 | QObject::connect(&timer, &QTimer::timeout, &o, [this]() { 316 | owner.checkForUpdate(); 317 | }); 318 | if (frequency == Frequency::EveryHour) { 319 | timer.start(); 320 | } 321 | } 322 | 323 | void setState(State const value) { 324 | if (value != state) { 325 | state = value; 326 | emit owner.stateChanged(); 327 | } 328 | } 329 | 330 | const UpdateInfo* mostRecentUpdate() const { 331 | if (onlineUpdateInfo.isValid()) { 332 | // Priority is the update from the server. 333 | return &onlineUpdateInfo; 334 | } else if (localUpdateInfo.isValid()) { 335 | // Then, local update, if one. 336 | return &localUpdateInfo; 337 | } else { 338 | return nullptr; 339 | } 340 | } 341 | 342 | UpdateAvailability updateAvailability() const { 343 | const auto update = mostRecentUpdate(); 344 | if (update && update->isValid()) { 345 | const auto currentVersionNumber = QVersionNumber::fromString(currentVersion); 346 | const auto& newVersionNumber = update->json.version; 347 | const auto newUpdateAvailable = QVersionNumber::compare(currentVersionNumber, newVersionNumber) < 0; 348 | return newUpdateAvailable ? UpdateAvailability::Available : UpdateAvailability::UpToDate; 349 | } 350 | return UpdateAvailability::Unknown; 351 | } 352 | 353 | bool changelogAvailable() const { 354 | return updateAvailability() == UpdateAvailability::Available ? mostRecentUpdate()->readyToDisplayChangelog() 355 | : false; 356 | } 357 | 358 | bool installerAvailable() const { 359 | if (updateAvailability() == UpdateAvailability::Available) { 360 | return installMode == InstallMode::ExecuteFile ? mostRecentUpdate()->readyToInstall() : true; 361 | } 362 | return false; 363 | } 364 | 365 | bool shouldCheckForUpdate() const { 366 | auto shouldCheckForUpdate = true; 367 | 368 | if (lastCheckTime.isValid()) { 369 | const auto currentTime = QDateTime::currentDateTime(); 370 | QDateTime comparisonTime; 371 | switch (frequency) { 372 | case Frequency::EveryStart: 373 | comparisonTime = lastCheckTime; 374 | break; 375 | case Frequency::EveryHour: 376 | comparisonTime = lastCheckTime.addSecs(3600); 377 | break; 378 | case Frequency::EveryDay: 379 | comparisonTime = lastCheckTime.addDays(1); 380 | break; 381 | case Frequency::EveryWeek: 382 | comparisonTime = lastCheckTime.addDays(7); 383 | break; 384 | case Frequency::EveryTwoWeeks: 385 | comparisonTime = lastCheckTime.addDays(14); 386 | break; 387 | case Frequency::EveryMonth: 388 | comparisonTime = lastCheckTime.addMonths(1); 389 | break; 390 | default: 391 | break; 392 | } 393 | shouldCheckForUpdate = comparisonTime < currentTime; 394 | } 395 | 396 | return shouldCheckForUpdate; 397 | } 398 | 399 | UpdateInfo checkForLocalUpdate() const { 400 | // Check presence of a JSON file. 401 | QSettings settings(settingsParameters.format, settingsParameters.scope, settingsParameters.organization, 402 | settingsParameters.application); 403 | const auto optFilePath = tryLoadSetting(settings, SETTINGS_KEY_LASTUPDATEJSON); 404 | 405 | if (!optFilePath.has_value()) { 406 | return UpdateInfo{}; 407 | } 408 | 409 | const auto& filePath = optFilePath.value(); 410 | if (filePath.isEmpty()) { 411 | return UpdateInfo{}; 412 | } 413 | 414 | QFile infoFile(filePath); 415 | if (!infoFile.exists()) { 416 | infoFile.close(); 417 | return UpdateInfo{}; 418 | } 419 | 420 | #if UPDATER_ENABLE_DEBUG 421 | qCDebug(CATEGORY_UPDATER) << "Found previously downloaded update data"; 422 | #endif 423 | 424 | // Try to open it. 425 | if (!infoFile.open(QIODevice::ReadOnly)) { 426 | #if UPDATER_ENABLE_DEBUG 427 | qCDebug(CATEGORY_UPDATER) << "Cannot open local JSON file"; 428 | #endif 429 | infoFile.close(); 430 | return UpdateInfo{}; 431 | } 432 | 433 | // Read it. 434 | const auto localJSON = UpdateJSON{ infoFile.readAll() }; 435 | infoFile.close(); 436 | 437 | if (!localJSON.isValid()) { 438 | #if UPDATER_ENABLE_DEBUG 439 | qCDebug(CATEGORY_UPDATER) << "Previously downloaded data is invalid"; 440 | #endif 441 | return UpdateInfo{}; 442 | } 443 | 444 | // Check presence of changelog and installer files along with the JSON file. 445 | const auto changelogFileName = localJSON.changelogUrl.fileName(); 446 | QFileInfo localChangelog(downloadsDir + '/' + changelogFileName); 447 | const auto installerFileName = localJSON.installerUrl.fileName(); 448 | QFileInfo localInstaller(downloadsDir + '/' + installerFileName); 449 | 450 | // Remove exisiting files if the whole bundle is not present. 451 | const auto allFilesExist = 452 | localChangelog.exists() && localChangelog.isFile() && localInstaller.exists() && localInstaller.isFile(); 453 | if (!allFilesExist) { 454 | oclero::clearDirectoryContent(downloadsDir); 455 | return UpdateInfo{}; 456 | } 457 | 458 | return UpdateInfo{ localJSON, localInstaller, localChangelog, {} }; 459 | } 460 | 461 | void notifyUpdateAvailable(const bool newUpdateAvailable) { 462 | // Signals for GUI. 463 | setState(State::Idle); 464 | emit owner.checkForUpdateFinished(); 465 | if (newUpdateAvailable) { 466 | emit owner.latestVersionChanged(); 467 | emit owner.latestVersionDateChanged(); 468 | } 469 | emit owner.updateAvailabilityChanged(); 470 | }; 471 | 472 | void onCheckForUpdateFinished(const QByteArray& data, bool cancelled, ErrorCode errorCode) { 473 | if (cancelled) { 474 | onlineUpdateInfo = {}; 475 | localUpdateInfo = {}; 476 | emit owner.checkForUpdateCancelled(); 477 | notifyUpdateAvailable(false); 478 | return; 479 | } 480 | 481 | // Save online info. 482 | const auto downloadedJSON = UpdateJSON{ data }; 483 | onlineUpdateInfo = UpdateInfo{ downloadedJSON, {}, {}, {} }; 484 | 485 | // Check for previously downloaded update, locally. 486 | #if UPDATER_ENABLE_DEBUG 487 | qCDebug(CATEGORY_UPDATER) << "Checking if an update is locally available..."; 488 | #endif 489 | localUpdateInfo = checkForLocalUpdate(); 490 | 491 | // Order of priority: 492 | // 1. Online information (if available and valid). 493 | // 2. Local information, previously downloaded (if available and valid). 494 | const auto update = mostRecentUpdate(); 495 | if (!update) { 496 | #if UPDATER_ENABLE_DEBUG 497 | qDebug(CATEGORY_UPDATER) << "No update available"; 498 | #endif 499 | emit owner.checkForUpdateFailed(errorCode); 500 | notifyUpdateAvailable(false); 501 | return; 502 | } 503 | 504 | // If the most recent is the one from the server, 505 | // wipe existing files because there are obsolete. 506 | if (update == &onlineUpdateInfo) { 507 | oclero::clearDirectoryContent(downloadsDir); 508 | 509 | // Write downloaded JSON to disk. 510 | const auto [success, saveJSONFilePath] = update->json.saveToFile(downloadsDir); 511 | if (!success) { 512 | emit owner.checkForUpdateFailed(ErrorCode::DiskError); 513 | notifyUpdateAvailable(false); 514 | return; 515 | } 516 | 517 | QSettings settings(settingsParameters.format, settingsParameters.scope, settingsParameters.organization, 518 | settingsParameters.application); 519 | saveSetting(settings, SETTINGS_KEY_LASTUPDATEJSON, saveJSONFilePath); 520 | } 521 | 522 | // Compare version numbers. 523 | const auto currentVersionNumber = QVersionNumber::fromString(currentVersion); 524 | const auto& newVersion = update->json.version; 525 | #if UPDATER_ENABLE_DEBUG 526 | qCDebug(CATEGORY_UPDATER) << "Current:" << currentVersion << "- Latest:" << newVersion; 527 | #endif 528 | 529 | // An update is available if the version is superior to the previous one. 530 | const auto newUpdateAvailable = QVersionNumber::compare(currentVersionNumber, newVersion) < 0; 531 | 532 | // Signals for GUI. 533 | notifyUpdateAvailable(newUpdateAvailable); 534 | } 535 | 536 | void onDownloadChangelogFinished(const QString& filePath) { 537 | #if UPDATER_ENABLE_DEBUG 538 | qCDebug(CATEGORY_UPDATER) << "Changelog downloaded @" << filePath; 539 | #endif 540 | onlineUpdateInfo.changelog = QFileInfo(filePath); 541 | 542 | setState(State::Idle); 543 | emit owner.changelogDownloadFinished(); 544 | emit owner.changelogAvailableChanged(); 545 | emit owner.latestChangelogChanged(); 546 | } 547 | 548 | void onDownloadInstallerFinished(const QString& filePath) { 549 | #if UPDATER_ENABLE_DEBUG 550 | qCDebug(CATEGORY_UPDATER) << "Installer downloaded @" << filePath; 551 | #endif 552 | onlineUpdateInfo.installer = QFileInfo(filePath); 553 | 554 | const auto checksumIsValid = 555 | QtDownloader::verifyFileChecksum(filePath, onlineUpdateInfo.json.checksum, onlineUpdateInfo.json.checksumType); 556 | setState(State::Idle); 557 | 558 | if (!checksumIsValid) { 559 | #if UPDATER_ENABLE_DEBUG 560 | qCDebug(CATEGORY_UPDATER) << "Checksum is invalid"; 561 | #endif 562 | emit owner.installerDownloadFailed(ErrorCode::ChecksumError); 563 | return; 564 | } 565 | #if UPDATER_ENABLE_DEBUG 566 | qCDebug(CATEGORY_UPDATER) << "Checksum is valid"; 567 | #endif 568 | emit owner.installerDownloadFinished(); 569 | emit owner.installerAvailableChanged(); 570 | } 571 | }; 572 | 573 | QtUpdater::ErrorCode mapError(QtDownloader::ErrorCode error) { 574 | switch (error) { 575 | case QtDownloader::ErrorCode::NoError: 576 | return QtUpdater::ErrorCode::NoError; 577 | case QtDownloader::ErrorCode::UrlIsInvalid: 578 | return QtUpdater::ErrorCode::UrlError; 579 | case QtDownloader::ErrorCode::LocalDirIsInvalid: 580 | case QtDownloader::ErrorCode::CannotCreateLocalDir: 581 | case QtDownloader::ErrorCode::CannotRemoveFile: 582 | case QtDownloader::ErrorCode::NotAllowedToWriteFile: 583 | case QtDownloader::ErrorCode::FileDoesNotExistOrIsCorrupted: 584 | case QtDownloader::ErrorCode::FileDoesNotEndWithSuffix: 585 | case QtDownloader::ErrorCode::CannotRenameFile: 586 | return QtUpdater::ErrorCode::DiskError; 587 | case QtDownloader::ErrorCode::NetworkError: 588 | return QtUpdater::ErrorCode::NetworkError; 589 | default: 590 | return QtUpdater::ErrorCode::UnknownError; 591 | } 592 | } 593 | 594 | #pragma region Ctor / Dtor 595 | 596 | QtUpdater::QtUpdater(QObject* parent) 597 | : QObject(parent) 598 | , _impl(new Impl(*this)) {} 599 | 600 | QtUpdater::QtUpdater(const QString& serverUrl, QObject* parent) 601 | : QObject(parent) 602 | , _impl(new Impl(*this, SettingsParameters{})) { 603 | setServerUrl(serverUrl); 604 | } 605 | 606 | QtUpdater::QtUpdater(const QString& serverUrl, const SettingsParameters& settingsParameters, QObject* parent) 607 | : QObject(parent) 608 | , _impl(new Impl(*this, settingsParameters)) { 609 | setServerUrl(serverUrl); 610 | } 611 | 612 | QtUpdater::~QtUpdater() {} 613 | 614 | #pragma endregion 615 | 616 | #pragma region Properties 617 | const QString& QtUpdater::temporaryDirectoryPath() const { 618 | return _impl->downloadsDir; 619 | } 620 | 621 | void QtUpdater::setTemporaryDirectoryPath(const QString& path) { 622 | if (path != _impl->downloadsDir) { 623 | _impl->downloadsDir = path; 624 | emit temporaryDirectoryPathChanged(); 625 | } 626 | } 627 | 628 | QtUpdater::UpdateAvailability QtUpdater::updateAvailability() const { 629 | return _impl->updateAvailability(); 630 | } 631 | 632 | bool QtUpdater::changelogAvailable() const { 633 | return _impl->changelogAvailable(); 634 | } 635 | 636 | bool QtUpdater::installerAvailable() const { 637 | return _impl->installerAvailable(); 638 | } 639 | 640 | const QString& QtUpdater::serverUrl() const { 641 | return _impl->serverUrl; 642 | } 643 | 644 | void QtUpdater::setServerUrl(const QString& serverUrl) { 645 | if (serverUrl != _impl->serverUrl) { 646 | _impl->serverUrl = serverUrl; 647 | emit serverUrlChanged(); 648 | 649 | // Reset data. 650 | _impl->localUpdateInfo = {}; 651 | _impl->onlineUpdateInfo = {}; 652 | _impl->timer.stop(); 653 | _impl->timer.start(); 654 | 655 | // If previous was empty, it means it was not yet set. 656 | if (_impl->serverUrlInitialized) { 657 | _impl->lastCheckTime = {}; 658 | emit updateAvailabilityChanged(); 659 | emit installerAvailableChanged(); 660 | } else { 661 | _impl->serverUrlInitialized = true; 662 | } 663 | } 664 | } 665 | 666 | const QString& QtUpdater::currentVersion() const { 667 | return _impl->currentVersion; 668 | } 669 | 670 | const QDateTime& QtUpdater::currentVersionDate() const { 671 | return _impl->currentVersionDate; 672 | } 673 | 674 | QString QtUpdater::latestVersion() const { 675 | if (_impl->onlineUpdateInfo.isValid()) { 676 | return _impl->onlineUpdateInfo.json.version.toString(); 677 | } else if (_impl->localUpdateInfo.isValid()) { 678 | return _impl->localUpdateInfo.json.version.toString(); 679 | } else { 680 | return _impl->currentVersion; 681 | } 682 | } 683 | 684 | QDateTime QtUpdater::latestVersionDate() const { 685 | if (_impl->onlineUpdateInfo.isValid()) { 686 | return _impl->onlineUpdateInfo.json.date; 687 | } else if (_impl->localUpdateInfo.isValid()) { 688 | return _impl->localUpdateInfo.json.date; 689 | } else { 690 | return _impl->currentVersionDate; 691 | } 692 | } 693 | 694 | const QString& QtUpdater::latestChangelog() const { 695 | static const QString fallback; 696 | if (const auto update = const_cast(_impl->mostRecentUpdate())) { 697 | return update->getChangelogContent(); 698 | } 699 | return fallback; 700 | } 701 | 702 | QtUpdater::State QtUpdater::state() const { 703 | return _impl->state; 704 | } 705 | 706 | QtUpdater::Frequency QtUpdater::frequency() const { 707 | return _impl->frequency; 708 | } 709 | 710 | void QtUpdater::setFrequency(Frequency frequency) { 711 | if (frequency != _impl->frequency) { 712 | _impl->frequency = frequency; 713 | emit frequencyChanged(); 714 | 715 | // Start timer if hourly check. 716 | if (frequency == Frequency::EveryHour) { 717 | _impl->timer.start(); 718 | } 719 | } 720 | } 721 | 722 | QDateTime QtUpdater::lastCheckTime() const { 723 | return _impl->lastCheckTime; 724 | } 725 | 726 | int QtUpdater::checkTimeout() const { 727 | return _impl->checkTimeout; 728 | } 729 | 730 | void QtUpdater::setCheckTimeout(int timeout) { 731 | if (timeout != _impl->checkTimeout) { 732 | _impl->checkTimeout = timeout; 733 | emit checkTimeoutChanged(); 734 | } 735 | } 736 | 737 | QtUpdater::InstallMode QtUpdater::installMode() const { 738 | return _impl->installMode; 739 | } 740 | 741 | void QtUpdater::setInstallMode(QtUpdater::InstallMode installMode) { 742 | if (installMode != _impl->installMode) { 743 | _impl->installMode = installMode; 744 | emit installModeChanged(); 745 | } 746 | } 747 | 748 | const QString& QtUpdater::installerDestinationDir() const { 749 | return _impl->installerDestinationDir; 750 | } 751 | 752 | void QtUpdater::setInstallerDestinationDir(const QString& path) { 753 | if (path != _impl->installerDestinationDir) { 754 | _impl->installerDestinationDir = path; 755 | emit installerDestinationDirChanged(); 756 | } 757 | } 758 | 759 | void QtUpdater::cancel() { 760 | const auto currentState = state(); 761 | if (currentState == State::Idle || currentState == State::InstallingUpdate) 762 | return; 763 | 764 | _impl->downloader.cancel(); 765 | _impl->state = State::Idle; 766 | emit stateChanged(); 767 | } 768 | 769 | #pragma endregion 770 | 771 | #pragma region Public slots 772 | 773 | void QtUpdater::checkForUpdate() { 774 | if (state() != State::Idle || _impl->serverUrl.isEmpty()) { 775 | return; 776 | } 777 | 778 | if (_impl->shouldCheckForUpdate()) { 779 | forceCheckForUpdate(); 780 | } 781 | } 782 | 783 | void QtUpdater::forceCheckForUpdate() { 784 | emit checkForUpdateForced(); 785 | 786 | if (state() != State::Idle || _impl->serverUrl.isEmpty()) { 787 | return; 788 | } 789 | 790 | // Reset data. 791 | _impl->localUpdateInfo = {}; 792 | _impl->onlineUpdateInfo = {}; 793 | 794 | // Change last checked time. 795 | _impl->lastCheckTime = QDateTime::currentDateTime(); 796 | QSettings settings(_impl->settingsParameters.format, _impl->settingsParameters.scope, 797 | _impl->settingsParameters.organization, _impl->settingsParameters.application); 798 | saveSetting(settings, SETTINGS_KEY_LASTCHECKTIME, _impl->lastCheckTime.toString(Qt::DateFormat::ISODate)); 799 | emit lastCheckTimeChanged(); 800 | 801 | // Start checking. 802 | _impl->setState(State::CheckingForUpdate); 803 | emit checkForUpdateStarted(); 804 | 805 | #if UPDATER_ENABLE_DEBUG 806 | qCDebug(CATEGORY_UPDATER) << "Checking for updates @" << url.toString() << "..."; 807 | #endif 808 | 809 | _impl->downloader.downloadData( 810 | _impl->serverUrl, 811 | [this](QtDownloader::ErrorCode const errorCode, const QByteArray& data) { 812 | if (errorCode != QtDownloader::ErrorCode::NoError) { 813 | emit checkForUpdateOnlineFailed(); 814 | } 815 | const auto cancelled = errorCode == QtDownloader::ErrorCode::Cancelled; 816 | const auto mappedErrorCode = mapError(errorCode); 817 | _impl->onCheckForUpdateFinished(data, cancelled, mappedErrorCode); 818 | }, 819 | [this](int const percentage) { 820 | emit checkForUpdateProgressChanged(percentage); 821 | }, 822 | _impl->checkTimeout); 823 | } 824 | 825 | void QtUpdater::downloadChangelog() { 826 | if (state() != State::Idle) { 827 | return; 828 | } 829 | 830 | // Check if local changelog. 831 | if (!_impl->onlineUpdateInfo.isValid()) { 832 | if (_impl->localUpdateInfo.readyToDisplayChangelog()) { 833 | emit changelogAvailableChanged(); 834 | } 835 | return; 836 | } 837 | 838 | _impl->setState(State::DownloadingChangelog); 839 | emit changelogDownloadStarted(); 840 | const auto& url = _impl->onlineUpdateInfo.json.changelogUrl; 841 | 842 | #if UPDATER_ENABLE_DEBUG 843 | qCDebug(CATEGORY_UPDATER) << "Downloading changelog @" << url.toString() << "..."; 844 | #endif 845 | 846 | if (!url.isValid()) { 847 | _impl->setState(State::Idle); 848 | emit changelogDownloadFailed(ErrorCode::UrlError); 849 | return; 850 | } 851 | const auto& dir = _impl->downloadsDir; 852 | _impl->downloader.downloadFile( 853 | url, dir, 854 | [this](QtDownloader::ErrorCode const errorCode, const QString& filePath) { 855 | if (errorCode == QtDownloader::ErrorCode::NoError) { 856 | _impl->onDownloadChangelogFinished(filePath); 857 | } else if (errorCode == QtDownloader::ErrorCode::Cancelled) { 858 | _impl->setState(State::Idle); 859 | emit changelogDownloadCancelled(); 860 | } else { 861 | _impl->setState(State::Idle); 862 | emit changelogDownloadFailed(mapError(errorCode)); 863 | } 864 | }, 865 | [this](int const percentage) { 866 | emit changelogDownloadProgressChanged(percentage); 867 | }, 868 | _impl->checkTimeout); 869 | } 870 | 871 | void QtUpdater::downloadInstaller() { 872 | if (state() != State::Idle) { 873 | return; 874 | } 875 | 876 | // Priority is given to server updates. 877 | if (!_impl->onlineUpdateInfo.isValid()) { 878 | // However, there might be a previously downloaded update, locally. 879 | if (_impl->localUpdateInfo.readyToInstall()) { 880 | emit installerAvailableChanged(); 881 | } 882 | return; 883 | } 884 | 885 | _impl->setState(State::DownloadingInstaller); 886 | emit installerDownloadStarted(); 887 | 888 | const auto& url = _impl->onlineUpdateInfo.json.installerUrl; 889 | 890 | #if UPDATER_ENABLE_DEBUG 891 | qCDebug(CATEGORY_UPDATER) << "Downloading installer @" << url.toString() << "..."; 892 | #endif 893 | 894 | if (!url.isValid()) { 895 | _impl->setState(State::Idle); 896 | emit installerDownloadFailed(ErrorCode::UrlError); 897 | return; 898 | } 899 | const auto& dir = _impl->downloadsDir; 900 | _impl->downloader.downloadFile( 901 | url, dir, 902 | [this](QtDownloader::ErrorCode const errorCode, const QString& filePath) { 903 | _impl->setState(State::Idle); 904 | if (errorCode == QtDownloader::ErrorCode::NoError) { 905 | _impl->onDownloadInstallerFinished(filePath); 906 | } else if (errorCode == QtDownloader::ErrorCode::Cancelled) { 907 | emit installerDownloadCancelled(); 908 | } else { 909 | emit installerDownloadFailed(mapError(errorCode)); 910 | } 911 | }, 912 | [this](int const percentage) { 913 | #if UPDATER_ENABLE_DEBUG 914 | qCDebug(CATEGORY_UPDATER) << "Downloading installer..." << percentage << "%"; 915 | #endif 916 | emit installerDownloadProgressChanged(percentage); 917 | }, 918 | _impl->checkTimeout); 919 | } 920 | 921 | void QtUpdater::installUpdate(const bool dry) { 922 | const auto raiseError = [this](ErrorCode error, const char* msg = nullptr) { 923 | Q_UNUSED(msg); 924 | #if UPDATER_ENABLE_DEBUG 925 | if (msg) { 926 | qCDebug(CATEGORY_UPDATER) << msg; 927 | } 928 | #endif 929 | emit installationFailed(error); 930 | }; 931 | 932 | if (state() != State::Idle || !_impl->installerAvailable()) { 933 | raiseError(ErrorCode::UnknownError, "Installer not available"); 934 | return; 935 | } 936 | 937 | emit installationStarted(); 938 | #if UPDATER_ENABLE_DEBUG 939 | qCDebug(CATEGORY_UPDATER) << "Installing update..."; 940 | #endif 941 | _impl->setState(State::InstallingUpdate); 942 | 943 | // Should not be null because 'installerAvailable()' returned 'true'. 944 | const auto update = _impl->mostRecentUpdate(); 945 | assert(update); 946 | if (!update) { 947 | return; 948 | } 949 | 950 | // Verify checksum before installing. 951 | if (update->json.checksumType != QtDownloader::ChecksumType::NoChecksum) { 952 | #if UPDATER_ENABLE_DEBUG 953 | qCDebug(CATEGORY_UPDATER) << "Verifying checksum..."; 954 | #endif 955 | if (!QtDownloader::verifyFileChecksum( 956 | update->installer.absoluteFilePath(), update->json.checksum, update->json.checksumType)) { 957 | raiseError(ErrorCode::ChecksumError, "Checksum is invalid"); 958 | return; 959 | } else { 960 | #if UPDATER_ENABLE_DEBUG 961 | qCDebug(CATEGORY_UPDATER) << "Checksum is valid"; 962 | #endif 963 | } 964 | } 965 | 966 | // For the tests, we don't stop the application. 967 | if (dry) { 968 | _impl->setState(State::Idle); 969 | emit installationFinished(); 970 | return; 971 | } 972 | 973 | // Start installer in a separate process. 974 | if (_impl->installMode == InstallMode::ExecuteFile) { 975 | #if UPDATER_ENABLE_DEBUG 976 | qCDebug(CATEGORY_UPDATER) << "Starting installer..."; 977 | #endif 978 | auto installerProcessSuccess = false; 979 | #if defined(Q_OS_WIN) 980 | installerProcessSuccess = QProcess::startDetached(update->installer.absoluteFilePath(), {}); 981 | #elif defined(Q_OS_MAC) 982 | installerProcessSuccess = QProcess::startDetached("open", { update->installer.absoluteFilePath() }); 983 | #else 984 | raiseError(ErrorCode::InstallerExecutionError, "OS not supported"); 985 | #endif 986 | if (!installerProcessSuccess) { 987 | raiseError(ErrorCode::InstallerExecutionError, "Failed to start uninstaller"); 988 | _impl->setState(State::Idle); 989 | return; 990 | } 991 | #if UPDATER_ENABLE_DEBUG 992 | qCDebug(CATEGORY_UPDATER) << "Installer started"; 993 | #endif 994 | 995 | // Quit the app. 996 | if (_impl->installMode == InstallMode::ExecuteFile) { 997 | #if UPDATER_ENABLE_DEBUG 998 | qCDebug(CATEGORY_UPDATER) << "App will quit to let the installer do the update"; 999 | #endif 1000 | QCoreApplication::quit(); 1001 | } 1002 | } else if (_impl->installMode == InstallMode::MoveFileToDir && !_impl->installerDestinationDir.isEmpty()) { 1003 | #if UPDATER_ENABLE_DEBUG 1004 | qCDebug(CATEGORY_UPDATER) << "Moving file..."; 1005 | #endif 1006 | const auto installerPath = update->installer.absoluteFilePath(); 1007 | const auto fileName = update->installer.fileName(); 1008 | const auto movedInstallerPath = _impl->installerDestinationDir + '/' + fileName; 1009 | if (!QFile::copy(installerPath, movedInstallerPath)) { 1010 | raiseError(ErrorCode::DiskError, "Can't copy file to new destination"); 1011 | } 1012 | if (!QFile::remove(installerPath)) { 1013 | raiseError(ErrorCode::DiskError, "Can't remove temporary file"); 1014 | } 1015 | } 1016 | 1017 | _impl->setState(State::Idle); 1018 | emit installationFinished(); 1019 | } 1020 | 1021 | #pragma endregion 1022 | } // namespace oclero 1023 | 1024 | #if defined UPDATER_ENABLE_DEBUG 1025 | # undef UPDATER_ENABLE_DEBUG 1026 | #endif 1027 | -------------------------------------------------------------------------------- /tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | set(TESTS_TARGET_NAME ${PROJECT_NAME}Tests) 2 | 3 | find_package(Qt5 4 | REQUIRED 5 | Core 6 | Test 7 | ) 8 | 9 | add_executable(${TESTS_TARGET_NAME}) 10 | set_target_properties(${TESTS_TARGET_NAME} 11 | PROPERTIES 12 | AUTOMOC ON 13 | AUTORCC ON 14 | INTERNAL_CONSOLE ON 15 | EXCLUDE_FROM_ALL ON 16 | FOLDER tests 17 | ) 18 | set(TESTS_SOURCES 19 | ${CMAKE_CURRENT_SOURCE_DIR}/src/main.cpp 20 | ${CMAKE_CURRENT_SOURCE_DIR}/src/QtUpdaterTests.hpp 21 | ${CMAKE_CURRENT_SOURCE_DIR}/src/QtUpdaterTests.cpp 22 | ) 23 | target_sources(${TESTS_TARGET_NAME} 24 | PRIVATE 25 | ${TESTS_SOURCES} 26 | ) 27 | target_include_directories(${TESTS_TARGET_NAME} 28 | PRIVATE 29 | ${CMAKE_CURRENT_SOURCE_DIR} 30 | # ${CPP_HTTPLIB_INCLUDE_DIRS} # cpp-httplib 31 | ) 32 | target_link_libraries(${TESTS_TARGET_NAME} 33 | PRIVATE 34 | ${PROJECT_NAMESPACE}::${PROJECT_NAME} 35 | Qt5::Core 36 | Qt5::Test 37 | httplib::httplib 38 | ) 39 | add_test(NAME ${TESTS_TARGET_NAME} 40 | COMMAND $ 41 | WORKING_DIRECTORY $) 42 | 43 | source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${TESTS_SOURCES}) 44 | 45 | target_deploy_qt(${TESTS_TARGET_NAME}) 46 | -------------------------------------------------------------------------------- /tests/src/QtUpdaterTests.cpp: -------------------------------------------------------------------------------- 1 | #include "QtUpdaterTests.hpp" 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | 12 | using namespace oclero; 13 | 14 | namespace { 15 | constexpr auto CURRENT_VERSION = "1.0.0"; 16 | constexpr auto LATEST_VERSION = "2.0.0"; 17 | constexpr auto SERVER_PORT = 8080; 18 | constexpr auto SERVER_HOST = "0.0.0.0"; 19 | constexpr auto SERVER_URL = "localhost"; 20 | 21 | constexpr auto APPCAST_QUERY_REGEX = R"(\/)"; 22 | constexpr auto INSTALLER_QUERY_REGEX = R"(\/installer-.+\.(exe|dmg)?)"; 23 | constexpr auto CHANGELOG_QUERY_REGEX = R"(\/changelog-.+\.md?)"; 24 | 25 | constexpr auto CONTENT_TYPE_JSON = "application/json"; 26 | constexpr auto CONTENT_TYPE_EXE = "application/vnd.microsoft.portable-executable"; 27 | constexpr auto CONTENT_TYPE_MD = "text/markdown"; 28 | 29 | constexpr auto DUMMY_INSTALLER_DATA = "This is just dummy data to simulate an installer file"; 30 | 31 | // Dummy markdown changelog. 32 | constexpr auto DUMMY_CHANGELOG = R"(# Changelog 33 | ## MyApp 2.0.0 34 | ### Bugfixes 35 | - Fix bug 1 36 | - Fix bug 1 37 | ### New Features 38 | - New Feature 1 39 | - New Feature 2 40 | )"; 41 | 42 | constexpr auto APPCAST_TEMPLATE = R"({ 43 | "version": "%1", 44 | "date": "%2", 45 | "checksum": "%3", 46 | "checksumType": "md5", 47 | "installerUrl": "%4/installer-%1.0.exe", 48 | "changelogUrl": "%4/changelog-%1.0.md" 49 | })"; 50 | 51 | static const QString SERVER_URL_FOR_CLIENT = "http://" + QString(SERVER_URL) + ':' + QString::number(SERVER_PORT); 52 | 53 | QString getInstallerChecksum(const char* data) { 54 | QByteArray installerData(data); 55 | QCryptographicHash hash(QCryptographicHash::Algorithm::Md5); 56 | hash.addData(installerData); 57 | const auto installerHash = hash.result().toHex(); 58 | return installerHash; 59 | } 60 | 61 | QString getAppCast(const QString& version) { 62 | static const auto checksum = getInstallerChecksum(DUMMY_INSTALLER_DATA); 63 | const auto todayDate = QDate::currentDate().toString("dd/MM/yyyy"); 64 | return QString(APPCAST_TEMPLATE).arg(version).arg(todayDate).arg(checksum).arg(SERVER_URL_FOR_CLIENT); 65 | } 66 | } // namespace 67 | 68 | void Tests::test_emptyServerUrl() { 69 | QtUpdater updater(""); 70 | 71 | auto hasStartedChecking = false; 72 | QObject::connect(&updater, &QtUpdater::checkForUpdateFinished, this, [&hasStartedChecking]() { 73 | hasStartedChecking = true; 74 | }); 75 | 76 | QObject::connect(&updater, &QtUpdater::checkForUpdateFailed, this, [&hasStartedChecking]() { 77 | hasStartedChecking = true; 78 | }); 79 | 80 | // Start checking. The updater should immediately fail. 81 | updater.forceCheckForUpdate(); 82 | 83 | QVERIFY(hasStartedChecking == false); 84 | } 85 | 86 | void Tests::test_invalidServerUrl() { 87 | QtUpdater updater("dummyInvalidUrl"); 88 | 89 | auto done = false; 90 | auto failed = false; 91 | QObject::connect(&updater, &QtUpdater::checkForUpdateFinished, this, [&done]() { 92 | done = true; 93 | }); 94 | QObject::connect(&updater, &QtUpdater::checkForUpdateFailed, this, [&done, &failed]() { 95 | failed = true; 96 | done = true; 97 | }); 98 | 99 | // Start checking. The updater should immediately fail. 100 | updater.forceCheckForUpdate(); 101 | 102 | // Wait for timeout. 103 | if (!QTest::qWaitFor( 104 | [&done]() { 105 | return done; 106 | }, 107 | updater.checkTimeout())) { 108 | QFAIL("Too late."); 109 | } 110 | 111 | QVERIFY(failed); 112 | } 113 | 114 | void Tests::test_validServerUrlButNoServer() { 115 | // Configure updater. 116 | QtUpdater updater(SERVER_URL_FOR_CLIENT); 117 | 118 | auto done = false; 119 | auto error = false; 120 | QObject::connect(&updater, &QtUpdater::checkForUpdateFinished, this, [&done]() { 121 | done = true; 122 | }); 123 | 124 | QObject::connect(&updater, &QtUpdater::checkForUpdateFailed, this, [&done, &error]() { 125 | error = true; 126 | done = true; 127 | }); 128 | 129 | // Start checking. It should fail after a timeout. 130 | updater.forceCheckForUpdate(); 131 | 132 | // Wait for the client to receive the response from the server. 133 | if (!QTest::qWaitFor( 134 | [&done]() { 135 | return done; 136 | }, 137 | updater.checkTimeout())) { 138 | QFAIL("Too late."); 139 | } 140 | 141 | QVERIFY(error); 142 | } 143 | 144 | void Tests::test_validAppcastUrl() { 145 | // Server. 146 | httplib::Server server; 147 | server.Get(APPCAST_QUERY_REGEX, [](const httplib::Request&, httplib::Response& response) { 148 | const auto appCast = getAppCast(LATEST_VERSION); 149 | response.set_content(appCast.toStdString(), CONTENT_TYPE_JSON); 150 | }); 151 | 152 | // Start server in a thread. 153 | std::thread t([&server]() { 154 | if (!server.listen(SERVER_HOST, SERVER_PORT)) { 155 | server.stop(); 156 | QFAIL("Can't start server"); 157 | } 158 | }); 159 | 160 | // Configure updater. 161 | QtUpdater updater(SERVER_URL_FOR_CLIENT); 162 | 163 | auto done = false; 164 | auto error = false; 165 | QObject::connect(&updater, &QtUpdater::checkForUpdateFinished, this, [&done]() { 166 | done = true; 167 | }); 168 | 169 | QObject::connect(&updater, &QtUpdater::checkForUpdateFailed, this, [&done, &error]() { 170 | error = true; 171 | done = true; 172 | }); 173 | 174 | // Start checking. 175 | updater.forceCheckForUpdate(); 176 | 177 | // Wait for the client to receive the response from the server. 178 | if (!QTest::qWaitFor( 179 | [&done]() { 180 | return done; 181 | }, 182 | updater.checkTimeout())) { 183 | QFAIL("Too late."); 184 | } 185 | server.stop(); 186 | t.join(); 187 | 188 | if (error) { 189 | QFAIL("Can't download latest version JSON"); 190 | return; 191 | } 192 | 193 | // Latest version should be the newest one. 194 | const auto updateAvailable = updater.updateAvailability() == QtUpdater::UpdateAvailability::Available; 195 | const auto latestVersion = updater.latestVersion(); 196 | QVERIFY(updateAvailable); 197 | QVERIFY(latestVersion == LATEST_VERSION); 198 | 199 | // A second check should not be made because a check has already been made the same day. 200 | done = false; 201 | updater.setFrequency(QtUpdater::Frequency::EveryDay); 202 | updater.checkForUpdate(); 203 | QVERIFY(!done); 204 | } 205 | 206 | void Tests::test_validAppcastUrlButNoServer() { 207 | // Configure updater. 208 | QtUpdater updater(SERVER_URL_FOR_CLIENT); 209 | 210 | auto done = false; 211 | auto error = false; 212 | QObject::connect(&updater, &QtUpdater::checkForUpdateFinished, this, [&done]() { 213 | done = true; 214 | }); 215 | 216 | QObject::connect(&updater, &QtUpdater::checkForUpdateFailed, this, [&done, &error]() { 217 | error = true; 218 | done = true; 219 | }); 220 | 221 | // Start checking. 222 | updater.forceCheckForUpdate(); 223 | 224 | // Wait for the timeout. 225 | if (!QTest::qWaitFor( 226 | [&done]() { 227 | return done; 228 | }, 229 | updater.checkTimeout())) { 230 | QFAIL("Too late."); 231 | } 232 | 233 | QVERIFY(error); 234 | 235 | // Latest version should stay the current one. 236 | const auto updateAvailable = updater.updateAvailability() == QtUpdater::UpdateAvailability::Available; 237 | const auto latestVersion = updater.latestVersion(); 238 | QVERIFY(!updateAvailable); 239 | QVERIFY(latestVersion == CURRENT_VERSION); 240 | } 241 | 242 | void Tests::test_validAppcastUrlButNoUpdate() { 243 | // Server. 244 | httplib::Server server; 245 | server.Get(APPCAST_QUERY_REGEX, [](const httplib::Request&, httplib::Response& response) { 246 | const auto appCast = getAppCast(CURRENT_VERSION); 247 | response.set_content(appCast.toStdString(), CONTENT_TYPE_JSON); 248 | }); 249 | 250 | // Start server in a thread. 251 | auto t = std::thread([&server]() { 252 | if (!server.listen(SERVER_HOST, SERVER_PORT)) { 253 | server.stop(); 254 | QFAIL("Can't start server"); 255 | } 256 | }); 257 | 258 | // Configure updater. 259 | QtUpdater updater(SERVER_URL_FOR_CLIENT); 260 | 261 | auto done = false; 262 | auto error = false; 263 | QObject::connect(&updater, &QtUpdater::checkForUpdateFinished, this, [&done]() { 264 | done = true; 265 | }); 266 | 267 | QObject::connect(&updater, &QtUpdater::checkForUpdateFailed, this, [&done, &error]() { 268 | error = true; 269 | done = true; 270 | }); 271 | 272 | // Verify that these signals are called (or not) correctly. 273 | auto updateAvailableChanged = false; 274 | auto latestVersionChanged = false; 275 | QObject::connect(&updater, &QtUpdater::updateAvailabilityChanged, this, [&updateAvailableChanged]() { 276 | // Should be always called. 277 | updateAvailableChanged = true; 278 | }); 279 | QObject::connect(&updater, &QtUpdater::latestVersionChanged, this, [&latestVersionChanged]() { 280 | // Should be called only if a (greater) new version exists. 281 | latestVersionChanged = true; 282 | }); 283 | 284 | // Start checking. 285 | updater.forceCheckForUpdate(); 286 | 287 | // Wait for the client to receive the response from the server. 288 | if (!QTest::qWaitFor( 289 | [&done]() { 290 | return done; 291 | }, 292 | updater.checkTimeout())) { 293 | QFAIL("Too late."); 294 | } 295 | server.stop(); 296 | t.join(); 297 | 298 | if (error) { 299 | QFAIL("Can't download latest version JSON"); 300 | return; 301 | } 302 | 303 | // Latest version should be the current one. 304 | const auto updateAvailable = updater.updateAvailability() == QtUpdater::UpdateAvailability::Available; 305 | const auto latestVersion = updater.latestVersion(); 306 | 307 | QVERIFY(!updateAvailable); 308 | QVERIFY(latestVersion == CURRENT_VERSION); 309 | QVERIFY(updateAvailableChanged); 310 | QVERIFY(!latestVersionChanged); 311 | } 312 | 313 | void Tests::test_validChangelogUrl() { 314 | // Server. 315 | httplib::Server server; 316 | server.Get(APPCAST_QUERY_REGEX, [](const httplib::Request&, httplib::Response& response) { 317 | const auto appCast = getAppCast(LATEST_VERSION); 318 | response.set_content(appCast.toStdString(), CONTENT_TYPE_JSON); 319 | }); 320 | server.Get(CHANGELOG_QUERY_REGEX, [](const httplib::Request&, httplib::Response& response) { 321 | response.set_content(DUMMY_CHANGELOG, CONTENT_TYPE_MD); 322 | }); 323 | 324 | // Start server in a thread. 325 | auto t = std::thread([&server]() { 326 | if (!server.listen(SERVER_HOST, SERVER_PORT)) { 327 | server.stop(); 328 | QFAIL("Can't start server"); 329 | } 330 | }); 331 | 332 | // Configure updater. 333 | QtUpdater updater(SERVER_URL_FOR_CLIENT); 334 | 335 | // Check for updates. 336 | auto checked = false; 337 | QObject::connect(&updater, &QtUpdater::checkForUpdateFinished, this, [&checked]() { 338 | checked = true; 339 | }); 340 | updater.forceCheckForUpdate(); 341 | if (!QTest::qWaitFor( 342 | [&checked]() { 343 | return checked; 344 | }, 345 | updater.checkTimeout())) { 346 | QFAIL("Too late."); 347 | } 348 | 349 | if (updater.updateAvailability() != QtUpdater::UpdateAvailability::Available) { 350 | QFAIL("Update should be available before downloading changelog"); 351 | return; 352 | } 353 | 354 | // Download changelog. 355 | auto downloadedChangelog = false; 356 | auto error = false; 357 | QObject::connect(&updater, &QtUpdater::changelogDownloadFinished, this, [&downloadedChangelog]() { 358 | downloadedChangelog = true; 359 | }); 360 | QObject::connect(&updater, &QtUpdater::changelogDownloadFailed, this, [&downloadedChangelog, &error]() { 361 | error = true; 362 | downloadedChangelog = true; 363 | }); 364 | updater.downloadChangelog(); 365 | if (!QTest::qWaitFor( 366 | [&downloadedChangelog]() { 367 | return downloadedChangelog; 368 | }, 369 | updater.checkTimeout())) { 370 | QFAIL("Too late."); 371 | } 372 | server.stop(); 373 | t.join(); 374 | 375 | if (error) { 376 | QFAIL("Can't download changelog"); 377 | return; 378 | } 379 | 380 | const auto changelogAvailable = updater.changelogAvailable(); 381 | const auto& latestChangelog = updater.latestChangelog(); 382 | QVERIFY(changelogAvailable); 383 | QVERIFY(latestChangelog == DUMMY_CHANGELOG); 384 | } 385 | 386 | void Tests::test_invalidChangelogUrl() { 387 | // Server. 388 | httplib::Server server; 389 | server.Get(APPCAST_QUERY_REGEX, [](const httplib::Request&, httplib::Response& response) { 390 | const auto appCast = getAppCast(LATEST_VERSION); 391 | response.set_content(appCast.toStdString(), CONTENT_TYPE_JSON); 392 | }); 393 | 394 | // Start server in a thread. 395 | auto t = std::thread([&server]() { 396 | if (!server.listen(SERVER_HOST, SERVER_PORT)) { 397 | server.stop(); 398 | QFAIL("Can't start server"); 399 | } 400 | }); 401 | 402 | // Configure updater. 403 | QtUpdater updater(SERVER_URL_FOR_CLIENT); 404 | 405 | // Check for updates. 406 | auto checked = false; 407 | QObject::connect(&updater, &QtUpdater::checkForUpdateFinished, this, [&checked]() { 408 | checked = true; 409 | }); 410 | updater.forceCheckForUpdate(); 411 | if (!QTest::qWaitFor( 412 | [&checked]() { 413 | return checked; 414 | }, 415 | updater.checkTimeout())) { 416 | QFAIL("Too late."); 417 | } 418 | 419 | if (updater.updateAvailability() != QtUpdater::UpdateAvailability::Available) { 420 | QFAIL("Update should be available before downloading changelog"); 421 | return; 422 | } 423 | 424 | // Download changelog. 425 | auto downloadedChangelog = false; 426 | auto error = false; 427 | QObject::connect(&updater, &QtUpdater::changelogDownloadFinished, this, [&downloadedChangelog]() { 428 | downloadedChangelog = true; 429 | }); 430 | QObject::connect(&updater, &QtUpdater::changelogDownloadFailed, this, [&downloadedChangelog, &error]() { 431 | error = true; 432 | downloadedChangelog = true; 433 | }); 434 | updater.downloadChangelog(); 435 | if (!QTest::qWaitFor( 436 | [&downloadedChangelog]() { 437 | return downloadedChangelog; 438 | }, 439 | updater.checkTimeout())) { 440 | QFAIL("Too late."); 441 | } 442 | server.stop(); 443 | t.join(); 444 | 445 | QVERIFY(error); 446 | 447 | const auto changelogAvailable = updater.changelogAvailable(); 448 | QVERIFY(!changelogAvailable); 449 | 450 | const auto& latestChangelog = updater.latestChangelog(); 451 | QVERIFY(latestChangelog.isEmpty()); 452 | } 453 | 454 | void Tests::test_validInstallerUrl() { 455 | // Server. 456 | httplib::Server server; 457 | server.Get(APPCAST_QUERY_REGEX, [](const httplib::Request&, httplib::Response& response) { 458 | const auto appCast = getAppCast(LATEST_VERSION); 459 | response.set_content(appCast.toStdString(), CONTENT_TYPE_JSON); 460 | }); 461 | server.Get(INSTALLER_QUERY_REGEX, [](const httplib::Request&, httplib::Response& response) { 462 | response.set_content(DUMMY_INSTALLER_DATA, CONTENT_TYPE_EXE); 463 | }); 464 | 465 | // Start server in a thread. 466 | auto t = std::thread([&server]() { 467 | if (!server.listen(SERVER_HOST, SERVER_PORT)) { 468 | server.stop(); 469 | QFAIL("Can't start server"); 470 | } 471 | }); 472 | 473 | // Configure updater. 474 | QtUpdater updater(SERVER_URL_FOR_CLIENT); 475 | 476 | // Check for updates. 477 | auto checked = false; 478 | QObject::connect(&updater, &QtUpdater::checkForUpdateFinished, this, [&checked]() { 479 | checked = true; 480 | }); 481 | updater.forceCheckForUpdate(); 482 | if (!QTest::qWaitFor( 483 | [&checked]() { 484 | return checked; 485 | }, 486 | updater.checkTimeout())) { 487 | QFAIL("Too late."); 488 | } 489 | 490 | if (updater.updateAvailability() != QtUpdater::UpdateAvailability::Available) { 491 | QFAIL("Update should be available before downloading changelog"); 492 | return; 493 | } 494 | 495 | // Download installer. 496 | auto downloadFinished = false; 497 | auto error = false; 498 | QObject::connect(&updater, &QtUpdater::installerDownloadFinished, this, [&downloadFinished]() { 499 | downloadFinished = true; 500 | }); 501 | QObject::connect(&updater, &QtUpdater::installerDownloadFailed, this, [&downloadFinished, &error]() { 502 | error = true; 503 | downloadFinished = true; 504 | }); 505 | updater.downloadInstaller(); 506 | if (!QTest::qWaitFor( 507 | [&downloadFinished]() { 508 | return downloadFinished; 509 | }, 510 | updater.checkTimeout())) { 511 | QFAIL("Too late."); 512 | } 513 | server.stop(); 514 | t.join(); 515 | 516 | if (error) { 517 | QFAIL("Can't download installer"); 518 | return; 519 | } 520 | 521 | const auto installerAvailable = updater.installerAvailable(); 522 | QVERIFY(installerAvailable); 523 | 524 | // Install update (synchronous). 525 | auto installationFailed = false; 526 | auto installationFinished = false; 527 | QObject::connect(&updater, &QtUpdater::installationFinished, this, [&installationFinished]() { 528 | installationFinished = true; 529 | }); 530 | QObject::connect(&updater, &QtUpdater::installationFailed, this, [&installationFinished, &installationFailed]() { 531 | installationFailed = true; 532 | installationFinished = true; 533 | }); 534 | updater.installUpdate(/*dry*/ true); 535 | QVERIFY(installationFinished); 536 | QVERIFY(!installationFailed); 537 | } 538 | 539 | void Tests::test_invalidInstallerUrl() { 540 | // Server. 541 | httplib::Server server; 542 | server.Get(APPCAST_QUERY_REGEX, [](const httplib::Request&, httplib::Response& response) { 543 | const auto appCast = getAppCast(LATEST_VERSION); 544 | response.set_content(appCast.toStdString(), CONTENT_TYPE_JSON); 545 | }); 546 | 547 | // Start server in a thread. 548 | auto t = std::thread([&server]() { 549 | if (!server.listen(SERVER_HOST, SERVER_PORT)) { 550 | server.stop(); 551 | QFAIL("Can't start server"); 552 | } 553 | }); 554 | 555 | // Configure updater. 556 | QtUpdater updater(SERVER_URL_FOR_CLIENT); 557 | 558 | // Check for updates. 559 | auto checked = false; 560 | QObject::connect(&updater, &QtUpdater::checkForUpdateFinished, this, [&checked]() { 561 | checked = true; 562 | }); 563 | updater.forceCheckForUpdate(); 564 | while (!checked) { 565 | QCoreApplication::processEvents(); 566 | } 567 | 568 | if (updater.updateAvailability() != QtUpdater::UpdateAvailability::Available) { 569 | QFAIL("Update should be available before downloading changelog"); 570 | return; 571 | } 572 | 573 | // Download installer. 574 | auto downloadFinished = false; 575 | auto error = false; 576 | QObject::connect(&updater, &QtUpdater::installerDownloadFinished, this, [&downloadFinished]() { 577 | downloadFinished = true; 578 | }); 579 | QObject::connect(&updater, &QtUpdater::installerDownloadFailed, this, [&downloadFinished, &error]() { 580 | error = true; 581 | downloadFinished = true; 582 | }); 583 | updater.downloadInstaller(); 584 | while (!downloadFinished) { 585 | QCoreApplication::processEvents(); 586 | } 587 | server.stop(); 588 | t.join(); 589 | 590 | QVERIFY(error); 591 | 592 | const auto installerAvailable = updater.installerAvailable(); 593 | QVERIFY(!installerAvailable); 594 | 595 | // Install update (synchronous). 596 | auto installationFailed = false; 597 | auto installationFinished = false; 598 | QObject::connect(&updater, &QtUpdater::installationFailed, this, [&installationFailed, &installationFinished]() { 599 | installationFailed = true; 600 | installationFinished = true; 601 | }); 602 | QObject::connect(&updater, &QtUpdater::installationFinished, this, [&installationFinished]() { 603 | installationFinished = true; 604 | }); 605 | updater.installUpdate(/*dry*/ true); 606 | QVERIFY(installationFinished); 607 | QVERIFY(installationFailed); 608 | } 609 | 610 | void Tests::test_cancel() { 611 | // Server. 612 | httplib::Server server; 613 | server.Get(APPCAST_QUERY_REGEX, [](const httplib::Request&, httplib::Response& response) { 614 | // Sleep to let some time to cancel the download. 615 | std::this_thread::sleep_for(std::chrono::milliseconds(10000)); 616 | const auto appCast = getAppCast(LATEST_VERSION); 617 | response.set_content(appCast.toStdString(), CONTENT_TYPE_JSON); 618 | }); 619 | 620 | // Start server in a thread. 621 | auto t = std::thread([&server]() { 622 | if (!server.listen(SERVER_HOST, SERVER_PORT)) { 623 | server.stop(); 624 | QFAIL("Can't start server"); 625 | } 626 | }); 627 | 628 | // Configure updater. 629 | QtUpdater updater(SERVER_URL_FOR_CLIENT); 630 | 631 | // Check for updates. 632 | auto checked = false; 633 | auto cancelled = false; 634 | QObject::connect(&updater, &QtUpdater::checkForUpdateFinished, this, [&checked]() { 635 | checked = true; 636 | }); 637 | QObject::connect(&updater, &QtUpdater::checkForUpdateCancelled, this, [&cancelled]() { 638 | cancelled = true; 639 | }); 640 | updater.forceCheckForUpdate(); 641 | updater.cancel(); 642 | 643 | if (!QTest::qWaitFor( 644 | [&checked]() { 645 | return checked; 646 | }, 647 | updater.checkTimeout())) { 648 | QFAIL("Too late."); 649 | } 650 | server.stop(); 651 | t.join(); 652 | 653 | QVERIFY(cancelled); 654 | } 655 | -------------------------------------------------------------------------------- /tests/src/QtUpdaterTests.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | class Tests : public QObject { 6 | Q_OBJECT 7 | 8 | public: 9 | using QObject::QObject; 10 | 11 | private slots: 12 | void test_emptyServerUrl(); 13 | void test_invalidServerUrl(); 14 | void test_validServerUrlButNoServer(); 15 | void test_validAppcastUrl(); 16 | void test_validAppcastUrlButNoServer(); 17 | void test_validAppcastUrlButNoUpdate(); 18 | 19 | void test_validChangelogUrl(); 20 | void test_invalidChangelogUrl(); 21 | 22 | void test_validInstallerUrl(); 23 | void test_invalidInstallerUrl(); 24 | 25 | void test_cancel(); 26 | }; 27 | -------------------------------------------------------------------------------- /tests/src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "QtUpdaterTests.hpp" 5 | 6 | int main(int argc, char* argv[]) { 7 | QTEST_SET_MAIN_SOURCE_PATH; 8 | 9 | // Necessary to get a socket name and to have an event loop running. 10 | QCoreApplication::setApplicationName("QtUpdaterTests"); 11 | QCoreApplication::setApplicationVersion("1.0.0"); 12 | QCoreApplication::setOrganizationName("oclero"); 13 | QCoreApplication app(argc, argv); 14 | 15 | Tests tests; 16 | const auto success = QTest::qExec(&tests) == 0; 17 | return success ? EXIT_SUCCESS : EXIT_FAILURE; 18 | } 19 | --------------------------------------------------------------------------------