├── .gitattributes ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Cargo.toml ├── Cross.toml ├── LICENSE ├── README.md ├── appveyor.yml ├── build.rs ├── ci ├── before_deploy.ps1 ├── before_deploy.sh ├── install.sh └── script.sh ├── clippy.toml ├── examples ├── github-private.key ├── github-public.key ├── github.rs └── gitlab.rs ├── readme.sh └── src ├── backends ├── gitea.rs ├── github.rs ├── gitlab.rs ├── mod.rs └── s3.rs ├── errors.rs ├── lib.rs ├── macros.rs ├── update.rs └── version.rs /.gitattributes: -------------------------------------------------------------------------------- 1 | *.key binary=true -diff 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | **/*.rs.bk 3 | Cargo.lock 4 | .idea/ 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Based on the "trust" template v0.1.1 2 | # https://github.com/japaric/trust/tree/v0.1.1 3 | 4 | dist: trusty 5 | language: rust 6 | services: docker 7 | sudo: required 8 | 9 | # TODO Rust builds on stable by default, this can be 10 | # overridden on a case by case basis down below. 11 | 12 | env: 13 | global: 14 | - CRATE_NAME=self_update 15 | 16 | matrix: 17 | # TODO These are all the build jobs. Adjust as necessary. Comment out what you 18 | # don't need 19 | include: 20 | # Linux 21 | - env: TARGET=i686-unknown-linux-gnu 22 | #- env: TARGET=i686-unknown-linux-musl 23 | - env: TARGET=x86_64-unknown-linux-gnu 24 | - env: TARGET=x86_64-unknown-linux-musl 25 | 26 | # OSX 27 | # - env: TARGET=i686-apple-darwin 28 | # os: osx 29 | - env: TARGET=x86_64-apple-darwin 30 | os: osx 31 | 32 | # *BSD 33 | - env: TARGET=i686-unknown-freebsd DISABLE_TESTS=1 34 | - env: TARGET=x86_64-unknown-freebsd DISABLE_TESTS=1 35 | #- env: TARGET=x86_64-unknown-netbsd DISABLE_TESTS=1 36 | 37 | ## Other architectures 38 | #- env: TARGET=aarch64-unknown-linux-gnu 39 | #- env: TARGET=armv7-unknown-linux-gnueabihf 40 | #- env: TARGET=mips-unknown-linux-gnu 41 | #- env: TARGET=mips64-unknown-linux-gnuabi64 42 | #- env: TARGET=mips64el-unknown-linux-gnuabi64 43 | #- env: TARGET=mipsel-unknown-linux-gnu 44 | #- env: TARGET=powerpc-unknown-linux-gnu 45 | #- env: TARGET=powerpc64-unknown-linux-gnu 46 | #- env: TARGET=powerpc64le-unknown-linux-gnu 47 | #- env: TARGET=s390x-unknown-linux-gnu DISABLE_TESTS=1 48 | 49 | # Testing other channels 50 | - env: TARGET=x86_64-unknown-linux-gnu 51 | rust: nightly 52 | - env: TARGET=x86_64-apple-darwin 53 | os: osx 54 | rust: nightly 55 | allow_failures: 56 | - rust: nightly 57 | fast_finish: true 58 | 59 | before_install: 60 | - set -e 61 | - rustup self update 62 | - rustup component add rustfmt-preview clippy 63 | 64 | install: 65 | - sh ci/install.sh 66 | - source ~/.cargo/env || true 67 | 68 | script: 69 | - bash ci/script.sh 70 | 71 | after_script: set +e 72 | 73 | before_deploy: 74 | - sh ci/before_deploy.sh 75 | 76 | deploy: 77 | # TODO update `api_key.secure` 78 | # - Create a `public_repo` GitHub token. Go to: https://github.com/settings/tokens/new 79 | # - Encrypt it: `travis encrypt 0123456789012345678901234567890123456789 80 | # - Paste the output down here 81 | api_key: 82 | secure: ZH4sTdq0/OBkz65DUl5nSjaQtnSmMThP2ZqHzQKLs8ogbm5NLJPKxD1JCOpb4fBK3loOBGXLeziBVz8aPefi0Gb9Po/aaY3hRWUrvgj43K4Y1Rkj63Ibf3nMm/GN4hZv5ZPyyY/eYXW6Wb7BuYKKKKxuSIYJ/2/CWB1Amc5cjfkZJ4QZ0VA0JL013GznSyuD9D0cJmVy3XYLKLYyrG8XaYr1i9TdK5kQcXsadTeqV7tldHP8d4mc0f1VYoFxX6IMpOO5qI99n9nw/Cac5BngZWnNGAtuzep/U6thnJr4AAFxKvgkDvLWc61jJFzJ6PEWOGJSWgxARwJMdzSNgQP6YqZbD9Vfh7gA7g0xgdXqn8XGe6tI9wr+2IY4orDhJ21nG8UGj3+zHIJa5Pi+gICoZ/6jAEnCB9aAP7O05o+iH5xROaabr9poyYb0LI7bohpzjxEqtP+jcMuyo56xktO2RT12pgGJPEEB9epqjqPYQkoyXyDbJvdGzYjHUfEtnkMK90QrfDe/XwTeqXh9Iiy53babI2gqoBSv3v3E0SEtGfdp72K6uvDPh4MgngT2d+BYGwMlX1Ooojxuwf9VUpZ+OsWqs4APv06YtellAtIvMZxntSNcpo7TB9YgY45C/GOtRj3GLgMaqUM+WNfW+fVu2AAqPAxiidIl6hpNJrnAllw= 83 | file_glob: true 84 | file: $CRATE_NAME-$TRAVIS_TAG-$TARGET.* 85 | on: 86 | # TODO Here you can pick which targets will generate binary releases 87 | # In this example, there are some targets that are tested using the stable 88 | # and nightly channels. This condition makes sure there is only one release 89 | # for such targets and that's generated using the stable channel 90 | condition: $TRAVIS_RUST_VERSION = stable 91 | tags: true 92 | provider: releases 93 | skip_cleanup: true 94 | 95 | cache: cargo 96 | before_cache: 97 | # Travis can't cache files that are not readable by "others" 98 | - chmod -R a+r $HOME/.cargo 99 | 100 | branches: 101 | only: 102 | # release tags 103 | - /^v\d+\.\d+\.\d+.*$/ 104 | - master 105 | 106 | notifications: 107 | email: 108 | on_success: never 109 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [unreleased] 4 | ### Added 5 | ### Changed 6 | ### Removed 7 | 8 | ## [0.42.0] 9 | ### Added 10 | - Improved release search/lookup capability to support filtering assets by identifier 11 | - Improved version specifications to support prelease tags and parallel supported versions 12 | ### Changed 13 | - Update reqwest features to allow http2 negotiation 14 | - Update quick-xml (0.37) and zipsign (0.1) 15 | - Specify per_page=100 when fetching github releases 16 | ### Removed 17 | 18 | ## [0.41.0] 19 | ### Added 20 | ### Changed 21 | - Update to zip 2.x 22 | ### Removed 23 | 24 | ## [0.40.0] 25 | ### Added 26 | ### Changed 27 | - `Release::asset_for` now searches for current `OS` and `ARCH` inside `asset.name` if `target` failed to match 28 | - Update `reqwest` to `0.12.0` 29 | - Update `hyper` to `1.2.0` 30 | - Support variable substitutions in `bin_path_in_archive` at runtime 31 | ### Removed 32 | 33 | ## [0.39.0] 34 | ### Added 35 | - Add `signatures` feature to support verifying zip/tar.gz artifacts using [zipsign](https://github.com/Kijewski/zipsign) 36 | ### Changed 37 | - MSRV = 1.64 38 | ### Removed 39 | 40 | ## [0.38.0] 41 | ### Added 42 | ### Changed 43 | - Use `self-replace` to replace the current executable 44 | ### Removed 45 | 46 | ## [0.37.0] 47 | ### Added 48 | ### Changed 49 | - Bugfix: use appropriate auth headers for each backend (fix gitlab private repo updates) 50 | ### Removed 51 | 52 | ## [0.36.0] 53 | ### Added 54 | ### Changed 55 | - For the gitlab backend, urlencode the repo owner in API calls to handle cases where the repo is owned by a subgroup 56 | ### Removed 57 | 58 | ## [0.35.0] 59 | ### Added 60 | ### Changed 61 | - Support selecting from multiple release artifacts by specifying an `identifier` 62 | - Update `quick-xml` to `0.23.0` 63 | ### Removed 64 | 65 | ## [0.34.0] 66 | ### Added 67 | - Add `with_url` method to `UpdateBuilder` 68 | ### Changed 69 | ### Removed 70 | 71 | ## [0.33.0] 72 | ### Added 73 | - Support for Gitea / Forgejo 74 | ### Changed 75 | ### Removed 76 | 77 | ## [0.32.0] 78 | ### Added 79 | - Support for self hosted gitlab servers 80 | ### Changed 81 | ### Removed 82 | 83 | ## [0.31.0] 84 | ### Added 85 | - Support S3 dualstack endpoints 86 | ### Changed 87 | - Update `indicatif` 0.16.0 -> 0.17.0 88 | ### Removed 89 | 90 | ## [0.30.0] 91 | ### Added 92 | ### Changed 93 | - Bump `semver` 0.11 -> 1.0 94 | ### Removed 95 | 96 | ## [0.29.0] 97 | ### Added 98 | ### Changed 99 | - Bump `zip` 0.5 -> 0.6 100 | - Bump `quick-xml` 0.20 -> 0.22 101 | ### Removed 102 | 103 | ## [0.28.0] 104 | ### Added 105 | ### Changed 106 | - Bump indicatif 0.15 -> 0.16 107 | ### Removed 108 | 109 | ## [0.27.0] 110 | ### Added 111 | ### Changed 112 | - Switch gitlab authorization header prefix from `token` to `Bearer` 113 | ### Removed 114 | 115 | ## [0.26.0] 116 | ### Added 117 | ### Changed 118 | - Clean up dangling temporary directories on Windows. 119 | ### Removed 120 | 121 | ## [0.25.0] 122 | ### Added 123 | ### Changed 124 | - Fix io error triggered when updating binary contained in a zipped folder. 125 | - Fix issues updating Windows binaries on non-`C:` drives. 126 | ### Removed 127 | 128 | ## [0.24.0] 129 | ### Added 130 | ### Changed 131 | - `UpdateBuilder.bin_name` will add the platform-specific exe suffix on the S3 backend. 132 | ### Removed 133 | 134 | ## [0.23.0] 135 | ### Added 136 | ### Changed 137 | - update `reqwest` to `0.11` 138 | - remove `hyper-old-types` dependency, replace the rel-link-header parsing 139 | with a manual parsing function: `find_rel_next_link` 140 | ### Removed 141 | 142 | ## [0.22.0] 143 | ### Added 144 | ### Changed 145 | - bump dependencies 146 | - print out tooling versions in CI 147 | ### Removed 148 | 149 | ## [0.21.0] 150 | ### Added 151 | - Add GCS support to S3 backend 152 | ### Changed 153 | - Fixed docs refering to github in s3 backend 154 | ### Removed 155 | 156 | ## [0.20.0] 157 | ### Added 158 | - Add DigitalOcean Spaces support to S3 backend 159 | ### Changed 160 | ### Removed 161 | 162 | ## [0.19.0] 163 | ### Added 164 | - Add `Download::set_header` for inserting into the download request's headers. 165 | ### Changed 166 | - Update readme example to add `Accept: application/octet-stream` header. Release parsing 167 | was updated in 0.7.0 to use the github-api download url instead of the browser 168 | url so auth headers can be passed. When using the github-api download url, you 169 | need to pass `Accept: application/octet-stream` in order to get back a 302 170 | redirecting you to the "raw" download url. This was already being handled in 171 | `ReleaseUpdate::update_extended`, but wasn't added to the readme example. 172 | ### Removed 173 | 174 | ## [0.18.0] 175 | ### Added 176 | - Allow specifying a custom github api url 177 | ### Changed 178 | ### Removed 179 | 180 | ## [0.17.0] 181 | ### Added 182 | - Support for Gitlab 183 | - Gitlab example 184 | ### Changed 185 | - `UpdateBuilder.bin_name` will add the platform-specific exe suffix (defined 186 | by `std::env::consts::EXE_SUFFIX`) to the end of binary names if it's missing. 187 | This was a fix for windows. 188 | ### Removed 189 | 190 | ## [0.16.0] 191 | ### Added 192 | ### Changed 193 | - switch from `tempdir` to `tempfile` 194 | ### Removed 195 | 196 | ## [0.15.0] 197 | ### Added 198 | - Handling for `.tgz` files 199 | ### Changed 200 | - Support version tags with or without leading `v` 201 | - S3, support path prefixes that contain directories 202 | ### Removed 203 | 204 | ## [0.14.0] 205 | ### Added 206 | - Expose `body` string in `Release` data 207 | ### Changed 208 | ### Removed 209 | 210 | ## [0.13.0] 211 | ### Added 212 | - Feature flag `rustls` to enable using [rustls](https://github.com/ctz/rustls) instead of native openssl implementations. 213 | ### Changed 214 | ### Removed 215 | 216 | ## [0.12.0] 217 | ### Added 218 | ### Changed 219 | - Make all archive and compression dependencies optional, available behind 220 | feature flags, and off by default. The feature flags are listed in the 221 | README. The common github-release use-case (tar.gz) requires the features 222 | `archive-tar compression-flate2` 223 | - Make the `update` module public 224 | ### Removed 225 | 226 | ## [0.11.1] 227 | ### Added 228 | ### Changed 229 | - add rust highlighting tag to doc example 230 | ### Removed 231 | 232 | ## [0.11.0] 233 | ### Added 234 | ### Changed 235 | - set executable bits on non-windows 236 | ### Removed 237 | 238 | ## [0.10.0] 239 | ### Added 240 | ### Changed 241 | - update reqwest to 0.10, add default user-agent to requests 242 | - update indicatif to 0.13 243 | ### Removed 244 | 245 | ## [0.9.0] 246 | ### Added 247 | - support for Amazon S3 as releases backend server 248 | ### Changed 249 | - use `Update` trait in GitHub backend implementation for code re-usability 250 | ### Removed 251 | 252 | ## [0.8.0] 253 | ### Added 254 | 255 | ### Changed 256 | - use the system temp directory on windows 257 | 258 | ### Removed 259 | 260 | ## [0.7.0] 261 | ### Added 262 | ### Changed 263 | - accept `auth_token` in `Update` to allow obtaining releases from private GitHub repos 264 | - use GitHub api url instead of browser url to download assets so that auth can be used for private repos 265 | - accept headers in `Download` that can be used in GET request to download url (required for passing in auth token for private GitHub repos) 266 | ### Removed 267 | 268 | ## [0.6.0] 269 | ### Added 270 | ### Changed 271 | - use indicatif instead of pbr 272 | - update to rust 2018 273 | - determine target arch at build time 274 | ### Removed 275 | 276 | 277 | ## [0.5.1] 278 | ### Added 279 | - expose a more detailed `GitHubUpdateStatus` 280 | 281 | ### Changed 282 | ### Removed 283 | 284 | 285 | ## [0.5.0] 286 | ### Added 287 | - zip archive support 288 | - option to extract a single file 289 | 290 | ### Changed 291 | - renamed github-updater `bin_path_in_tarball` to `bin_path_in_archive` 292 | 293 | ### Removed 294 | 295 | 296 | ## [0.4.5] 297 | ### Added 298 | - freebsd support 299 | 300 | ### Changed 301 | 302 | ### Removed 303 | 304 | 305 | ## [0.4.4] 306 | ### Added 307 | 308 | ### Changed 309 | - bump reqwest 310 | 311 | ### Removed 312 | 313 | 314 | ## [0.4.3] 315 | ### Added 316 | 317 | ### Changed 318 | - Update readme - mention `trust` for producing releases 319 | - Update `version` module docs 320 | 321 | ### Removed 322 | - `macro` module is no longer public 323 | - `cargo_crate_version!` is still exported 324 | 325 | 326 | ## [0.4.2] 327 | ### Added 328 | - `version` module for comparing semver tags more explicitly 329 | 330 | ### Changed 331 | - Add deprecation warning for replacing `should_update` with `version::bump_is_compatible` 332 | - Update the github `update` method to display the compatibility of new release versions. 333 | 334 | ### Removed 335 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ### Contributing 2 | 3 | 4 | After making changes: 5 | 6 | - Run tests: `cargo test` 7 | - Run example file as a simple integration test: `cargo run --example github` 8 | - Run cargo-fmt: 9 | ``` 10 | # make sure rust-fmt is installed 11 | rustup self update 12 | rustup component add rustfmt-preview clippy 13 | 14 | # check 15 | cargo fmt --all -- --check 16 | cargo clippy --all-targets --all-features --examples --tests 17 | # apply fixes 18 | cargo fmt --all 19 | ``` 20 | - The project README.md is generated from the crate docs in `src/lib.rs` using `cargo-readme` 21 | - All readme-content should be added/edited in the `src/lib` crate-level doc section, 22 | and then the `readme.sh` script should be run to update the README.md. 23 | ``` 24 | cargo install cargo-readme 25 | ./readme.sh 26 | ``` 27 | - Update the CHANGELOG.md `unreleased` section with a summary of your changes 28 | - Open a PR to trigger CI builds for all platforms 29 | 30 | 31 | *Thank you!* 32 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "self_update" 3 | version = "0.42.0" 4 | description = "Self updates for standalone executables" 5 | repository = "https://github.com/jaemk/self_update" 6 | keywords = ["update", "upgrade", "download", "release"] 7 | categories = ["command-line-utilities"] 8 | license = "MIT" 9 | readme = "README.md" 10 | authors = ["James Kominick "] 11 | exclude = ["/ci/*", ".travis.yml", "appveyor.yml"] 12 | edition = "2018" 13 | rust = "1.64" 14 | 15 | [dependencies] 16 | serde_json = "1" 17 | tempfile = "3" 18 | flate2 = { version = "1", optional = true } 19 | tar = { version = "0.4", optional = true } 20 | semver = "1.0" 21 | zip = { version = "2", default-features = false, features = ["time"], optional = true } 22 | either = { version = "1", optional = true } 23 | reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls", "http2"] } 24 | hyper = "1" 25 | indicatif = "0.17" 26 | quick-xml = "0.37" 27 | regex = "1" 28 | log = "0.4" 29 | urlencoding = "2.1" 30 | self-replace = "1" 31 | zipsign-api = { version = "0.1", default-features = false, optional = true } 32 | 33 | [features] 34 | default = ["reqwest/default-tls"] 35 | archive-zip = ["zip", "zipsign-api?/verify-zip"] 36 | compression-zip-bzip2 = ["archive-zip", "zip/bzip2"] 37 | compression-zip-deflate = ["archive-zip", "zip/deflate"] 38 | archive-tar = ["tar", "zipsign-api?/verify-tar"] 39 | compression-flate2 = ["archive-tar", "flate2", "either"] 40 | rustls = ["reqwest/rustls-tls"] 41 | signatures = ["dep:zipsign-api"] 42 | 43 | [package.metadata.docs.rs] 44 | # Whether to pass `--all-features` to Cargo (default: false) 45 | all-features = true 46 | -------------------------------------------------------------------------------- /Cross.toml: -------------------------------------------------------------------------------- 1 | [build.env] 2 | passthrough = [ 3 | "TARGET", 4 | ] 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 James Kominick 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 | # self_update 2 | 3 | 4 | [![Build status](https://ci.appveyor.com/api/projects/status/xlkq8rd73cla4ixw/branch/master?svg=true)](https://ci.appveyor.com/project/jaemk/self-update/branch/master) 5 | [![Build Status](https://travis-ci.org/jaemk/self_update.svg?branch=master)](https://travis-ci.org/jaemk/self_update) 6 | [![crates.io:clin](https://img.shields.io/crates/v/self_update.svg?label=self_update)](https://crates.io/crates/self_update) 7 | [![docs](https://docs.rs/self_update/badge.svg)](https://docs.rs/self_update) 8 | 9 | 10 | `self_update` provides updaters for updating rust executables in-place from various release 11 | distribution backends. 12 | 13 | ## Usage 14 | 15 | Update (replace) the current executable with the latest release downloaded 16 | from `https://api.github.com/repos/jaemk/self_update/releases/latest`. 17 | Note, the [`trust`](https://github.com/japaric/trust) project provides a nice setup for 18 | producing release-builds via CI (travis/appveyor). 19 | 20 | ### Features 21 | 22 | The following [cargo features](https://doc.rust-lang.org/cargo/reference/manifest.html#the-features-section) are 23 | available (but _disabled_ by default): 24 | 25 | * `archive-tar`: Support for _tar_ archive format; 26 | * `archive-zip`: Support for _zip_ archive format; 27 | * `compression-flate2`: Support for _gzip_ compression; 28 | * `compression-zip-deflate`: Support for _zip_'s _deflate_ compression format; 29 | * `compression-zip-bzip2`: Support for _zip_'s _bzip2_ compression format; 30 | * `rustls`: Use [pure rust TLS implementation](https://github.com/ctz/rustls) for network requests. This feature does _not_ support 32bit macOS; 31 | * `signatures`: Use [zipsign](https://github.com/Kijewski/zipsign) to verify `.zip` and `.tar.gz` artifacts. Artifacts are assumed to have been signed using zipsign. 32 | 33 | Please activate the feature(s) needed by your release files. 34 | 35 | ### Example 36 | 37 | Run the following example to see `self_update` in action: 38 | 39 | `cargo run --example github --features "archive-tar archive-zip compression-flate2 compression-zip-deflate"`. 40 | 41 | There's also an equivalent example for gitlab: 42 | 43 | `cargo run --example gitlab --features "archive-tar archive-zip compression-flate2 compression-zip-deflate"`. 44 | 45 | which runs something roughly equivalent to: 46 | 47 | ```rust 48 | use self_update::cargo_crate_version; 49 | 50 | fn update() -> Result<(), Box> { 51 | let status = self_update::backends::github::Update::configure() 52 | .repo_owner("jaemk") 53 | .repo_name("self_update") 54 | .bin_name("github") 55 | .show_download_progress(true) 56 | .current_version(cargo_crate_version!()) 57 | .build()? 58 | .update()?; 59 | println!("Update status: `{}`!", status.version()); 60 | Ok(()) 61 | } 62 | ``` 63 | 64 | Amazon S3, Google GCS, and DigitalOcean Spaces are also supported through the `S3` backend to check for new releases. Provided a `bucket_name` 65 | and `asset_prefix` string, `self_update` will look up all matching files using the following format 66 | as a convention for the filenames: `[directory/]--.`. 67 | Leading directories will be stripped from the file name allowing the use of subdirectories in the S3 bucket, 68 | and any file not matching the format, or not matching the provided prefix string, will be ignored. 69 | 70 | ```rust 71 | use self_update::cargo_crate_version; 72 | 73 | fn update() -> Result<(), Box<::std::error::Error>> { 74 | let status = self_update::backends::s3::Update::configure() 75 | .bucket_name("self_update_releases") 76 | .asset_prefix("something/self_update") 77 | .region("eu-west-2") 78 | .bin_name("self_update_example") 79 | .show_download_progress(true) 80 | .current_version(cargo_crate_version!()) 81 | .build()? 82 | .update()?; 83 | println!("S3 Update status: `{}`!", status.version()); 84 | Ok(()) 85 | } 86 | ``` 87 | 88 | Separate utilities are also exposed (**NOTE**: the following example _requires_ the `archive-tar` feature, 89 | see the [features](#features) section above). The `self_replace` crate is re-exported for convenience: 90 | 91 | ```rust 92 | fn update() -> Result<(), Box> { 93 | let releases = self_update::backends::github::ReleaseList::configure() 94 | .repo_owner("jaemk") 95 | .repo_name("self_update") 96 | .build()? 97 | .fetch()?; 98 | println!("found releases:"); 99 | println!("{:#?}\n", releases); 100 | 101 | // get the first available release 102 | let asset = releases[0] 103 | .asset_for(&self_update::get_target(), None) 104 | .unwrap(); 105 | 106 | let tmp_dir = tempfile::Builder::new() 107 | .prefix("self_update") 108 | .tempdir_in(::std::env::current_dir()?)?; 109 | let tmp_tarball_path = tmp_dir.path().join(&asset.name); 110 | let tmp_tarball = ::std::fs::File::open(&tmp_tarball_path)?; 111 | 112 | self_update::Download::from_url(&asset.download_url) 113 | .set_header(reqwest::header::ACCEPT, "application/octet-stream".parse()?) 114 | .download_to(&tmp_tarball)?; 115 | 116 | let bin_name = std::path::PathBuf::from("self_update_bin"); 117 | self_update::Extract::from_source(&tmp_tarball_path) 118 | .archive(self_update::ArchiveKind::Tar(Some(self_update::Compression::Gz))) 119 | .extract_file(&tmp_dir.path(), &bin_name)?; 120 | 121 | let new_exe = tmp_dir.path().join(bin_name); 122 | self_replace::self_replace(new_exe)?; 123 | 124 | Ok(()) 125 | } 126 | ``` 127 | 128 | ### Troubleshooting 129 | 130 | When using cross compilation tools such as cross if you want to use rustls and not openssl 131 | 132 | ```toml 133 | self_update = { version = "0.27.0", features = ["rustls"], default-features = false } 134 | ``` 135 | 136 | 137 | License: MIT 138 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # Based on the "trust" template v0.1.1 2 | # https://github.com/japaric/trust/tree/v0.1.1 3 | 4 | environment: 5 | global: 6 | # TODO This is the Rust channel that build jobs will use by default but can be 7 | # overridden on a case by case basis down below 8 | RUST_VERSION: stable 9 | 10 | # TODO Update this to match the name of your project. 11 | CRATE_NAME: self_update 12 | 13 | # TODO These are all the build jobs. Adjust as necessary. Comment out what you 14 | # don't need 15 | matrix: 16 | # MinGW 17 | - TARGET: i686-pc-windows-gnu 18 | - TARGET: x86_64-pc-windows-gnu 19 | 20 | # MSVC 21 | - TARGET: i686-pc-windows-msvc 22 | - TARGET: x86_64-pc-windows-msvc 23 | 24 | # Testing other channels 25 | #- TARGET: x86_64-pc-windows-gnu 26 | # RUST_VERSION: nightly 27 | #- TARGET: x86_64-pc-windows-msvc 28 | # RUST_VERSION: nightly 29 | 30 | install: 31 | - ps: >- 32 | If ($Env:TARGET -eq 'x86_64-pc-windows-gnu') { 33 | $Env:PATH += ';C:\msys64\mingw64\bin' 34 | } ElseIf ($Env:TARGET -eq 'i686-pc-windows-gnu') { 35 | $Env:PATH += ';C:\msys64\mingw32\bin' 36 | } 37 | - curl -sSf -o rustup-init.exe https://win.rustup.rs/ 38 | - rustup-init.exe -y --default-host %TARGET% --default-toolchain %RUST_VERSION% 39 | - set PATH=%PATH%;C:\Users\appveyor\.cargo\bin 40 | - rustc -Vv 41 | - cargo -V 42 | 43 | # TODO This is the "test phase", tweak it as you see fit 44 | test_script: 45 | # we don't run the "test phase" when doing deploys 46 | - if [%APPVEYOR_REPO_TAG%]==[false] ( 47 | cargo test --target %TARGET% --release 48 | ) 49 | 50 | before_deploy: 51 | # TODO Update this to build the artifacts that matter to you 52 | - cargo build --example github --target %TARGET% --release 53 | - ps: ci\before_deploy.ps1 54 | 55 | deploy: 56 | artifact: /.*\.zip/ 57 | # TODO update `auth_token.secure` 58 | # - Create a `public_repo` GitHub token. Go to: https://github.com/settings/tokens/new 59 | # - Encrypt it. Go to https://ci.appveyor.com/tools/encrypt 60 | # - Paste the output down here 61 | auth_token: 62 | secure: 0JzFf2cMrM1/xDUj64eYUlcseRTy0xDBFqQK4sPx/PCt278zQ6jOzyrV0lrdbIBn 63 | description: '' 64 | on: 65 | # TODO Here you can pick which targets will generate binary releases 66 | # In this example, there are some targets that are tested using the stable 67 | # and nightly channels. This condition makes sure there is only one release 68 | # for such targets and that's generated using the stable channel 69 | RUST_VERSION: stable 70 | appveyor_repo_tag: true 71 | provider: GitHub 72 | 73 | cache: 74 | - C:\Users\appveyor\.cargo\registry 75 | - target 76 | 77 | branches: 78 | only: 79 | # Release tags 80 | - /^v\d+\.\d+\.\d+.*$/ 81 | - master 82 | 83 | notifications: 84 | - provider: Email 85 | on_build_success: false 86 | 87 | # Building is done in the test phase, so we disable Appveyor's build phase. 88 | build: false 89 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!( 3 | "cargo:rustc-env=TARGET={}", 4 | std::env::var("TARGET").unwrap() 5 | ); 6 | } 7 | -------------------------------------------------------------------------------- /ci/before_deploy.ps1: -------------------------------------------------------------------------------- 1 | # This script takes care of packaging the build artifacts that will go in the 2 | # release zipfile 3 | 4 | $SRC_DIR = $PWD.Path 5 | $STAGE = [System.Guid]::NewGuid().ToString() 6 | 7 | Set-Location $ENV:Temp 8 | New-Item -Type Directory -Name $STAGE 9 | Set-Location $STAGE 10 | 11 | $ZIP = "$SRC_DIR\$($Env:CRATE_NAME)-$($Env:APPVEYOR_REPO_TAG_NAME)-$($Env:TARGET).zip" 12 | 13 | # TODO Update this to package the right artifacts 14 | Copy-Item "$SRC_DIR\target\$($Env:TARGET)\release\examples\github.exe" '.\' 15 | 16 | 7z a "$ZIP" * 17 | 18 | Push-AppveyorArtifact "$ZIP" 19 | 20 | Remove-Item *.* -Force 21 | Set-Location .. 22 | Remove-Item $STAGE 23 | Set-Location $SRC_DIR 24 | -------------------------------------------------------------------------------- /ci/before_deploy.sh: -------------------------------------------------------------------------------- 1 | # This script takes care of building your crate and packaging it for release 2 | 3 | set -ex 4 | 5 | main() { 6 | local src=$(pwd) \ 7 | stage= 8 | 9 | case $TRAVIS_OS_NAME in 10 | linux) 11 | stage=$(mktemp -d) 12 | ;; 13 | osx) 14 | stage=$(mktemp -d -t tmp) 15 | ;; 16 | esac 17 | 18 | test -f Cargo.lock || cargo generate-lockfile 19 | 20 | # TODO Update this to build the artifacts that matter to you 21 | cross build --example github --target $TARGET --release --all-features 22 | 23 | # TODO Update this to package the right artifacts 24 | cp target/$TARGET/release/examples/github $stage/ 25 | 26 | cd $stage 27 | tar czf $src/$CRATE_NAME-$TRAVIS_TAG-$TARGET.tar.gz * 28 | cd $src 29 | 30 | rm -rf $stage 31 | } 32 | 33 | main 34 | -------------------------------------------------------------------------------- /ci/install.sh: -------------------------------------------------------------------------------- 1 | set -ex 2 | 3 | main() { 4 | local target= 5 | if [ $TRAVIS_OS_NAME = linux ]; then 6 | target=x86_64-unknown-linux-musl 7 | sort=sort 8 | else 9 | target=x86_64-apple-darwin 10 | sort=gsort # for `sort --sort-version`, from brew's coreutils. 11 | fi 12 | 13 | # This fetches latest stable release 14 | local tag="v0.1.16" 15 | # local tag=$(git ls-remote --tags --refs --exit-code https://github.com/japaric/cross \ 16 | # | cut -d/ -f3 \ 17 | # | grep -E '^v[0.1.0-9.]+$' \ 18 | # | $sort --version-sort \ 19 | # | tail -n1) 20 | curl -LSfs https://japaric.github.io/trust/install.sh | \ 21 | sh -s -- \ 22 | --force \ 23 | --git japaric/cross \ 24 | --tag $tag \ 25 | --target $target 26 | } 27 | 28 | main 29 | -------------------------------------------------------------------------------- /ci/script.sh: -------------------------------------------------------------------------------- 1 | # This script takes care of testing your crate 2 | 3 | set -ex 4 | 5 | # TODO This is the "test phase", tweak it as you see fit 6 | main() { 7 | #cross build --target $TARGET 8 | #cross build --target $TARGET --release 9 | 10 | cargo fmt --version 11 | cargo fmt --all -- --check 12 | cargo clippy --version 13 | cargo clippy --all-targets --all-features --examples --tests 14 | 15 | if [ ! -z $DISABLE_TESTS ]; then 16 | return 17 | fi 18 | 19 | #cross test --target $TARGET 20 | cross test --target $TARGET --release --all-features 21 | 22 | #cross run --target $TARGET 23 | #cross run --target $TARGET --release 24 | } 25 | 26 | # we don't run the "test phase" when doing deploys 27 | if [ -z $TRAVIS_TAG ]; then 28 | main 29 | fi 30 | -------------------------------------------------------------------------------- /clippy.toml: -------------------------------------------------------------------------------- 1 | disallowed-names = ["foo", "baz", "quux"] 2 | -------------------------------------------------------------------------------- /examples/github-private.key: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaemk/self_update/196ca0008c11786d54026af89628ef761112381a/examples/github-private.key -------------------------------------------------------------------------------- /examples/github-public.key: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaemk/self_update/196ca0008c11786d54026af89628ef761112381a/examples/github-public.key -------------------------------------------------------------------------------- /examples/github.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | Example updating an executable to the latest version released via GitHub 3 | 4 | `cargo run --example github --features "archive-tar archive-zip compression-flate2 compression-zip-deflate"`. 5 | 6 | Include `signatures` in the features list to enable zipsign verification 7 | */ 8 | 9 | // For the `cargo_crate_version!` macro 10 | #[macro_use] 11 | extern crate self_update; 12 | 13 | fn run() -> Result<(), Box> { 14 | let mut rel_builder = self_update::backends::github::ReleaseList::configure(); 15 | 16 | #[cfg(feature = "signatures")] 17 | rel_builder.repo_owner("Kijewski"); 18 | #[cfg(not(feature = "signatures"))] 19 | rel_builder.repo_owner("jaemk"); 20 | 21 | let releases = rel_builder.repo_name("self_update").build()?.fetch()?; 22 | println!("found releases:"); 23 | println!("{:#?}\n", releases); 24 | 25 | let mut status_builder = self_update::backends::github::Update::configure(); 26 | 27 | #[cfg(feature = "signatures")] 28 | status_builder 29 | .repo_owner("Kijewski") 30 | .verifying_keys([*include_bytes!("github-public.key")]); 31 | #[cfg(not(feature = "signatures"))] 32 | status_builder.repo_owner("jaemk"); 33 | 34 | let status = status_builder 35 | .repo_name("self_update") 36 | .bin_name("github") 37 | .show_download_progress(true) 38 | //.target_version_tag("v9.9.10") 39 | //.show_output(false) 40 | //.no_confirm(true) 41 | // 42 | // For private repos, you will need to provide a GitHub auth token 43 | // **Make sure not to bake the token into your app**; it is recommended 44 | // you obtain it via another mechanism, such as environment variables 45 | // or prompting the user for input 46 | //.auth_token(env!("DOWNLOAD_AUTH_TOKEN")) 47 | .current_version(cargo_crate_version!()) 48 | .build()? 49 | .update()?; 50 | println!("Update status: `{}`!", status.version()); 51 | Ok(()) 52 | } 53 | 54 | pub fn main() { 55 | if let Err(e) = run() { 56 | println!("[ERROR] {}", e); 57 | ::std::process::exit(1); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /examples/gitlab.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | Example updating an executable to the latest version released via Gitlab 3 | */ 4 | 5 | // For the `cargo_crate_version!` macro 6 | #[macro_use] 7 | extern crate self_update; 8 | 9 | fn run() -> Result<(), Box> { 10 | let releases = self_update::backends::gitlab::ReleaseList::configure() 11 | .repo_owner("jaemk") 12 | .repo_name("self_update") 13 | .build()? 14 | .fetch()?; 15 | println!("found releases:"); 16 | println!("{:#?}\n", releases); 17 | 18 | let status = self_update::backends::gitlab::Update::configure() 19 | .repo_owner("jaemk") 20 | .repo_name("self_update") 21 | .bin_name("github") 22 | .show_download_progress(true) 23 | //.target_version_tag("v9.9.10") 24 | //.show_output(false) 25 | //.no_confirm(true) 26 | // 27 | // For private repos, you will need to provide an auth token 28 | // **Make sure not to bake the token into your app**; it is recommended 29 | // you obtain it via another mechanism, such as environment variables 30 | // or prompting the user for input 31 | //.auth_token(env!("DOWNLOAD_AUTH_TOKEN")) 32 | .current_version(cargo_crate_version!()) 33 | .build()? 34 | .update()?; 35 | println!("Update status: `{}`!", status.version()); 36 | Ok(()) 37 | } 38 | 39 | pub fn main() { 40 | if let Err(e) = run() { 41 | println!("[ERROR] {}", e); 42 | ::std::process::exit(1); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /readme.sh: -------------------------------------------------------------------------------- 1 | cargo readme --no-indent-headings > README.md 2 | -------------------------------------------------------------------------------- /src/backends/gitea.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | gitea releases 3 | */ 4 | use std::env::{self, consts::EXE_SUFFIX}; 5 | use std::path::{Path, PathBuf}; 6 | 7 | use reqwest::{self, header}; 8 | 9 | use crate::backends::find_rel_next_link; 10 | use crate::version::bump_is_greater; 11 | use crate::{ 12 | errors::*, 13 | get_target, 14 | update::{Release, ReleaseAsset, ReleaseUpdate}, 15 | DEFAULT_PROGRESS_CHARS, DEFAULT_PROGRESS_TEMPLATE, 16 | }; 17 | 18 | impl ReleaseAsset { 19 | /// Parse a release-asset json object 20 | /// 21 | /// Errors: 22 | /// * Missing required name & download-url keys 23 | fn from_asset_gitea(asset: &serde_json::Value) -> Result { 24 | let download_url = asset["browser_download_url"] 25 | .as_str() 26 | .ok_or_else(|| format_err!(Error::Release, "Asset missing `browser_download_url`"))?; 27 | let name = asset["name"] 28 | .as_str() 29 | .ok_or_else(|| format_err!(Error::Release, "Asset missing `name`"))?; 30 | Ok(ReleaseAsset { 31 | download_url: download_url.to_owned(), 32 | name: name.to_owned(), 33 | }) 34 | } 35 | } 36 | 37 | impl Release { 38 | fn from_release_gitea(release: &serde_json::Value) -> Result { 39 | let tag = release["tag_name"] 40 | .as_str() 41 | .ok_or_else(|| format_err!(Error::Release, "Release missing `tag_name`"))?; 42 | let date = release["created_at"] 43 | .as_str() 44 | .ok_or_else(|| format_err!(Error::Release, "Release missing `created_at`"))?; 45 | let name = release["name"].as_str().unwrap_or(tag); 46 | let assets = release["assets"] 47 | .as_array() 48 | .ok_or_else(|| format_err!(Error::Release, "No assets found"))?; 49 | let body = release["body"].as_str().map(String::from); 50 | let assets = assets 51 | .iter() 52 | .map(ReleaseAsset::from_asset_gitea) 53 | .collect::>>()?; 54 | Ok(Release { 55 | name: name.to_owned(), 56 | version: tag.trim_start_matches('v').to_owned(), 57 | date: date.to_owned(), 58 | body, 59 | assets, 60 | }) 61 | } 62 | } 63 | 64 | /// `ReleaseList` Builder 65 | #[derive(Clone, Debug)] 66 | pub struct ReleaseListBuilder { 67 | host: Option, 68 | repo_owner: Option, 69 | repo_name: Option, 70 | target: Option, 71 | auth_token: Option, 72 | } 73 | impl ReleaseListBuilder { 74 | /// Set the gitea `host` url 75 | pub fn with_host(&mut self, host: &str) -> &mut Self { 76 | self.host = Some(host.to_owned()); 77 | self 78 | } 79 | 80 | /// Set the repo owner, used to build a gitea api url 81 | pub fn repo_owner(&mut self, owner: &str) -> &mut Self { 82 | self.repo_owner = Some(owner.to_owned()); 83 | self 84 | } 85 | 86 | /// Set the repo name, used to build a gitea api url 87 | pub fn repo_name(&mut self, name: &str) -> &mut Self { 88 | self.repo_name = Some(name.to_owned()); 89 | self 90 | } 91 | 92 | /// Set the optional arch `target` name, used to filter available releases 93 | pub fn with_target(&mut self, target: &str) -> &mut Self { 94 | self.target = Some(target.to_owned()); 95 | self 96 | } 97 | 98 | /// Set the authorization token, used in requests to the gitea api url 99 | /// 100 | /// This is to support private repos where you need a gitea auth token. 101 | /// **Make sure not to bake the token into your app**; it is recommended 102 | /// you obtain it via another mechanism, such as environment variables 103 | /// or prompting the user for input 104 | pub fn auth_token(&mut self, auth_token: &str) -> &mut Self { 105 | self.auth_token = Some(auth_token.to_owned()); 106 | self 107 | } 108 | 109 | /// Verify builder args, returning a `ReleaseList` 110 | pub fn build(&self) -> Result { 111 | Ok(ReleaseList { 112 | host: if let Some(ref host) = self.host { 113 | host.to_owned() 114 | } else { 115 | bail!(Error::Config, "`host` required") 116 | }, 117 | repo_owner: if let Some(ref owner) = self.repo_owner { 118 | owner.to_owned() 119 | } else { 120 | bail!(Error::Config, "`repo_owner` required") 121 | }, 122 | repo_name: if let Some(ref name) = self.repo_name { 123 | name.to_owned() 124 | } else { 125 | bail!(Error::Config, "`repo_name` required") 126 | }, 127 | target: self.target.clone(), 128 | auth_token: self.auth_token.clone(), 129 | }) 130 | } 131 | } 132 | 133 | /// `ReleaseList` provides a builder api for querying a gitea repo, 134 | /// returning a `Vec` of available `Release`s 135 | #[derive(Clone, Debug)] 136 | pub struct ReleaseList { 137 | host: String, 138 | repo_owner: String, 139 | repo_name: String, 140 | target: Option, 141 | auth_token: Option, 142 | } 143 | impl ReleaseList { 144 | /// Initialize a ReleaseListBuilder 145 | pub fn configure() -> ReleaseListBuilder { 146 | ReleaseListBuilder { 147 | host: None, 148 | repo_owner: None, 149 | repo_name: None, 150 | target: None, 151 | auth_token: None, 152 | } 153 | } 154 | 155 | /// Retrieve a list of `Release`s. 156 | /// If specified, filter for those containing a specified `target` 157 | pub fn fetch(self) -> Result> { 158 | set_ssl_vars!(); 159 | let api_url = format!( 160 | "{}/api/v1/repos/{}/{}/releases", 161 | self.host, self.repo_owner, self.repo_name 162 | ); 163 | 164 | let releases = self.fetch_releases(&api_url)?; 165 | let releases = match self.target { 166 | None => releases, 167 | Some(ref target) => releases 168 | .into_iter() 169 | .filter(|r| r.has_target_asset(target)) 170 | .collect::>(), 171 | }; 172 | Ok(releases) 173 | } 174 | 175 | fn fetch_releases(&self, url: &str) -> Result> { 176 | let client = reqwest::blocking::ClientBuilder::new() 177 | .use_rustls_tls() 178 | .http2_adaptive_window(true) 179 | .build()?; 180 | let resp = client 181 | .get(url) 182 | .headers(api_headers(&self.auth_token)?) 183 | .send()?; 184 | if !resp.status().is_success() { 185 | bail!( 186 | Error::Network, 187 | "api request failed with status: {:?} - for: {:?}", 188 | resp.status(), 189 | url 190 | ) 191 | } 192 | let headers = resp.headers().clone(); 193 | 194 | let releases = resp.json::()?; 195 | let releases = releases 196 | .as_array() 197 | .ok_or_else(|| format_err!(Error::Release, "No releases found"))?; 198 | let mut releases = releases 199 | .iter() 200 | .map(Release::from_release_gitea) 201 | .collect::>>()?; 202 | 203 | // handle paged responses containing `Link` header: 204 | // `Link: ; rel="next"` 205 | let links = headers.get_all(reqwest::header::LINK); 206 | 207 | let next_link = links 208 | .iter() 209 | .filter_map(|link| { 210 | if let Ok(link) = link.to_str() { 211 | find_rel_next_link(link) 212 | } else { 213 | None 214 | } 215 | }) 216 | .next(); 217 | 218 | Ok(match next_link { 219 | None => releases, 220 | Some(link) => { 221 | releases.extend(self.fetch_releases(link)?); 222 | releases 223 | } 224 | }) 225 | } 226 | } 227 | 228 | /// `gitea::Update` builder 229 | /// 230 | /// Configure download and installation from 231 | /// `https:///api/v1/repos///releases` 232 | #[derive(Debug)] 233 | pub struct UpdateBuilder { 234 | host: Option, 235 | repo_owner: Option, 236 | repo_name: Option, 237 | target: Option, 238 | identifier: Option, 239 | bin_name: Option, 240 | bin_install_path: Option, 241 | bin_path_in_archive: Option, 242 | show_download_progress: bool, 243 | show_output: bool, 244 | no_confirm: bool, 245 | current_version: Option, 246 | target_version: Option, 247 | progress_template: String, 248 | progress_chars: String, 249 | auth_token: Option, 250 | #[cfg(feature = "signatures")] 251 | verifying_keys: Vec<[u8; zipsign_api::PUBLIC_KEY_LENGTH]>, 252 | } 253 | 254 | impl UpdateBuilder { 255 | /// Initialize a new builder 256 | pub fn new() -> Self { 257 | Default::default() 258 | } 259 | 260 | /// Set the gitea `host` url 261 | pub fn with_host(&mut self, host: &str) -> &mut Self { 262 | self.host = Some(host.to_owned()); 263 | self 264 | } 265 | 266 | /// Set the repo owner, used to build a gitea api url 267 | pub fn repo_owner(&mut self, owner: &str) -> &mut Self { 268 | self.repo_owner = Some(owner.to_owned()); 269 | self 270 | } 271 | 272 | /// Set the repo name, used to build a gitea api url 273 | pub fn repo_name(&mut self, name: &str) -> &mut Self { 274 | self.repo_name = Some(name.to_owned()); 275 | self 276 | } 277 | 278 | /// Set the current app version, used to compare against the latest available version. 279 | /// The `cargo_crate_version!` macro can be used to pull the version from your `Cargo.toml` 280 | pub fn current_version(&mut self, ver: &str) -> &mut Self { 281 | self.current_version = Some(ver.to_owned()); 282 | self 283 | } 284 | 285 | /// Set the target version tag to update to. This will be used to search for a release 286 | /// by tag name: 287 | /// `/repos/:owner%2F:repo/releases/:tag` 288 | /// 289 | /// If not specified, the latest available release is used. 290 | pub fn target_version_tag(&mut self, ver: &str) -> &mut Self { 291 | self.target_version = Some(ver.to_owned()); 292 | self 293 | } 294 | 295 | /// Set the target triple that will be downloaded, e.g. `x86_64-unknown-linux-gnu`. 296 | /// 297 | /// If unspecified, the build target of the crate will be used 298 | pub fn target(&mut self, target: &str) -> &mut Self { 299 | self.target = Some(target.to_owned()); 300 | self 301 | } 302 | 303 | /// Set the identifiable token for the asset in case of multiple compatible assets 304 | /// 305 | /// If unspecified, the first asset matching the target will be chosen 306 | pub fn identifier(&mut self, identifier: &str) -> &mut Self { 307 | self.identifier = Some(identifier.to_owned()); 308 | self 309 | } 310 | 311 | /// Set the exe's name. Also sets `bin_path_in_archive` if it hasn't already been set. 312 | /// 313 | /// This method will append the platform specific executable file suffix 314 | /// (see `std::env::consts::EXE_SUFFIX`) to the name if it's missing. 315 | pub fn bin_name(&mut self, name: &str) -> &mut Self { 316 | let raw_bin_name = format!("{}{}", name.trim_end_matches(EXE_SUFFIX), EXE_SUFFIX); 317 | if self.bin_path_in_archive.is_none() { 318 | self.bin_path_in_archive = Some(raw_bin_name.clone()); 319 | } 320 | self.bin_name = Some(raw_bin_name); 321 | self 322 | } 323 | 324 | /// Set the installation path for the new exe, defaults to the current 325 | /// executable's path 326 | pub fn bin_install_path>(&mut self, bin_install_path: A) -> &mut Self { 327 | self.bin_install_path = Some(PathBuf::from(bin_install_path.as_ref())); 328 | self 329 | } 330 | 331 | /// Set the path of the exe inside the release tarball. This is the location 332 | /// of the executable relative to the base of the tar'd directory and is the 333 | /// path that will be copied to the `bin_install_path`. If not specified, this 334 | /// will default to the value of `bin_name`. This only needs to be specified if 335 | /// the path to the binary (from the root of the tarball) is not equal to just 336 | /// the `bin_name`. 337 | /// 338 | /// This also supports variable paths: 339 | /// - `{{ bin }}` is replaced with the value of `bin_name` 340 | /// - `{{ target }}` is replaced with the value of `target` 341 | /// - `{{ version }}` is replaced with the value of `target_version` if set, 342 | /// otherwise the value of the latest available release version is used. 343 | /// 344 | /// # Example 345 | /// 346 | /// For a `myapp` binary with `windows` target and latest release version `1.2.3`, 347 | /// the tarball `myapp.tar.gz` has the contents: 348 | /// 349 | /// ```shell 350 | /// myapp.tar/ 351 | /// |------- windows-1.2.3-bin/ 352 | /// | |--- myapp # <-- executable 353 | /// ``` 354 | /// 355 | /// The path provided should be: 356 | /// 357 | /// ``` 358 | /// # use self_update::backends::gitea::Update; 359 | /// # fn run() -> Result<(), Box<::std::error::Error>> { 360 | /// Update::configure() 361 | /// .bin_path_in_archive("{{ target }}-{{ version }}-bin/{{ bin }}") 362 | /// # .build()?; 363 | /// # Ok(()) 364 | /// # } 365 | /// ``` 366 | pub fn bin_path_in_archive(&mut self, bin_path: &str) -> &mut Self { 367 | self.bin_path_in_archive = Some(bin_path.to_owned()); 368 | self 369 | } 370 | 371 | /// Toggle download progress bar, defaults to `off`. 372 | pub fn show_download_progress(&mut self, show: bool) -> &mut Self { 373 | self.show_download_progress = show; 374 | self 375 | } 376 | 377 | /// Set download progress style. 378 | pub fn set_progress_style( 379 | &mut self, 380 | progress_template: String, 381 | progress_chars: String, 382 | ) -> &mut Self { 383 | self.progress_template = progress_template; 384 | self.progress_chars = progress_chars; 385 | self 386 | } 387 | 388 | /// Toggle update output information, defaults to `true`. 389 | pub fn show_output(&mut self, show: bool) -> &mut Self { 390 | self.show_output = show; 391 | self 392 | } 393 | 394 | /// Toggle download confirmation. Defaults to `false`. 395 | pub fn no_confirm(&mut self, no_confirm: bool) -> &mut Self { 396 | self.no_confirm = no_confirm; 397 | self 398 | } 399 | 400 | /// Set the authorization token, used in requests to the gitea api url 401 | /// 402 | /// This is to support private repos where you need a gitea auth token. 403 | /// **Make sure not to bake the token into your app**; it is recommended 404 | /// you obtain it via another mechanism, such as environment variables 405 | /// or prompting the user for input 406 | pub fn auth_token(&mut self, auth_token: &str) -> &mut Self { 407 | self.auth_token = Some(auth_token.to_owned()); 408 | self 409 | } 410 | 411 | /// Specify a slice of ed25519ph verifying keys to validate a download's authenticy 412 | /// 413 | /// If the feature is activated AND at least one key was provided, a download is verifying. 414 | /// At least one key has to match. 415 | #[cfg(feature = "signatures")] 416 | pub fn verifying_keys( 417 | &mut self, 418 | keys: impl Into>, 419 | ) -> &mut Self { 420 | self.verifying_keys = keys.into(); 421 | self 422 | } 423 | 424 | /// Confirm config and create a ready-to-use `Update` 425 | /// 426 | /// * Errors: 427 | /// * Config - Invalid `Update` configuration 428 | pub fn build(&self) -> Result> { 429 | let bin_install_path = if let Some(v) = &self.bin_install_path { 430 | v.clone() 431 | } else { 432 | env::current_exe()? 433 | }; 434 | 435 | Ok(Box::new(Update { 436 | host: if let Some(ref host) = self.host { 437 | host.to_owned() 438 | } else { 439 | bail!(Error::Config, "`host` required") 440 | }, 441 | repo_owner: if let Some(ref owner) = self.repo_owner { 442 | owner.to_owned() 443 | } else { 444 | bail!(Error::Config, "`repo_owner` required") 445 | }, 446 | repo_name: if let Some(ref name) = self.repo_name { 447 | name.to_owned() 448 | } else { 449 | bail!(Error::Config, "`repo_name` required") 450 | }, 451 | target: self 452 | .target 453 | .as_ref() 454 | .map(|t| t.to_owned()) 455 | .unwrap_or_else(|| get_target().to_owned()), 456 | identifier: self.identifier.clone(), 457 | bin_name: if let Some(ref name) = self.bin_name { 458 | name.to_owned() 459 | } else { 460 | bail!(Error::Config, "`bin_name` required") 461 | }, 462 | bin_install_path, 463 | bin_path_in_archive: if let Some(ref bin_path) = self.bin_path_in_archive { 464 | bin_path.to_owned() 465 | } else { 466 | bail!(Error::Config, "`bin_path_in_archive` required") 467 | }, 468 | current_version: if let Some(ref ver) = self.current_version { 469 | ver.to_owned() 470 | } else { 471 | bail!(Error::Config, "`current_version` required") 472 | }, 473 | target_version: self.target_version.as_ref().map(|v| v.to_owned()), 474 | show_download_progress: self.show_download_progress, 475 | progress_template: self.progress_template.clone(), 476 | progress_chars: self.progress_chars.clone(), 477 | show_output: self.show_output, 478 | no_confirm: self.no_confirm, 479 | auth_token: self.auth_token.clone(), 480 | #[cfg(feature = "signatures")] 481 | verifying_keys: self.verifying_keys.clone(), 482 | })) 483 | } 484 | } 485 | 486 | /// Updates to a specified or latest release distributed via gitea 487 | #[derive(Debug)] 488 | pub struct Update { 489 | host: String, 490 | repo_owner: String, 491 | repo_name: String, 492 | target: String, 493 | identifier: Option, 494 | current_version: String, 495 | target_version: Option, 496 | bin_name: String, 497 | bin_install_path: PathBuf, 498 | bin_path_in_archive: String, 499 | show_download_progress: bool, 500 | show_output: bool, 501 | no_confirm: bool, 502 | progress_template: String, 503 | progress_chars: String, 504 | auth_token: Option, 505 | #[cfg(feature = "signatures")] 506 | verifying_keys: Vec<[u8; zipsign_api::PUBLIC_KEY_LENGTH]>, 507 | } 508 | impl Update { 509 | /// Initialize a new `Update` builder 510 | pub fn configure() -> UpdateBuilder { 511 | UpdateBuilder::new() 512 | } 513 | } 514 | 515 | impl ReleaseUpdate for Update { 516 | fn get_latest_release(&self) -> Result { 517 | set_ssl_vars!(); 518 | let api_url = format!( 519 | "{}/api/v1/repos/{}/{}/releases", 520 | self.host, self.repo_owner, self.repo_name 521 | ); 522 | let client = reqwest::blocking::ClientBuilder::new() 523 | .use_rustls_tls() 524 | .http2_adaptive_window(true) 525 | .build()?; 526 | let resp = client 527 | .get(&api_url) 528 | .headers(self.api_headers(&self.auth_token)?) 529 | .send()?; 530 | if !resp.status().is_success() { 531 | bail!( 532 | Error::Network, 533 | "api request failed with status: {:?} - for: {:?}", 534 | resp.status(), 535 | api_url 536 | ) 537 | } 538 | let json = resp.json::()?; 539 | Release::from_release_gitea(&json[0]) 540 | } 541 | 542 | fn get_latest_releases(&self, current_version: &str) -> Result> { 543 | set_ssl_vars!(); 544 | let api_url = format!( 545 | "{}/api/v1/repos/{}/{}/releases", 546 | self.host, self.repo_owner, self.repo_name 547 | ); 548 | let resp = reqwest::blocking::Client::new() 549 | .get(&api_url) 550 | .headers(self.api_headers(&self.auth_token)?) 551 | .send()?; 552 | if !resp.status().is_success() { 553 | bail!( 554 | Error::Network, 555 | "api request failed with status: {:?} - for: {:?}", 556 | resp.status(), 557 | api_url 558 | ) 559 | } 560 | 561 | let json = resp.json::()?; 562 | json.as_array() 563 | .ok_or_else(|| format_err!(Error::Release, "No releases found")) 564 | .and_then(|releases| { 565 | releases 566 | .iter() 567 | .map(Release::from_release_gitea) 568 | .filter(|r| { 569 | r.as_ref().map_or(false, |r| { 570 | bump_is_greater(current_version, &r.version).unwrap_or(false) 571 | }) 572 | }) 573 | .collect::>>() 574 | }) 575 | } 576 | 577 | fn get_release_version(&self, ver: &str) -> Result { 578 | set_ssl_vars!(); 579 | let api_url = format!( 580 | "{}/api/v1/repos/{}/{}/releases/tags/{}", 581 | self.host, self.repo_owner, self.repo_name, ver 582 | ); 583 | let client = reqwest::blocking::ClientBuilder::new() 584 | .use_rustls_tls() 585 | .http2_adaptive_window(true) 586 | .build()?; 587 | let resp = client 588 | .get(&api_url) 589 | .headers(self.api_headers(&self.auth_token)?) 590 | .send()?; 591 | if !resp.status().is_success() { 592 | bail!( 593 | Error::Network, 594 | "api request failed with status: {:?} - for: {:?}", 595 | resp.status(), 596 | api_url 597 | ) 598 | } 599 | let json = resp.json::()?; 600 | Release::from_release_gitea(&json) 601 | } 602 | 603 | fn current_version(&self) -> String { 604 | self.current_version.to_owned() 605 | } 606 | 607 | fn target(&self) -> String { 608 | self.target.clone() 609 | } 610 | 611 | fn target_version(&self) -> Option { 612 | self.target_version.clone() 613 | } 614 | 615 | fn identifier(&self) -> Option { 616 | self.identifier.clone() 617 | } 618 | 619 | fn bin_name(&self) -> String { 620 | self.bin_name.clone() 621 | } 622 | 623 | fn bin_install_path(&self) -> PathBuf { 624 | self.bin_install_path.clone() 625 | } 626 | 627 | fn bin_path_in_archive(&self) -> String { 628 | self.bin_path_in_archive.clone() 629 | } 630 | 631 | fn show_download_progress(&self) -> bool { 632 | self.show_download_progress 633 | } 634 | 635 | fn show_output(&self) -> bool { 636 | self.show_output 637 | } 638 | 639 | fn no_confirm(&self) -> bool { 640 | self.no_confirm 641 | } 642 | 643 | fn progress_template(&self) -> String { 644 | self.progress_template.to_owned() 645 | } 646 | 647 | fn progress_chars(&self) -> String { 648 | self.progress_chars.to_owned() 649 | } 650 | 651 | fn auth_token(&self) -> Option { 652 | self.auth_token.clone() 653 | } 654 | 655 | fn api_headers(&self, auth_token: &Option) -> Result { 656 | api_headers(auth_token) 657 | } 658 | 659 | #[cfg(feature = "signatures")] 660 | fn verifying_keys(&self) -> &[[u8; zipsign_api::PUBLIC_KEY_LENGTH]] { 661 | &self.verifying_keys 662 | } 663 | } 664 | 665 | impl Default for UpdateBuilder { 666 | fn default() -> Self { 667 | Self { 668 | host: None, 669 | repo_owner: None, 670 | repo_name: None, 671 | target: None, 672 | identifier: None, 673 | bin_name: None, 674 | bin_install_path: None, 675 | bin_path_in_archive: None, 676 | show_download_progress: false, 677 | show_output: true, 678 | no_confirm: false, 679 | current_version: None, 680 | target_version: None, 681 | progress_template: DEFAULT_PROGRESS_TEMPLATE.to_string(), 682 | progress_chars: DEFAULT_PROGRESS_CHARS.to_string(), 683 | auth_token: None, 684 | #[cfg(feature = "signatures")] 685 | verifying_keys: vec![], 686 | } 687 | } 688 | } 689 | 690 | fn api_headers(auth_token: &Option) -> Result { 691 | let mut headers = header::HeaderMap::new(); 692 | headers.insert( 693 | header::USER_AGENT, 694 | "rust-reqwest/self-update" 695 | .parse() 696 | .expect("gitea invalid user-agent"), 697 | ); 698 | 699 | if let Some(token) = auth_token { 700 | headers.insert( 701 | header::AUTHORIZATION, 702 | format!("token {}", token) 703 | .parse() 704 | .map_err(|err| Error::Config(format!("Failed to parse auth token: {}", err)))?, 705 | ); 706 | }; 707 | 708 | Ok(headers) 709 | } 710 | -------------------------------------------------------------------------------- /src/backends/github.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | GitHub releases 3 | */ 4 | use hyper::HeaderMap; 5 | use std::env::{self, consts::EXE_SUFFIX}; 6 | use std::path::{Path, PathBuf}; 7 | 8 | use reqwest::{self, header}; 9 | 10 | use crate::backends::find_rel_next_link; 11 | use crate::version::bump_is_greater; 12 | use crate::{ 13 | errors::*, 14 | get_target, 15 | update::{Release, ReleaseAsset, ReleaseUpdate}, 16 | DEFAULT_PROGRESS_CHARS, DEFAULT_PROGRESS_TEMPLATE, 17 | }; 18 | 19 | impl ReleaseAsset { 20 | /// Parse a release-asset json object 21 | /// 22 | /// Errors: 23 | /// * Missing required name & download-url keys 24 | fn from_asset(asset: &serde_json::Value) -> Result { 25 | let download_url = asset["url"] 26 | .as_str() 27 | .ok_or_else(|| format_err!(Error::Release, "Asset missing `url`"))?; 28 | let name = asset["name"] 29 | .as_str() 30 | .ok_or_else(|| format_err!(Error::Release, "Asset missing `name`"))?; 31 | Ok(ReleaseAsset { 32 | download_url: download_url.to_owned(), 33 | name: name.to_owned(), 34 | }) 35 | } 36 | } 37 | 38 | impl Release { 39 | fn from_release(release: &serde_json::Value) -> Result { 40 | let tag = release["tag_name"] 41 | .as_str() 42 | .ok_or_else(|| format_err!(Error::Release, "Release missing `tag_name`"))?; 43 | let date = release["created_at"] 44 | .as_str() 45 | .ok_or_else(|| format_err!(Error::Release, "Release missing `created_at`"))?; 46 | let name = release["name"].as_str().unwrap_or(tag); 47 | let assets = release["assets"] 48 | .as_array() 49 | .ok_or_else(|| format_err!(Error::Release, "No assets found"))?; 50 | let body = release["body"].as_str().map(String::from); 51 | let assets = assets 52 | .iter() 53 | .map(ReleaseAsset::from_asset) 54 | .collect::>>()?; 55 | Ok(Release { 56 | name: name.to_owned(), 57 | version: tag.trim_start_matches('v').to_owned(), 58 | date: date.to_owned(), 59 | body, 60 | assets, 61 | }) 62 | } 63 | } 64 | 65 | /// `ReleaseList` Builder 66 | #[derive(Clone, Debug)] 67 | pub struct ReleaseListBuilder { 68 | repo_owner: Option, 69 | repo_name: Option, 70 | target: Option, 71 | auth_token: Option, 72 | custom_url: Option, 73 | } 74 | impl ReleaseListBuilder { 75 | /// Set the repo owner, used to build a github api url 76 | pub fn repo_owner(&mut self, owner: &str) -> &mut Self { 77 | self.repo_owner = Some(owner.to_owned()); 78 | self 79 | } 80 | 81 | /// Set the repo name, used to build a github api url 82 | pub fn repo_name(&mut self, name: &str) -> &mut Self { 83 | self.repo_name = Some(name.to_owned()); 84 | self 85 | } 86 | 87 | /// Set the optional arch `target` name, used to filter available releases 88 | pub fn with_target(&mut self, target: &str) -> &mut Self { 89 | self.target = Some(target.to_owned()); 90 | self 91 | } 92 | 93 | /// Set the optional github url, e.g. for a github enterprise installation. 94 | /// The url should provide the path to your API endpoint and end without a trailing slash, 95 | /// for example `https://api.github.com` or `https://github.mycorp.com/api/v3` 96 | pub fn with_url(&mut self, url: &str) -> &mut Self { 97 | self.custom_url = Some(url.to_owned()); 98 | self 99 | } 100 | 101 | /// Set the authorization token, used in requests to the github api url 102 | /// 103 | /// This is to support private repos where you need a GitHub auth token. 104 | /// **Make sure not to bake the token into your app**; it is recommended 105 | /// you obtain it via another mechanism, such as environment variables 106 | /// or prompting the user for input 107 | pub fn auth_token(&mut self, auth_token: &str) -> &mut Self { 108 | self.auth_token = Some(auth_token.to_owned()); 109 | self 110 | } 111 | 112 | /// Verify builder args, returning a `ReleaseList` 113 | pub fn build(&self) -> Result { 114 | Ok(ReleaseList { 115 | repo_owner: if let Some(ref owner) = self.repo_owner { 116 | owner.to_owned() 117 | } else { 118 | bail!(Error::Config, "`repo_owner` required") 119 | }, 120 | repo_name: if let Some(ref name) = self.repo_name { 121 | name.to_owned() 122 | } else { 123 | bail!(Error::Config, "`repo_name` required") 124 | }, 125 | target: self.target.clone(), 126 | auth_token: self.auth_token.clone(), 127 | custom_url: self.custom_url.clone(), 128 | }) 129 | } 130 | } 131 | 132 | /// `ReleaseList` provides a builder api for querying a GitHub repo, 133 | /// returning a `Vec` of available `Release`s 134 | #[derive(Clone, Debug)] 135 | pub struct ReleaseList { 136 | repo_owner: String, 137 | repo_name: String, 138 | target: Option, 139 | auth_token: Option, 140 | custom_url: Option, 141 | } 142 | impl ReleaseList { 143 | /// Initialize a ReleaseListBuilder 144 | pub fn configure() -> ReleaseListBuilder { 145 | ReleaseListBuilder { 146 | repo_owner: None, 147 | repo_name: None, 148 | target: None, 149 | auth_token: None, 150 | custom_url: None, 151 | } 152 | } 153 | 154 | /// Retrieve a list of `Release`s. 155 | /// If specified, filter for those containing a specified `target` 156 | pub fn fetch(self) -> Result> { 157 | set_ssl_vars!(); 158 | let api_url = format!( 159 | "{}/repos/{}/{}/releases", 160 | self.custom_url 161 | .as_ref() 162 | .unwrap_or(&"https://api.github.com".to_string()), 163 | self.repo_owner, 164 | self.repo_name 165 | ); 166 | let releases = self.fetch_releases(&api_url)?; 167 | let releases = match self.target { 168 | None => releases, 169 | Some(ref target) => releases 170 | .into_iter() 171 | .filter(|r| r.has_target_asset(target)) 172 | .collect::>(), 173 | }; 174 | Ok(releases) 175 | } 176 | 177 | fn fetch_releases(&self, url: &str) -> Result> { 178 | let client = reqwest::blocking::ClientBuilder::new() 179 | .use_rustls_tls() 180 | .http2_adaptive_window(true) 181 | .build()?; 182 | let resp = client 183 | .get(url) 184 | .headers(api_headers(&self.auth_token)?) 185 | .query(&[("per_page", "100")]) 186 | .send()?; 187 | if !resp.status().is_success() { 188 | bail!( 189 | Error::Network, 190 | "api request failed with status: {:?} - for: {:?}", 191 | resp.status(), 192 | url 193 | ) 194 | } 195 | let headers = resp.headers().clone(); 196 | 197 | let releases = resp.json::()?; 198 | let releases = releases 199 | .as_array() 200 | .ok_or_else(|| format_err!(Error::Release, "No releases found"))?; 201 | let mut releases = releases 202 | .iter() 203 | .map(Release::from_release) 204 | .collect::>>()?; 205 | 206 | // handle paged responses containing `Link` header: 207 | // `Link: ; rel="next"` 208 | let links = headers.get_all(reqwest::header::LINK); 209 | 210 | let next_link = links 211 | .iter() 212 | .filter_map(|link| { 213 | if let Ok(link) = link.to_str() { 214 | find_rel_next_link(link) 215 | } else { 216 | None 217 | } 218 | }) 219 | .next(); 220 | 221 | Ok(match next_link { 222 | None => releases, 223 | Some(link) => { 224 | releases.extend(self.fetch_releases(link)?); 225 | releases 226 | } 227 | }) 228 | } 229 | } 230 | 231 | /// `github::Update` builder 232 | /// 233 | /// Configure download and installation from 234 | /// `https://api.github.com/repos///releases/latest` 235 | #[derive(Debug)] 236 | pub struct UpdateBuilder { 237 | repo_owner: Option, 238 | repo_name: Option, 239 | target: Option, 240 | identifier: Option, 241 | bin_name: Option, 242 | bin_install_path: Option, 243 | bin_path_in_archive: Option, 244 | show_download_progress: bool, 245 | show_output: bool, 246 | no_confirm: bool, 247 | current_version: Option, 248 | target_version: Option, 249 | progress_template: String, 250 | progress_chars: String, 251 | auth_token: Option, 252 | custom_url: Option, 253 | #[cfg(feature = "signatures")] 254 | verifying_keys: Vec<[u8; zipsign_api::PUBLIC_KEY_LENGTH]>, 255 | } 256 | 257 | impl UpdateBuilder { 258 | /// Initialize a new builder 259 | pub fn new() -> Self { 260 | Default::default() 261 | } 262 | 263 | /// Set the repo owner, used to build a github api url 264 | pub fn repo_owner(&mut self, owner: &str) -> &mut Self { 265 | self.repo_owner = Some(owner.to_owned()); 266 | self 267 | } 268 | 269 | /// Set the repo name, used to build a github api url 270 | pub fn repo_name(&mut self, name: &str) -> &mut Self { 271 | self.repo_name = Some(name.to_owned()); 272 | self 273 | } 274 | 275 | /// Set the optional github url, e.g. for a github enterprise installation. 276 | /// The url should provide the path to your API endpoint and end without a trailing slash, 277 | /// for example `https://api.github.com` or `https://github.mycorp.com/api/v3` 278 | pub fn with_url(&mut self, url: &str) -> &mut Self { 279 | self.custom_url = Some(url.to_owned()); 280 | self 281 | } 282 | 283 | /// Set the current app version, used to compare against the latest available version. 284 | /// The `cargo_crate_version!` macro can be used to pull the version from your `Cargo.toml` 285 | pub fn current_version(&mut self, ver: &str) -> &mut Self { 286 | self.current_version = Some(ver.to_owned()); 287 | self 288 | } 289 | 290 | /// Set the target version tag to update to. This will be used to search for a release 291 | /// by tag name: 292 | /// `/repos/:owner/:repo/releases/tags/:tag` 293 | /// 294 | /// If not specified, the latest available release is used. 295 | pub fn target_version_tag(&mut self, ver: &str) -> &mut Self { 296 | self.target_version = Some(ver.to_owned()); 297 | self 298 | } 299 | 300 | /// Set the target triple that will be downloaded, e.g. `x86_64-unknown-linux-gnu`. 301 | /// 302 | /// If unspecified, the build target of the crate will be used 303 | pub fn target(&mut self, target: &str) -> &mut Self { 304 | self.target = Some(target.to_owned()); 305 | self 306 | } 307 | 308 | /// Set the identifiable token for the asset in case of multiple compatible assets 309 | /// 310 | /// If unspecified, the first asset matching the target will be chosen 311 | pub fn identifier(&mut self, identifier: &str) -> &mut Self { 312 | self.identifier = Some(identifier.to_owned()); 313 | self 314 | } 315 | 316 | /// Set the exe's name. Also sets `bin_path_in_archive` if it hasn't already been set. 317 | /// 318 | /// This method will append the platform specific executable file suffix 319 | /// (see `std::env::consts::EXE_SUFFIX`) to the name if it's missing. 320 | pub fn bin_name(&mut self, name: &str) -> &mut Self { 321 | let raw_bin_name = format!("{}{}", name.trim_end_matches(EXE_SUFFIX), EXE_SUFFIX); 322 | if self.bin_path_in_archive.is_none() { 323 | self.bin_path_in_archive = Some(raw_bin_name.clone()); 324 | } 325 | self.bin_name = Some(raw_bin_name); 326 | self 327 | } 328 | 329 | /// Set the installation path for the new exe, defaults to the current 330 | /// executable's path 331 | pub fn bin_install_path>(&mut self, bin_install_path: A) -> &mut Self { 332 | self.bin_install_path = Some(PathBuf::from(bin_install_path.as_ref())); 333 | self 334 | } 335 | 336 | /// Set the path of the exe inside the release tarball. This is the location 337 | /// of the executable relative to the base of the tar'd directory and is the 338 | /// path that will be copied to the `bin_install_path`. If not specified, this 339 | /// will default to the value of `bin_name`. This only needs to be specified if 340 | /// the path to the binary (from the root of the tarball) is not equal to just 341 | /// the `bin_name`. 342 | /// 343 | /// This also supports variable paths: 344 | /// - `{{ bin }}` is replaced with the value of `bin_name` 345 | /// - `{{ target }}` is replaced with the value of `target` 346 | /// - `{{ version }}` is replaced with the value of `target_version` if set, 347 | /// otherwise the value of the latest available release version is used. 348 | /// 349 | /// # Example 350 | /// 351 | /// For a `myapp` binary with `windows` target and latest release version `1.2.3`, 352 | /// the tarball `myapp.tar.gz` has the contents: 353 | /// 354 | /// ```shell 355 | /// myapp.tar/ 356 | /// |------- windows-1.2.3-bin/ 357 | /// | |--- myapp # <-- executable 358 | /// ``` 359 | /// 360 | /// The path provided should be: 361 | /// 362 | /// ``` 363 | /// # use self_update::backends::github::Update; 364 | /// # fn run() -> Result<(), Box<::std::error::Error>> { 365 | /// Update::configure() 366 | /// .bin_path_in_archive("{{ target }}-{{ version }}-bin/{{ bin }}") 367 | /// # .build()?; 368 | /// # Ok(()) 369 | /// # } 370 | /// ``` 371 | pub fn bin_path_in_archive(&mut self, bin_path: &str) -> &mut Self { 372 | self.bin_path_in_archive = Some(bin_path.to_owned()); 373 | self 374 | } 375 | 376 | /// Toggle download progress bar, defaults to `off`. 377 | pub fn show_download_progress(&mut self, show: bool) -> &mut Self { 378 | self.show_download_progress = show; 379 | self 380 | } 381 | 382 | /// Set download progress style. 383 | pub fn set_progress_style( 384 | &mut self, 385 | progress_template: String, 386 | progress_chars: String, 387 | ) -> &mut Self { 388 | self.progress_template = progress_template; 389 | self.progress_chars = progress_chars; 390 | self 391 | } 392 | 393 | /// Toggle update output information, defaults to `true`. 394 | pub fn show_output(&mut self, show: bool) -> &mut Self { 395 | self.show_output = show; 396 | self 397 | } 398 | 399 | /// Toggle download confirmation. Defaults to `false`. 400 | pub fn no_confirm(&mut self, no_confirm: bool) -> &mut Self { 401 | self.no_confirm = no_confirm; 402 | self 403 | } 404 | 405 | /// Set the authorization token, used in requests to the github api url 406 | /// 407 | /// This is to support private repos where you need a GitHub auth token. 408 | /// **Make sure not to bake the token into your app**; it is recommended 409 | /// you obtain it via another mechanism, such as environment variables 410 | /// or prompting the user for input 411 | pub fn auth_token(&mut self, auth_token: &str) -> &mut Self { 412 | self.auth_token = Some(auth_token.to_owned()); 413 | self 414 | } 415 | 416 | /// Specify a slice of ed25519ph verifying keys to validate a download's authenticy 417 | /// 418 | /// If the feature is activated AND at least one key was provided, a download is verifying. 419 | /// At least one key has to match. 420 | #[cfg(feature = "signatures")] 421 | pub fn verifying_keys( 422 | &mut self, 423 | keys: impl Into>, 424 | ) -> &mut Self { 425 | self.verifying_keys = keys.into(); 426 | self 427 | } 428 | 429 | /// Confirm config and create a ready-to-use `Update` 430 | /// 431 | /// * Errors: 432 | /// * Config - Invalid `Update` configuration 433 | pub fn build(&self) -> Result> { 434 | let bin_install_path = if let Some(v) = &self.bin_install_path { 435 | v.clone() 436 | } else { 437 | env::current_exe()? 438 | }; 439 | 440 | Ok(Box::new(Update { 441 | repo_owner: if let Some(ref owner) = self.repo_owner { 442 | owner.to_owned() 443 | } else { 444 | bail!(Error::Config, "`repo_owner` required") 445 | }, 446 | repo_name: if let Some(ref name) = self.repo_name { 447 | name.to_owned() 448 | } else { 449 | bail!(Error::Config, "`repo_name` required") 450 | }, 451 | target: self 452 | .target 453 | .as_ref() 454 | .map(|t| t.to_owned()) 455 | .unwrap_or_else(|| get_target().to_owned()), 456 | identifier: self.identifier.clone(), 457 | bin_name: if let Some(ref name) = self.bin_name { 458 | name.to_owned() 459 | } else { 460 | bail!(Error::Config, "`bin_name` required") 461 | }, 462 | bin_install_path, 463 | bin_path_in_archive: if let Some(ref bin_path) = self.bin_path_in_archive { 464 | bin_path.to_owned() 465 | } else { 466 | bail!(Error::Config, "`bin_path_in_archive` required") 467 | }, 468 | current_version: if let Some(ref ver) = self.current_version { 469 | ver.to_owned() 470 | } else { 471 | bail!(Error::Config, "`current_version` required") 472 | }, 473 | target_version: self.target_version.as_ref().map(|v| v.to_owned()), 474 | show_download_progress: self.show_download_progress, 475 | progress_template: self.progress_template.clone(), 476 | progress_chars: self.progress_chars.clone(), 477 | show_output: self.show_output, 478 | no_confirm: self.no_confirm, 479 | auth_token: self.auth_token.clone(), 480 | custom_url: self.custom_url.clone(), 481 | #[cfg(feature = "signatures")] 482 | verifying_keys: self.verifying_keys.clone(), 483 | })) 484 | } 485 | } 486 | 487 | /// Updates to a specified or latest release distributed via GitHub 488 | #[derive(Debug)] 489 | pub struct Update { 490 | repo_owner: String, 491 | repo_name: String, 492 | target: String, 493 | identifier: Option, 494 | current_version: String, 495 | target_version: Option, 496 | bin_name: String, 497 | bin_install_path: PathBuf, 498 | bin_path_in_archive: String, 499 | show_download_progress: bool, 500 | show_output: bool, 501 | no_confirm: bool, 502 | progress_template: String, 503 | progress_chars: String, 504 | auth_token: Option, 505 | custom_url: Option, 506 | #[cfg(feature = "signatures")] 507 | verifying_keys: Vec<[u8; zipsign_api::PUBLIC_KEY_LENGTH]>, 508 | } 509 | impl Update { 510 | /// Initialize a new `Update` builder 511 | pub fn configure() -> UpdateBuilder { 512 | UpdateBuilder::new() 513 | } 514 | } 515 | 516 | impl ReleaseUpdate for Update { 517 | fn get_latest_release(&self) -> Result { 518 | set_ssl_vars!(); 519 | let api_url = format!( 520 | "{}/repos/{}/{}/releases/latest", 521 | self.custom_url 522 | .as_ref() 523 | .unwrap_or(&"https://api.github.com".to_string()), 524 | self.repo_owner, 525 | self.repo_name 526 | ); 527 | let client = reqwest::blocking::ClientBuilder::new() 528 | .use_rustls_tls() 529 | .http2_adaptive_window(true) 530 | .build()?; 531 | let resp = client 532 | .get(&api_url) 533 | .headers(api_headers(&self.auth_token)?) 534 | .send()?; 535 | if !resp.status().is_success() { 536 | bail!( 537 | Error::Network, 538 | "api request failed with status: {:?} - for: {:?}", 539 | resp.status(), 540 | api_url 541 | ) 542 | } 543 | let json = resp.json::()?; 544 | Release::from_release(&json) 545 | } 546 | 547 | fn get_latest_releases(&self, current_version: &str) -> Result> { 548 | set_ssl_vars!(); 549 | let api_url = format!( 550 | "{}/repos/{}/{}/releases", 551 | self.custom_url 552 | .as_ref() 553 | .unwrap_or(&"https://api.github.com".to_string()), 554 | self.repo_owner, 555 | self.repo_name 556 | ); 557 | let resp = reqwest::blocking::Client::new() 558 | .get(&api_url) 559 | .headers(api_headers(&self.auth_token)?) 560 | .send()?; 561 | if !resp.status().is_success() { 562 | bail!( 563 | Error::Network, 564 | "api request failed with status: {:?} - for: {:?}", 565 | resp.status(), 566 | api_url 567 | ) 568 | } 569 | 570 | let json = resp.json::()?; 571 | json.as_array() 572 | .ok_or_else(|| format_err!(Error::Release, "No releases found")) 573 | .and_then(|releases| { 574 | releases 575 | .iter() 576 | .map(Release::from_release) 577 | .filter(|r| { 578 | r.as_ref().map_or(false, |r| { 579 | bump_is_greater(current_version, &r.version).unwrap_or(false) 580 | }) 581 | }) 582 | .collect::>>() 583 | }) 584 | } 585 | 586 | fn get_release_version(&self, ver: &str) -> Result { 587 | set_ssl_vars!(); 588 | let api_url = format!( 589 | "{}/repos/{}/{}/releases/tags/{}", 590 | self.custom_url 591 | .as_ref() 592 | .unwrap_or(&"https://api.github.com".to_string()), 593 | self.repo_owner, 594 | self.repo_name, 595 | ver 596 | ); 597 | let client = reqwest::blocking::ClientBuilder::new() 598 | .use_rustls_tls() 599 | .http2_adaptive_window(true) 600 | .build()?; 601 | let resp = client 602 | .get(&api_url) 603 | .headers(api_headers(&self.auth_token)?) 604 | .send()?; 605 | if !resp.status().is_success() { 606 | bail!( 607 | Error::Network, 608 | "api request failed with status: {:?} - for: {:?}", 609 | resp.status(), 610 | api_url 611 | ) 612 | } 613 | let json = resp.json::()?; 614 | Release::from_release(&json) 615 | } 616 | 617 | fn current_version(&self) -> String { 618 | self.current_version.to_owned() 619 | } 620 | 621 | fn target(&self) -> String { 622 | self.target.clone() 623 | } 624 | 625 | fn target_version(&self) -> Option { 626 | self.target_version.clone() 627 | } 628 | 629 | fn identifier(&self) -> Option { 630 | self.identifier.clone() 631 | } 632 | 633 | fn bin_name(&self) -> String { 634 | self.bin_name.clone() 635 | } 636 | 637 | fn bin_install_path(&self) -> PathBuf { 638 | self.bin_install_path.clone() 639 | } 640 | 641 | fn bin_path_in_archive(&self) -> String { 642 | self.bin_path_in_archive.clone() 643 | } 644 | 645 | fn show_download_progress(&self) -> bool { 646 | self.show_download_progress 647 | } 648 | 649 | fn show_output(&self) -> bool { 650 | self.show_output 651 | } 652 | 653 | fn no_confirm(&self) -> bool { 654 | self.no_confirm 655 | } 656 | 657 | fn progress_template(&self) -> String { 658 | self.progress_template.to_owned() 659 | } 660 | 661 | fn progress_chars(&self) -> String { 662 | self.progress_chars.to_owned() 663 | } 664 | 665 | fn auth_token(&self) -> Option { 666 | self.auth_token.clone() 667 | } 668 | 669 | fn api_headers(&self, auth_token: &Option) -> Result { 670 | api_headers(auth_token) 671 | } 672 | 673 | #[cfg(feature = "signatures")] 674 | fn verifying_keys(&self) -> &[[u8; zipsign_api::PUBLIC_KEY_LENGTH]] { 675 | &self.verifying_keys 676 | } 677 | } 678 | 679 | impl Default for UpdateBuilder { 680 | fn default() -> Self { 681 | Self { 682 | repo_owner: None, 683 | repo_name: None, 684 | target: None, 685 | identifier: None, 686 | bin_name: None, 687 | bin_install_path: None, 688 | bin_path_in_archive: None, 689 | show_download_progress: false, 690 | show_output: true, 691 | no_confirm: false, 692 | current_version: None, 693 | target_version: None, 694 | progress_template: DEFAULT_PROGRESS_TEMPLATE.to_string(), 695 | progress_chars: DEFAULT_PROGRESS_CHARS.to_string(), 696 | auth_token: None, 697 | custom_url: None, 698 | #[cfg(feature = "signatures")] 699 | verifying_keys: vec![], 700 | } 701 | } 702 | } 703 | 704 | fn api_headers(auth_token: &Option) -> Result { 705 | let mut headers = header::HeaderMap::new(); 706 | headers.insert( 707 | header::USER_AGENT, 708 | "rust-reqwest/self-update" 709 | .parse() 710 | .expect("github invalid user-agent"), 711 | ); 712 | 713 | if let Some(token) = auth_token { 714 | headers.insert( 715 | header::AUTHORIZATION, 716 | format!("token {}", token) 717 | .parse() 718 | .map_err(|err| Error::Config(format!("Failed to parse auth token: {}", err)))?, 719 | ); 720 | }; 721 | 722 | Ok(headers) 723 | } 724 | -------------------------------------------------------------------------------- /src/backends/gitlab.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | Gitlab releases 3 | */ 4 | use std::env::{self, consts::EXE_SUFFIX}; 5 | use std::path::{Path, PathBuf}; 6 | 7 | use reqwest::{self, header}; 8 | 9 | use crate::backends::find_rel_next_link; 10 | use crate::version::bump_is_greater; 11 | use crate::{ 12 | errors::*, 13 | get_target, 14 | update::{Release, ReleaseAsset, ReleaseUpdate}, 15 | DEFAULT_PROGRESS_CHARS, DEFAULT_PROGRESS_TEMPLATE, 16 | }; 17 | 18 | impl ReleaseAsset { 19 | /// Parse a release-asset json object 20 | /// 21 | /// Errors: 22 | /// * Missing required name & download-url keys 23 | fn from_asset_gitlab(asset: &serde_json::Value) -> Result { 24 | let download_url = asset["url"] 25 | .as_str() 26 | .ok_or_else(|| format_err!(Error::Release, "Asset missing `url`"))?; 27 | let name = asset["name"] 28 | .as_str() 29 | .ok_or_else(|| format_err!(Error::Release, "Asset missing `name`"))?; 30 | Ok(ReleaseAsset { 31 | download_url: download_url.to_owned(), 32 | name: name.to_owned(), 33 | }) 34 | } 35 | } 36 | 37 | impl Release { 38 | fn from_release_gitlab(release: &serde_json::Value) -> Result { 39 | let tag = release["tag_name"] 40 | .as_str() 41 | .ok_or_else(|| format_err!(Error::Release, "Release missing `tag_name`"))?; 42 | let date = release["created_at"] 43 | .as_str() 44 | .ok_or_else(|| format_err!(Error::Release, "Release missing `created_at`"))?; 45 | let name = release["name"].as_str().unwrap_or(tag); 46 | let assets = release["assets"]["links"] 47 | .as_array() 48 | .ok_or_else(|| format_err!(Error::Release, "No assets found"))?; 49 | let body = release["description"].as_str().map(String::from); 50 | let assets = assets 51 | .iter() 52 | .map(ReleaseAsset::from_asset_gitlab) 53 | .collect::>>()?; 54 | Ok(Release { 55 | name: name.to_owned(), 56 | version: tag.trim_start_matches('v').to_owned(), 57 | date: date.to_owned(), 58 | body, 59 | assets, 60 | }) 61 | } 62 | } 63 | 64 | /// `ReleaseList` Builder 65 | #[derive(Clone, Debug)] 66 | pub struct ReleaseListBuilder { 67 | host: String, 68 | repo_owner: Option, 69 | repo_name: Option, 70 | target: Option, 71 | auth_token: Option, 72 | } 73 | impl ReleaseListBuilder { 74 | /// Set the gitlab `host` url 75 | pub fn with_host(&mut self, host: &str) -> &mut Self { 76 | self.host = host.to_owned(); 77 | self 78 | } 79 | 80 | /// Set the repo owner, used to build a gitlab api url 81 | pub fn repo_owner(&mut self, owner: &str) -> &mut Self { 82 | self.repo_owner = Some(owner.to_owned()); 83 | self 84 | } 85 | 86 | /// Set the repo name, used to build a gitlab api url 87 | pub fn repo_name(&mut self, name: &str) -> &mut Self { 88 | self.repo_name = Some(name.to_owned()); 89 | self 90 | } 91 | 92 | /// Set the optional arch `target` name, used to filter available releases 93 | pub fn with_target(&mut self, target: &str) -> &mut Self { 94 | self.target = Some(target.to_owned()); 95 | self 96 | } 97 | 98 | /// Set the authorization token, used in requests to the gitlab api url 99 | /// 100 | /// This is to support private repos where you need a Gitlab auth token. 101 | /// **Make sure not to bake the token into your app**; it is recommended 102 | /// you obtain it via another mechanism, such as environment variables 103 | /// or prompting the user for input 104 | pub fn auth_token(&mut self, auth_token: &str) -> &mut Self { 105 | self.auth_token = Some(auth_token.to_owned()); 106 | self 107 | } 108 | 109 | /// Verify builder args, returning a `ReleaseList` 110 | pub fn build(&self) -> Result { 111 | Ok(ReleaseList { 112 | host: self.host.clone(), 113 | repo_owner: if let Some(ref owner) = self.repo_owner { 114 | owner.to_owned() 115 | } else { 116 | bail!(Error::Config, "`repo_owner` required") 117 | }, 118 | repo_name: if let Some(ref name) = self.repo_name { 119 | name.to_owned() 120 | } else { 121 | bail!(Error::Config, "`repo_name` required") 122 | }, 123 | target: self.target.clone(), 124 | auth_token: self.auth_token.clone(), 125 | }) 126 | } 127 | } 128 | 129 | /// `ReleaseList` provides a builder api for querying a Gitlab repo, 130 | /// returning a `Vec` of available `Release`s 131 | #[derive(Clone, Debug)] 132 | pub struct ReleaseList { 133 | host: String, 134 | repo_owner: String, 135 | repo_name: String, 136 | target: Option, 137 | auth_token: Option, 138 | } 139 | impl ReleaseList { 140 | /// Initialize a ReleaseListBuilder 141 | pub fn configure() -> ReleaseListBuilder { 142 | ReleaseListBuilder { 143 | host: String::from("https://gitlab.com"), 144 | repo_owner: None, 145 | repo_name: None, 146 | target: None, 147 | auth_token: None, 148 | } 149 | } 150 | 151 | /// Retrieve a list of `Release`s. 152 | /// If specified, filter for those containing a specified `target` 153 | pub fn fetch(self) -> Result> { 154 | set_ssl_vars!(); 155 | let api_url = format!( 156 | "{}/api/v4/projects/{}%2F{}/releases", 157 | self.host, 158 | urlencoding::encode(&self.repo_owner), 159 | self.repo_name 160 | ); 161 | let releases = self.fetch_releases(&api_url)?; 162 | let releases = match self.target { 163 | None => releases, 164 | Some(ref target) => releases 165 | .into_iter() 166 | .filter(|r| r.has_target_asset(target)) 167 | .collect::>(), 168 | }; 169 | Ok(releases) 170 | } 171 | 172 | fn fetch_releases(&self, url: &str) -> Result> { 173 | let client = reqwest::blocking::ClientBuilder::new() 174 | .use_rustls_tls() 175 | .http2_adaptive_window(true) 176 | .build()?; 177 | let resp = client 178 | .get(url) 179 | .headers(api_headers(&self.auth_token)?) 180 | .send()?; 181 | if !resp.status().is_success() { 182 | bail!( 183 | Error::Network, 184 | "api request failed with status: {:?} - for: {:?}", 185 | resp.status(), 186 | url 187 | ) 188 | } 189 | let headers = resp.headers().clone(); 190 | 191 | let releases = resp.json::()?; 192 | let releases = releases 193 | .as_array() 194 | .ok_or_else(|| format_err!(Error::Release, "No releases found"))?; 195 | let mut releases = releases 196 | .iter() 197 | .map(Release::from_release_gitlab) 198 | .collect::>>()?; 199 | 200 | // handle paged responses containing `Link` header: 201 | // `Link: ; rel="next"` 202 | let links = headers.get_all(reqwest::header::LINK); 203 | 204 | let next_link = links 205 | .iter() 206 | .filter_map(|link| { 207 | if let Ok(link) = link.to_str() { 208 | find_rel_next_link(link) 209 | } else { 210 | None 211 | } 212 | }) 213 | .next(); 214 | 215 | Ok(match next_link { 216 | None => releases, 217 | Some(link) => { 218 | releases.extend(self.fetch_releases(link)?); 219 | releases 220 | } 221 | }) 222 | } 223 | } 224 | 225 | /// `gitlab::Update` builder 226 | /// 227 | /// Configure download and installation from 228 | /// `https://gitlab.com/api/v4/projects/%2F/releases` 229 | #[derive(Debug)] 230 | pub struct UpdateBuilder { 231 | host: String, 232 | repo_owner: Option, 233 | repo_name: Option, 234 | target: Option, 235 | bin_name: Option, 236 | bin_install_path: Option, 237 | bin_path_in_archive: Option, 238 | show_download_progress: bool, 239 | show_output: bool, 240 | no_confirm: bool, 241 | current_version: Option, 242 | target_version: Option, 243 | progress_template: String, 244 | progress_chars: String, 245 | auth_token: Option, 246 | #[cfg(feature = "signatures")] 247 | verifying_keys: Vec<[u8; zipsign_api::PUBLIC_KEY_LENGTH]>, 248 | } 249 | 250 | impl UpdateBuilder { 251 | /// Initialize a new builder 252 | pub fn new() -> Self { 253 | Default::default() 254 | } 255 | 256 | /// Set the gitlab `host` url 257 | pub fn with_host(&mut self, host: &str) -> &mut Self { 258 | self.host = host.to_owned(); 259 | self 260 | } 261 | 262 | /// Set the repo owner, used to build a gitlab api url 263 | pub fn repo_owner(&mut self, owner: &str) -> &mut Self { 264 | self.repo_owner = Some(owner.to_owned()); 265 | self 266 | } 267 | 268 | /// Set the repo name, used to build a gitlab api url 269 | pub fn repo_name(&mut self, name: &str) -> &mut Self { 270 | self.repo_name = Some(name.to_owned()); 271 | self 272 | } 273 | 274 | /// Set the current app version, used to compare against the latest available version. 275 | /// The `cargo_crate_version!` macro can be used to pull the version from your `Cargo.toml` 276 | pub fn current_version(&mut self, ver: &str) -> &mut Self { 277 | self.current_version = Some(ver.to_owned()); 278 | self 279 | } 280 | 281 | /// Set the target version tag to update to. This will be used to search for a release 282 | /// by tag name: 283 | /// `/repos/:owner%2F:repo/releases/:tag` 284 | /// 285 | /// If not specified, the latest available release is used. 286 | pub fn target_version_tag(&mut self, ver: &str) -> &mut Self { 287 | self.target_version = Some(ver.to_owned()); 288 | self 289 | } 290 | 291 | /// Set the target triple that will be downloaded, e.g. `x86_64-unknown-linux-gnu`. 292 | /// 293 | /// If unspecified, the build target of the crate will be used 294 | pub fn target(&mut self, target: &str) -> &mut Self { 295 | self.target = Some(target.to_owned()); 296 | self 297 | } 298 | 299 | /// Set the exe's name. Also sets `bin_path_in_archive` if it hasn't already been set. 300 | /// 301 | /// This method will append the platform specific executable file suffix 302 | /// (see `std::env::consts::EXE_SUFFIX`) to the name if it's missing. 303 | pub fn bin_name(&mut self, name: &str) -> &mut Self { 304 | let raw_bin_name = format!("{}{}", name.trim_end_matches(EXE_SUFFIX), EXE_SUFFIX); 305 | if self.bin_path_in_archive.is_none() { 306 | self.bin_path_in_archive = Some(raw_bin_name.to_owned()); 307 | } 308 | self.bin_name = Some(raw_bin_name); 309 | self 310 | } 311 | 312 | /// Set the installation path for the new exe, defaults to the current 313 | /// executable's path 314 | pub fn bin_install_path>(&mut self, bin_install_path: A) -> &mut Self { 315 | self.bin_install_path = Some(PathBuf::from(bin_install_path.as_ref())); 316 | self 317 | } 318 | 319 | /// Set the path of the exe inside the release tarball. This is the location 320 | /// of the executable relative to the base of the tar'd directory and is the 321 | /// path that will be copied to the `bin_install_path`. If not specified, this 322 | /// will default to the value of `bin_name`. This only needs to be specified if 323 | /// the path to the binary (from the root of the tarball) is not equal to just 324 | /// the `bin_name`. 325 | /// 326 | /// This also supports variable paths: 327 | /// - `{{ bin }}` is replaced with the value of `bin_name` 328 | /// - `{{ target }}` is replaced with the value of `target` 329 | /// - `{{ version }}` is replaced with the value of `target_version` if set, 330 | /// otherwise the value of the latest available release version is used. 331 | /// 332 | /// # Example 333 | /// 334 | /// For a `myapp` binary with `windows` target and latest release version `1.2.3`, 335 | /// the tarball `myapp.tar.gz` has the contents: 336 | /// 337 | /// ```shell 338 | /// myapp.tar/ 339 | /// |------- windows-1.2.3-bin/ 340 | /// | |--- myapp # <-- executable 341 | /// ``` 342 | /// 343 | /// The path provided should be: 344 | /// 345 | /// ``` 346 | /// # use self_update::backends::gitlab::Update; 347 | /// # fn run() -> Result<(), Box<::std::error::Error>> { 348 | /// Update::configure() 349 | /// .bin_path_in_archive("{{ target }}-{{ version }}-bin/{{ bin }}") 350 | /// # .build()?; 351 | /// # Ok(()) 352 | /// # } 353 | /// ``` 354 | pub fn bin_path_in_archive(&mut self, bin_path: &str) -> &mut Self { 355 | self.bin_path_in_archive = Some(bin_path.to_owned()); 356 | self 357 | } 358 | 359 | /// Toggle download progress bar, defaults to `off`. 360 | pub fn show_download_progress(&mut self, show: bool) -> &mut Self { 361 | self.show_download_progress = show; 362 | self 363 | } 364 | 365 | /// Set download progress style. 366 | pub fn set_progress_style( 367 | &mut self, 368 | progress_template: String, 369 | progress_chars: String, 370 | ) -> &mut Self { 371 | self.progress_template = progress_template; 372 | self.progress_chars = progress_chars; 373 | self 374 | } 375 | 376 | /// Toggle update output information, defaults to `true`. 377 | pub fn show_output(&mut self, show: bool) -> &mut Self { 378 | self.show_output = show; 379 | self 380 | } 381 | 382 | /// Toggle download confirmation. Defaults to `false`. 383 | pub fn no_confirm(&mut self, no_confirm: bool) -> &mut Self { 384 | self.no_confirm = no_confirm; 385 | self 386 | } 387 | 388 | /// Set the authorization token, used in requests to the gitlab api url 389 | /// 390 | /// This is to support private repos where you need a Gitlab auth token. 391 | /// **Make sure not to bake the token into your app**; it is recommended 392 | /// you obtain it via another mechanism, such as environment variables 393 | /// or prompting the user for input 394 | pub fn auth_token(&mut self, auth_token: &str) -> &mut Self { 395 | self.auth_token = Some(auth_token.to_owned()); 396 | self 397 | } 398 | 399 | /// Specify a slice of ed25519ph verifying keys to validate a download's authenticy 400 | /// 401 | /// If the feature is activated AND at least one key was provided, a download is verifying. 402 | /// At least one key has to match. 403 | #[cfg(feature = "signatures")] 404 | pub fn verifying_keys( 405 | &mut self, 406 | keys: impl Into>, 407 | ) -> &mut Self { 408 | self.verifying_keys = keys.into(); 409 | self 410 | } 411 | 412 | /// Confirm config and create a ready-to-use `Update` 413 | /// 414 | /// * Errors: 415 | /// * Config - Invalid `Update` configuration 416 | pub fn build(&self) -> Result> { 417 | let bin_install_path = if let Some(v) = &self.bin_install_path { 418 | v.clone() 419 | } else { 420 | env::current_exe()? 421 | }; 422 | 423 | Ok(Box::new(Update { 424 | host: self.host.to_owned(), 425 | repo_owner: if let Some(ref owner) = self.repo_owner { 426 | owner.to_owned() 427 | } else { 428 | bail!(Error::Config, "`repo_owner` required") 429 | }, 430 | repo_name: if let Some(ref name) = self.repo_name { 431 | name.to_owned() 432 | } else { 433 | bail!(Error::Config, "`repo_name` required") 434 | }, 435 | target: self 436 | .target 437 | .as_ref() 438 | .map(|t| t.to_owned()) 439 | .unwrap_or_else(|| get_target().to_owned()), 440 | bin_name: if let Some(ref name) = self.bin_name { 441 | name.to_owned() 442 | } else { 443 | bail!(Error::Config, "`bin_name` required") 444 | }, 445 | bin_install_path, 446 | bin_path_in_archive: if let Some(ref bin_path) = self.bin_path_in_archive { 447 | bin_path.to_owned() 448 | } else { 449 | bail!(Error::Config, "`bin_path_in_archive` required") 450 | }, 451 | current_version: if let Some(ref ver) = self.current_version { 452 | ver.to_owned() 453 | } else { 454 | bail!(Error::Config, "`current_version` required") 455 | }, 456 | target_version: self.target_version.as_ref().map(|v| v.to_owned()), 457 | show_download_progress: self.show_download_progress, 458 | progress_template: self.progress_template.clone(), 459 | progress_chars: self.progress_chars.clone(), 460 | show_output: self.show_output, 461 | no_confirm: self.no_confirm, 462 | auth_token: self.auth_token.clone(), 463 | #[cfg(feature = "signatures")] 464 | verifying_keys: self.verifying_keys.clone(), 465 | })) 466 | } 467 | } 468 | 469 | /// Updates to a specified or latest release distributed via Gitlab 470 | #[derive(Debug)] 471 | pub struct Update { 472 | host: String, 473 | repo_owner: String, 474 | repo_name: String, 475 | target: String, 476 | current_version: String, 477 | target_version: Option, 478 | bin_name: String, 479 | bin_install_path: PathBuf, 480 | bin_path_in_archive: String, 481 | show_download_progress: bool, 482 | show_output: bool, 483 | no_confirm: bool, 484 | progress_template: String, 485 | progress_chars: String, 486 | auth_token: Option, 487 | #[cfg(feature = "signatures")] 488 | verifying_keys: Vec<[u8; zipsign_api::PUBLIC_KEY_LENGTH]>, 489 | } 490 | impl Update { 491 | /// Initialize a new `Update` builder 492 | pub fn configure() -> UpdateBuilder { 493 | UpdateBuilder::new() 494 | } 495 | } 496 | 497 | impl ReleaseUpdate for Update { 498 | fn get_latest_release(&self) -> Result { 499 | set_ssl_vars!(); 500 | let api_url = format!( 501 | "{}/api/v4/projects/{}%2F{}/releases", 502 | self.host, 503 | urlencoding::encode(&self.repo_owner), 504 | self.repo_name 505 | ); 506 | let client = reqwest::blocking::ClientBuilder::new() 507 | .use_rustls_tls() 508 | .http2_adaptive_window(true) 509 | .build()?; 510 | let resp = client 511 | .get(&api_url) 512 | .headers(self.api_headers(&self.auth_token)?) 513 | .send()?; 514 | if !resp.status().is_success() { 515 | bail!( 516 | Error::Network, 517 | "api request failed with status: {:?} - for: {:?}", 518 | resp.status(), 519 | api_url 520 | ) 521 | } 522 | let json = resp.json::()?; 523 | Release::from_release_gitlab(&json[0]) 524 | } 525 | 526 | fn get_latest_releases(&self, current_version: &str) -> Result> { 527 | set_ssl_vars!(); 528 | let api_url = format!( 529 | "{}/api/v4/projects/{}%2F{}/releases", 530 | self.host, 531 | urlencoding::encode(&self.repo_owner), 532 | self.repo_name 533 | ); 534 | let resp = reqwest::blocking::Client::new() 535 | .get(&api_url) 536 | .headers(self.api_headers(&self.auth_token)?) 537 | .send()?; 538 | if !resp.status().is_success() { 539 | bail!( 540 | Error::Network, 541 | "api request failed with status: {:?} - for: {:?}", 542 | resp.status(), 543 | api_url 544 | ) 545 | } 546 | 547 | let json = resp.json::()?; 548 | json.as_array() 549 | .ok_or_else(|| format_err!(Error::Release, "No releases found")) 550 | .and_then(|releases| { 551 | releases 552 | .iter() 553 | .map(Release::from_release_gitlab) 554 | .filter(|r| { 555 | r.as_ref().map_or(false, |r| { 556 | bump_is_greater(current_version, &r.version).unwrap_or(false) 557 | }) 558 | }) 559 | .collect::>>() 560 | }) 561 | } 562 | 563 | fn get_release_version(&self, ver: &str) -> Result { 564 | set_ssl_vars!(); 565 | let api_url = format!( 566 | "{}/api/v4/projects/{}%2F{}/releases/{}", 567 | self.host, 568 | urlencoding::encode(&self.repo_owner), 569 | self.repo_name, 570 | ver 571 | ); 572 | let client = reqwest::blocking::ClientBuilder::new() 573 | .use_rustls_tls() 574 | .http2_adaptive_window(true) 575 | .build()?; 576 | let resp = client 577 | .get(&api_url) 578 | .headers(self.api_headers(&self.auth_token)?) 579 | .send()?; 580 | if !resp.status().is_success() { 581 | bail!( 582 | Error::Network, 583 | "api request failed with status: {:?} - for: {:?}", 584 | resp.status(), 585 | api_url 586 | ) 587 | } 588 | let json = resp.json::()?; 589 | Release::from_release_gitlab(&json) 590 | } 591 | 592 | fn current_version(&self) -> String { 593 | self.current_version.to_owned() 594 | } 595 | 596 | fn target(&self) -> String { 597 | self.target.clone() 598 | } 599 | 600 | fn target_version(&self) -> Option { 601 | self.target_version.clone() 602 | } 603 | 604 | fn bin_name(&self) -> String { 605 | self.bin_name.clone() 606 | } 607 | 608 | fn bin_install_path(&self) -> PathBuf { 609 | self.bin_install_path.clone() 610 | } 611 | 612 | fn bin_path_in_archive(&self) -> String { 613 | self.bin_path_in_archive.clone() 614 | } 615 | 616 | fn show_download_progress(&self) -> bool { 617 | self.show_download_progress 618 | } 619 | 620 | fn show_output(&self) -> bool { 621 | self.show_output 622 | } 623 | 624 | fn no_confirm(&self) -> bool { 625 | self.no_confirm 626 | } 627 | 628 | fn progress_template(&self) -> String { 629 | self.progress_template.to_owned() 630 | } 631 | 632 | fn progress_chars(&self) -> String { 633 | self.progress_chars.to_owned() 634 | } 635 | 636 | fn auth_token(&self) -> Option { 637 | self.auth_token.clone() 638 | } 639 | 640 | fn api_headers(&self, auth_token: &Option) -> Result { 641 | api_headers(auth_token) 642 | } 643 | 644 | #[cfg(feature = "signatures")] 645 | fn verifying_keys(&self) -> &[[u8; zipsign_api::PUBLIC_KEY_LENGTH]] { 646 | &self.verifying_keys 647 | } 648 | } 649 | 650 | impl Default for UpdateBuilder { 651 | fn default() -> Self { 652 | Self { 653 | host: String::from("https://gitlab.com"), 654 | repo_owner: None, 655 | repo_name: None, 656 | target: None, 657 | bin_name: None, 658 | bin_install_path: None, 659 | bin_path_in_archive: None, 660 | show_download_progress: false, 661 | show_output: true, 662 | no_confirm: false, 663 | current_version: None, 664 | target_version: None, 665 | progress_template: DEFAULT_PROGRESS_TEMPLATE.to_string(), 666 | progress_chars: DEFAULT_PROGRESS_CHARS.to_string(), 667 | auth_token: None, 668 | #[cfg(feature = "signatures")] 669 | verifying_keys: vec![], 670 | } 671 | } 672 | } 673 | 674 | fn api_headers(auth_token: &Option) -> Result { 675 | let mut headers = header::HeaderMap::new(); 676 | headers.insert( 677 | header::USER_AGENT, 678 | "rust-reqwest/self-update" 679 | .parse() 680 | .expect("gitlab invalid user-agent"), 681 | ); 682 | 683 | if let Some(token) = auth_token { 684 | headers.insert( 685 | header::AUTHORIZATION, 686 | format!("Bearer {}", token) 687 | .parse() 688 | .map_err(|err| Error::Config(format!("Failed to parse auth token: {}", err)))?, 689 | ); 690 | }; 691 | 692 | Ok(headers) 693 | } 694 | -------------------------------------------------------------------------------- /src/backends/mod.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | Collection of modules supporting various release distribution backends 3 | */ 4 | 5 | pub mod gitea; 6 | pub mod github; 7 | pub mod gitlab; 8 | pub mod s3; 9 | 10 | /// Search for the first "rel" link-header uri in a full link header string. 11 | /// Seems like reqwest/hyper threw away their link-header parser implementation... 12 | /// 13 | /// ex: 14 | /// `Link: ; rel="next"` 15 | /// `Link: ; rel="next"` 16 | /// 17 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link 18 | /// header values may contain multiple values separated by commas 19 | /// `Link: ; rel="next", ; rel="next"` 20 | pub(crate) fn find_rel_next_link(link_str: &str) -> Option<&str> { 21 | for link in link_str.split(',') { 22 | let mut uri = None; 23 | let mut is_rel_next = false; 24 | for part in link.split(';') { 25 | let part = part.trim(); 26 | if part.starts_with('<') && part.ends_with('>') { 27 | uri = Some(part.trim_start_matches('<').trim_end_matches('>')); 28 | } else if part.starts_with("rel=") { 29 | let part = part 30 | .trim_start_matches("rel=") 31 | .trim_end_matches('"') 32 | .trim_start_matches('"'); 33 | if part == "next" { 34 | is_rel_next = true; 35 | } 36 | } 37 | 38 | if is_rel_next && uri.is_some() { 39 | return uri; 40 | } 41 | } 42 | } 43 | None 44 | } 45 | 46 | #[cfg(test)] 47 | mod test { 48 | use crate::backends::find_rel_next_link; 49 | 50 | #[test] 51 | fn test_find_rel_link() { 52 | let val = r##" ; rel="next" "##; 53 | let link = find_rel_next_link(val); 54 | assert_eq!(link, Some("https://api.github.com/resource?page=2")); 55 | 56 | let val = r##" ; rel="next" "##; 57 | let link = find_rel_next_link(val); 58 | assert_eq!( 59 | link, 60 | Some("https://gitlab.com/api/v4/projects/13083/releases?id=13083&page=2&per_page=20") 61 | ); 62 | 63 | // returns the first one 64 | let val = r##" ; rel="next", ; rel="next" "##; 65 | let link = find_rel_next_link(val); 66 | assert_eq!(link, Some("https://place.com")); 67 | 68 | // bad format, returns the second one 69 | let val = r##" https://bad-format.com; rel="next", ; rel="next" "##; 70 | let link = find_rel_next_link(val); 71 | assert_eq!(link, Some("https://wow.com")); 72 | 73 | // all bad format, returns none 74 | let val = r##" https://bad-format.com; rel="next",