├── .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 | [](https://mit-license.org/)
8 | [](https://www.qt.io)
9 | [](https://www.qt.io)
10 | [](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 |
--------------------------------------------------------------------------------