mirror of
https://github.com/yuzu-emu/liftinstall.git
synced 2025-11-04 23:24:51 +00:00
Compare commits
195 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f46fed17b7 | ||
|
|
31b3e7e3a6 | ||
|
|
fec5587b87 | ||
|
|
d28b19c25a | ||
|
|
08ab3c369e | ||
|
|
d8df3b3114 | ||
|
|
61a7db2005 | ||
|
|
7b8bf579f2 | ||
|
|
3ea6aa9852 | ||
|
|
faeb4885bf | ||
|
|
85ed6275f8 | ||
|
|
f3d0d06a09 | ||
|
|
8c795396eb | ||
|
|
e6600e3b17 | ||
|
|
89be1c0d84 | ||
|
|
42e092f54d | ||
|
|
278af40b37 | ||
|
|
8917ba88ca | ||
|
|
b87dab83d8 | ||
|
|
79b799f655 | ||
|
|
c61c068ed0 | ||
|
|
3727e4185b | ||
|
|
d6cb916a9c | ||
|
|
d269677b2c | ||
|
|
9a27b24f05 | ||
|
|
3fc8583646 | ||
|
|
a3f0d0f999 | ||
|
|
d194ed5dd5 | ||
|
|
0decda8232 | ||
|
|
2103a8ec15 | ||
|
|
a86bd209a8 | ||
|
|
eff81e6d99 | ||
|
|
d63473ec9c | ||
|
|
1fd97b6e42 | ||
|
|
0cfa44330d | ||
|
|
679312f101 | ||
|
|
2120abf299 | ||
|
|
a8db5ff8c4 | ||
|
|
faba49c025 | ||
|
|
3196736d36 | ||
|
|
f809e6cb23 | ||
|
|
77a26c1496 | ||
|
|
e990138200 | ||
|
|
6e7d045794 | ||
|
|
0d4022d348 | ||
|
|
109322836b | ||
|
|
fbf7640657 | ||
|
|
2d42189e5e | ||
|
|
dde96db57c | ||
|
|
a816cbe767 | ||
|
|
95ee7a1739 | ||
|
|
061944079b | ||
|
|
e54199ad6f | ||
|
|
4ed1ffb5c3 | ||
|
|
2958c583af | ||
|
|
825e9cc1c3 | ||
|
|
810ef5fb25 | ||
|
|
89e1b2f91f | ||
|
|
f13b2fe93d | ||
|
|
df0414b26e | ||
|
|
a9de893cca | ||
|
|
bdda585f12 | ||
|
|
27aa9924f3 | ||
|
|
322f72609f | ||
|
|
ca994e49d3 | ||
|
|
41918c709c | ||
|
|
48fa172169 | ||
|
|
928661db77 | ||
|
|
45c562d723 | ||
|
|
c7628c1474 | ||
|
|
34fd140a9e | ||
|
|
cd7fb8de28 | ||
|
|
01419e5da4 | ||
|
|
3ce4504a5b | ||
|
|
5003edd43d | ||
|
|
87efd394a1 | ||
|
|
8e212460d8 | ||
|
|
4bb84d98b3 | ||
|
|
1dbf078728 | ||
|
|
8e8d729019 | ||
|
|
9fcfe0c77b | ||
|
|
74cefc277e | ||
|
|
adbd4a304d | ||
|
|
351be36f05 | ||
|
|
9866a32c10 | ||
|
|
3537b5823f | ||
|
|
5ff1486f69 | ||
|
|
7acefbc8cb | ||
|
|
ef71b707cb | ||
|
|
c68ebcb61e | ||
|
|
93e24ea06a | ||
|
|
d9d8b92cc4 | ||
|
|
76a77d3caf | ||
|
|
ea8b631aa2 | ||
|
|
f848e8fb53 | ||
|
|
d9e4e5ecc2 | ||
|
|
6210a2668f | ||
|
|
d2399d97e4 | ||
|
|
b9e825faa5 | ||
|
|
630f2231ab | ||
|
|
91fb88aa98 | ||
|
|
713b85b59a | ||
|
|
ca6ac320c2 | ||
|
|
9999c52ea8 | ||
|
|
6cae746192 | ||
|
|
732e344605 | ||
|
|
9b58c273d1 | ||
|
|
b356f0057f | ||
|
|
d339816695 | ||
|
|
d2ad619d87 | ||
|
|
128c1b1f41 | ||
|
|
37d27a82ba | ||
|
|
eb556c8cab | ||
|
|
6af46ec703 | ||
|
|
db2176763d | ||
|
|
fccd1c9bd2 | ||
|
|
0e190ecdc6 | ||
|
|
f89cb19602 | ||
|
|
7392e1ef91 | ||
|
|
9cf5e745d4 | ||
|
|
de4246536e | ||
|
|
30f817e4fa | ||
|
|
6845ed9ad7 | ||
|
|
1639e74b98 | ||
|
|
c176658e28 | ||
|
|
d79fd3e6e1 | ||
|
|
d3c3b77e6b | ||
|
|
561f0071bd | ||
|
|
e72a5f0420 | ||
|
|
2b4b59320e | ||
|
|
004a49587c | ||
|
|
d20c17964e | ||
|
|
288518cd78 | ||
|
|
fc40f6691c | ||
|
|
74cecab186 | ||
|
|
9bec77a2db | ||
|
|
5409b32bf0 | ||
|
|
a7057dfed3 | ||
|
|
c4b4c597fa | ||
|
|
26997ba229 | ||
|
|
3bd85bac8d | ||
|
|
474fb71efd | ||
|
|
548daa1b2b | ||
|
|
bdbab4dc4d | ||
|
|
ca8defda7e | ||
|
|
6853ade29c | ||
|
|
b6122349d6 | ||
|
|
12081db009 | ||
|
|
3abc0a1b11 | ||
|
|
56cdaabbae | ||
|
|
a02e8a1624 | ||
|
|
4b158036da | ||
|
|
eb6475bac6 | ||
|
|
c7cef0b49d | ||
|
|
2ee02bbf46 | ||
|
|
68109894f1 | ||
|
|
6d443805fc | ||
|
|
c4139f7e37 | ||
|
|
6272c294c8 | ||
|
|
5d31fd0129 | ||
|
|
e69443c22e | ||
|
|
c8699b6e62 | ||
|
|
e83cf6cf4e | ||
|
|
9a28807423 | ||
|
|
b3b686ed53 | ||
|
|
f80db92188 | ||
|
|
4578450bff | ||
|
|
5603981af1 | ||
|
|
341a6a6537 | ||
|
|
27d0a05ade | ||
|
|
6c19b8b0d1 | ||
|
|
ff574c9d73 | ||
|
|
761ce91299 | ||
|
|
44e0ebdab4 | ||
|
|
270a17cd86 | ||
|
|
f24d1112dd | ||
|
|
3109d48dce | ||
|
|
5d53ef7a2e | ||
|
|
4d50a0f8f8 | ||
|
|
a447ef25b6 | ||
|
|
9d1f4c2576 | ||
|
|
30bb49e1fb | ||
|
|
8b6c2c1708 | ||
|
|
0d63e0ab21 | ||
|
|
269b083ec8 | ||
|
|
ae63bc7dab | ||
|
|
d3fb463f20 | ||
|
|
137d2ec539 | ||
|
|
fed2d28aa8 | ||
|
|
66e2473a40 | ||
|
|
d236eeec0c | ||
|
|
6aa5da8795 | ||
|
|
21eeb54b80 | ||
|
|
eff17acf08 | ||
|
|
f2af419b95 |
58
.github/workflows/test-build.yml
vendored
Normal file
58
.github/workflows/test-build.yml
vendored
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
name: Rust
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macOS-latest]
|
||||
|
||||
steps:
|
||||
- uses: hecrj/setup-rust-action@master
|
||||
with:
|
||||
rust-version: stable
|
||||
|
||||
- name: Install dependencies
|
||||
run: sudo apt-get update && sudo apt-get install -y libwebkit2gtk-4.0-dev libssl-dev libappindicator3-dev
|
||||
if: runner.os == 'Linux'
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- name: Get cargo cache directory path
|
||||
id: cargo-cache-dir-path
|
||||
run: echo "::set-output name=dir::$HOME/.cargo/"
|
||||
|
||||
- uses: actions/cache@v2
|
||||
id: yarn-cache
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
|
||||
- uses: actions/cache@v2
|
||||
id: cargo-cache
|
||||
with:
|
||||
path: ${{ steps.cargo-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('Cargo.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-cargo-
|
||||
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '16.x'
|
||||
- run: npm install -g yarn
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
- name: Download Webview2
|
||||
run: Invoke-WebRequest -Uri 'https://go.microsoft.com/fwlink/p/?LinkId=2124703' -OutFile 'MicrosoftEdgeWebview2Setup.exe'
|
||||
if: runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
|
||||
- name: Build
|
||||
run: cargo build --verbose
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -5,3 +5,5 @@
|
|||
**/*.rs.bk
|
||||
|
||||
*.log
|
||||
|
||||
*.exe
|
||||
|
|
|
|||
11
.travis.yml
11
.travis.yml
|
|
@ -1,11 +0,0 @@
|
|||
os: linux
|
||||
dist: trusty
|
||||
sudo: required
|
||||
services:
|
||||
- docker
|
||||
|
||||
install:
|
||||
- docker pull ubuntu:18.04
|
||||
|
||||
script:
|
||||
- docker run -v $(pwd):/liftinstall ubuntu:18.04 /bin/bash -ex /liftinstall/.travis/build.sh
|
||||
|
|
@ -1,10 +1,6 @@
|
|||
#!/usr/bin/env bash
|
||||
cd /liftinstall
|
||||
cd /liftinstall || exit 1
|
||||
|
||||
apt update
|
||||
apt install -y libwebkit2gtk-4.0-dev libssl-dev
|
||||
yarn --cwd ui
|
||||
|
||||
curl https://sh.rustup.rs -sSf | sh -s -- -y
|
||||
export PATH=~/.cargo/bin:$PATH
|
||||
|
||||
cargo build
|
||||
cargo build --release
|
||||
|
|
|
|||
4
.travis/exec.sh
Normal file
4
.travis/exec.sh
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
#!/bin/bash -ex
|
||||
|
||||
# the UID for the container yuzu user is 1027
|
||||
docker run -u root -v $(pwd):/liftinstall -t yuzuemu/build-environments:linux-liftinstall /bin/bash /liftinstall/.travis/build.sh
|
||||
8
.tx/config
Executable file
8
.tx/config
Executable file
|
|
@ -0,0 +1,8 @@
|
|||
[main]
|
||||
host = https://www.transifex.com
|
||||
|
||||
[o:yuzu-emulator:p:yuzu:r:installer]
|
||||
file_filter = ui/translations/<lang>.po
|
||||
source_file = ui/translations/en.po
|
||||
source_lang = en
|
||||
type = PO
|
||||
4139
Cargo.lock
generated
4139
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
81
Cargo.toml
81
Cargo.toml
|
|
@ -1,55 +1,84 @@
|
|||
[package]
|
||||
name = "liftinstall"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
edition = "2018"
|
||||
authors = ["James <jselby@jselby.net>"]
|
||||
repository = "https://github.com/j-selby/liftinstall.git"
|
||||
documentation = "https://liftinstall.jselby.net"
|
||||
description = "An adaptable installer for your application."
|
||||
build = "build.rs"
|
||||
resolver = "2"
|
||||
|
||||
[dependencies]
|
||||
web-view = {git = "https://github.com/Boscop/web-view.git", rev = "555f422d09cbb94e82a728d47e9e07ca91963f6e"}
|
||||
anyhow = "^1"
|
||||
wry = "0.12"
|
||||
tinyfiledialogs = "3.8"
|
||||
|
||||
hyper = "0.11.27"
|
||||
futures = "*"
|
||||
mime_guess = "1.8.3"
|
||||
url = "*"
|
||||
futures = "0.1.29"
|
||||
mime_guess = "2.0"
|
||||
url = "2.2"
|
||||
|
||||
reqwest = "0.9.0"
|
||||
number_prefix = "0.2.7"
|
||||
reqwest = "0.9.22"
|
||||
number_prefix = "0.4"
|
||||
|
||||
serde = "1.0.27"
|
||||
serde_derive = "1.0.27"
|
||||
serde_json = "1.0.9"
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
serde_json = "1.0"
|
||||
|
||||
toml = "0.4"
|
||||
toml = "0.5"
|
||||
|
||||
semver = {version = "0.9.0", features = ["serde"]}
|
||||
regex = "0.2"
|
||||
semver = {version = "1.0", features = ["serde"]}
|
||||
regex = "1.4"
|
||||
|
||||
dirs = "1.0"
|
||||
zip = "0.4.2"
|
||||
xz-decom = {git = "https://github.com/j-selby/xz-decom.git", rev = "9ebf3d00d9ff909c39eec1d2cf7e6e068ce214e5"}
|
||||
dirs = "^4"
|
||||
zip = "0.6"
|
||||
xz2 = "0.1"
|
||||
tar = "0.4"
|
||||
|
||||
log = "0.4"
|
||||
fern = "0.5"
|
||||
chrono = "0.4.5"
|
||||
fern = "0.6"
|
||||
chrono = "0.4"
|
||||
|
||||
clap = "2.32.0"
|
||||
clap = "2.33"
|
||||
|
||||
sysinfo = "*"
|
||||
# used to open a link to the users default browser
|
||||
webbrowser = "0.8"
|
||||
# used in JWT based package authentication
|
||||
jsonwebtoken = "^8"
|
||||
# used to decode the public key for verifying JWT tokens
|
||||
base64 = "0.13"
|
||||
|
||||
[build-dependencies]
|
||||
walkdir = "2"
|
||||
serde = "1.0.27"
|
||||
serde_derive = "1.0.27"
|
||||
toml = "0.4"
|
||||
walkdir = "2.3"
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
toml = "0.5"
|
||||
which = "4.0"
|
||||
image = { version = "0.24", default-features = false, features = ["ico"] }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
# NFD is needed on Windows, as web-view doesn't work correctly here
|
||||
nfd = "0.0.4"
|
||||
winapi = { version = "0.3", features = ["psapi", "winbase", "winioctl", "winnt"] }
|
||||
widestring = "^1"
|
||||
webview2 = "0.1"
|
||||
tempfile = "3"
|
||||
|
||||
[target.'cfg(not(windows))'.dependencies]
|
||||
sysinfo = "0.26"
|
||||
slug = "0.1"
|
||||
|
||||
[target.'cfg(windows)'.build-dependencies]
|
||||
winres = "0.1"
|
||||
cc = "1.0"
|
||||
|
||||
[profile.release]
|
||||
#panic = "abort"
|
||||
lto = true
|
||||
opt-level = "z"
|
||||
codegen-units = 1
|
||||
incremental = false
|
||||
|
||||
#[profile.release.overrides."*"] # +
|
||||
#opt-level = "z"
|
||||
#codegen-units = 1
|
||||
#incremental = false
|
||||
|
|
|
|||
18
Justfile
Normal file
18
Justfile
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
ui-build:
|
||||
yarn --cwd {{ justfile_directory() }}/ui/ install
|
||||
yarn --cwd {{ justfile_directory() }}/ui/ build
|
||||
|
||||
ui-test:
|
||||
cd {{ justfile_directory() }}/ui/ && node mock-server.js &
|
||||
yarn --cwd {{ justfile_directory() }}/ui/ serve
|
||||
|
||||
update-i18n:
|
||||
#!/bin/bash -e
|
||||
[ -z "${TX_PULL}" ] || tx pull -a --minimum-perc 85
|
||||
for i in {{ justfile_directory() }}/ui/translations/*.po; do
|
||||
TARGET_FILE="$(basename $i)"
|
||||
TARGET_LANG="${TARGET_FILE/.po/}"
|
||||
OUTPUT="{{ justfile_directory() }}/ui/src/locales/${TARGET_LANG}.json"
|
||||
i18next-conv -l en -s "$i" -t "$OUTPUT" -K
|
||||
node {{ justfile_directory() }}/ui/unbreak-translations.js "$OUTPUT" "$OUTPUT"
|
||||
done
|
||||
|
|
@ -22,6 +22,7 @@ For more detailed instructions, look at the usage documentation above.
|
|||
There are are few system dependencies depending on your platform:
|
||||
- For all platforms, `cargo` should be available on your PATH. [Rustup](https://rustup.rs/) is the
|
||||
recommended way to achieve this. Stable or Nightly Rust works fine.
|
||||
- Have node.js and Yarn available on your PATH (for building UI components, not needed at runtime).
|
||||
- For Windows (MSVC), you need Visual Studio installed.
|
||||
- For Windows (Mingw), you need `gcc`/`g++` available on the PATH.
|
||||
- For Mac, you need Xcode installed, and Clang/etc available on the PATH.
|
||||
|
|
@ -33,8 +34,8 @@ apt install -y build-essential libwebkit2gtk-4.0-dev libssl-dev
|
|||
|
||||
In order to build yourself an installer, as a bare minimum, you need to:
|
||||
|
||||
- Add your favicon to `static/favicon.ico`
|
||||
- Add your logo to `static/logo.png`
|
||||
- Add your favicon to `ui/public/favicon.ico`
|
||||
- Add your logo to `ui/src/assets/logo.png`
|
||||
- Modify the bootstrap configuration file as needed (`config.PLATFORM.toml`).
|
||||
- Have the main configuration file somewhere useful, reachable over HTTP.
|
||||
- Run:
|
||||
|
|
|
|||
13
SECURITY.md
Normal file
13
SECURITY.md
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
As `liftinstall` is a template for your project, no specific versioning is
|
||||
provided at this time, though a rough version is given in the Cargo file.
|
||||
|
||||
Only the latest version from this file will be supported.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
For any specific security concerns/vulnerabilities, please email me directly
|
||||
at security *at* jselby.net.
|
||||
|
|
@ -1,2 +1,2 @@
|
|||
name = "yuzu"
|
||||
target_url = "https://raw.githubusercontent.com/yuzu-emu/liftinstall/master/config.linux.v1.toml"
|
||||
target_url = "https://raw.githubusercontent.com/yuzu-emu/liftinstall/master/config.linux.v3.toml"
|
||||
|
|
|
|||
3
bootstrap.macos.toml
Normal file
3
bootstrap.macos.toml
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# fake configuration for CI purpose only
|
||||
name = "yuzu"
|
||||
target_url = "https://raw.githubusercontent.com/j-selby/test-installer/master/config.linux.v2.toml"
|
||||
|
|
@ -1,2 +1,2 @@
|
|||
name = "yuzu"
|
||||
target_url = "https://raw.githubusercontent.com/yuzu-emu/liftinstall/master/config.windows.v5.toml"
|
||||
target_url = "https://raw.githubusercontent.com/yuzu-emu/liftinstall/master/config.windows.v10.toml"
|
||||
|
|
|
|||
133
build.rs
133
build.rs
|
|
@ -1,5 +1,3 @@
|
|||
extern crate walkdir;
|
||||
|
||||
#[cfg(windows)]
|
||||
extern crate winres;
|
||||
|
||||
|
|
@ -11,23 +9,21 @@ extern crate serde;
|
|||
extern crate serde_derive;
|
||||
extern crate toml;
|
||||
|
||||
use walkdir::WalkDir;
|
||||
extern crate which;
|
||||
|
||||
use std::env;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use std::fs::copy;
|
||||
use std::fs::create_dir_all;
|
||||
use std::fs::File;
|
||||
|
||||
use std::io::BufRead;
|
||||
use std::io::BufReader;
|
||||
use std::io::Read;
|
||||
use std::io::Write;
|
||||
use std::process::Command;
|
||||
|
||||
use std::env::consts::OS;
|
||||
|
||||
const FILES_TO_PREPROCESS: &'static [&'static str] = &["helpers.js", "views.js"];
|
||||
use image::imageops::FilterType;
|
||||
|
||||
/// Describes the application itself.
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
|
@ -39,7 +35,7 @@ pub struct BaseAttributes {
|
|||
#[cfg(windows)]
|
||||
fn handle_binary(config: &BaseAttributes) {
|
||||
let mut res = winres::WindowsResource::new();
|
||||
res.set_icon("static/favicon.ico");
|
||||
res.set_icon("ui/public/favicon.ico");
|
||||
res.set(
|
||||
"FileDescription",
|
||||
&format!("Interactive installer for {}", config.name),
|
||||
|
|
@ -53,6 +49,8 @@ fn handle_binary(config: &BaseAttributes) {
|
|||
|
||||
cc::Build::new()
|
||||
.cpp(true)
|
||||
.define("_WIN32_WINNT", Some("0x0600"))
|
||||
.define("WINVER", Some("0x0600"))
|
||||
.file("src/native/interop.cpp")
|
||||
.compile("interop");
|
||||
}
|
||||
|
|
@ -62,9 +60,18 @@ fn handle_binary(_config: &BaseAttributes) {}
|
|||
|
||||
fn main() {
|
||||
let output_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
|
||||
let current_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
|
||||
let ui_dir = current_dir.join("ui");
|
||||
|
||||
let os = OS.to_lowercase();
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
if std::fs::metadata("MicrosoftEdgeWebview2Setup.exe").is_err() {
|
||||
panic!("Please download MicrosoftEdgeWebview2Setup.exe from https://go.microsoft.com/fwlink/p/?LinkId=2124703 and put the file at the workspace root!");
|
||||
}
|
||||
}
|
||||
|
||||
// Find target config
|
||||
let target_config = PathBuf::from(format!("bootstrap.{}.toml", os));
|
||||
|
||||
|
|
@ -92,80 +99,42 @@ fn main() {
|
|||
// Copy for the main build
|
||||
copy(&target_config, output_dir.join("bootstrap.toml")).expect("Unable to copy config file");
|
||||
|
||||
// Copy files from static/ to build dir
|
||||
for entry in WalkDir::new("static") {
|
||||
let entry = entry.expect("Unable to read output directory");
|
||||
let yarn_binary =
|
||||
which::which("yarn").expect("Failed to find yarn - please go ahead and install it!");
|
||||
|
||||
let output_file = output_dir.join(entry.path());
|
||||
// bundle the icon
|
||||
let mut f = File::create(output_dir.join("icon-data.bin")).unwrap();
|
||||
let icon_file = image::open("ui/public/favicon.ico").expect("Unable to read the icon file");
|
||||
let icon_data = icon_file
|
||||
.resize_exact(48, 48, FilterType::Triangle)
|
||||
.to_rgba8();
|
||||
f.write_all(&icon_data.into_vec()).unwrap();
|
||||
|
||||
if entry.path().is_dir() {
|
||||
create_dir_all(output_file).expect("Unable to create dir");
|
||||
} else {
|
||||
let filename = entry
|
||||
.path()
|
||||
.file_name()
|
||||
.expect("Unable to parse filename")
|
||||
// Build and deploy frontend files
|
||||
Command::new(&yarn_binary)
|
||||
.arg("--version")
|
||||
.spawn()
|
||||
.expect("Yarn could not be launched");
|
||||
Command::new(&yarn_binary)
|
||||
.arg("--cwd")
|
||||
.arg(ui_dir.to_str().expect("Unable to covert path"))
|
||||
.spawn()
|
||||
.unwrap()
|
||||
.wait()
|
||||
.expect("Unable to install Node.JS dependencies using Yarn");
|
||||
let return_code = Command::new(&yarn_binary)
|
||||
.args(&[
|
||||
"--cwd",
|
||||
ui_dir.to_str().expect("Unable to covert path"),
|
||||
"run",
|
||||
"build",
|
||||
"--dest",
|
||||
output_dir
|
||||
.join("static")
|
||||
.to_str()
|
||||
.expect("Unable to convert to string");
|
||||
|
||||
if FILES_TO_PREPROCESS.contains(&filename) {
|
||||
// Do basic preprocessing - transcribe template string
|
||||
let source = BufReader::new(File::open(entry.path()).expect("Unable to copy file"));
|
||||
let mut target = File::create(output_file).expect("Unable to copy file");
|
||||
|
||||
let mut is_template_string = false;
|
||||
|
||||
for line in source.lines() {
|
||||
let line = line.expect("Unable to read line from JS file");
|
||||
|
||||
let mut is_break = false;
|
||||
let mut is_quote = false;
|
||||
|
||||
let mut output_line = String::new();
|
||||
|
||||
if is_template_string {
|
||||
output_line += "\"";
|
||||
}
|
||||
|
||||
for c in line.chars() {
|
||||
if c == '\\' {
|
||||
is_break = true;
|
||||
output_line.push('\\');
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == '\"' || c == '\'') && !is_break && !is_template_string {
|
||||
is_quote = !is_quote;
|
||||
}
|
||||
|
||||
if c == '`' && !is_break && !is_quote {
|
||||
output_line += "\"";
|
||||
is_template_string = !is_template_string;
|
||||
continue;
|
||||
}
|
||||
|
||||
if c == '"' && !is_break && is_template_string {
|
||||
output_line += "\\\"";
|
||||
continue;
|
||||
}
|
||||
|
||||
is_break = false;
|
||||
output_line.push(c);
|
||||
}
|
||||
|
||||
if is_template_string {
|
||||
output_line += "\" +";
|
||||
}
|
||||
|
||||
output_line.push('\n');
|
||||
|
||||
target
|
||||
.write(output_line.as_bytes())
|
||||
.expect("Unable to write line");
|
||||
}
|
||||
} else {
|
||||
copy(entry.path(), output_file).expect("Unable to copy file");
|
||||
}
|
||||
}
|
||||
}
|
||||
.expect("Unable to convert path"),
|
||||
])
|
||||
.status()
|
||||
.expect("Unable to build frontend assets using Webpack");
|
||||
assert!(return_code.success());
|
||||
}
|
||||
|
|
|
|||
52
config.linux.v2.toml
Normal file
52
config.linux.v2.toml
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
installing_message = "Reminder: yuzu is an <b>experimental</b> emulator. Stuff will break!"
|
||||
hide_advanced = true
|
||||
|
||||
[authentication]
|
||||
# Base64 encoded version of the public key for validating the JWT token. Must be in DER format
|
||||
pub_key_base64 = "MIIBCgKCAQEAs5K6s49JVV9LBMzDrkORsoPSYsv1sCXDtxjp4pn8p0uPSvJAsbNNmdIgCjfSULzbHLM28MblnI4zYP8ZgKtkjdg+Ic5WQbS5iBAkf18zMafpOrotTArLsgZSmUfNYt0SOiN17D+sq/Ov/CKXRM9CttKkEbanBTVqkx7sxsHVbkI6tDvkboSaNeVPHzHlfAbvGrUo5cbAFCB/KnRsoxr+g7jLKTxU1w4xb/pIs91h80AXV/yZPXL6ItPM3/0noIRXjmoeYWf2sFQaFALNB2Kef0p6/hoHYUQP04ZSIL3Q+v13z5X2YJIlI4eLg+iD25QYm9V8oP3+Xro4vd47a0/maQIDAQAB"
|
||||
# URL to authenticate against. This must return a JWT token with their permissions and a custom claim patreonInfo with the following structure
|
||||
# "patreonInfo": { "linked": false, "activeSubscription": false }
|
||||
# If successful, the frontend will use this JWT token as a Bearer Authentication when requesting the binaries to download
|
||||
auth_url = "https://api.yuzu-emu.org/jwt/installer/"
|
||||
[authentication.validation]
|
||||
iss = "citra-core"
|
||||
aud = "installer"
|
||||
|
||||
[[packages]]
|
||||
name = "yuzu"
|
||||
description = "Includes frequent updates to yuzu with all the latest reviewed and tested features."
|
||||
default = true
|
||||
[packages.source]
|
||||
name = "github"
|
||||
match = "^yuzu-linux-[0-9]*-[0-9a-f]*.tar.xz$"
|
||||
[packages.source.config]
|
||||
repo = "yuzu-emu/yuzu-mainline"
|
||||
[[packages.shortcuts]]
|
||||
name = "yuzu"
|
||||
relative_path = "yuzu-windows-msvc/yuzu.exe"
|
||||
description = "Launch yuzu"
|
||||
|
||||
[[packages]]
|
||||
name = "yuzu Early Access"
|
||||
description = "Bonus preview release for project supporters. Thanks for your support!"
|
||||
# Displayed when the package has no authentication for the user
|
||||
need_authentication_description = "Click here to sign in with your yuzu account for Early Access"
|
||||
# Displayed when the package has an authentication, but the user has not linked their account
|
||||
need_link_description = "You are signed in, but you need to link your Patreon account! Click here for more details"
|
||||
# Displayed when the package has an authentication, but the user has not linked their account
|
||||
need_subscription_description = "You are signed in, but you need to link your Patreon account! Click here for more details"
|
||||
# Displayed when the package has an authentication, but the user has not linked their account
|
||||
need_reward_tier_description = "You are signed in, but are not backing an eligible reward tier! Click here for more details"
|
||||
requires_authorization = true
|
||||
# puts a "new" ribbon the package select
|
||||
is_new = true
|
||||
[packages.source]
|
||||
name = "patreon"
|
||||
match = "^yuzu-linux-[0-9]*-[0-9a-f]*.tar.xz$"
|
||||
[packages.source.config]
|
||||
repo = "earlyaccess"
|
||||
[[packages.shortcuts]]
|
||||
name = "yuzu Early Access"
|
||||
relative_path = "yuzu-linux-earlyaccess/yuzu.exe"
|
||||
description = "Launch yuzu Early Access"
|
||||
|
||||
58
config.linux.v3.toml
Normal file
58
config.linux.v3.toml
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
installing_message = "Reminder: yuzu is an <b>experimental</b> emulator. Stuff will break!"
|
||||
hide_advanced = true
|
||||
|
||||
[authentication]
|
||||
# Base64 encoded version of the public key for validating the JWT token. Must be in DER format
|
||||
pub_key_base64 = "MIIBCgKCAQEAs5K6s49JVV9LBMzDrkORsoPSYsv1sCXDtxjp4pn8p0uPSvJAsbNNmdIgCjfSULzbHLM28MblnI4zYP8ZgKtkjdg+Ic5WQbS5iBAkf18zMafpOrotTArLsgZSmUfNYt0SOiN17D+sq/Ov/CKXRM9CttKkEbanBTVqkx7sxsHVbkI6tDvkboSaNeVPHzHlfAbvGrUo5cbAFCB/KnRsoxr+g7jLKTxU1w4xb/pIs91h80AXV/yZPXL6ItPM3/0noIRXjmoeYWf2sFQaFALNB2Kef0p6/hoHYUQP04ZSIL3Q+v13z5X2YJIlI4eLg+iD25QYm9V8oP3+Xro4vd47a0/maQIDAQAB"
|
||||
# URL to authenticate against. This must return a JWT token with their permissions and a custom claim patreonInfo with the following structure
|
||||
# "patreonInfo": { "linked": false, "activeSubscription": false }
|
||||
# If successful, the frontend will use this JWT token as a Bearer Authentication when requesting the binaries to download
|
||||
auth_url = "https://api.yuzu-emu.org/jwt/installer/"
|
||||
[authentication.validation]
|
||||
iss = "citra-core"
|
||||
aud = "installer"
|
||||
|
||||
[[packages]]
|
||||
name = "yuzu Early Access"
|
||||
description = "Preview release with the newest features for the supporters."
|
||||
icon = "thicc_logo_installer__ea_shadow.png"
|
||||
requires_authorization = true
|
||||
# puts a "new" ribbon the package select
|
||||
is_new = true
|
||||
[packages.extended_description]
|
||||
no_action_description = "Thank you for your support!"
|
||||
# Displayed when the package has no authentication for the user
|
||||
need_authentication_description = "Click here to sign in with your yuzu account for Early Access"
|
||||
# Displayed when the package has an authentication, but the user has not linked their account
|
||||
need_link_description = "You are signed in, but you need to link your Patreon account! Click here for more details"
|
||||
# Displayed when the package has an authentication, but the user has not linked their account
|
||||
need_subscription_description = "You are signed in, but you need to link your Patreon account! Click here for more details"
|
||||
# Displayed when the package has an authentication, but the user has not linked their account
|
||||
need_reward_tier_description = "You are signed in, but are not backing an eligible reward tier! Click here for more details"
|
||||
|
||||
[packages.source]
|
||||
name = "patreon"
|
||||
match = "^yuzu-linux-[0-9]*-[0-9a-f]*.tar.xz$"
|
||||
[packages.source.config]
|
||||
repo = "earlyaccess"
|
||||
[[packages.shortcuts]]
|
||||
name = "yuzu Early Access"
|
||||
relative_path = "yuzu-linux-early-access/yuzu-early-access.AppImage"
|
||||
description = "Launch yuzu Early Access"
|
||||
|
||||
|
||||
[[packages]]
|
||||
name = "yuzu"
|
||||
description = "Includes frequent updates to yuzu with all the latest reviewed and tested features."
|
||||
icon = "thicc_logo_installer_shadow.png"
|
||||
default = true
|
||||
[packages.source]
|
||||
name = "github"
|
||||
match = "^yuzu-linux-[0-9]*-[0-9a-f]*.tar.xz$"
|
||||
[packages.source.config]
|
||||
repo = "yuzu-emu/yuzu-mainline"
|
||||
[[packages.shortcuts]]
|
||||
name = "yuzu"
|
||||
relative_path = "yuzu-linux-mainline/yuzu-mainline.AppImage"
|
||||
description = "Launch yuzu"
|
||||
|
||||
58
config.windows.v10.toml
Normal file
58
config.windows.v10.toml
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
installing_message = "Reminder: yuzu is an <b>experimental</b> emulator. Stuff will break!"
|
||||
hide_advanced = true
|
||||
|
||||
[authentication]
|
||||
# Base64 encoded version of the public key for validating the JWT token. Must be in DER format
|
||||
pub_key_base64 = "MIIBCgKCAQEAs5K6s49JVV9LBMzDrkORsoPSYsv1sCXDtxjp4pn8p0uPSvJAsbNNmdIgCjfSULzbHLM28MblnI4zYP8ZgKtkjdg+Ic5WQbS5iBAkf18zMafpOrotTArLsgZSmUfNYt0SOiN17D+sq/Ov/CKXRM9CttKkEbanBTVqkx7sxsHVbkI6tDvkboSaNeVPHzHlfAbvGrUo5cbAFCB/KnRsoxr+g7jLKTxU1w4xb/pIs91h80AXV/yZPXL6ItPM3/0noIRXjmoeYWf2sFQaFALNB2Kef0p6/hoHYUQP04ZSIL3Q+v13z5X2YJIlI4eLg+iD25QYm9V8oP3+Xro4vd47a0/maQIDAQAB"
|
||||
# URL to authenticate against. This must return a JWT token with their permissions and a custom claim patreonInfo with the following structure
|
||||
# "patreonInfo": { "linked": false, "activeSubscription": false }
|
||||
# If successful, the frontend will use this JWT token as a Bearer Authentication when requesting the binaries to download
|
||||
auth_url = "https://api.yuzu-emu.org/jwt/installer/"
|
||||
[authentication.validation]
|
||||
iss = "citra-core"
|
||||
aud = "installer"
|
||||
|
||||
[[packages]]
|
||||
name = "yuzu Early Access"
|
||||
description = "Preview release with the newest features for the supporters."
|
||||
icon = "thicc_logo_installer__ea_shadow.png"
|
||||
requires_authorization = true
|
||||
# puts a "new" ribbon the package select
|
||||
is_new = true
|
||||
[packages.extended_description]
|
||||
no_action_description = "Thank you for your support!"
|
||||
# Displayed when the package has no authentication for the user
|
||||
need_authentication_description = "Click here to sign in with your yuzu account for Early Access"
|
||||
# Displayed when the package has an authentication, but the user has not linked their account
|
||||
need_link_description = "You are signed in, but you need to link your Patreon account! Click here for more details"
|
||||
# Displayed when the package has an authentication, but the user has not linked their account
|
||||
need_subscription_description = "You are signed in, but you need to link your Patreon account! Click here for more details"
|
||||
# Displayed when the package has an authentication, but the user has not linked their account
|
||||
need_reward_tier_description = "You are signed in, but are not backing an eligible reward tier! Click here for more details"
|
||||
|
||||
[packages.source]
|
||||
name = "patreon"
|
||||
match = "^yuzu-windows-msvc-[0-9]*-[0-9a-f]*.tar.xz$"
|
||||
[packages.source.config]
|
||||
repo = "earlyaccess"
|
||||
[[packages.shortcuts]]
|
||||
name = "yuzu Early Access"
|
||||
relative_path = "yuzu-windows-msvc-early-access/yuzu.exe"
|
||||
description = "Launch yuzu Early Access"
|
||||
|
||||
|
||||
[[packages]]
|
||||
name = "yuzu"
|
||||
description = "Includes frequent updates to yuzu with all the latest reviewed and tested features."
|
||||
icon = "thicc_logo_installer_shadow.png"
|
||||
default = true
|
||||
[packages.source]
|
||||
name = "github"
|
||||
match = "^yuzu-windows-msvc-[0-9]*-[0-9a-f]*.tar.xz$"
|
||||
[packages.source.config]
|
||||
repo = "yuzu-emu/yuzu-mainline"
|
||||
[[packages.shortcuts]]
|
||||
name = "yuzu"
|
||||
relative_path = "yuzu-windows-msvc/yuzu.exe"
|
||||
description = "Launch yuzu"
|
||||
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
installing_message = "Reminder: yuzu is an <b>experimental</b> emulator. Stuff will break!"
|
||||
hide_advanced = true
|
||||
new_tool = "https://github.com/yuzu-emu/liftinstall/releases/download/1.3/yuzu_install.exe"
|
||||
new_tool = "https://github.com/yuzu-emu/liftinstall/releases/download/1.4/yuzu_install.exe"
|
||||
|
||||
[[packages]]
|
||||
name = "yuzu Nightly"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
installing_message = "Reminder: yuzu is an <b>experimental</b> emulator. Stuff will break!"
|
||||
hide_advanced = true
|
||||
new_tool = "https://github.com/yuzu-emu/liftinstall/releases/download/1.3/yuzu_install.exe"
|
||||
new_tool = "https://github.com/yuzu-emu/liftinstall/releases/download/1.4/yuzu_install.exe"
|
||||
|
||||
[[packages]]
|
||||
name = "yuzu Nightly"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
installing_message = "Reminder: yuzu is an <b>experimental</b> emulator. Stuff will break!"
|
||||
hide_advanced = true
|
||||
new_tool = "https://github.com/yuzu-emu/liftinstall/releases/download/1.3/yuzu_install.exe"
|
||||
new_tool = "https://github.com/yuzu-emu/liftinstall/releases/download/1.4/yuzu_install.exe"
|
||||
|
||||
[[packages]]
|
||||
name = "yuzu Nightly"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
installing_message = "Reminder: yuzu is an <b>experimental</b> emulator. Stuff will break!"
|
||||
hide_advanced = true
|
||||
new_tool = "https://github.com/yuzu-emu/liftinstall/releases/download/1.4/yuzu_install.exe"
|
||||
|
||||
[[packages]]
|
||||
name = "yuzu Nightly"
|
||||
|
|
|
|||
31
config.windows.v6.toml
Normal file
31
config.windows.v6.toml
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
installing_message = "Reminder: yuzu is an <b>experimental</b> emulator. Stuff will break!"
|
||||
hide_advanced = true
|
||||
new_tool = "https://github.com/yuzu-emu/liftinstall/releases/download/1.5/yuzu_install.exe"
|
||||
|
||||
[[packages]]
|
||||
name = "yuzu Nightly"
|
||||
description = "The nightly build of yuzu contains already reviewed and tested features."
|
||||
[packages.source]
|
||||
name = "github"
|
||||
match = "^yuzu-windows-msvc-[0-9]*-[0-9a-f]*.zip$"
|
||||
[packages.source.config]
|
||||
repo = "yuzu-emu/yuzu-nightly"
|
||||
[[packages.shortcuts]]
|
||||
name = "yuzu Nightly"
|
||||
relative_path = "nightly/yuzu.exe"
|
||||
description = "Launch yuzu (Nightly version)"
|
||||
|
||||
[[packages]]
|
||||
name = "yuzu Canary"
|
||||
description = "The canary build of yuzu has additional features that are still waiting on review."
|
||||
default = true
|
||||
[packages.source]
|
||||
name = "github"
|
||||
match = "^yuzu-windows-msvc-[0-9]*-[0-9a-f]*.zip$"
|
||||
[packages.source.config]
|
||||
repo = "yuzu-emu/yuzu-canary"
|
||||
[[packages.shortcuts]]
|
||||
name = "yuzu Canary"
|
||||
relative_path = "canary/yuzu.exe"
|
||||
description = "Launch yuzu (Canary version)"
|
||||
|
||||
31
config.windows.v7.toml
Normal file
31
config.windows.v7.toml
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
installing_message = "Reminder: yuzu is an <b>experimental</b> emulator. Stuff will break!"
|
||||
hide_advanced = true
|
||||
new_tool = "https://github.com/yuzu-emu/liftinstall/releases/download/1.6/yuzu_install.exe"
|
||||
|
||||
[[packages]]
|
||||
name = "yuzu Nightly"
|
||||
description = "The nightly build of yuzu contains already reviewed and tested features."
|
||||
[packages.source]
|
||||
name = "github"
|
||||
match = "^yuzu-windows-msvc-[0-9]*-[0-9a-f]*.zip$"
|
||||
[packages.source.config]
|
||||
repo = "yuzu-emu/yuzu-nightly"
|
||||
[[packages.shortcuts]]
|
||||
name = "yuzu Nightly"
|
||||
relative_path = "nightly/yuzu.exe"
|
||||
description = "Launch yuzu (Nightly version)"
|
||||
|
||||
[[packages]]
|
||||
name = "yuzu Canary"
|
||||
description = "The canary build of yuzu has additional features that are still waiting on review."
|
||||
default = true
|
||||
[packages.source]
|
||||
name = "github"
|
||||
match = "^yuzu-windows-msvc-[0-9]*-[0-9a-f]*.zip$"
|
||||
[packages.source.config]
|
||||
repo = "yuzu-emu/yuzu-canary"
|
||||
[[packages.shortcuts]]
|
||||
name = "yuzu Canary"
|
||||
relative_path = "canary/yuzu.exe"
|
||||
description = "Launch yuzu (Canary version)"
|
||||
|
||||
17
config.windows.v8.toml
Normal file
17
config.windows.v8.toml
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
installing_message = "Reminder: yuzu is an <b>experimental</b> emulator. Stuff will break!"
|
||||
hide_advanced = true
|
||||
new_tool = "https://github.com/yuzu-emu/liftinstall/releases/download/1.7/yuzu_install.exe"
|
||||
|
||||
[[packages]]
|
||||
name = "yuzu"
|
||||
description = "Includes frequent updates to yuzu with all the latest reviewed and tested features."
|
||||
default = true
|
||||
[packages.source]
|
||||
name = "github"
|
||||
match = "^yuzu-windows-msvc-[0-9]*-[0-9a-f]*.tar.xz$"
|
||||
[packages.source.config]
|
||||
repo = "yuzu-emu/yuzu-mainline"
|
||||
[[packages.shortcuts]]
|
||||
name = "yuzu"
|
||||
relative_path = "yuzu-windows-msvc/yuzu.exe"
|
||||
description = "Launch yuzu"
|
||||
52
config.windows.v9.toml
Normal file
52
config.windows.v9.toml
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
installing_message = "Reminder: yuzu is an <b>experimental</b> emulator. Stuff will break!"
|
||||
hide_advanced = true
|
||||
|
||||
[authentication]
|
||||
# Base64 encoded version of the public key for validating the JWT token. Must be in DER format
|
||||
pub_key_base64 = "MIIBCgKCAQEAs5K6s49JVV9LBMzDrkORsoPSYsv1sCXDtxjp4pn8p0uPSvJAsbNNmdIgCjfSULzbHLM28MblnI4zYP8ZgKtkjdg+Ic5WQbS5iBAkf18zMafpOrotTArLsgZSmUfNYt0SOiN17D+sq/Ov/CKXRM9CttKkEbanBTVqkx7sxsHVbkI6tDvkboSaNeVPHzHlfAbvGrUo5cbAFCB/KnRsoxr+g7jLKTxU1w4xb/pIs91h80AXV/yZPXL6ItPM3/0noIRXjmoeYWf2sFQaFALNB2Kef0p6/hoHYUQP04ZSIL3Q+v13z5X2YJIlI4eLg+iD25QYm9V8oP3+Xro4vd47a0/maQIDAQAB"
|
||||
# URL to authenticate against. This must return a JWT token with their permissions and a custom claim patreonInfo with the following structure
|
||||
# "patreonInfo": { "linked": false, "activeSubscription": false }
|
||||
# If successful, the frontend will use this JWT token as a Bearer Authentication when requesting the binaries to download
|
||||
auth_url = "https://api.yuzu-emu.org/jwt/installer/"
|
||||
[authentication.validation]
|
||||
iss = "citra-core"
|
||||
aud = "installer"
|
||||
|
||||
[[packages]]
|
||||
name = "yuzu"
|
||||
description = "Includes frequent updates to yuzu with all the latest reviewed and tested features."
|
||||
default = true
|
||||
[packages.source]
|
||||
name = "github"
|
||||
match = "^yuzu-windows-msvc-[0-9]*-[0-9a-f]*.tar.xz$"
|
||||
[packages.source.config]
|
||||
repo = "yuzu-emu/yuzu-mainline"
|
||||
[[packages.shortcuts]]
|
||||
name = "yuzu"
|
||||
relative_path = "yuzu-windows-msvc/yuzu.exe"
|
||||
description = "Launch yuzu"
|
||||
|
||||
[[packages]]
|
||||
name = "yuzu Early Access"
|
||||
description = "Bonus preview release for project supporters. Thanks for your support!"
|
||||
# Displayed when the package has no authentication for the user
|
||||
need_authentication_description = "Click here to sign in with your yuzu account for Early Access"
|
||||
# Displayed when the package has an authentication, but the user has not linked their account
|
||||
need_link_description = "You are signed in, but you need to link your Patreon account! Click here for more details"
|
||||
# Displayed when the package has an authentication, but the user has not linked their account
|
||||
need_subscription_description = "You are signed in, but you need to link your Patreon account! Click here for more details"
|
||||
# Displayed when the package has an authentication, but the user has not linked their account
|
||||
need_reward_tier_description = "You are signed in, but are not backing an eligible reward tier! Click here for more details"
|
||||
requires_authorization = true
|
||||
# puts a "new" ribbon the package select
|
||||
is_new = true
|
||||
[packages.source]
|
||||
name = "patreon"
|
||||
match = "^yuzu-windows-msvc-[0-9]*-[0-9a-f]*.tar.xz$"
|
||||
[packages.source.config]
|
||||
repo = "earlyaccess"
|
||||
[[packages.shortcuts]]
|
||||
name = "yuzu Early Access"
|
||||
relative_path = "yuzu-windows-msvc-early-access/yuzu.exe"
|
||||
description = "Launch yuzu Early Access"
|
||||
|
||||
|
|
@ -10,13 +10,13 @@ use std::io::Read;
|
|||
use std::iter::Iterator;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use xz_decom;
|
||||
use xz2::read::XzDecoder;
|
||||
|
||||
pub trait Archive<'a> {
|
||||
/// func: iterator value, max size, file name, file contents
|
||||
fn for_each(
|
||||
&mut self,
|
||||
func: &mut FnMut(usize, Option<usize>, PathBuf, &mut Read) -> Result<(), String>,
|
||||
func: &mut dyn FnMut(usize, Option<usize>, PathBuf, &mut dyn Read) -> Result<(), String>,
|
||||
) -> Result<(), String>;
|
||||
}
|
||||
|
||||
|
|
@ -27,7 +27,7 @@ struct ZipArchive<'a> {
|
|||
impl<'a> Archive<'a> for ZipArchive<'a> {
|
||||
fn for_each(
|
||||
&mut self,
|
||||
func: &mut FnMut(usize, Option<usize>, PathBuf, &mut Read) -> Result<(), String>,
|
||||
func: &mut dyn FnMut(usize, Option<usize>, PathBuf, &mut dyn Read) -> Result<(), String>,
|
||||
) -> Result<(), String> {
|
||||
let max = self.archive.len();
|
||||
|
||||
|
|
@ -41,7 +41,7 @@ impl<'a> Archive<'a> for ZipArchive<'a> {
|
|||
continue;
|
||||
}
|
||||
|
||||
func(i, Some(max), archive.sanitized_name(), &mut archive)?;
|
||||
func(i, Some(max), archive.mangled_name(), &mut archive)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
@ -49,13 +49,13 @@ impl<'a> Archive<'a> for ZipArchive<'a> {
|
|||
}
|
||||
|
||||
struct TarArchive<'a> {
|
||||
archive: UpstreamTarArchive<Box<Read + 'a>>,
|
||||
archive: UpstreamTarArchive<Box<dyn Read + 'a>>,
|
||||
}
|
||||
|
||||
impl<'a> Archive<'a> for TarArchive<'a> {
|
||||
fn for_each(
|
||||
&mut self,
|
||||
func: &mut FnMut(usize, Option<usize>, PathBuf, &mut Read) -> Result<(), String>,
|
||||
func: &mut dyn FnMut(usize, Option<usize>, PathBuf, &mut dyn Read) -> Result<(), String>,
|
||||
) -> Result<(), String> {
|
||||
let entries = self
|
||||
.archive
|
||||
|
|
@ -83,7 +83,7 @@ impl<'a> Archive<'a> for TarArchive<'a> {
|
|||
}
|
||||
|
||||
/// Reads the named archive with an archive implementation.
|
||||
pub fn read_archive<'a>(name: &str, data: &'a [u8]) -> Result<Box<Archive<'a> + 'a>, String> {
|
||||
pub fn read_archive<'a>(name: &str, data: &'a [u8]) -> Result<Box<dyn Archive<'a> + 'a>, String> {
|
||||
if name.ends_with(".zip") {
|
||||
// Decompress a .zip file
|
||||
let archive = UpstreamZipArchive::new(Cursor::new(data))
|
||||
|
|
@ -92,10 +92,13 @@ pub fn read_archive<'a>(name: &str, data: &'a [u8]) -> Result<Box<Archive<'a> +
|
|||
Ok(Box::new(ZipArchive { archive }))
|
||||
} else if name.ends_with(".tar.xz") {
|
||||
// Decompress a .tar.xz file
|
||||
let decompressed_data = xz_decom::decompress(data)
|
||||
.map_err(|x| format!("Failed to build decompressor: {:?}", x))?;
|
||||
let mut decompresser = XzDecoder::new(data);
|
||||
let mut decompressed_data = Vec::new();
|
||||
decompresser
|
||||
.read_to_end(&mut decompressed_data)
|
||||
.map_err(|x| format!("Failed to decompress data: {:?}", x))?;
|
||||
|
||||
let decompressed_contents: Box<Read> = Box::new(Cursor::new(decompressed_data));
|
||||
let decompressed_contents: Box<dyn Read> = Box::new(Cursor::new(decompressed_data));
|
||||
|
||||
let tar = UpstreamTarArchive::new(decompressed_contents);
|
||||
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ use toml::de::Error as TomlError;
|
|||
|
||||
use serde_json::{self, Error as SerdeError};
|
||||
|
||||
use sources::get_by_name;
|
||||
use sources::types::Release;
|
||||
use crate::sources::get_by_name;
|
||||
use crate::sources::types::Release;
|
||||
|
||||
/// Description of the source of a package.
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
|
|
@ -25,6 +25,23 @@ pub struct PackageShortcut {
|
|||
pub name: String,
|
||||
pub relative_path: String,
|
||||
pub description: String,
|
||||
#[serde(default)]
|
||||
pub has_desktop_shortcut: bool,
|
||||
}
|
||||
|
||||
/// Extra description for authentication and authorization state for a package
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct PackageExtendedDescription {
|
||||
#[serde(default)]
|
||||
pub no_action_description: Option<String>,
|
||||
#[serde(default)]
|
||||
pub need_authentication_description: Option<String>,
|
||||
#[serde(default)]
|
||||
pub need_link_description: Option<String>,
|
||||
#[serde(default)]
|
||||
pub need_subscription_description: Option<String>,
|
||||
#[serde(default)]
|
||||
pub need_reward_tier_description: Option<String>,
|
||||
}
|
||||
|
||||
/// Describes a overview of a individual package.
|
||||
|
|
@ -32,10 +49,34 @@ pub struct PackageShortcut {
|
|||
pub struct PackageDescription {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
#[serde(default)]
|
||||
pub icon: Option<String>,
|
||||
pub default: Option<bool>,
|
||||
pub source: PackageSource,
|
||||
#[serde(default)]
|
||||
pub shortcuts: Vec<PackageShortcut>,
|
||||
#[serde(default)]
|
||||
pub requires_authorization: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub is_new: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub extended_description: Option<PackageExtendedDescription>,
|
||||
}
|
||||
|
||||
/// Configuration for validating the JWT token
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct JWTValidation {
|
||||
pub iss: Option<String>,
|
||||
// This can technically be a Vec as well, but thats a pain to support atm
|
||||
pub aud: Option<String>,
|
||||
}
|
||||
|
||||
/// The configuration for this release.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct AuthenticationConfig {
|
||||
pub pub_key_base64: String,
|
||||
pub auth_url: String,
|
||||
pub validation: Option<JWTValidation>,
|
||||
}
|
||||
|
||||
/// Describes the application itself.
|
||||
|
|
@ -43,6 +84,8 @@ pub struct PackageDescription {
|
|||
pub struct BaseAttributes {
|
||||
pub name: String,
|
||||
pub target_url: String,
|
||||
#[serde(default)]
|
||||
pub recovery: bool,
|
||||
}
|
||||
|
||||
impl BaseAttributes {
|
||||
|
|
@ -66,6 +109,8 @@ pub struct Config {
|
|||
pub packages: Vec<PackageDescription>,
|
||||
#[serde(default)]
|
||||
pub hide_advanced: bool,
|
||||
#[serde(default)]
|
||||
pub authentication: Option<AuthenticationConfig>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
|
|
|
|||
29
src/frontend/mod.rs
Normal file
29
src/frontend/mod.rs
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
//! frontend/mod.rs
|
||||
//!
|
||||
//! Provides the frontend interface, including HTTP server.
|
||||
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use crate::installer::InstallerFramework;
|
||||
use crate::logging::LoggingErrors;
|
||||
|
||||
pub mod rest;
|
||||
mod ui;
|
||||
|
||||
/// Launches the main web server + UI. Returns when the framework has been consumed + web UI closed.
|
||||
pub fn launch(app_name: &str, is_launcher: bool, framework: InstallerFramework) {
|
||||
let framework = Arc::new(RwLock::new(framework));
|
||||
|
||||
let (servers, address) = rest::server::spawn_servers(framework.clone());
|
||||
|
||||
ui::start_ui(app_name, &address, is_launcher).log_expect("Failed to start UI");
|
||||
|
||||
// Explicitly hint that we want the servers instance until here.
|
||||
drop(servers);
|
||||
|
||||
framework
|
||||
.write()
|
||||
.log_expect("Failed to write to framework to finalize")
|
||||
.shutdown()
|
||||
.log_expect("Failed to finalize framework");
|
||||
}
|
||||
|
|
@ -2,7 +2,8 @@
|
|||
|
||||
extern crate mime_guess;
|
||||
|
||||
use assets::mime_guess::{get_mime_type, octet_stream};
|
||||
use self::mime_guess::from_ext;
|
||||
use self::mime_guess::mime::APPLICATION_OCTET_STREAM;
|
||||
|
||||
macro_rules! include_files_as_assets {
|
||||
( $target_match:expr, $( $file_name:expr ),* ) => {
|
||||
|
|
@ -23,9 +24,9 @@ pub fn file_from_string(file_path: &str) -> Option<(String, &'static [u8])> {
|
|||
Some(ext_ptr) => {
|
||||
let ext = &file_path[ext_ptr + 1..];
|
||||
|
||||
get_mime_type(ext)
|
||||
from_ext(ext).first_or_octet_stream()
|
||||
}
|
||||
None => octet_stream(),
|
||||
None => APPLICATION_OCTET_STREAM,
|
||||
};
|
||||
|
||||
let string_mime = guessed_mime.to_string();
|
||||
|
|
@ -34,18 +35,21 @@ pub fn file_from_string(file_path: &str) -> Option<(String, &'static [u8])> {
|
|||
file_path,
|
||||
"/index.html",
|
||||
"/favicon.ico",
|
||||
"/logo.png",
|
||||
"/how-to-open.png",
|
||||
"/css/bulma.min.css",
|
||||
"/css/main.css",
|
||||
"/img/light_mode_installer_logo.png",
|
||||
"/img/dark_mode_installer_logo.png",
|
||||
"/thicc_logo_installer__ea_shadow.png",
|
||||
"/thicc_logo_installer_shadow.png",
|
||||
"/img/how-to-open.png",
|
||||
"/css/app.css",
|
||||
"/css/chunk-vendors.css",
|
||||
"/fonts/roboto-v18-latin-regular.eot",
|
||||
"/fonts/roboto-v18-latin-regular.woff",
|
||||
"/fonts/roboto-v18-latin-regular.woff2",
|
||||
"/js/vue.min.js",
|
||||
"/js/vue-router.min.js",
|
||||
"/js/helpers.js",
|
||||
"/js/views.js",
|
||||
"/js/main.js"
|
||||
"/fonts/materialdesignicons-webfont.eot",
|
||||
"/fonts/materialdesignicons-webfont.woff",
|
||||
"/fonts/materialdesignicons-webfont.woff2",
|
||||
"/js/chunk-vendors.js",
|
||||
"/js/app.js"
|
||||
)?;
|
||||
|
||||
Some((string_mime, contents))
|
||||
7
src/frontend/rest/mod.rs
Normal file
7
src/frontend/rest/mod.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
//! frontend/rest/mod.rs
|
||||
//!
|
||||
//! Contains the main web server used within the application.
|
||||
|
||||
mod assets;
|
||||
pub mod server;
|
||||
pub mod services;
|
||||
86
src/frontend/rest/server.rs
Normal file
86
src/frontend/rest/server.rs
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
//! frontend/rest/server.rs
|
||||
//!
|
||||
//! Contains the over-arching server object + methods to manipulate it.
|
||||
|
||||
use crate::frontend::rest::services::WebService;
|
||||
|
||||
use crate::installer::InstallerFramework;
|
||||
|
||||
use crate::logging::LoggingErrors;
|
||||
|
||||
use hyper::server::Http;
|
||||
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use std::net::{SocketAddr, TcpListener, ToSocketAddrs};
|
||||
|
||||
use std::thread;
|
||||
use std::thread::JoinHandle;
|
||||
|
||||
/// Acts as a communication mechanism between the Hyper WebService and the rest of the
|
||||
/// application.
|
||||
pub struct WebServer {
|
||||
_handle: JoinHandle<()>,
|
||||
}
|
||||
|
||||
impl WebServer {
|
||||
/// Creates a new web server with the specified address.
|
||||
pub fn with_addr(
|
||||
framework: Arc<RwLock<InstallerFramework>>,
|
||||
addr: SocketAddr,
|
||||
) -> Result<Self, hyper::Error> {
|
||||
let handle = thread::spawn(move || {
|
||||
let server = Http::new()
|
||||
.bind(&addr, move || Ok(WebService::new(framework.clone())))
|
||||
.log_expect("Failed to bind to port");
|
||||
|
||||
server.run().log_expect("Failed to run HTTP server");
|
||||
});
|
||||
|
||||
Ok(WebServer { _handle: handle })
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawns a server instance on all local interfaces.
|
||||
///
|
||||
/// Returns server instances + http address of service running.
|
||||
pub fn spawn_servers(framework: Arc<RwLock<InstallerFramework>>) -> (Vec<WebServer>, String) {
|
||||
// Firstly, allocate us an epidermal port
|
||||
let target_port = {
|
||||
let listener = TcpListener::bind("127.0.0.1:0")
|
||||
.log_expect("At least one local address should be free");
|
||||
listener
|
||||
.local_addr()
|
||||
.log_expect("Should be able to pull address from listener")
|
||||
.port()
|
||||
};
|
||||
|
||||
// Now, iterate over all ports
|
||||
let addresses = "localhost:0"
|
||||
.to_socket_addrs()
|
||||
.log_expect("No localhost address found");
|
||||
|
||||
let mut instances = Vec::with_capacity(addresses.len());
|
||||
let mut http_address = None;
|
||||
|
||||
// Startup HTTP server for handling the web view
|
||||
for mut address in addresses {
|
||||
address.set_port(target_port);
|
||||
|
||||
let server = WebServer::with_addr(framework.clone(), address)
|
||||
.log_expect("Failed to bind to address");
|
||||
|
||||
info!("Spawning server instance @ {:?}", address);
|
||||
|
||||
http_address = Some(address);
|
||||
|
||||
instances.push(server);
|
||||
}
|
||||
|
||||
let http_address = http_address.log_expect("No HTTP address found");
|
||||
|
||||
(
|
||||
instances,
|
||||
format!("http://localhost:{}", http_address.port()),
|
||||
)
|
||||
}
|
||||
29
src/frontend/rest/services/attributes.rs
Normal file
29
src/frontend/rest/services/attributes.rs
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
//! frontend/rest/services/attributes.rs
|
||||
//!
|
||||
//! The /api/attr call returns an executable script containing session variables.
|
||||
|
||||
use crate::frontend::rest::services::default_future;
|
||||
use crate::frontend::rest::services::Future;
|
||||
use crate::frontend::rest::services::Request;
|
||||
use crate::frontend::rest::services::Response;
|
||||
use crate::frontend::rest::services::WebService;
|
||||
|
||||
use hyper::header::{ContentLength, ContentType};
|
||||
|
||||
use crate::logging::LoggingErrors;
|
||||
|
||||
pub fn handle(service: &WebService, _req: Request) -> Future {
|
||||
let framework = service.get_framework_read();
|
||||
|
||||
let file = framework
|
||||
.base_attributes
|
||||
.to_json_str()
|
||||
.log_expect("Failed to render JSON representation of config");
|
||||
|
||||
default_future(
|
||||
Response::new()
|
||||
.with_header(ContentLength(file.len() as u64))
|
||||
.with_header(ContentType::json())
|
||||
.with_body(file),
|
||||
)
|
||||
}
|
||||
293
src/frontend/rest/services/authentication.rs
Normal file
293
src/frontend/rest/services/authentication.rs
Normal file
|
|
@ -0,0 +1,293 @@
|
|||
//! frontend/rest/services/authentication.rs
|
||||
//!
|
||||
//! Provides mechanisms to authenticate users using JWT.
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
use futures::{Future, Stream};
|
||||
|
||||
use hyper::header::{ContentLength, ContentType};
|
||||
|
||||
use jsonwebtoken::DecodingKey;
|
||||
use jwt::{decode, Algorithm, Validation};
|
||||
|
||||
use reqwest::header::USER_AGENT;
|
||||
|
||||
use crate::frontend::rest::services::Future as InternalFuture;
|
||||
use crate::frontend::rest::services::{default_future, Request, Response, WebService};
|
||||
|
||||
use crate::http::{build_async_client, build_client};
|
||||
|
||||
use crate::config::JWTValidation;
|
||||
|
||||
use crate::logging::LoggingErrors;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct Auth {
|
||||
username: String,
|
||||
token: String,
|
||||
jwt_token: Option<JWTClaims>,
|
||||
}
|
||||
|
||||
/// claims struct, it needs to derive `Serialize` and/or `Deserialize`
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct JWTClaims {
|
||||
pub sub: String,
|
||||
pub iss: String,
|
||||
pub aud: String,
|
||||
pub exp: usize,
|
||||
#[serde(default)]
|
||||
pub roles: Vec<String>,
|
||||
#[serde(rename = "releaseChannels", default)]
|
||||
pub channels: Vec<String>,
|
||||
#[serde(rename = "isPatreonAccountLinked")]
|
||||
pub is_linked: bool,
|
||||
#[serde(rename = "isPatreonSubscriptionActive")]
|
||||
pub is_subscribed: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
struct AuthRequest {
|
||||
username: String,
|
||||
token: String,
|
||||
}
|
||||
|
||||
/// Calls the given server to obtain a JWT token and returns a Future<String> with the response
|
||||
pub fn authenticate_async(
|
||||
url: String,
|
||||
username: String,
|
||||
token: String,
|
||||
) -> Box<dyn futures::Future<Item = String, Error = String>> {
|
||||
// Build the HTTP client up
|
||||
let client = match build_async_client() {
|
||||
Ok(v) => v,
|
||||
Err(_) => {
|
||||
return Box::new(futures::future::err(
|
||||
"Unable to build async web client".to_string(),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
Box::new(client.post(&url)
|
||||
.header(USER_AGENT, "liftinstall (j-selby)")
|
||||
.header("X-USERNAME", username.clone())
|
||||
.header("X-TOKEN", token.clone())
|
||||
.send()
|
||||
.map_err(|err| {
|
||||
format!("stream error {:?}, client: {:?}, http: {:?}, redirect: {:?}, serialization: {:?}, timeout: {:?}, server: {:?}",
|
||||
err, err.is_client_error(), err.is_http(), err.is_redirect(),
|
||||
err.is_serialization(), err.is_timeout(), err.is_server_error())
|
||||
})
|
||||
.map(|mut response| {
|
||||
match response.status() {
|
||||
reqwest::StatusCode::OK =>
|
||||
Ok(response.text()
|
||||
.map_err(|e| {
|
||||
format!("Error while converting the response to text {:?}", e)
|
||||
})),
|
||||
_ => {
|
||||
Err(format!("Error wrong response code from server {:?}", response.status()))
|
||||
}
|
||||
}
|
||||
})
|
||||
.and_then(|x| x)
|
||||
.flatten()
|
||||
)
|
||||
}
|
||||
|
||||
pub fn authenticate_sync(url: String, username: String, token: String) -> Result<String, String> {
|
||||
// Build the HTTP client up
|
||||
let client = build_client()?;
|
||||
|
||||
let mut response = client.post(&url)
|
||||
.header(USER_AGENT, "liftinstall (j-selby)")
|
||||
.header("X-USERNAME", username.clone())
|
||||
.header("X-TOKEN", token.clone())
|
||||
.send()
|
||||
.map_err(|err| {
|
||||
format!("stream error {:?}, client: {:?}, http: {:?}, redirect: {:?}, serialization: {:?}, timeout: {:?}, server: {:?}",
|
||||
err, err.is_client_error(), err.is_http(), err.is_redirect(),
|
||||
err.is_serialization(), err.is_timeout(), err.is_server_error())
|
||||
})?;
|
||||
|
||||
match response.status() {
|
||||
reqwest::StatusCode::OK => Ok(response
|
||||
.text()
|
||||
.map_err(|e| format!("Error while converting the response to text {:?}", e))?),
|
||||
_ => Err(format!(
|
||||
"Error wrong response code from server {:?}",
|
||||
response.status()
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn validate_token(
|
||||
body: String,
|
||||
pub_key_base64: String,
|
||||
validation: Option<JWTValidation>,
|
||||
) -> Result<JWTClaims, String> {
|
||||
// Get the public key for this authentication url
|
||||
let pub_key = if pub_key_base64.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
base64::decode(&pub_key_base64).map_err(|e| {
|
||||
format!(
|
||||
"Configured public key was not empty and did not decode as base64 {:?}",
|
||||
e
|
||||
)
|
||||
})?
|
||||
};
|
||||
|
||||
// Configure validation for audience and issuer if the configuration provides it
|
||||
let mut validation = match validation {
|
||||
Some(v) => {
|
||||
let mut valid = Validation::new(Algorithm::RS256);
|
||||
valid.iss = v.iss.map(|iss| {
|
||||
let mut issuer = HashSet::new();
|
||||
issuer.insert(iss);
|
||||
issuer
|
||||
});
|
||||
if let &Some(ref v) = &v.aud {
|
||||
valid.set_audience(&[v]);
|
||||
}
|
||||
valid
|
||||
}
|
||||
None => Validation::default(),
|
||||
};
|
||||
validation.validate_exp = false;
|
||||
validation.validate_nbf = false;
|
||||
// Verify the JWT token
|
||||
decode::<JWTClaims>(&body, &DecodingKey::from_rsa_der(&pub_key), &validation)
|
||||
.map(|tok| tok.claims)
|
||||
.map_err(|err| {
|
||||
format!(
|
||||
"Error while decoding the JWT. error: {:?} jwt: {:?}",
|
||||
err, body
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn handle(service: &WebService, _req: Request) -> InternalFuture {
|
||||
info!("Handling authentication");
|
||||
let framework = service
|
||||
.framework
|
||||
.read()
|
||||
.log_expect("InstallerFramework has been dirtied");
|
||||
let credentials = framework.database.credentials.clone();
|
||||
let config = framework
|
||||
.config
|
||||
.clone()
|
||||
.log_expect("No in-memory configuration found");
|
||||
|
||||
// If authentication isn't configured, just return immediately
|
||||
if config.authentication.is_none() {
|
||||
return default_future(Response::new().with_status(hyper::Ok).with_body("{}"));
|
||||
}
|
||||
|
||||
// Create moveable framework references so that the lambdas can write to them later
|
||||
let write_cred_fw = Arc::clone(&service.framework);
|
||||
|
||||
Box::new(
|
||||
_req.body()
|
||||
.concat2()
|
||||
.map(move |body| {
|
||||
let req = serde_json::from_slice::<AuthRequest>(&body);
|
||||
if req.is_err() {
|
||||
warn!("Failed to parse auth request from the frontend");
|
||||
return default_future(
|
||||
Response::new().with_status(hyper::StatusCode::BadRequest),
|
||||
);
|
||||
}
|
||||
let req = req.unwrap();
|
||||
|
||||
// Determine which credentials we should use
|
||||
let (username, token) = {
|
||||
let req_username = req.username;
|
||||
let req_token = req.token;
|
||||
|
||||
// if the user didn't provide credentials, and theres nothing stored in the
|
||||
// database, return an early error
|
||||
let req_cred_valid = !req_username.is_empty() && !req_token.is_empty();
|
||||
let stored_cred_valid =
|
||||
!credentials.username.is_empty() && !credentials.token.is_empty();
|
||||
|
||||
if !req_cred_valid && !stored_cred_valid {
|
||||
info!("No passed in credential and no stored credentials to validate");
|
||||
return default_future(Response::new().with_status(hyper::BadRequest));
|
||||
}
|
||||
|
||||
if req_cred_valid {
|
||||
(req_username.clone(), req_token.clone())
|
||||
} else {
|
||||
(credentials.username.clone(), credentials.token.clone())
|
||||
}
|
||||
};
|
||||
|
||||
// second copy of the credentials so we can move them into a different closure
|
||||
let (username_clone, token_clone) = (username.clone(), token.clone());
|
||||
|
||||
let authentication = config
|
||||
.authentication
|
||||
.log_expect("No authentication configuration");
|
||||
let auth_url = authentication.auth_url.clone();
|
||||
let pub_key_base64 = authentication.pub_key_base64.clone();
|
||||
let validation = authentication.validation.clone();
|
||||
|
||||
// call the authentication URL to see if we are authenticated
|
||||
Box::new(
|
||||
authenticate_async(auth_url, username.clone(), token.clone())
|
||||
.map(|body| validate_token(body, pub_key_base64, validation))
|
||||
.and_then(|res| res)
|
||||
.map(move |claims| {
|
||||
let out = Auth {
|
||||
username: username_clone,
|
||||
token: token_clone,
|
||||
jwt_token: Some(claims.clone()),
|
||||
};
|
||||
// Convert the json to a string and return the json token
|
||||
match serde_json::to_string(&out) {
|
||||
Ok(v) => Ok(v),
|
||||
Err(e) => Err(format!(
|
||||
"Error while converting the claims to JSON string: {:?}",
|
||||
e
|
||||
)),
|
||||
}
|
||||
})
|
||||
.and_then(|res| res)
|
||||
.map(move |json| {
|
||||
{
|
||||
// Store the validated username and password into the installer database
|
||||
let mut framework = write_cred_fw
|
||||
.write()
|
||||
.log_expect("InstallerFramework has been dirtied");
|
||||
framework.database.credentials.username = username;
|
||||
framework.database.credentials.token = token;
|
||||
}
|
||||
|
||||
// Finally return the JSON with the response
|
||||
info!("successfully verified username and token");
|
||||
Response::new()
|
||||
.with_header(ContentLength(json.len() as u64))
|
||||
.with_header(ContentType::json())
|
||||
.with_status(hyper::StatusCode::Ok)
|
||||
.with_body(json)
|
||||
})
|
||||
.map_err(|err| {
|
||||
error!(
|
||||
"Got an internal error while processing user token: {:?}",
|
||||
err
|
||||
);
|
||||
Response::new().with_status(hyper::StatusCode::InternalServerError)
|
||||
})
|
||||
.or_else(|err| {
|
||||
// Convert the Err value into an Ok value since the error code from
|
||||
// this HTTP request is an Ok(response)
|
||||
Ok(err)
|
||||
}),
|
||||
)
|
||||
})
|
||||
// Flatten the internal future into the output response future
|
||||
.flatten(),
|
||||
)
|
||||
}
|
||||
31
src/frontend/rest/services/browser.rs
Normal file
31
src/frontend/rest/services/browser.rs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
//! frontend/rest/services/browser.rs
|
||||
//!
|
||||
//! Launches the user's web browser on request from the frontend.
|
||||
|
||||
use crate::frontend::rest::services::Future as InternalFuture;
|
||||
use crate::frontend::rest::services::{Request, Response, WebService};
|
||||
use crate::logging::LoggingErrors;
|
||||
use futures::{Future, Stream};
|
||||
use hyper::header::ContentType;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
struct OpenRequest {
|
||||
url: String,
|
||||
}
|
||||
|
||||
pub fn handle(_service: &WebService, req: Request) -> InternalFuture {
|
||||
Box::new(req.body().concat2().map(move |body| {
|
||||
let req: OpenRequest = serde_json::from_slice(&body).log_expect("Malformed request");
|
||||
if webbrowser::open(&req.url).is_ok() {
|
||||
Response::new()
|
||||
.with_status(hyper::Ok)
|
||||
.with_header(ContentType::json())
|
||||
.with_body("{}")
|
||||
} else {
|
||||
Response::new()
|
||||
.with_status(hyper::BadRequest)
|
||||
.with_header(ContentType::json())
|
||||
.with_body("{}")
|
||||
}
|
||||
}))
|
||||
}
|
||||
84
src/frontend/rest/services/config.rs
Normal file
84
src/frontend/rest/services/config.rs
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
//! frontend/rest/services/config.rs
|
||||
//!
|
||||
//! The /api/config call returns the current installer framework configuration.
|
||||
//!
|
||||
//! This endpoint should be usable directly from a <script> tag during loading.
|
||||
|
||||
use crate::frontend::rest::services::Future;
|
||||
use crate::frontend::rest::services::Request;
|
||||
use crate::frontend::rest::services::Response;
|
||||
use crate::frontend::rest::services::WebService;
|
||||
|
||||
use hyper::header::{ContentLength, ContentType};
|
||||
|
||||
use crate::logging::LoggingErrors;
|
||||
|
||||
use crate::config::Config;
|
||||
|
||||
use crate::http::build_async_client;
|
||||
|
||||
use futures::stream::Stream;
|
||||
use futures::Future as _;
|
||||
|
||||
pub fn handle(service: &WebService, _req: Request) -> Future {
|
||||
let framework_url = {
|
||||
service
|
||||
.get_framework_read()
|
||||
.base_attributes
|
||||
.target_url
|
||||
.clone()
|
||||
};
|
||||
|
||||
info!("Downloading configuration from {:?}...", framework_url);
|
||||
|
||||
let framework = service.framework.clone();
|
||||
|
||||
// Hyper doesn't allow for clients to do sync network operations in a async future.
|
||||
// This smallish pipeline joins the two together.
|
||||
Box::new(
|
||||
build_async_client()
|
||||
.log_expect("Failed to build async client")
|
||||
.get(&framework_url)
|
||||
.send()
|
||||
.map_err(|x| {
|
||||
error!("HTTP error while downloading configuration file: {:?}", x);
|
||||
hyper::Error::Incomplete
|
||||
})
|
||||
.and_then(|x| {
|
||||
x.into_body().concat2().map_err(|x| {
|
||||
error!("HTTP error while parsing configuration file: {:?}", x);
|
||||
hyper::Error::Incomplete
|
||||
})
|
||||
})
|
||||
.and_then(move |x| {
|
||||
let x = String::from_utf8(x.to_vec()).map_err(|x| {
|
||||
error!("UTF-8 error while parsing configuration file: {:?}", x);
|
||||
hyper::Error::Incomplete
|
||||
})?;
|
||||
|
||||
let config = Config::from_toml_str(&x).map_err(|x| {
|
||||
error!("Serde error while parsing configuration file: {:?}", x);
|
||||
hyper::Error::Incomplete
|
||||
})?;
|
||||
|
||||
let mut framework = framework
|
||||
.write()
|
||||
.log_expect("Failed to get write lock for framework");
|
||||
|
||||
framework.config = Some(config);
|
||||
|
||||
info!("Configuration file downloaded successfully.");
|
||||
|
||||
let file = framework
|
||||
.get_config()
|
||||
.log_expect("Config should be loaded by now")
|
||||
.to_json_str()
|
||||
.log_expect("Failed to render JSON representation of config");
|
||||
|
||||
Ok(Response::new()
|
||||
.with_header(ContentLength(file.len() as u64))
|
||||
.with_header(ContentType::json())
|
||||
.with_body(file))
|
||||
}),
|
||||
)
|
||||
}
|
||||
27
src/frontend/rest/services/dark_mode.rs
Normal file
27
src/frontend/rest/services/dark_mode.rs
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
//! frontend/rest/services/dark_mode.rs
|
||||
//!
|
||||
//! This call returns if dark mode is enabled on the system currently.
|
||||
|
||||
use crate::frontend::rest::services::default_future;
|
||||
use crate::frontend::rest::services::Future;
|
||||
use crate::frontend::rest::services::Request;
|
||||
use crate::frontend::rest::services::Response;
|
||||
use crate::frontend::rest::services::WebService;
|
||||
|
||||
use hyper::header::{ContentLength, ContentType};
|
||||
|
||||
use crate::logging::LoggingErrors;
|
||||
|
||||
use crate::native::is_dark_mode_active;
|
||||
|
||||
pub fn handle(_service: &WebService, _req: Request) -> Future {
|
||||
let file = serde_json::to_string(&is_dark_mode_active())
|
||||
.log_expect("Failed to render JSON payload of installation status object");
|
||||
|
||||
default_future(
|
||||
Response::new()
|
||||
.with_header(ContentLength(file.len() as u64))
|
||||
.with_header(ContentType::json())
|
||||
.with_body(file),
|
||||
)
|
||||
}
|
||||
35
src/frontend/rest/services/default_path.rs
Normal file
35
src/frontend/rest/services/default_path.rs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
//! frontend/rest/services/default_path.rs
|
||||
//!
|
||||
//! The /api/default-path returns the default path for the application to install into.
|
||||
|
||||
use crate::frontend::rest::services::default_future;
|
||||
use crate::frontend::rest::services::Future;
|
||||
use crate::frontend::rest::services::Request;
|
||||
use crate::frontend::rest::services::Response;
|
||||
use crate::frontend::rest::services::WebService;
|
||||
|
||||
use hyper::header::{ContentLength, ContentType};
|
||||
|
||||
use crate::logging::LoggingErrors;
|
||||
|
||||
/// Struct used by serde to send a JSON payload to the client containing an optional value.
|
||||
#[derive(Serialize)]
|
||||
struct FileSelection {
|
||||
path: Option<String>,
|
||||
}
|
||||
|
||||
pub fn handle(service: &WebService, _req: Request) -> Future {
|
||||
let path = { service.get_framework_read().get_default_path() };
|
||||
|
||||
let response = FileSelection { path };
|
||||
|
||||
let file = serde_json::to_string(&response)
|
||||
.log_expect("Failed to render JSON payload of default path object");
|
||||
|
||||
default_future(
|
||||
Response::new()
|
||||
.with_header(ContentLength(file.len() as u64))
|
||||
.with_header(ContentType::json())
|
||||
.with_body(file),
|
||||
)
|
||||
}
|
||||
32
src/frontend/rest/services/exit.rs
Normal file
32
src/frontend/rest/services/exit.rs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
//! frontend/rest/services/exit.rs
|
||||
//!
|
||||
//! The /api/exit closes down the application.
|
||||
|
||||
use crate::frontend::rest::services::default_future;
|
||||
use crate::frontend::rest::services::Future;
|
||||
use crate::frontend::rest::services::Request;
|
||||
use crate::frontend::rest::services::Response;
|
||||
use crate::frontend::rest::services::WebService;
|
||||
|
||||
use hyper::header::ContentType;
|
||||
use hyper::StatusCode;
|
||||
|
||||
use std::process::exit;
|
||||
|
||||
pub fn handle(service: &WebService, _req: Request) -> Future {
|
||||
match service.get_framework_write().shutdown() {
|
||||
Ok(_) => {
|
||||
exit(0);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to complete framework shutdown: {:?}", e);
|
||||
|
||||
default_future(
|
||||
Response::new()
|
||||
.with_status(StatusCode::InternalServerError)
|
||||
.with_header(ContentType::plaintext())
|
||||
.with_body(format!("Failed to complete framework shutdown - {}", e)),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
100
src/frontend/rest/services/install.rs
Normal file
100
src/frontend/rest/services/install.rs
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
//! frontend/rest/services/install.rs
|
||||
//!
|
||||
//! The /api/install call installs a set of packages dictated by a POST request.
|
||||
|
||||
use crate::frontend::rest::services::stream_progress;
|
||||
use crate::frontend::rest::services::Future;
|
||||
use crate::frontend::rest::services::Request;
|
||||
use crate::frontend::rest::services::WebService;
|
||||
|
||||
use crate::logging::LoggingErrors;
|
||||
|
||||
use crate::installer::InstallMessage;
|
||||
|
||||
use futures::future::Future as _;
|
||||
use futures::stream::Stream;
|
||||
|
||||
use url::form_urlencoded;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub fn handle(service: &WebService, req: Request) -> Future {
|
||||
let framework = service.framework.clone();
|
||||
|
||||
Box::new(req.body().concat2().map(move |b| {
|
||||
let results = form_urlencoded::parse(b.as_ref())
|
||||
.into_owned()
|
||||
.collect::<HashMap<String, String>>();
|
||||
|
||||
let mut to_install = Vec::new();
|
||||
let mut path: Option<String> = None;
|
||||
let mut force_install = false;
|
||||
let mut install_desktop_shortcut = false;
|
||||
|
||||
// Transform results into just an array of stuff to install
|
||||
for (key, value) in &results {
|
||||
if key == "path" {
|
||||
path = Some(value.to_owned());
|
||||
continue;
|
||||
} else if key == "installDesktopShortcut" {
|
||||
info!("Found installDesktopShortcut {:?}", value);
|
||||
install_desktop_shortcut = value == "true";
|
||||
continue;
|
||||
}
|
||||
|
||||
if key == "mode" && value == "force" {
|
||||
force_install = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if value == "true" {
|
||||
to_install.push(key.to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
if !install_desktop_shortcut {
|
||||
let framework_ref = framework
|
||||
.read()
|
||||
.log_expect("InstallerFramework has been dirtied");
|
||||
install_desktop_shortcut = framework_ref.preexisting_install
|
||||
&& framework_ref
|
||||
.database
|
||||
.packages
|
||||
.first()
|
||||
.and_then(|x| Some(x.shortcuts.len() > 1))
|
||||
.unwrap_or(false);
|
||||
}
|
||||
|
||||
// The frontend always provides this
|
||||
let path =
|
||||
path.log_expect("No path specified by frontend when one should have already existed");
|
||||
|
||||
stream_progress(move |sender| {
|
||||
let mut framework = framework
|
||||
.write()
|
||||
.log_expect("InstallerFramework has been dirtied");
|
||||
|
||||
let new_install = !framework.preexisting_install;
|
||||
if new_install {
|
||||
framework.set_install_dir(&path);
|
||||
}
|
||||
|
||||
if let Err(v) = framework.install(
|
||||
to_install,
|
||||
&sender,
|
||||
new_install,
|
||||
install_desktop_shortcut,
|
||||
force_install,
|
||||
) {
|
||||
error!("Install error occurred: {:?}", v);
|
||||
if let Err(v) = sender.send(InstallMessage::Error(v)) {
|
||||
error!("Failed to send install error: {:?}", v);
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(v) = sender.send(InstallMessage::EOF) {
|
||||
error!("Failed to send EOF to client: {:?}", v);
|
||||
}
|
||||
})
|
||||
}))
|
||||
}
|
||||
32
src/frontend/rest/services/installation_status.rs
Normal file
32
src/frontend/rest/services/installation_status.rs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
//! frontend/rest/services/installation_status.rs
|
||||
//!
|
||||
//! The /api/installation-status call returns metadata relating to the current status of
|
||||
//! the installation.
|
||||
//!
|
||||
//! e.g. if the application is in maintenance mode
|
||||
|
||||
use crate::frontend::rest::services::default_future;
|
||||
use crate::frontend::rest::services::Future;
|
||||
use crate::frontend::rest::services::Request;
|
||||
use crate::frontend::rest::services::Response;
|
||||
use crate::frontend::rest::services::WebService;
|
||||
|
||||
use hyper::header::{ContentLength, ContentType};
|
||||
|
||||
use crate::logging::LoggingErrors;
|
||||
|
||||
pub fn handle(service: &WebService, _req: Request) -> Future {
|
||||
let framework = service.get_framework_read();
|
||||
|
||||
let response = framework.get_installation_status();
|
||||
|
||||
let file = serde_json::to_string(&response)
|
||||
.log_expect("Failed to render JSON payload of installation status object");
|
||||
|
||||
default_future(
|
||||
Response::new()
|
||||
.with_header(ContentLength(file.len() as u64))
|
||||
.with_header(ContentType::json())
|
||||
.with_body(file),
|
||||
)
|
||||
}
|
||||
158
src/frontend/rest/services/mod.rs
Normal file
158
src/frontend/rest/services/mod.rs
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
//! frontend/rest/services/mod.rs
|
||||
//!
|
||||
//! Provides all services used by the REST server.
|
||||
|
||||
use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||
|
||||
use crate::installer::{InstallMessage, InstallerFramework};
|
||||
|
||||
use hyper::server::Service;
|
||||
use hyper::{Method, StatusCode};
|
||||
|
||||
use crate::logging::LoggingErrors;
|
||||
|
||||
use std::sync::mpsc::{channel, Sender};
|
||||
|
||||
use std::thread;
|
||||
|
||||
use hyper::header::ContentType;
|
||||
|
||||
use futures::future::Future as _;
|
||||
use futures::sink::Sink;
|
||||
|
||||
mod attributes;
|
||||
pub mod authentication;
|
||||
mod browser;
|
||||
mod config;
|
||||
mod dark_mode;
|
||||
mod default_path;
|
||||
mod exit;
|
||||
mod install;
|
||||
mod installation_status;
|
||||
mod packages;
|
||||
mod static_files;
|
||||
mod uninstall;
|
||||
mod update_updater;
|
||||
mod verify_path;
|
||||
mod view_folder;
|
||||
|
||||
/// Expected incoming Request format from Hyper.
|
||||
pub type Request = hyper::server::Request;
|
||||
|
||||
/// Completed response type returned by the server.
|
||||
pub type Response = hyper::server::Response;
|
||||
|
||||
/// Error type returned by the server.
|
||||
pub type Error = hyper::Error;
|
||||
|
||||
/// The return type used by function calls to the web server.
|
||||
pub type Future = Box<dyn futures::Future<Item = Response, Error = Error>>;
|
||||
|
||||
/// If advanced functionality is not needed, return a default instant future.
|
||||
pub fn default_future(response: Response) -> Future {
|
||||
Box::new(futures::future::ok(response))
|
||||
}
|
||||
|
||||
/// Encapsulates JSON as a injectable Javascript script.
|
||||
pub fn encapsulate_json(field_name: &str, json: &str) -> String {
|
||||
format!("var {} = {};", field_name, json)
|
||||
}
|
||||
|
||||
/// Streams messages from a specified task to the client in a thread.
|
||||
pub fn stream_progress<F: 'static>(function: F) -> Response
|
||||
where
|
||||
F: FnOnce(Sender<InstallMessage>) -> () + Send,
|
||||
{
|
||||
let (sender, receiver) = channel();
|
||||
let (tx, rx) = hyper::Body::pair();
|
||||
|
||||
// Startup a thread to do this operation for us
|
||||
thread::spawn(move || function(sender));
|
||||
|
||||
// Spawn a thread for transforming messages to chunk messages
|
||||
thread::spawn(move || {
|
||||
let mut tx = tx;
|
||||
loop {
|
||||
let response = receiver
|
||||
.recv()
|
||||
.log_expect("Failed to receive message from runner thread");
|
||||
|
||||
if let InstallMessage::EOF = response {
|
||||
break;
|
||||
}
|
||||
|
||||
let mut response = serde_json::to_string(&response)
|
||||
.log_expect("Failed to render JSON logging response payload");
|
||||
response.push('\n');
|
||||
tx = tx
|
||||
.send(Ok(response.into_bytes().into()))
|
||||
.wait()
|
||||
.log_expect("Failed to write JSON response payload to client");
|
||||
}
|
||||
});
|
||||
|
||||
Response::new()
|
||||
.with_header(ContentType::plaintext())
|
||||
.with_body(rx)
|
||||
}
|
||||
|
||||
/// Holds internal state for a single Hyper instance. Multiple will exist.
|
||||
pub struct WebService {
|
||||
framework: Arc<RwLock<InstallerFramework>>,
|
||||
}
|
||||
|
||||
impl WebService {
|
||||
/// Returns an immutable reference to the framework. May block.
|
||||
pub fn get_framework_read(&self) -> RwLockReadGuard<InstallerFramework> {
|
||||
self.framework
|
||||
.read()
|
||||
.log_expect("InstallerFramework has been dirtied")
|
||||
}
|
||||
|
||||
/// Returns an immutable reference to the framework. May block.
|
||||
pub fn get_framework_write(&self) -> RwLockWriteGuard<InstallerFramework> {
|
||||
self.framework
|
||||
.write()
|
||||
.log_expect("InstallerFramework has been dirtied")
|
||||
}
|
||||
|
||||
/// Creates a new WebService instance. Multiple are likely going to exist at once,
|
||||
/// so create a lock to hold this.
|
||||
pub fn new(framework: Arc<RwLock<InstallerFramework>>) -> WebService {
|
||||
WebService { framework }
|
||||
}
|
||||
}
|
||||
|
||||
impl Service for WebService {
|
||||
type Request = Request;
|
||||
type Response = Response;
|
||||
type Error = Error;
|
||||
type Future = Future;
|
||||
|
||||
fn call(&self, req: Self::Request) -> Self::Future {
|
||||
let method = req.method().clone();
|
||||
let path = req.path().to_string();
|
||||
|
||||
match (method, path.as_str()) {
|
||||
(Method::Get, "/api/attrs") => attributes::handle(self, req),
|
||||
(Method::Get, "/api/config") => config::handle(self, req),
|
||||
(Method::Get, "/api/dark-mode") => dark_mode::handle(self, req),
|
||||
(Method::Get, "/api/default-path") => default_path::handle(self, req),
|
||||
(Method::Get, "/api/exit") => exit::handle(self, req),
|
||||
(Method::Get, "/api/packages") => packages::handle(self, req),
|
||||
(Method::Get, "/api/installation-status") => installation_status::handle(self, req),
|
||||
(Method::Get, "/api/view-local-folder") => view_folder::handle(self, req),
|
||||
(Method::Post, "/api/check-auth") => authentication::handle(self, req),
|
||||
(Method::Post, "/api/start-install") => install::handle(self, req),
|
||||
(Method::Post, "/api/open-browser") => browser::handle(self, req),
|
||||
(Method::Post, "/api/uninstall") => uninstall::handle(self, req),
|
||||
(Method::Post, "/api/update-updater") => update_updater::handle(self, req),
|
||||
(Method::Post, "/api/verify-path") => verify_path::handle(self, req),
|
||||
(Method::Get, _) => static_files::handle(self, req),
|
||||
e => {
|
||||
info!("Returned 404 for {:?}", e);
|
||||
default_future(Response::new().with_status(StatusCode::NotFound))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
31
src/frontend/rest/services/packages.rs
Normal file
31
src/frontend/rest/services/packages.rs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
//! frontend/rest/services/packages.rs
|
||||
//!
|
||||
//! The /api/packages call returns all the currently installed packages.
|
||||
|
||||
use crate::frontend::rest::services::default_future;
|
||||
use crate::frontend::rest::services::encapsulate_json;
|
||||
use crate::frontend::rest::services::Future;
|
||||
use crate::frontend::rest::services::Request;
|
||||
use crate::frontend::rest::services::Response;
|
||||
use crate::frontend::rest::services::WebService;
|
||||
|
||||
use hyper::header::{ContentLength, ContentType};
|
||||
|
||||
use crate::logging::LoggingErrors;
|
||||
|
||||
pub fn handle(service: &WebService, _req: Request) -> Future {
|
||||
let framework = service.get_framework_read();
|
||||
|
||||
let file = encapsulate_json(
|
||||
"packages",
|
||||
&serde_json::to_string(&framework.database)
|
||||
.log_expect("Failed to render JSON representation of database"),
|
||||
);
|
||||
|
||||
default_future(
|
||||
Response::new()
|
||||
.with_header(ContentLength(file.len() as u64))
|
||||
.with_header(ContentType::json())
|
||||
.with_body(file),
|
||||
)
|
||||
}
|
||||
42
src/frontend/rest/services/static_files.rs
Normal file
42
src/frontend/rest/services/static_files.rs
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
//! frontend/rest/services/static_files.rs
|
||||
//!
|
||||
//! The static files call returns static files embedded within the executable.
|
||||
//!
|
||||
//! e.g. index.html, main.js, ...
|
||||
|
||||
use crate::frontend::rest::assets;
|
||||
|
||||
use crate::frontend::rest::services::default_future;
|
||||
use crate::frontend::rest::services::Future;
|
||||
use crate::frontend::rest::services::Request;
|
||||
use crate::frontend::rest::services::Response;
|
||||
use crate::frontend::rest::services::WebService;
|
||||
|
||||
use hyper::header::{ContentLength, ContentType};
|
||||
use hyper::StatusCode;
|
||||
|
||||
use crate::logging::LoggingErrors;
|
||||
|
||||
pub fn handle(_service: &WebService, req: Request) -> Future {
|
||||
// At this point, we have a web browser client. Search for a index page
|
||||
// if needed
|
||||
let mut path: String = req.path().to_owned();
|
||||
if path.ends_with('/') {
|
||||
path += "index.html";
|
||||
}
|
||||
|
||||
default_future(match assets::file_from_string(&path) {
|
||||
Some((content_type, file)) => {
|
||||
let content_type = ContentType(
|
||||
content_type
|
||||
.parse()
|
||||
.log_expect("Failed to parse content type into correct representation"),
|
||||
);
|
||||
Response::new()
|
||||
.with_header(ContentLength(file.len() as u64))
|
||||
.with_header(content_type)
|
||||
.with_body(file)
|
||||
}
|
||||
None => Response::new().with_status(StatusCode::NotFound),
|
||||
})
|
||||
}
|
||||
34
src/frontend/rest/services/uninstall.rs
Normal file
34
src/frontend/rest/services/uninstall.rs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
//! frontend/rest/services/uninstall.rs
|
||||
//!
|
||||
//! The /api/uninstall call uninstalls all packages.
|
||||
|
||||
use crate::frontend::rest::services::default_future;
|
||||
use crate::frontend::rest::services::stream_progress;
|
||||
use crate::frontend::rest::services::Future;
|
||||
use crate::frontend::rest::services::Request;
|
||||
use crate::frontend::rest::services::WebService;
|
||||
|
||||
use crate::logging::LoggingErrors;
|
||||
|
||||
use crate::installer::InstallMessage;
|
||||
|
||||
pub fn handle(service: &WebService, _req: Request) -> Future {
|
||||
let framework = service.framework.clone();
|
||||
|
||||
default_future(stream_progress(move |sender| {
|
||||
let mut framework = framework
|
||||
.write()
|
||||
.log_expect("InstallerFramework has been dirtied");
|
||||
|
||||
if let Err(v) = framework.uninstall(&sender) {
|
||||
error!("Uninstall error occurred: {:?}", v);
|
||||
if let Err(v) = sender.send(InstallMessage::Error(v)) {
|
||||
error!("Failed to send uninstall error: {:?}", v);
|
||||
};
|
||||
}
|
||||
|
||||
if let Err(v) = sender.send(InstallMessage::EOF) {
|
||||
error!("Failed to send EOF to client: {:?}", v);
|
||||
}
|
||||
}))
|
||||
}
|
||||
34
src/frontend/rest/services/update_updater.rs
Normal file
34
src/frontend/rest/services/update_updater.rs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
//! frontend/rest/services/update_updater.rs
|
||||
//!
|
||||
//! The /api/update-updater call attempts to update the currently running updater.
|
||||
|
||||
use crate::frontend::rest::services::default_future;
|
||||
use crate::frontend::rest::services::stream_progress;
|
||||
use crate::frontend::rest::services::Future;
|
||||
use crate::frontend::rest::services::Request;
|
||||
use crate::frontend::rest::services::WebService;
|
||||
|
||||
use crate::logging::LoggingErrors;
|
||||
|
||||
use crate::installer::InstallMessage;
|
||||
|
||||
pub fn handle(service: &WebService, _req: Request) -> Future {
|
||||
let framework = service.framework.clone();
|
||||
|
||||
default_future(stream_progress(move |sender| {
|
||||
let mut framework = framework
|
||||
.write()
|
||||
.log_expect("InstallerFramework has been dirtied");
|
||||
|
||||
if let Err(v) = framework.update_updater(&sender) {
|
||||
error!("Self-update error occurred: {:?}", v);
|
||||
if let Err(v) = sender.send(InstallMessage::Error(v)) {
|
||||
error!("Failed to send self-update error: {:?}", v);
|
||||
};
|
||||
}
|
||||
|
||||
if let Err(v) = sender.send(InstallMessage::EOF) {
|
||||
error!("Failed to send EOF to client: {:?}", v);
|
||||
}
|
||||
}))
|
||||
}
|
||||
48
src/frontend/rest/services/verify_path.rs
Normal file
48
src/frontend/rest/services/verify_path.rs
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
//! frontend/rest/services/verify_path.rs
|
||||
//!
|
||||
//! The /api/verify-path returns whether the path exists or not.
|
||||
|
||||
use crate::frontend::rest::services::Future;
|
||||
use crate::frontend::rest::services::Request;
|
||||
use crate::frontend::rest::services::Response;
|
||||
use crate::frontend::rest::services::WebService;
|
||||
use url::form_urlencoded;
|
||||
|
||||
use hyper::header::{ContentLength, ContentType};
|
||||
|
||||
use futures::future::Future as _;
|
||||
use futures::stream::Stream;
|
||||
|
||||
use crate::logging::LoggingErrors;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Struct used by serde to send a JSON payload to the client containing an optional value.
|
||||
#[derive(Serialize)]
|
||||
struct VerifyResponse {
|
||||
exists: bool,
|
||||
}
|
||||
|
||||
pub fn handle(_service: &WebService, req: Request) -> Future {
|
||||
Box::new(req.body().concat2().map(move |b| {
|
||||
let results = form_urlencoded::parse(b.as_ref())
|
||||
.into_owned()
|
||||
.collect::<HashMap<String, String>>();
|
||||
let mut exists = false;
|
||||
if let Some(path) = results.get("path") {
|
||||
let path = PathBuf::from(path);
|
||||
exists = path.is_dir();
|
||||
}
|
||||
|
||||
let response = VerifyResponse { exists };
|
||||
|
||||
let file = serde_json::to_string(&response)
|
||||
.log_expect("Failed to render JSON payload of default path object");
|
||||
|
||||
Response::new()
|
||||
.with_header(ContentLength(file.len() as u64))
|
||||
.with_header(ContentType::json())
|
||||
.with_body(file)
|
||||
}))
|
||||
}
|
||||
35
src/frontend/rest/services/view_folder.rs
Normal file
35
src/frontend/rest/services/view_folder.rs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
//! frontend/rest/services/view_folder.rs
|
||||
//!
|
||||
//! The /api/view-local-folder returns whether the path exists or not.
|
||||
//! Side-effect: will open the folder in the default file manager if it exists.
|
||||
|
||||
use super::default_future;
|
||||
use crate::frontend::rest::services::Future;
|
||||
use crate::frontend::rest::services::Request;
|
||||
use crate::frontend::rest::services::Response;
|
||||
use crate::frontend::rest::services::WebService;
|
||||
|
||||
use hyper::header::{ContentLength, ContentType};
|
||||
|
||||
use crate::logging::LoggingErrors;
|
||||
use crate::native::open_in_shell;
|
||||
|
||||
pub fn handle(service: &WebService, _: Request) -> Future {
|
||||
let framework = service.get_framework_read();
|
||||
let mut response = false;
|
||||
let path = framework.install_path.clone();
|
||||
if let Some(path) = path {
|
||||
response = true;
|
||||
open_in_shell(path.as_path());
|
||||
}
|
||||
|
||||
let file = serde_json::to_string(&response)
|
||||
.log_expect("Failed to render JSON payload of installation status object");
|
||||
|
||||
default_future(
|
||||
Response::new()
|
||||
.with_header(ContentLength(file.len() as u64))
|
||||
.with_header(ContentType::json())
|
||||
.with_body(file),
|
||||
)
|
||||
}
|
||||
86
src/frontend/ui/mod.rs
Normal file
86
src/frontend/ui/mod.rs
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
//! frontend/ui/mod.rs
|
||||
//!
|
||||
//! Provides a web-view UI.
|
||||
|
||||
use anyhow::Result;
|
||||
use wry::{
|
||||
application::{
|
||||
dpi::LogicalSize,
|
||||
event::{Event, StartCause, WindowEvent},
|
||||
event_loop::{ControlFlow, EventLoop},
|
||||
window::{Icon, WindowBuilder},
|
||||
},
|
||||
webview::{RpcResponse, WebViewBuilder},
|
||||
};
|
||||
|
||||
use log::Level;
|
||||
|
||||
use crate::logging::LoggingErrors;
|
||||
|
||||
const ICON_DATA: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/icon-data.bin"));
|
||||
|
||||
/// Starts the main web UI. Will return when UI is closed.
|
||||
pub fn start_ui(app_name: &str, http_address: &str, is_launcher: bool) -> Result<()> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
crate::native::prepare_install_webview2(app_name).log_expect("Unable to install webview2");
|
||||
}
|
||||
let size = if is_launcher {
|
||||
(600.0, 300.0)
|
||||
} else {
|
||||
(1024.0, 600.0)
|
||||
};
|
||||
info!("Spawning web view instance");
|
||||
|
||||
let window_icon =
|
||||
Icon::from_rgba(ICON_DATA.to_vec(), 48, 48).log_expect("Unable to construct window icon");
|
||||
let event_loop = EventLoop::new();
|
||||
let window = WindowBuilder::new()
|
||||
.with_title(format!("{} Installer", app_name))
|
||||
.with_window_icon(Some(window_icon))
|
||||
.with_inner_size(LogicalSize::new(size.0, size.1))
|
||||
.with_resizable(false)
|
||||
.build(&event_loop)?;
|
||||
let _webview = WebViewBuilder::new(window)?
|
||||
.with_url(http_address)?
|
||||
.with_rpc_handler(|_, mut event| {
|
||||
debug!("Incoming payload: {:?}", event);
|
||||
match event.method.as_str() {
|
||||
"Test" => (),
|
||||
"Log" => {
|
||||
if let Some(msg) = event.params.take() {
|
||||
if let Ok(msg) = serde_json::from_value::<(String, String)>(msg) {
|
||||
let kind = match msg.0.as_str() {
|
||||
"info" | "log" => Level::Info,
|
||||
"warn" => Level::Warn,
|
||||
_ => Level::Error,
|
||||
};
|
||||
log!(target: "liftinstall::frontend::js", kind, "{}", msg.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
"SelectInstallDir" => {
|
||||
let result =
|
||||
tinyfiledialogs::select_folder_dialog("Select a install directory...", "")
|
||||
.and_then(|v| serde_json::to_value(v).ok());
|
||||
return Some(RpcResponse::new_result(event.id, result));
|
||||
}
|
||||
_ => warn!("Unknown RPC method: {}", event.method),
|
||||
}
|
||||
None
|
||||
})
|
||||
.build()?;
|
||||
|
||||
event_loop.run(move |event, _, control_flow| {
|
||||
*control_flow = ControlFlow::Wait;
|
||||
|
||||
match event {
|
||||
Event::NewEvents(StartCause::Init) => info!("Webview started"),
|
||||
Event::WindowEvent {
|
||||
event: WindowEvent::CloseRequested,
|
||||
..
|
||||
} => *control_flow = ControlFlow::Exit,
|
||||
_ => (),
|
||||
}
|
||||
});
|
||||
}
|
||||
51
src/http.rs
51
src/http.rs
|
|
@ -7,14 +7,16 @@ use reqwest::header::CONTENT_LENGTH;
|
|||
use std::io::Read;
|
||||
use std::time::Duration;
|
||||
|
||||
use reqwest::r#async::Client as AsyncClient;
|
||||
use reqwest::Client;
|
||||
use reqwest::StatusCode;
|
||||
|
||||
/// Asserts that a URL is valid HTTPS, else returns an error.
|
||||
pub fn assert_ssl(url: &str) -> Result<(), String> {
|
||||
if url.starts_with("https://") {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("Specified URL was not https"))
|
||||
Err("Specified URL was not https".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -26,32 +28,49 @@ pub fn build_client() -> Result<Client, String> {
|
|||
.map_err(|x| format!("Unable to build client: {:?}", x))
|
||||
}
|
||||
|
||||
/// Downloads a text file from the specified URL.
|
||||
pub fn download_text(url: &str) -> Result<String, String> {
|
||||
assert_ssl(url)?;
|
||||
|
||||
let mut client = build_client()?
|
||||
.get(url)
|
||||
.send()
|
||||
.map_err(|x| format!("Failed to GET resource: {:?}", x))?;
|
||||
|
||||
client
|
||||
.text()
|
||||
.map_err(|v| format!("Failed to get text from resource: {:?}", v))
|
||||
/// Builds a customised async HTTP client.
|
||||
pub fn build_async_client() -> Result<AsyncClient, String> {
|
||||
AsyncClient::builder()
|
||||
.timeout(Duration::from_secs(8))
|
||||
.build()
|
||||
.map_err(|x| format!("Unable to build client: {:?}", x))
|
||||
}
|
||||
|
||||
/// Streams a file from a HTTP server.
|
||||
pub fn stream_file<F>(url: &str, mut callback: F) -> Result<(), String>
|
||||
pub fn stream_file<F>(
|
||||
url: &str,
|
||||
authorization: Option<String>,
|
||||
mut callback: F,
|
||||
) -> Result<(), String>
|
||||
where
|
||||
F: FnMut(Vec<u8>, u64) -> (),
|
||||
{
|
||||
assert_ssl(url)?;
|
||||
|
||||
let mut client = build_client()?
|
||||
.get(url)
|
||||
let mut client = build_client()?.get(url);
|
||||
|
||||
if let Some(auth) = authorization {
|
||||
client = client.header("Authorization", format!("Bearer {}", auth));
|
||||
}
|
||||
|
||||
let mut client = client
|
||||
.send()
|
||||
.map_err(|x| format!("Failed to GET resource: {:?}", x))?;
|
||||
|
||||
match client.status() {
|
||||
StatusCode::OK => {}
|
||||
StatusCode::TOO_MANY_REQUESTS => {
|
||||
return Err(
|
||||
"Your token has exceeded the number of daily allowable IP addresses. \
|
||||
Please wait 24 hours and try again."
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
x => {
|
||||
return Err(format!("Bad status code: {:?}.", x));
|
||||
}
|
||||
}
|
||||
|
||||
let size = match client.headers().get(CONTENT_LENGTH) {
|
||||
Some(ref v) => v
|
||||
.to_str()
|
||||
|
|
|
|||
114
src/installer.rs
114
src/installer.rs
|
|
@ -18,29 +18,32 @@ use std::sync::mpsc::Sender;
|
|||
use std::io::copy;
|
||||
use std::io::Cursor;
|
||||
|
||||
use std::process::exit;
|
||||
use std::process::Command;
|
||||
use std::process::{exit, Stdio};
|
||||
|
||||
use config::BaseAttributes;
|
||||
use config::Config;
|
||||
use crate::config::BaseAttributes;
|
||||
use crate::config::Config;
|
||||
|
||||
use sources::types::Version;
|
||||
use crate::sources::types::Version;
|
||||
|
||||
use tasks::install::InstallTask;
|
||||
use tasks::uninstall::UninstallTask;
|
||||
use tasks::uninstall_global_shortcut::UninstallGlobalShortcutsTask;
|
||||
use tasks::DependencyTree;
|
||||
use tasks::TaskMessage;
|
||||
use crate::tasks::install::InstallTask;
|
||||
use crate::tasks::uninstall::UninstallTask;
|
||||
use crate::tasks::uninstall_global_shortcut::UninstallGlobalShortcutsTask;
|
||||
use crate::tasks::DependencyTree;
|
||||
use crate::tasks::TaskMessage;
|
||||
|
||||
use logging::LoggingErrors;
|
||||
use crate::logging::LoggingErrors;
|
||||
|
||||
use dirs::home_dir;
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::fs::remove_file;
|
||||
|
||||
use http;
|
||||
use crate::http;
|
||||
|
||||
use number_prefix::{decimal_prefix, Prefixed, Standalone};
|
||||
use number_prefix::NumberPrefix::{self, Prefixed, Standalone};
|
||||
|
||||
use crate::native;
|
||||
|
||||
/// A message thrown during the installation of packages.
|
||||
#[derive(Serialize)]
|
||||
|
|
@ -48,14 +51,25 @@ pub enum InstallMessage {
|
|||
Status(String, f64),
|
||||
PackageInstalled,
|
||||
Error(String),
|
||||
AuthorizationRequired(String),
|
||||
EOF,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Default, Clone)]
|
||||
pub struct Credentials {
|
||||
#[serde(default)]
|
||||
pub username: String,
|
||||
#[serde(default)]
|
||||
pub token: String,
|
||||
}
|
||||
|
||||
/// Metadata about the current installation itself.
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct InstallationDatabase {
|
||||
pub packages: Vec<LocalInstallation>,
|
||||
pub shortcuts: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub credentials: Credentials,
|
||||
}
|
||||
|
||||
impl InstallationDatabase {
|
||||
|
|
@ -64,6 +78,10 @@ impl InstallationDatabase {
|
|||
InstallationDatabase {
|
||||
packages: Vec::new(),
|
||||
shortcuts: Vec::new(),
|
||||
credentials: Credentials {
|
||||
username: String::new(),
|
||||
token: String::new(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -80,6 +98,7 @@ pub struct InstallerFramework {
|
|||
// If we just completed an uninstall, and we should clean up after ourselves.
|
||||
pub burn_after_exit: bool,
|
||||
pub launcher_path: Option<String>,
|
||||
pub is_windows: bool,
|
||||
}
|
||||
|
||||
/// Contains basic properties on the status of the session. Subset of InstallationFramework.
|
||||
|
|
@ -100,19 +119,25 @@ pub struct LocalInstallation {
|
|||
/// Relative paths to generated files
|
||||
pub files: Vec<String>,
|
||||
/// Absolute paths to generated shortcut files
|
||||
pub shortcuts: Vec<String>,
|
||||
pub shortcuts: HashSet<String>,
|
||||
}
|
||||
|
||||
macro_rules! declare_messenger_callback {
|
||||
($target:expr) => {
|
||||
&|msg: &TaskMessage| match msg {
|
||||
&TaskMessage::DisplayMessage(msg, progress) => {
|
||||
&|msg: &TaskMessage| match *msg {
|
||||
TaskMessage::DisplayMessage(msg, progress) => {
|
||||
if let Err(v) = $target.send(InstallMessage::Status(msg.to_string(), progress as _))
|
||||
{
|
||||
error!("Failed to submit queue message: {:?}", v);
|
||||
}
|
||||
}
|
||||
&TaskMessage::PackageInstalled => {
|
||||
TaskMessage::AuthorizationRequired(msg) => {
|
||||
if let Err(v) = $target.send(InstallMessage::AuthorizationRequired(msg.to_string()))
|
||||
{
|
||||
error!("Failed to submit queue message: {:?}", v);
|
||||
}
|
||||
}
|
||||
TaskMessage::PackageInstalled => {
|
||||
if let Err(v) = $target.send(InstallMessage::PackageInstalled) {
|
||||
error!("Failed to submit queue message: {:?}", v);
|
||||
}
|
||||
|
|
@ -151,11 +176,14 @@ impl InstallerFramework {
|
|||
/// items: Array of named packages to be installed/kept
|
||||
/// messages: Channel used to send progress messages
|
||||
/// fresh_install: If the install directory must be empty
|
||||
/// force_install: If the install directory should be erased first
|
||||
pub fn install(
|
||||
&mut self,
|
||||
items: Vec<String>,
|
||||
messages: &Sender<InstallMessage>,
|
||||
fresh_install: bool,
|
||||
create_desktop_shortcuts: bool,
|
||||
force_install: bool,
|
||||
) -> Result<(), String> {
|
||||
info!(
|
||||
"Framework: Installing {:?} to {:?}",
|
||||
|
|
@ -184,6 +212,8 @@ impl InstallerFramework {
|
|||
items,
|
||||
uninstall_items,
|
||||
fresh_install,
|
||||
create_desktop_shortcuts,
|
||||
force_install,
|
||||
});
|
||||
|
||||
let mut tree = DependencyTree::build(task);
|
||||
|
|
@ -250,7 +280,7 @@ impl InstallerFramework {
|
|||
let mut downloaded = 0;
|
||||
let mut data_storage: Vec<u8> = Vec::new();
|
||||
|
||||
http::stream_file(tool, |data, size| {
|
||||
http::stream_file(tool, None, |data, size| {
|
||||
{
|
||||
data_storage.extend_from_slice(&data);
|
||||
}
|
||||
|
|
@ -264,11 +294,11 @@ impl InstallerFramework {
|
|||
};
|
||||
|
||||
// Pretty print data volumes
|
||||
let pretty_current = match decimal_prefix(downloaded as f64) {
|
||||
let pretty_current = match NumberPrefix::decimal(downloaded as f64) {
|
||||
Standalone(bytes) => format!("{} bytes", bytes),
|
||||
Prefixed(prefix, n) => format!("{:.0} {}B", n, prefix),
|
||||
};
|
||||
let pretty_total = match decimal_prefix(size as f64) {
|
||||
let pretty_total = match NumberPrefix::decimal(size as f64) {
|
||||
Standalone(bytes) => format!("{} bytes", bytes),
|
||||
Prefixed(prefix, n) => format!("{:.0} {}B", n, prefix),
|
||||
};
|
||||
|
|
@ -328,7 +358,8 @@ impl InstallerFramework {
|
|||
x.to_str()
|
||||
.log_expect("Unable to convert argument to String")
|
||||
.to_string()
|
||||
}).collect();
|
||||
})
|
||||
.collect();
|
||||
|
||||
{
|
||||
let new_app_file = match File::create(&args_file) {
|
||||
|
|
@ -393,6 +424,29 @@ impl InstallerFramework {
|
|||
}
|
||||
}
|
||||
|
||||
/// Shuts down the installer instance.
|
||||
pub fn shutdown(&mut self) -> Result<(), String> {
|
||||
info!("Shutting down installer framework...");
|
||||
|
||||
if let Some(ref v) = self.launcher_path.take() {
|
||||
info!("Launching {:?}", v);
|
||||
|
||||
Command::new(v)
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()
|
||||
.map_err(|x| format!("Unable to start application: {:?}", x))?;
|
||||
}
|
||||
|
||||
if self.burn_after_exit {
|
||||
info!("Requesting that self be deleted after exit.");
|
||||
native::burn_on_exit(&self.base_attributes.name);
|
||||
self.burn_after_exit = false;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Creates a new instance of the Installer Framework with a specified Config.
|
||||
pub fn new(attrs: BaseAttributes) -> Self {
|
||||
InstallerFramework {
|
||||
|
|
@ -404,6 +458,25 @@ impl InstallerFramework {
|
|||
is_launcher: false,
|
||||
burn_after_exit: false,
|
||||
launcher_path: None,
|
||||
is_windows: cfg!(windows),
|
||||
}
|
||||
}
|
||||
|
||||
/// The special recovery mode for the Installer Framework.
|
||||
pub fn new_recovery_mode(attrs: BaseAttributes, install_path: &Path) -> Self {
|
||||
InstallerFramework {
|
||||
base_attributes: BaseAttributes {
|
||||
recovery: true,
|
||||
..attrs
|
||||
},
|
||||
config: None,
|
||||
database: InstallationDatabase::new(),
|
||||
install_path: Some(install_path.to_path_buf()),
|
||||
preexisting_install: true,
|
||||
is_launcher: false,
|
||||
burn_after_exit: false,
|
||||
launcher_path: None,
|
||||
is_windows: cfg!(windows),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -431,6 +504,7 @@ impl InstallerFramework {
|
|||
is_launcher: false,
|
||||
burn_after_exit: false,
|
||||
launcher_path: None,
|
||||
is_windows: cfg!(windows),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,8 @@ pub fn setup_logger(file_name: String) -> Result<(), fern::InitError> {
|
|||
record.level(),
|
||||
message
|
||||
))
|
||||
}).level(log::LevelFilter::Info)
|
||||
})
|
||||
.level(log::LevelFilter::Info)
|
||||
.chain(io::stdout())
|
||||
.chain(fern::log_file(file_name)?)
|
||||
.apply()?;
|
||||
|
|
|
|||
300
src/main.rs
300
src/main.rs
|
|
@ -7,10 +7,7 @@
|
|||
#![deny(unsafe_code)]
|
||||
#![deny(missing_docs)]
|
||||
|
||||
#[cfg(windows)]
|
||||
extern crate nfd;
|
||||
|
||||
extern crate web_view;
|
||||
extern crate wry;
|
||||
|
||||
extern crate futures;
|
||||
extern crate hyper;
|
||||
|
|
@ -30,7 +27,7 @@ extern crate semver;
|
|||
|
||||
extern crate dirs;
|
||||
extern crate tar;
|
||||
extern crate xz_decom;
|
||||
extern crate xz2;
|
||||
extern crate zip;
|
||||
|
||||
extern crate fern;
|
||||
|
|
@ -40,59 +37,44 @@ extern crate log;
|
|||
extern crate chrono;
|
||||
|
||||
extern crate clap;
|
||||
#[cfg(windows)]
|
||||
extern crate widestring;
|
||||
#[cfg(windows)]
|
||||
extern crate winapi;
|
||||
|
||||
#[cfg(not(windows))]
|
||||
extern crate slug;
|
||||
#[cfg(not(windows))]
|
||||
extern crate sysinfo;
|
||||
|
||||
extern crate jsonwebtoken as jwt;
|
||||
|
||||
extern crate base64;
|
||||
|
||||
mod archives;
|
||||
mod assets;
|
||||
mod config;
|
||||
mod frontend;
|
||||
mod http;
|
||||
mod installer;
|
||||
mod logging;
|
||||
mod native;
|
||||
mod rest;
|
||||
mod self_update;
|
||||
mod sources;
|
||||
mod tasks;
|
||||
|
||||
use web_view::*;
|
||||
|
||||
use installer::InstallerFramework;
|
||||
|
||||
#[cfg(windows)]
|
||||
use nfd::Response;
|
||||
|
||||
use rest::WebServer;
|
||||
|
||||
use std::net::TcpListener;
|
||||
use std::net::ToSocketAddrs;
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::sync::RwLock;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use std::process::exit;
|
||||
use std::process::Command;
|
||||
use std::{thread, time};
|
||||
|
||||
use std::fs::remove_file;
|
||||
use std::fs::File;
|
||||
|
||||
use logging::LoggingErrors;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::App;
|
||||
use clap::Arg;
|
||||
use log::Level;
|
||||
|
||||
use config::BaseAttributes;
|
||||
use std::fs;
|
||||
use std::process::{exit, Command, Stdio};
|
||||
|
||||
static RAW_CONFIG: &'static str = include_str!(concat!(env!("OUT_DIR"), "/bootstrap.toml"));
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
enum CallbackType {
|
||||
SelectInstallDir { callback_name: String },
|
||||
Log { msg: String, kind: String },
|
||||
}
|
||||
const RAW_CONFIG: &str = include_str!(concat!(env!("OUT_DIR"), "/bootstrap.toml"));
|
||||
|
||||
fn main() {
|
||||
let config = BaseAttributes::from_toml_str(RAW_CONFIG).expect("Config file could not be read");
|
||||
|
|
@ -100,6 +82,7 @@ fn main() {
|
|||
logging::setup_logger(format!("{}_installer.log", config.name))
|
||||
.expect("Unable to setup logging!");
|
||||
|
||||
// Parse CLI arguments
|
||||
let app_name = config.name.clone();
|
||||
|
||||
let app_about = format!("An interactive installer for {}", app_name);
|
||||
|
|
@ -112,7 +95,8 @@ fn main() {
|
|||
.value_name("TARGET")
|
||||
.help("Launches the specified executable after checking for updates")
|
||||
.takes_value(true),
|
||||
).arg(
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("swap")
|
||||
.long("swap")
|
||||
.value_name("TARGET")
|
||||
|
|
@ -125,109 +109,43 @@ fn main() {
|
|||
|
||||
info!("{} installer", app_name);
|
||||
|
||||
// Handle self-updating if needed
|
||||
let current_exe = std::env::current_exe().log_expect("Current executable could not be found");
|
||||
let current_path = current_exe
|
||||
.parent()
|
||||
.log_expect("Parent directory of executable could not be found");
|
||||
|
||||
// Check to see if we are currently in a self-update
|
||||
if let Some(to_path) = matches.value_of("swap") {
|
||||
let to_path = PathBuf::from(to_path);
|
||||
|
||||
// Sleep a little bit to allow Windows to close the previous file handle
|
||||
thread::sleep(time::Duration::from_millis(3000));
|
||||
|
||||
info!(
|
||||
"Swapping installer from {} to {}",
|
||||
current_exe.display(),
|
||||
to_path.display()
|
||||
);
|
||||
|
||||
// Attempt it a few times because Windows can hold a lock
|
||||
for i in 1..=5 {
|
||||
let swap_result = if cfg!(windows) {
|
||||
use std::fs::copy;
|
||||
|
||||
copy(¤t_exe, &to_path).map(|_x| ())
|
||||
} else {
|
||||
use std::fs::rename;
|
||||
|
||||
rename(¤t_exe, &to_path)
|
||||
};
|
||||
|
||||
match swap_result {
|
||||
Ok(_) => break,
|
||||
Err(e) => {
|
||||
if i < 5 {
|
||||
info!("Copy attempt failed: {:?}, retrying in 3 seconds.", e);
|
||||
thread::sleep(time::Duration::from_millis(3000));
|
||||
} else {
|
||||
let _: () = Err(e).log_expect("Copying new binary failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Command::new(to_path)
|
||||
.spawn()
|
||||
.log_expect("Unable to start child process");
|
||||
|
||||
exit(0);
|
||||
// Handle self-updating if needed
|
||||
self_update::perform_swap(¤t_exe, matches.value_of("swap"));
|
||||
if let Some(new_matches) = self_update::check_args(reinterpret_app, current_path) {
|
||||
matches = new_matches;
|
||||
}
|
||||
self_update::cleanup(current_path);
|
||||
|
||||
// If we just finished a update, we need to inject our previous command line arguments
|
||||
let args_file = current_path.join("args.json");
|
||||
|
||||
if args_file.exists() {
|
||||
let database: Vec<String> = {
|
||||
let metadata_file =
|
||||
File::open(&args_file).log_expect("Unable to open args file handle");
|
||||
|
||||
serde_json::from_reader(metadata_file).log_expect("Unable to read metadata file")
|
||||
};
|
||||
|
||||
matches = reinterpret_app.get_matches_from(database);
|
||||
|
||||
info!("Parsed command line arguments from original instance");
|
||||
remove_file(args_file).log_expect("Unable to clean up args file");
|
||||
}
|
||||
|
||||
// Cleanup any remaining new maintenance tool instances if they exist
|
||||
if cfg!(windows) {
|
||||
let updater_executable = current_path.join("maintenancetool_new.exe");
|
||||
|
||||
if updater_executable.exists() {
|
||||
// Sleep a little bit to allow Windows to close the previous file handle
|
||||
thread::sleep(time::Duration::from_millis(3000));
|
||||
|
||||
// Attempt it a few times because Windows can hold a lock
|
||||
for i in 1..=5 {
|
||||
let swap_result = remove_file(&updater_executable);
|
||||
match swap_result {
|
||||
Ok(_) => break,
|
||||
Err(e) => {
|
||||
if i < 5 {
|
||||
info!("Cleanup attempt failed: {:?}, retrying in 3 seconds.", e);
|
||||
thread::sleep(time::Duration::from_millis(3000));
|
||||
} else {
|
||||
warn!("Deleting temp binary failed after 5 attempts: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load in metadata as to learn about the environment
|
||||
// Load in metadata + setup the installer framework
|
||||
let mut fresh_install = false;
|
||||
let metadata_file = current_path.join("metadata.json");
|
||||
let mut framework = if metadata_file.exists() {
|
||||
info!("Using pre-existing metadata file: {:?}", metadata_file);
|
||||
InstallerFramework::new_with_db(config, current_path).log_expect("Unable to parse metadata")
|
||||
InstallerFramework::new_with_db(config.clone(), current_path).unwrap_or_else(|e| {
|
||||
error!("Failed to load metadata: {:?}", e);
|
||||
warn!("Entering recovery mode");
|
||||
InstallerFramework::new_recovery_mode(config, current_path)
|
||||
})
|
||||
} else {
|
||||
info!("Starting fresh install");
|
||||
fresh_install = true;
|
||||
InstallerFramework::new(config)
|
||||
};
|
||||
|
||||
// check for existing installs if we are running as a fresh install
|
||||
let installed_path = PathBuf::from(framework.get_default_path().unwrap());
|
||||
if fresh_install && installed_path.join("metadata.json").exists() {
|
||||
info!("Existing install detected! Copying Trying to launch this install instead");
|
||||
// Ignore the return value from this since it should exit the application if its successful
|
||||
let _ = replace_existing_install(¤t_exe, &installed_path);
|
||||
}
|
||||
|
||||
let is_launcher = if let Some(string) = matches.value_of("launcher") {
|
||||
framework.is_launcher = true;
|
||||
framework.launcher_path = Some(string.to_string());
|
||||
|
|
@ -236,97 +154,53 @@ fn main() {
|
|||
false
|
||||
};
|
||||
|
||||
// Firstly, allocate us an epidermal port
|
||||
let target_port = {
|
||||
let listener = TcpListener::bind("127.0.0.1:0")
|
||||
.log_expect("At least one local address should be free");
|
||||
listener
|
||||
.local_addr()
|
||||
.log_expect("Should be able to pull address from listener")
|
||||
.port()
|
||||
// Start up the UI
|
||||
frontend::launch(&app_name, is_launcher, framework);
|
||||
}
|
||||
|
||||
fn replace_existing_install(current_exe: &PathBuf, installed_path: &PathBuf) -> Result<(), String> {
|
||||
// Generate installer path
|
||||
let platform_extension = if cfg!(windows) {
|
||||
"maintenancetool.exe"
|
||||
} else {
|
||||
"maintenancetool"
|
||||
};
|
||||
|
||||
// Now, iterate over all ports
|
||||
let addresses = "localhost:0"
|
||||
.to_socket_addrs()
|
||||
.log_expect("No localhost address found");
|
||||
let new_tool = if cfg!(windows) {
|
||||
"maintenancetool_new.exe"
|
||||
} else {
|
||||
"maintenancetool_new"
|
||||
};
|
||||
|
||||
let mut servers = Vec::new();
|
||||
let mut http_address = None;
|
||||
|
||||
let framework = Arc::new(RwLock::new(framework));
|
||||
|
||||
// Startup HTTP server for handling the web view
|
||||
for mut address in addresses {
|
||||
address.set_port(target_port);
|
||||
|
||||
let server = WebServer::with_addr(framework.clone(), address)
|
||||
.log_expect("Failed to bind to address");
|
||||
|
||||
info!("Server: {:?}", address);
|
||||
|
||||
http_address = Some(address);
|
||||
|
||||
servers.push(server);
|
||||
if let Err(v) = fs::copy(current_exe, installed_path.join(new_tool)) {
|
||||
return Err(format!("Unable to copy installer binary: {:?}", v));
|
||||
}
|
||||
|
||||
let http_address = http_address.log_expect("No HTTP address found");
|
||||
let existing = installed_path
|
||||
.join(platform_extension)
|
||||
.into_os_string()
|
||||
.into_string();
|
||||
let new = installed_path.join(new_tool).into_os_string().into_string();
|
||||
if existing.is_ok() && new.is_ok() {
|
||||
// Remove NTFS alternate stream which tells the operating system that the updater was downloaded from the internet
|
||||
if cfg!(windows) {
|
||||
let _ = fs::remove_file(
|
||||
installed_path.join("maintenancetool_new.exe:Zone.Identifier:$DATA"),
|
||||
);
|
||||
}
|
||||
info!("Launching {:?}", existing);
|
||||
let success = Command::new(new.unwrap())
|
||||
.arg("--swap")
|
||||
.arg(existing.unwrap())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.spawn();
|
||||
if success.is_ok() {
|
||||
exit(0);
|
||||
} else {
|
||||
error!("Unable to start existing yuzu maintenance tool. Launching old one instead");
|
||||
}
|
||||
}
|
||||
|
||||
let http_address = format!("http://localhost:{}", http_address.port());
|
||||
|
||||
// Init the web view
|
||||
let size = if is_launcher { (600, 300) } else { (1024, 500) };
|
||||
|
||||
let resizable = false;
|
||||
let debug = true;
|
||||
|
||||
run(
|
||||
&format!("{} Installer", app_name),
|
||||
Content::Url(http_address),
|
||||
Some(size),
|
||||
resizable,
|
||||
debug,
|
||||
|_| {},
|
||||
|wv, msg, _| {
|
||||
let command: CallbackType =
|
||||
serde_json::from_str(msg).log_expect(&format!("Unable to parse string: {:?}", msg));
|
||||
|
||||
debug!("Incoming payload: {:?}", command);
|
||||
|
||||
match command {
|
||||
CallbackType::SelectInstallDir { callback_name } => {
|
||||
#[cfg(windows)]
|
||||
let result = match nfd::open_pick_folder(None)
|
||||
.log_expect("Unable to open folder dialog")
|
||||
{
|
||||
Response::Okay(v) => v,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
#[cfg(not(windows))]
|
||||
let result =
|
||||
wv.dialog(Dialog::ChooseDirectory, "Select a install directory...", "");
|
||||
|
||||
if !result.is_empty() {
|
||||
let result = serde_json::to_string(&result)
|
||||
.log_expect("Unable to serialize response");
|
||||
let command = format!("{}({});", callback_name, result);
|
||||
debug!("Injecting response: {}", command);
|
||||
wv.eval(&command);
|
||||
}
|
||||
}
|
||||
CallbackType::Log { msg, kind } => {
|
||||
let kind = match kind.as_ref() {
|
||||
"info" | "log" => Level::Info,
|
||||
"warn" => Level::Warn,
|
||||
"error" => Level::Error,
|
||||
_ => Level::Error,
|
||||
};
|
||||
|
||||
log!(target: "liftinstall::frontend-js", kind, "{}", msg);
|
||||
}
|
||||
}
|
||||
},
|
||||
(),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,76 +2,107 @@
|
|||
* Misc interop helpers.
|
||||
**/
|
||||
|
||||
// Explicitly use the Unicode version of the APIs
|
||||
#ifndef UNICODE
|
||||
#define UNICODE
|
||||
#endif
|
||||
|
||||
#ifndef _UNICODE
|
||||
#define _UNICODE
|
||||
#endif
|
||||
|
||||
#include "windows.h"
|
||||
#include "winnls.h"
|
||||
#include "shobjidl.h"
|
||||
#include "objbase.h"
|
||||
#include "objidl.h"
|
||||
#include "shlguid.h"
|
||||
#include "shlobj.h"
|
||||
|
||||
// https://stackoverflow.com/questions/52101827/windows-10-getsyscolor-does-not-get-dark-ui-color-theme
|
||||
extern "C" int isDarkThemeActive()
|
||||
{
|
||||
DWORD type;
|
||||
DWORD value;
|
||||
DWORD count = 4;
|
||||
LSTATUS st = RegGetValue(
|
||||
HKEY_CURRENT_USER,
|
||||
TEXT("Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"),
|
||||
TEXT("AppsUseLightTheme"),
|
||||
RRF_RT_REG_DWORD,
|
||||
&type,
|
||||
&value,
|
||||
&count);
|
||||
if (st == ERROR_SUCCESS && type == REG_DWORD)
|
||||
return value == 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
extern "C" int saveShortcut(
|
||||
const char *shortcutPath,
|
||||
const char *description,
|
||||
const char *path,
|
||||
const char *args,
|
||||
const char *workingDir) {
|
||||
char* errStr = NULL;
|
||||
const wchar_t *shortcutPath,
|
||||
const wchar_t *description,
|
||||
const wchar_t *path,
|
||||
const wchar_t *args,
|
||||
const wchar_t *workingDir,
|
||||
const wchar_t *exePath)
|
||||
{
|
||||
char *errStr = NULL;
|
||||
HRESULT h;
|
||||
IShellLink* shellLink = NULL;
|
||||
IPersistFile* persistFile = NULL;
|
||||
|
||||
#ifdef _WIN64
|
||||
wchar_t wName[MAX_PATH+1];
|
||||
#else
|
||||
WORD wName[MAX_PATH+1];
|
||||
#endif
|
||||
|
||||
int id;
|
||||
IShellLink *shellLink = NULL;
|
||||
IPersistFile *persistFile = NULL;
|
||||
|
||||
// Initialize the COM library
|
||||
h = CoInitialize(NULL);
|
||||
if (FAILED(h)) {
|
||||
if (FAILED(h))
|
||||
{
|
||||
errStr = "Failed to initialize COM library";
|
||||
goto err;
|
||||
}
|
||||
|
||||
h = CoCreateInstance( CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER,
|
||||
IID_IShellLink, (PVOID*)&shellLink );
|
||||
if (FAILED(h)) {
|
||||
h = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER,
|
||||
IID_IShellLink, (PVOID *)&shellLink);
|
||||
if (FAILED(h))
|
||||
{
|
||||
errStr = "Failed to create IShellLink";
|
||||
goto err;
|
||||
}
|
||||
|
||||
h = shellLink->QueryInterface(IID_IPersistFile, (PVOID*)&persistFile);
|
||||
if (FAILED(h)) {
|
||||
h = shellLink->QueryInterface(IID_IPersistFile, (PVOID *)&persistFile);
|
||||
if (FAILED(h))
|
||||
{
|
||||
errStr = "Failed to get IPersistFile";
|
||||
goto err;
|
||||
}
|
||||
|
||||
//Append the shortcut name to the folder
|
||||
MultiByteToWideChar(CP_UTF8,0,shortcutPath,-1,wName,MAX_PATH);
|
||||
|
||||
// Load the file if it exists, to get the values for anything
|
||||
// that we do not set. Ignore errors, such as if it does not exist.
|
||||
h = persistFile->Load(wName, 0);
|
||||
h = persistFile->Load(shortcutPath, 0);
|
||||
|
||||
// Set the fields for which the application has set a value
|
||||
if (description!=NULL)
|
||||
if (description != NULL)
|
||||
shellLink->SetDescription(description);
|
||||
if (path!=NULL)
|
||||
if (path != NULL)
|
||||
shellLink->SetPath(path);
|
||||
if (args!=NULL)
|
||||
// default to using the first icon in the exe (usually correct)
|
||||
if (exePath != NULL)
|
||||
shellLink->SetIconLocation(exePath, 0);
|
||||
if (args != NULL)
|
||||
shellLink->SetArguments(args);
|
||||
if (workingDir!=NULL)
|
||||
if (workingDir != NULL)
|
||||
shellLink->SetWorkingDirectory(workingDir);
|
||||
|
||||
//Save the shortcut to disk
|
||||
h = persistFile->Save(wName, TRUE);
|
||||
if (FAILED(h)) {
|
||||
h = persistFile->Save(shortcutPath, TRUE);
|
||||
if (FAILED(h))
|
||||
{
|
||||
errStr = "Failed to save shortcut";
|
||||
goto err;
|
||||
}
|
||||
|
||||
// Notify that a new shortcut was created using the shell api
|
||||
SHChangeNotify(SHCNE_CREATE, SHCNF_PATH, shortcutPath, NULL);
|
||||
SHChangeNotify(SHCNE_UPDATEITEM, SHCNF_PATH, shortcutPath, NULL);
|
||||
|
||||
persistFile->Release();
|
||||
shellLink->Release();
|
||||
CoUninitialize();
|
||||
|
|
@ -87,3 +118,65 @@ err:
|
|||
return h;
|
||||
}
|
||||
|
||||
extern "C" int spawnDetached(const wchar_t *app, const wchar_t *cmdline)
|
||||
{
|
||||
STARTUPINFOW si;
|
||||
PROCESS_INFORMATION pi;
|
||||
// make non-constant copy of the parameters
|
||||
// this is allowed per https://docs.microsoft.com/en-us/windows/desktop/api/processthreadsapi/nf-processthreadsapi-createprocessw#security-remarks
|
||||
wchar_t *app_copy = _wcsdup(app);
|
||||
wchar_t *cmdline_copy = _wcsdup(cmdline);
|
||||
|
||||
if (app_copy == NULL || cmdline_copy == NULL)
|
||||
{
|
||||
return GetLastError();
|
||||
}
|
||||
|
||||
ZeroMemory(&si, sizeof(si));
|
||||
si.cb = sizeof(si);
|
||||
ZeroMemory(&pi, sizeof(pi));
|
||||
|
||||
if (!CreateProcessW(app, // module name
|
||||
(LPWSTR)cmdline, // Command line, unicode is allowed
|
||||
NULL, // Process handle not inheritable
|
||||
NULL, // Thread handle not inheritable
|
||||
FALSE, // Set handle inheritance to FALSE
|
||||
CREATE_NO_WINDOW, // Create without window
|
||||
NULL, // Use parent's environment block
|
||||
NULL, // Use parent's starting directory
|
||||
&si, // Pointer to STARTUPINFO structure
|
||||
&pi) // Pointer to PROCESS_INFORMATION structure
|
||||
)
|
||||
{
|
||||
return GetLastError();
|
||||
}
|
||||
|
||||
// Close process and thread handles.
|
||||
CloseHandle(pi.hProcess);
|
||||
CloseHandle(pi.hThread);
|
||||
return 0;
|
||||
}
|
||||
|
||||
extern "C" HRESULT getSystemFolder(wchar_t *out_path)
|
||||
{
|
||||
PWSTR path = NULL;
|
||||
HRESULT result = SHGetKnownFolderPath(FOLDERID_System, 0, NULL, &path);
|
||||
if (result == S_OK)
|
||||
{
|
||||
wcscpy_s(out_path, MAX_PATH + 1, path);
|
||||
CoTaskMemFree(path);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
extern "C" HRESULT getDesktopFolder(wchar_t *out_path)
|
||||
{
|
||||
PWSTR path = NULL;
|
||||
HRESULT result = SHGetKnownFolderPath(FOLDERID_Desktop, 0, NULL, &path);
|
||||
if (result == S_OK)
|
||||
{
|
||||
wcscpy_s(out_path, MAX_PATH + 1, path);
|
||||
CoTaskMemFree(path);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,26 +1,95 @@
|
|||
//! Natives/platform specific interactions.
|
||||
|
||||
/// Basic definition of some running process.
|
||||
#[derive(Debug)]
|
||||
pub struct Process {
|
||||
pub pid: usize,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
mod natives {
|
||||
#![allow(non_upper_case_globals)]
|
||||
#![allow(non_camel_case_types)]
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use std::ffi::CString;
|
||||
const PROCESS_LEN: usize = 10192;
|
||||
const WV2_INSTALLER_DATA: &[u8] = include_bytes!("../../MicrosoftEdgeWebview2Setup.exe");
|
||||
|
||||
use logging::LoggingErrors;
|
||||
use crate::logging::LoggingErrors;
|
||||
|
||||
use std::env;
|
||||
use std::io::Write;
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
use std::path::Path;
|
||||
|
||||
use winapi::shared::minwindef::{DWORD, FALSE, MAX_PATH};
|
||||
|
||||
use winapi::shared::winerror::HRESULT;
|
||||
use winapi::um::processthreadsapi::OpenProcess;
|
||||
use winapi::um::psapi::{
|
||||
EnumProcessModulesEx, GetModuleFileNameExW, K32EnumProcesses, LIST_MODULES_ALL,
|
||||
};
|
||||
use winapi::um::shellapi::ShellExecuteW;
|
||||
use winapi::um::winnt::{
|
||||
HANDLE, PROCESS_QUERY_INFORMATION, PROCESS_TERMINATE, PROCESS_VM_READ,
|
||||
};
|
||||
use winapi::um::winuser::SW_SHOWDEFAULT;
|
||||
|
||||
use std::process::Command;
|
||||
use tempfile::Builder;
|
||||
use tinyfiledialogs::{message_box_yes_no, MessageBoxIcon, YesNo};
|
||||
use webview2::EnvironmentBuilder;
|
||||
use widestring::U16CString;
|
||||
|
||||
extern "C" {
|
||||
pub fn saveShortcut(
|
||||
shortcutPath: *const ::std::os::raw::c_char,
|
||||
description: *const ::std::os::raw::c_char,
|
||||
path: *const ::std::os::raw::c_char,
|
||||
args: *const ::std::os::raw::c_char,
|
||||
workingDir: *const ::std::os::raw::c_char,
|
||||
shortcutPath: *const winapi::ctypes::wchar_t,
|
||||
description: *const winapi::ctypes::wchar_t,
|
||||
path: *const winapi::ctypes::wchar_t,
|
||||
args: *const winapi::ctypes::wchar_t,
|
||||
workingDir: *const winapi::ctypes::wchar_t,
|
||||
exePath: *const winapi::ctypes::wchar_t,
|
||||
) -> ::std::os::raw::c_int;
|
||||
|
||||
pub fn isDarkThemeActive() -> ::std::os::raw::c_uint;
|
||||
|
||||
pub fn spawnDetached(
|
||||
app: *const winapi::ctypes::wchar_t,
|
||||
cmdline: *const winapi::ctypes::wchar_t,
|
||||
) -> ::std::os::raw::c_int;
|
||||
|
||||
pub fn getSystemFolder(out_path: *mut ::std::os::raw::c_ushort) -> HRESULT;
|
||||
|
||||
pub fn getDesktopFolder(out_path: *mut ::std::os::raw::c_ushort) -> HRESULT;
|
||||
}
|
||||
|
||||
pub fn prepare_install_webview2(name: &str) -> Result<(), String> {
|
||||
if EnvironmentBuilder::default()
|
||||
.get_available_browser_version_string()
|
||||
.is_ok()
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
if message_box_yes_no(&format!("{} installer", name), &format!("{} installer now requires Webview2 runtime to function properly.\nDo you wish to install it now?", name), MessageBoxIcon::Question, YesNo::Yes) == YesNo::No {
|
||||
std::process::exit(1);
|
||||
}
|
||||
let mut installer_file = Builder::new()
|
||||
.suffix(".exe")
|
||||
.tempfile()
|
||||
.log_expect("Unable to open the webview2 installer file");
|
||||
installer_file
|
||||
.write_all(&WV2_INSTALLER_DATA)
|
||||
.log_expect("Unable to write the webview2 installer file");
|
||||
let path = installer_file.path().to_owned();
|
||||
installer_file.keep().log_unwrap();
|
||||
Command::new(&path)
|
||||
.arg("/install")
|
||||
.spawn()
|
||||
.log_expect("Unable to run the webview2 installer")
|
||||
.wait()
|
||||
.log_unwrap();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Needed here for Windows interop
|
||||
|
|
@ -31,24 +100,77 @@ mod natives {
|
|||
target: &str,
|
||||
args: &str,
|
||||
working_dir: &str,
|
||||
exe_path: &str,
|
||||
) -> Result<String, String> {
|
||||
let source_file = format!(
|
||||
"{}\\Microsoft\\Windows\\Start Menu\\Programs\\{}.lnk",
|
||||
env::var("APPDATA").log_expect("APPDATA is bad, apparently"),
|
||||
name
|
||||
);
|
||||
create_shortcut_inner(
|
||||
source_file,
|
||||
name,
|
||||
description,
|
||||
target,
|
||||
args,
|
||||
working_dir,
|
||||
exe_path,
|
||||
)
|
||||
}
|
||||
|
||||
// Needed here for Windows interop
|
||||
#[allow(unsafe_code)]
|
||||
pub fn create_desktop_shortcut(
|
||||
name: &str,
|
||||
description: &str,
|
||||
target: &str,
|
||||
args: &str,
|
||||
working_dir: &str,
|
||||
exe_path: &str,
|
||||
) -> Result<String, String> {
|
||||
let mut cmd_path = [0u16; MAX_PATH + 1];
|
||||
let _result = unsafe { getDesktopFolder(cmd_path.as_mut_ptr()) };
|
||||
let source_path = format!(
|
||||
"{}\\{}.lnk",
|
||||
String::from_utf16_lossy(&cmd_path[..count_u16(&cmd_path)]).as_str(),
|
||||
name
|
||||
);
|
||||
create_shortcut_inner(
|
||||
source_path,
|
||||
name,
|
||||
description,
|
||||
target,
|
||||
args,
|
||||
working_dir,
|
||||
exe_path,
|
||||
)
|
||||
}
|
||||
|
||||
// Needed here for Windows interop
|
||||
#[allow(unsafe_code)]
|
||||
fn create_shortcut_inner(
|
||||
source_file: String,
|
||||
_name: &str,
|
||||
description: &str,
|
||||
target: &str,
|
||||
args: &str,
|
||||
working_dir: &str,
|
||||
exe_path: &str,
|
||||
) -> Result<String, String> {
|
||||
info!("Generating shortcut @ {:?}", source_file);
|
||||
|
||||
let native_target_dir = CString::new(source_file.clone())
|
||||
.log_expect("Error while converting to C-style string");
|
||||
let native_target_dir = U16CString::from_str(source_file.clone())
|
||||
.log_expect("Error while converting to wchar_t");
|
||||
let native_description =
|
||||
CString::new(description).log_expect("Error while converting to C-style string");
|
||||
U16CString::from_str(description).log_expect("Error while converting to wchar_t");
|
||||
let native_target =
|
||||
CString::new(target).log_expect("Error while converting to C-style string");
|
||||
let native_args = CString::new(args).log_expect("Error while converting to C-style string");
|
||||
U16CString::from_str(target).log_expect("Error while converting to wchar_t");
|
||||
let native_args =
|
||||
U16CString::from_str(args).log_expect("Error while converting to wchar_t");
|
||||
let native_working_dir =
|
||||
CString::new(working_dir).log_expect("Error while converting to C-style string");
|
||||
U16CString::from_str(working_dir).log_expect("Error while converting to wchar_t");
|
||||
let native_exe_path =
|
||||
U16CString::from_str(exe_path).log_expect("Error while converting to wchar_t");
|
||||
|
||||
let shortcutResult = unsafe {
|
||||
saveShortcut(
|
||||
|
|
@ -57,6 +179,7 @@ mod natives {
|
|||
native_target.as_ptr(),
|
||||
native_args.as_ptr(),
|
||||
native_working_dir.as_ptr(),
|
||||
native_exe_path.as_ptr(),
|
||||
)
|
||||
};
|
||||
|
||||
|
|
@ -69,6 +192,37 @@ mod natives {
|
|||
}
|
||||
}
|
||||
|
||||
// Needed to call unsafe function `ShellExecuteW` from `winapi` crate
|
||||
#[allow(unsafe_code)]
|
||||
pub fn open_in_shell(path: &Path) {
|
||||
let native_verb = U16CString::from_str("open").unwrap();
|
||||
// https://doc.rust-lang.org/std/os/windows/ffi/trait.OsStrExt.html#tymethod.encode_wide
|
||||
let mut native_path: Vec<u16> = path.as_os_str().encode_wide().collect();
|
||||
native_path.push(0); // NULL terminator
|
||||
unsafe {
|
||||
ShellExecuteW(
|
||||
std::ptr::null_mut(),
|
||||
native_verb.as_ptr(),
|
||||
native_path.as_ptr(),
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
SW_SHOWDEFAULT,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn count_u16(u16str: &[u16]) -> usize {
|
||||
let mut pos = 0;
|
||||
for x in u16str.iter() {
|
||||
if *x == 0 {
|
||||
break;
|
||||
}
|
||||
pos += 1;
|
||||
}
|
||||
pos
|
||||
}
|
||||
|
||||
/// Cleans up the installer
|
||||
pub fn burn_on_exit(app_name: &str) {
|
||||
let current_exe = env::current_exe().log_expect("Current executable could not be found");
|
||||
|
|
@ -82,6 +236,7 @@ mod natives {
|
|||
.to_str()
|
||||
.log_expect("Unable to convert tool path to string")
|
||||
.replace(" ", "\\ ");
|
||||
let tool_wv = format!("{}.WebView2", tool);
|
||||
|
||||
let log = path.join(format!("{}_installer.log", app_name));
|
||||
let log = log
|
||||
|
|
@ -89,55 +244,271 @@ mod natives {
|
|||
.log_expect("Unable to convert log path to string")
|
||||
.replace(" ", "\\ ");
|
||||
|
||||
let target_arguments = format!("ping 127.0.0.1 -n 3 > nul && del {} {}", tool, log);
|
||||
let install_path = path
|
||||
.to_str()
|
||||
.log_expect("Unable to convert path to string")
|
||||
.replace(" ", "\\ ");
|
||||
|
||||
let target_arguments = format!(
|
||||
"/C choice /C Y /N /D Y /T 2 & del {} {} & rmdir /Q /S {} & rmdir {}",
|
||||
tool, log, tool_wv, install_path
|
||||
);
|
||||
|
||||
info!("Launching cmd with {:?}", target_arguments);
|
||||
|
||||
Command::new("C:\\Windows\\system32\\cmd.exe")
|
||||
.arg("/C")
|
||||
.arg(&target_arguments)
|
||||
.spawn()
|
||||
.log_expect("Unable to start child process");
|
||||
// Needs to use `spawnDetached` which is an unsafe C/C++ function from interop.cpp
|
||||
#[allow(unsafe_code)]
|
||||
let spawn_result: i32 = unsafe {
|
||||
let mut cmd_path = [0u16; MAX_PATH + 1];
|
||||
let result = getSystemFolder(cmd_path.as_mut_ptr());
|
||||
let mut pos = 0;
|
||||
for x in cmd_path.iter() {
|
||||
if *x == 0 {
|
||||
break;
|
||||
}
|
||||
pos += 1;
|
||||
}
|
||||
if result != winapi::shared::winerror::S_OK {
|
||||
return;
|
||||
}
|
||||
|
||||
spawnDetached(
|
||||
U16CString::from_str(
|
||||
format!("{}\\cmd.exe", String::from_utf16_lossy(&cmd_path[..pos])).as_str(),
|
||||
)
|
||||
.log_expect("Unable to convert string to wchar_t")
|
||||
.as_ptr(),
|
||||
U16CString::from_str(target_arguments.as_str())
|
||||
.log_expect("Unable to convert string to wchar_t")
|
||||
.as_ptr(),
|
||||
)
|
||||
};
|
||||
|
||||
if spawn_result != 0 {
|
||||
warn!("Unable to start child process");
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unsafe_code)]
|
||||
fn get_process_handler(pid: u32) -> Option<HANDLE> {
|
||||
if pid == 0 {
|
||||
return None;
|
||||
}
|
||||
let options = PROCESS_QUERY_INFORMATION | PROCESS_VM_READ | PROCESS_TERMINATE;
|
||||
let process_handler = unsafe { OpenProcess(options, FALSE, pid as DWORD) };
|
||||
if process_handler.is_null() {
|
||||
let options = PROCESS_QUERY_INFORMATION | PROCESS_VM_READ;
|
||||
let process_handler = unsafe { OpenProcess(options, FALSE, pid as DWORD) };
|
||||
if process_handler.is_null() {
|
||||
None
|
||||
} else {
|
||||
Some(process_handler)
|
||||
}
|
||||
} else {
|
||||
Some(process_handler)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a list of running processes
|
||||
#[allow(unsafe_code)]
|
||||
pub fn get_process_names() -> Vec<super::Process> {
|
||||
// Port from https://github.com/GuillaumeGomez/sysinfo/blob/master/src/windows/system.rs
|
||||
// I think that 10192 as length will be enough to get all processes at once...
|
||||
let mut process_ids = [0 as DWORD; PROCESS_LEN];
|
||||
let mut cb_needed = 0;
|
||||
|
||||
let size = ::std::mem::size_of::<DWORD>() * process_ids.len();
|
||||
unsafe {
|
||||
if K32EnumProcesses(process_ids.as_mut_ptr(), size as DWORD, &mut cb_needed) == 0 {
|
||||
return vec![];
|
||||
}
|
||||
}
|
||||
|
||||
let nb_processes = cb_needed / ::std::mem::size_of::<DWORD>() as DWORD;
|
||||
|
||||
let mut processes = Vec::new();
|
||||
|
||||
for i in 0..nb_processes {
|
||||
let pid = process_ids[i as usize];
|
||||
|
||||
unsafe {
|
||||
if let Some(process_handler) = get_process_handler(pid) {
|
||||
let mut h_mod = ::std::ptr::null_mut();
|
||||
let mut process_name = [0u16; MAX_PATH + 1];
|
||||
let mut cb_needed = 0;
|
||||
|
||||
if EnumProcessModulesEx(
|
||||
process_handler,
|
||||
&mut h_mod,
|
||||
::std::mem::size_of::<DWORD>() as DWORD,
|
||||
&mut cb_needed,
|
||||
LIST_MODULES_ALL,
|
||||
) != 0
|
||||
{
|
||||
GetModuleFileNameExW(
|
||||
process_handler,
|
||||
h_mod,
|
||||
process_name.as_mut_ptr(),
|
||||
MAX_PATH as DWORD + 1,
|
||||
);
|
||||
|
||||
let mut pos = 0;
|
||||
for x in process_name.iter() {
|
||||
if *x == 0 {
|
||||
break;
|
||||
}
|
||||
pos += 1;
|
||||
}
|
||||
let name = String::from_utf16_lossy(&process_name[..pos]);
|
||||
|
||||
processes.push(super::Process {
|
||||
pid: pid as _,
|
||||
name,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
processes
|
||||
}
|
||||
|
||||
// Needed here for Windows interop
|
||||
#[allow(unsafe_code)]
|
||||
pub fn is_dark_mode_active() -> bool {
|
||||
unsafe { isDarkThemeActive() == 1 }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
mod natives {
|
||||
use std::fs::remove_file;
|
||||
use std::fs::{remove_dir, remove_file};
|
||||
|
||||
use std::env;
|
||||
|
||||
use logging::LoggingErrors;
|
||||
use crate::logging::LoggingErrors;
|
||||
|
||||
use sysinfo::{PidExt, ProcessExt, SystemExt};
|
||||
|
||||
use dirs;
|
||||
|
||||
use slug::slugify;
|
||||
use std::fs::{create_dir_all, File};
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn create_shortcut(
|
||||
name: &str,
|
||||
description: &str,
|
||||
target: &str,
|
||||
args: &str,
|
||||
working_dir: &str,
|
||||
exe_path: &str,
|
||||
) -> Result<String, String> {
|
||||
// TODO: no-op
|
||||
warn!("create_shortcut is stubbed!");
|
||||
// FIXME: no icon will be shown since no icon is provided
|
||||
let data_local_dir = dirs::data_local_dir();
|
||||
match data_local_dir {
|
||||
Some(x) => {
|
||||
let mut path = x;
|
||||
path.push("applications");
|
||||
match create_dir_all(path.to_path_buf()) {
|
||||
Ok(_) => (),
|
||||
Err(e) => {
|
||||
return Err(format!(
|
||||
"Local data directory does not exist and cannot be created: {}",
|
||||
e
|
||||
));
|
||||
}
|
||||
};
|
||||
path.push(format!("yuzu-maintenance-tool_{}.desktop", slugify(name))); // file name
|
||||
let desktop_file = format!(
|
||||
"[Desktop Entry]\nType=Application\nName={}\nExec=\"{}\" {}\nComment={}\nPath={}\nIcon=yuzu\n",
|
||||
name, target, args, description, working_dir
|
||||
);
|
||||
let desktop_f = File::create(path);
|
||||
let mut desktop_f = match desktop_f {
|
||||
Ok(file) => file,
|
||||
Err(e) => return Err(format!("Unable to create desktop file: {}", e)),
|
||||
};
|
||||
let desktop_f = desktop_f.write_all(desktop_file.as_bytes());
|
||||
match desktop_f {
|
||||
Ok(_) => Ok("".to_string()),
|
||||
Err(e) => Err(format!("Unable to write desktop file: {}", e)),
|
||||
}
|
||||
}
|
||||
// return error when failed to acquire local data directory
|
||||
None => Err("Unable to determine local data directory".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn create_shortcut(
|
||||
name: &str,
|
||||
description: &str,
|
||||
target: &str,
|
||||
args: &str,
|
||||
working_dir: &str,
|
||||
_exe_path: &str,
|
||||
) -> Result<String, String> {
|
||||
warn!("STUB! Creating shortcut is not implemented on macOS");
|
||||
Ok("".to_string())
|
||||
}
|
||||
|
||||
pub fn open_in_shell(path: &Path) {
|
||||
let shell: &str;
|
||||
if cfg!(target_os = "linux") {
|
||||
shell = "xdg-open";
|
||||
} else if cfg!(target_os = "macos") {
|
||||
shell = "open";
|
||||
} else {
|
||||
warn!("Unsupported platform");
|
||||
return;
|
||||
}
|
||||
Command::new(shell).arg(path).spawn().ok();
|
||||
}
|
||||
|
||||
/// Cleans up the installer
|
||||
pub fn burn_on_exit(app_name: &str) {
|
||||
let current_exe = env::current_exe().log_expect("Current executable could not be found");
|
||||
let exe_dir = current_exe
|
||||
.parent()
|
||||
.log_expect("Current executable directory cannot be found");
|
||||
|
||||
if let Err(e) = remove_file(exe_dir.join(format!("{}_installer.log", app_name))) {
|
||||
// No regular logging now.
|
||||
eprintln!("Failed to delete maintenance log: {:?}", e);
|
||||
};
|
||||
|
||||
// Thank god for *nix platforms
|
||||
if let Err(e) = remove_file(¤t_exe) {
|
||||
// No regular logging now.
|
||||
eprintln!("Failed to delete maintenancetool: {:?}", e);
|
||||
};
|
||||
// delete the directory if not empty and ignore errors (since we can't handle errors anymore)
|
||||
remove_dir(exe_dir).ok();
|
||||
}
|
||||
|
||||
let current_dir = env::current_dir().log_expect("Current directory cannot be found");
|
||||
/// Returns a list of running processes
|
||||
pub fn get_process_names() -> Vec<super::Process> {
|
||||
// a platform-independent implementation using sysinfo crate
|
||||
let mut processes: Vec<super::Process> = Vec::new();
|
||||
let mut system = sysinfo::System::new();
|
||||
system.refresh_all();
|
||||
for (pid, procs) in system.processes() {
|
||||
processes.push(super::Process {
|
||||
pid: pid.as_u32() as usize,
|
||||
name: procs.name().to_string(),
|
||||
});
|
||||
}
|
||||
processes // return running processes
|
||||
}
|
||||
|
||||
if let Err(e) = remove_file(current_dir.join(format!("{}_installer.log", app_name))) {
|
||||
// No regular logging now.
|
||||
eprintln!("Failed to delete installer log: {:?}", e);
|
||||
};
|
||||
/// Returns if dark mode is active on this system.
|
||||
pub fn is_dark_mode_active() -> bool {
|
||||
// No-op
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
473
src/rest.rs
473
src/rest.rs
|
|
@ -1,473 +0,0 @@
|
|||
//! rest.rs
|
||||
//!
|
||||
//! Provides a HTTP/REST server for both frontend<->backend communication, as well
|
||||
//! as talking to external applications.
|
||||
|
||||
use serde_json;
|
||||
|
||||
use futures::future;
|
||||
use futures::Future;
|
||||
use futures::Sink;
|
||||
use futures::Stream;
|
||||
|
||||
use hyper::header::{ContentLength, ContentType};
|
||||
use hyper::server::{Http, Request, Response, Service};
|
||||
use hyper::{self, Error as HyperError, Get, Post, StatusCode};
|
||||
|
||||
use url::form_urlencoded;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::net::SocketAddr;
|
||||
use std::process::exit;
|
||||
use std::process::Command;
|
||||
use std::process::Stdio;
|
||||
use std::sync::mpsc::channel;
|
||||
use std::sync::Arc;
|
||||
use std::sync::RwLock;
|
||||
use std::thread::{self, JoinHandle};
|
||||
|
||||
use assets;
|
||||
|
||||
use installer::InstallMessage;
|
||||
use installer::InstallerFramework;
|
||||
|
||||
use logging::LoggingErrors;
|
||||
|
||||
use http;
|
||||
|
||||
use config::Config;
|
||||
|
||||
use native;
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct FileSelection {
|
||||
path: Option<String>,
|
||||
}
|
||||
|
||||
/// Acts as a communication mechanism between the Hyper WebService and the rest of the
|
||||
/// application.
|
||||
pub struct WebServer {
|
||||
_handle: JoinHandle<()>,
|
||||
}
|
||||
|
||||
impl WebServer {
|
||||
/// Creates a new web server with the specified address.
|
||||
pub fn with_addr(
|
||||
framework: Arc<RwLock<InstallerFramework>>,
|
||||
addr: SocketAddr,
|
||||
) -> Result<Self, HyperError> {
|
||||
let handle = thread::spawn(move || {
|
||||
let server = Http::new()
|
||||
.bind(&addr, move || {
|
||||
Ok(WebService {
|
||||
framework: framework.clone(),
|
||||
})
|
||||
}).log_expect("Failed to bind to port");
|
||||
|
||||
server.run().log_expect("Failed to run HTTP server");
|
||||
});
|
||||
|
||||
Ok(WebServer { _handle: handle })
|
||||
}
|
||||
}
|
||||
|
||||
/// Holds internal state for Hyper
|
||||
struct WebService {
|
||||
framework: Arc<RwLock<InstallerFramework>>,
|
||||
}
|
||||
|
||||
impl Service for WebService {
|
||||
type Request = Request;
|
||||
type Response = Response;
|
||||
type Error = hyper::Error;
|
||||
type Future = Box<Future<Item = Self::Response, Error = Self::Error>>;
|
||||
|
||||
/// HTTP request handler
|
||||
fn call(&self, req: Self::Request) -> Self::Future {
|
||||
Box::new(future::ok(match (req.method(), req.path()) {
|
||||
// This endpoint should be usable directly from a <script> tag during loading.
|
||||
(&Get, "/api/attrs") => {
|
||||
let framework = self
|
||||
.framework
|
||||
.read()
|
||||
.log_expect("InstallerFramework has been dirtied");
|
||||
|
||||
let file = encapsulate_json(
|
||||
"base_attributes",
|
||||
&framework
|
||||
.base_attributes
|
||||
.to_json_str()
|
||||
.log_expect("Failed to render JSON representation of config"),
|
||||
);
|
||||
|
||||
Response::<hyper::Body>::new()
|
||||
.with_header(ContentLength(file.len() as u64))
|
||||
.with_header(ContentType::json())
|
||||
.with_body(file)
|
||||
}
|
||||
// Returns the web config loaded
|
||||
(&Get, "/api/config") => {
|
||||
let mut framework = self
|
||||
.framework
|
||||
.write()
|
||||
.log_expect("InstallerFramework has been dirtied");
|
||||
|
||||
info!(
|
||||
"Downloading configuration from {:?}...",
|
||||
framework.base_attributes.target_url
|
||||
);
|
||||
|
||||
match http::download_text(&framework.base_attributes.target_url)
|
||||
.map(|x| Config::from_toml_str(&x))
|
||||
{
|
||||
Ok(Ok(config)) => {
|
||||
framework.config = Some(config.clone());
|
||||
|
||||
info!("Configuration file downloaded successfully.");
|
||||
|
||||
let file = framework
|
||||
.get_config()
|
||||
.log_expect("Config should be loaded by now")
|
||||
.to_json_str()
|
||||
.log_expect("Failed to render JSON representation of config");
|
||||
|
||||
Response::<hyper::Body>::new()
|
||||
.with_header(ContentLength(file.len() as u64))
|
||||
.with_header(ContentType::json())
|
||||
.with_body(file)
|
||||
}
|
||||
Ok(Err(v)) => {
|
||||
error!("Bad configuration file: {:?}", v);
|
||||
|
||||
Response::<hyper::Body>::new()
|
||||
.with_status(StatusCode::ServiceUnavailable)
|
||||
.with_header(ContentType::plaintext())
|
||||
.with_body("Bad HTTP response")
|
||||
}
|
||||
Err(v) => {
|
||||
error!(
|
||||
"General connectivity error while downloading config: {:?}",
|
||||
v
|
||||
);
|
||||
|
||||
Response::<hyper::Body>::new()
|
||||
.with_status(StatusCode::ServiceUnavailable)
|
||||
.with_header(ContentLength(v.len() as u64))
|
||||
.with_header(ContentType::plaintext())
|
||||
.with_body(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
// This endpoint should be usable directly from a <script> tag during loading.
|
||||
(&Get, "/api/packages") => {
|
||||
let framework = self
|
||||
.framework
|
||||
.read()
|
||||
.log_expect("InstallerFramework has been dirtied");
|
||||
|
||||
let file = encapsulate_json(
|
||||
"packages",
|
||||
&serde_json::to_string(&framework.database)
|
||||
.log_expect("Failed to render JSON representation of database"),
|
||||
);
|
||||
|
||||
Response::<hyper::Body>::new()
|
||||
.with_header(ContentLength(file.len() as u64))
|
||||
.with_header(ContentType::json())
|
||||
.with_body(file)
|
||||
}
|
||||
// Returns the default path for a installation
|
||||
(&Get, "/api/default-path") => {
|
||||
let framework = self
|
||||
.framework
|
||||
.read()
|
||||
.log_expect("InstallerFramework has been dirtied");
|
||||
let path = framework.get_default_path();
|
||||
|
||||
let response = FileSelection { path };
|
||||
|
||||
let file = serde_json::to_string(&response)
|
||||
.log_expect("Failed to render JSON payload of default path object");
|
||||
|
||||
Response::<hyper::Body>::new()
|
||||
.with_header(ContentLength(file.len() as u64))
|
||||
.with_header(ContentType::json())
|
||||
.with_body(file)
|
||||
}
|
||||
// Immediately exits the application
|
||||
(&Get, "/api/exit") => {
|
||||
let framework = self
|
||||
.framework
|
||||
.read()
|
||||
.log_expect("InstallerFramework has been dirtied");
|
||||
|
||||
if let Some(ref v) = framework.launcher_path {
|
||||
Command::new(v)
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()
|
||||
.log_expect("Unable to start child process");
|
||||
}
|
||||
|
||||
if framework.burn_after_exit {
|
||||
native::burn_on_exit(&framework.base_attributes.name);
|
||||
}
|
||||
|
||||
exit(0);
|
||||
}
|
||||
// Gets properties such as if the application is in maintenance mode
|
||||
(&Get, "/api/installation-status") => {
|
||||
let framework = self
|
||||
.framework
|
||||
.read()
|
||||
.log_expect("InstallerFramework has been dirtied");
|
||||
|
||||
let response = framework.get_installation_status();
|
||||
|
||||
let file = serde_json::to_string(&response)
|
||||
.log_expect("Failed to render JSON payload of installation status object");
|
||||
|
||||
Response::<hyper::Body>::new()
|
||||
.with_header(ContentLength(file.len() as u64))
|
||||
.with_header(ContentType::json())
|
||||
.with_body(file)
|
||||
}
|
||||
// Streams the installation of a particular set of packages
|
||||
(&Post, "/api/uninstall") => {
|
||||
// We need to bit of pipelining to get this to work
|
||||
let framework = self.framework.clone();
|
||||
|
||||
return Box::new(req.body().concat2().map(move |_b| {
|
||||
let (sender, receiver) = channel();
|
||||
let (tx, rx) = hyper::Body::pair();
|
||||
|
||||
// Startup a thread to do this operation for us
|
||||
thread::spawn(move || {
|
||||
let mut framework = framework
|
||||
.write()
|
||||
.log_expect("InstallerFramework has been dirtied");
|
||||
|
||||
if let Err(v) = framework.uninstall(&sender) {
|
||||
error!("Uninstall error occurred: {:?}", v);
|
||||
if let Err(v) = sender.send(InstallMessage::Error(v)) {
|
||||
error!("Failed to send uninstall error: {:?}", v);
|
||||
};
|
||||
}
|
||||
|
||||
if let Err(v) = sender.send(InstallMessage::EOF) {
|
||||
error!("Failed to send EOF to client: {:?}", v);
|
||||
}
|
||||
});
|
||||
|
||||
// Spawn a thread for transforming messages to chunk messages
|
||||
thread::spawn(move || {
|
||||
let mut tx = tx;
|
||||
loop {
|
||||
let response = receiver
|
||||
.recv()
|
||||
.log_expect("Failed to receive message from runner thread");
|
||||
|
||||
if let InstallMessage::EOF = response {
|
||||
break;
|
||||
}
|
||||
|
||||
let mut response = serde_json::to_string(&response)
|
||||
.log_expect("Failed to render JSON logging response payload");
|
||||
response.push('\n');
|
||||
tx = tx
|
||||
.send(Ok(response.into_bytes().into()))
|
||||
.wait()
|
||||
.log_expect("Failed to write JSON response payload to client");
|
||||
}
|
||||
});
|
||||
|
||||
Response::<hyper::Body>::new()
|
||||
//.with_header(ContentLength(file.len() as u64))
|
||||
.with_header(ContentType::plaintext())
|
||||
.with_body(rx)
|
||||
}));
|
||||
}
|
||||
// Updates the installer
|
||||
(&Post, "/api/update-updater") => {
|
||||
// We need to bit of pipelining to get this to work
|
||||
let framework = self.framework.clone();
|
||||
|
||||
return Box::new(req.body().concat2().map(move |_b| {
|
||||
let (sender, receiver) = channel();
|
||||
let (tx, rx) = hyper::Body::pair();
|
||||
|
||||
// Startup a thread to do this operation for us
|
||||
thread::spawn(move || {
|
||||
let mut framework = framework
|
||||
.write()
|
||||
.log_expect("InstallerFramework has been dirtied");
|
||||
|
||||
if let Err(v) = framework.update_updater(&sender) {
|
||||
error!("Self-update error occurred: {:?}", v);
|
||||
if let Err(v) = sender.send(InstallMessage::Error(v)) {
|
||||
error!("Failed to send self-update error: {:?}", v);
|
||||
};
|
||||
}
|
||||
|
||||
if let Err(v) = sender.send(InstallMessage::EOF) {
|
||||
error!("Failed to send EOF to client: {:?}", v);
|
||||
}
|
||||
});
|
||||
|
||||
// Spawn a thread for transforming messages to chunk messages
|
||||
thread::spawn(move || {
|
||||
let mut tx = tx;
|
||||
loop {
|
||||
let response = receiver
|
||||
.recv()
|
||||
.log_expect("Failed to receive message from runner thread");
|
||||
|
||||
if let InstallMessage::EOF = response {
|
||||
break;
|
||||
}
|
||||
|
||||
let mut response = serde_json::to_string(&response)
|
||||
.log_expect("Failed to render JSON logging response payload");
|
||||
response.push('\n');
|
||||
tx = tx
|
||||
.send(Ok(response.into_bytes().into()))
|
||||
.wait()
|
||||
.log_expect("Failed to write JSON response payload to client");
|
||||
}
|
||||
});
|
||||
|
||||
Response::<hyper::Body>::new()
|
||||
//.with_header(ContentLength(file.len() as u64))
|
||||
.with_header(ContentType::plaintext())
|
||||
.with_body(rx)
|
||||
}));
|
||||
}
|
||||
// Streams the installation of a particular set of packages
|
||||
(&Post, "/api/start-install") => {
|
||||
// We need to bit of pipelining to get this to work
|
||||
let framework = self.framework.clone();
|
||||
|
||||
return Box::new(req.body().concat2().map(move |b| {
|
||||
let results = form_urlencoded::parse(b.as_ref())
|
||||
.into_owned()
|
||||
.collect::<HashMap<String, String>>();
|
||||
|
||||
let mut to_install = Vec::new();
|
||||
let mut path: Option<String> = None;
|
||||
|
||||
// Transform results into just an array of stuff to install
|
||||
for (key, value) in &results {
|
||||
if key == "path" {
|
||||
path = Some(value.to_owned());
|
||||
continue;
|
||||
}
|
||||
|
||||
if value == "true" {
|
||||
to_install.push(key.to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
// The frontend always provides this
|
||||
let path = path.log_expect(
|
||||
"No path specified by frontend when one should have already existed",
|
||||
);
|
||||
|
||||
let (sender, receiver) = channel();
|
||||
let (tx, rx) = hyper::Body::pair();
|
||||
|
||||
// Startup a thread to do this operation for us
|
||||
thread::spawn(move || {
|
||||
let mut framework = framework
|
||||
.write()
|
||||
.log_expect("InstallerFramework has been dirtied");
|
||||
|
||||
let new_install = !framework.preexisting_install;
|
||||
if new_install {
|
||||
framework.set_install_dir(&path);
|
||||
}
|
||||
|
||||
if let Err(v) = framework.install(to_install, &sender, new_install) {
|
||||
error!("Install error occurred: {:?}", v);
|
||||
if let Err(v) = sender.send(InstallMessage::Error(v)) {
|
||||
error!("Failed to send install error: {:?}", v);
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(v) = sender.send(InstallMessage::EOF) {
|
||||
error!("Failed to send EOF to client: {:?}", v);
|
||||
}
|
||||
});
|
||||
|
||||
// Spawn a thread for transforming messages to chunk messages
|
||||
thread::spawn(move || {
|
||||
let mut tx = tx;
|
||||
loop {
|
||||
let mut panic_after_finish = false;
|
||||
|
||||
let response = match receiver
|
||||
.recv() {
|
||||
Ok(v) => v,
|
||||
Err(v) => {
|
||||
error!("Queue message failed: {:?}", v);
|
||||
panic_after_finish = true;
|
||||
InstallMessage::Error("Internal error".to_string())
|
||||
}
|
||||
};
|
||||
|
||||
if let InstallMessage::EOF = response {
|
||||
break;
|
||||
}
|
||||
|
||||
let mut response = serde_json::to_string(&response)
|
||||
.log_expect("Failed to render JSON logging response payload");
|
||||
response.push('\n');
|
||||
tx = tx
|
||||
.send(Ok(response.into_bytes().into()))
|
||||
.wait()
|
||||
.log_expect("Failed to write JSON response payload to client");
|
||||
|
||||
if panic_after_finish {
|
||||
panic!("Failed to read from queue (flushed error message successfully)");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Response::<hyper::Body>::new()
|
||||
//.with_header(ContentLength(file.len() as u64))
|
||||
.with_header(ContentType::plaintext())
|
||||
.with_body(rx)
|
||||
}));
|
||||
}
|
||||
|
||||
// Static file handler
|
||||
(&Get, _) => {
|
||||
// At this point, we have a web browser client. Search for a index page
|
||||
// if needed
|
||||
let mut path: String = req.path().to_owned();
|
||||
if path.ends_with('/') {
|
||||
path += "index.html";
|
||||
}
|
||||
|
||||
match assets::file_from_string(&path) {
|
||||
Some((content_type, file)) => {
|
||||
let content_type = ContentType(content_type.parse().log_expect(
|
||||
"Failed to parse content type into correct representation",
|
||||
));
|
||||
Response::<hyper::Body>::new()
|
||||
.with_header(ContentLength(file.len() as u64))
|
||||
.with_header(content_type)
|
||||
.with_body(file)
|
||||
}
|
||||
None => Response::new().with_status(StatusCode::NotFound),
|
||||
}
|
||||
}
|
||||
// Fallthrough for POST/PUT/CONNECT/...
|
||||
_ => Response::new().with_status(StatusCode::NotFound),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/// Encapsulates JSON as a injectable Javascript script.
|
||||
fn encapsulate_json(field_name: &str, json: &str) -> String {
|
||||
format!("var {} = {};", field_name, json)
|
||||
}
|
||||
111
src/self_update.rs
Normal file
111
src/self_update.rs
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
//! self_update.rs
|
||||
//!
|
||||
//! Handles different components of self-updating.
|
||||
|
||||
use std::fs::{remove_file, File};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{exit, Command};
|
||||
use std::{thread, time};
|
||||
|
||||
use clap::{App, ArgMatches};
|
||||
|
||||
use crate::logging::LoggingErrors;
|
||||
|
||||
/// Swaps around the main executable if needed.
|
||||
pub fn perform_swap(current_exe: &PathBuf, to_path: Option<&str>) {
|
||||
// Check to see if we are currently in a self-update
|
||||
if let Some(to_path) = to_path {
|
||||
let to_path = PathBuf::from(to_path);
|
||||
|
||||
// Sleep a little bit to allow Windows to close the previous file handle
|
||||
thread::sleep(time::Duration::from_millis(3000));
|
||||
|
||||
info!(
|
||||
"Swapping installer from {} to {}",
|
||||
current_exe.display(),
|
||||
to_path.display()
|
||||
);
|
||||
|
||||
// Attempt it a few times because Windows can hold a lock
|
||||
for i in 1..=5 {
|
||||
let swap_result = if cfg!(windows) {
|
||||
use std::fs::copy;
|
||||
|
||||
copy(¤t_exe, &to_path).map(|_x| ())
|
||||
} else {
|
||||
use std::fs::rename;
|
||||
|
||||
rename(¤t_exe, &to_path)
|
||||
};
|
||||
|
||||
match swap_result {
|
||||
Ok(_) => break,
|
||||
Err(e) => {
|
||||
if i < 5 {
|
||||
info!("Copy attempt failed: {:?}, retrying in 3 seconds.", e);
|
||||
thread::sleep(time::Duration::from_millis(3000));
|
||||
} else {
|
||||
Err::<(), _>(e).log_expect("Copying new binary failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Command::new(to_path)
|
||||
.spawn()
|
||||
.log_expect("Unable to start child process");
|
||||
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check_args<'a>(app: App<'a, '_>, current_path: &Path) -> Option<ArgMatches<'a>> {
|
||||
// If we just finished a update, we need to inject our previous command line arguments
|
||||
let args_file = current_path.join("args.json");
|
||||
|
||||
if args_file.exists() {
|
||||
let database: Vec<String> = {
|
||||
let metadata_file =
|
||||
File::open(&args_file).log_expect("Unable to open args file handle");
|
||||
|
||||
serde_json::from_reader(metadata_file).log_expect("Unable to read metadata file")
|
||||
};
|
||||
|
||||
let matches = app.get_matches_from(database);
|
||||
|
||||
info!("Parsed command line arguments from original instance");
|
||||
remove_file(args_file).log_expect("Unable to clean up args file");
|
||||
|
||||
Some(matches)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cleanup(current_path: &Path) {
|
||||
// Cleanup any remaining new maintenance tool instances if they exist
|
||||
if cfg!(windows) {
|
||||
let updater_executable = current_path.join("maintenancetool_new.exe");
|
||||
|
||||
if updater_executable.exists() {
|
||||
// Sleep a little bit to allow Windows to close the previous file handle
|
||||
thread::sleep(time::Duration::from_millis(3000));
|
||||
|
||||
// Attempt it a few times because Windows can hold a lock
|
||||
for i in 1..=5 {
|
||||
let swap_result = remove_file(&updater_executable);
|
||||
match swap_result {
|
||||
Ok(_) => break,
|
||||
Err(e) => {
|
||||
if i < 5 {
|
||||
info!("Cleanup attempt failed: {:?}, retrying in 3 seconds.", e);
|
||||
thread::sleep(time::Duration::from_millis(3000));
|
||||
} else {
|
||||
warn!("Deleting temp binary failed after 5 attempts: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,9 +7,9 @@ use reqwest::StatusCode;
|
|||
|
||||
use serde_json;
|
||||
|
||||
use sources::types::*;
|
||||
use crate::sources::types::*;
|
||||
|
||||
use http::build_client;
|
||||
use crate::http::build_client;
|
||||
|
||||
pub struct GithubReleases {}
|
||||
|
||||
|
|
@ -41,17 +41,19 @@ impl ReleaseSource for GithubReleases {
|
|||
.get(&format!(
|
||||
"https://api.github.com/repos/{}/releases",
|
||||
config.repo
|
||||
)).header(USER_AGENT, "liftinstall (j-selby)")
|
||||
))
|
||||
.header(USER_AGENT, "liftinstall (j-selby)")
|
||||
.send()
|
||||
.map_err(|x| format!("Error while sending HTTP request: {:?}", x))?;
|
||||
|
||||
match response.status() {
|
||||
StatusCode::OK => {}
|
||||
StatusCode::FORBIDDEN => {
|
||||
return Err(format!(
|
||||
return Err(
|
||||
"GitHub is rate limiting you. Try moving to a internet connection \
|
||||
that isn't shared, and/or disabling VPNs."
|
||||
));
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
return Err(format!("Bad status code: {:?}.", response.status()));
|
||||
|
|
@ -87,20 +89,25 @@ impl ReleaseSource for GithubReleases {
|
|||
let string = match asset["name"].as_str() {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
return Err("JSON payload missing information about release name".to_string())
|
||||
return Err(
|
||||
"JSON payload missing information about release name".to_string()
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let url = match asset["browser_download_url"].as_str() {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
return Err("JSON payload missing information about release URL".to_string())
|
||||
return Err(
|
||||
"JSON payload missing information about release URL".to_string()
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
files.push(File {
|
||||
name: string.to_string(),
|
||||
url: url.to_string(),
|
||||
requires_authorization: false,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,12 +6,15 @@ pub mod types;
|
|||
|
||||
pub mod github;
|
||||
|
||||
pub mod patreon;
|
||||
|
||||
use self::types::ReleaseSource;
|
||||
|
||||
/// Returns a ReleaseSource by a name, if possible
|
||||
pub fn get_by_name(name: &str) -> Option<Box<ReleaseSource>> {
|
||||
pub fn get_by_name(name: &str) -> Option<Box<dyn ReleaseSource>> {
|
||||
match name {
|
||||
"github" => Some(Box::new(github::GithubReleases::new())),
|
||||
"patreon" => Some(Box::new(patreon::PatreonReleases::new())),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
102
src/sources/patreon.rs
Normal file
102
src/sources/patreon.rs
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
//! patreon.rs
|
||||
//!
|
||||
//! Contains the yuzu-emu core API implementation of a release source.
|
||||
|
||||
use crate::http::build_client;
|
||||
use crate::sources::types::*;
|
||||
use reqwest::header::USER_AGENT;
|
||||
use reqwest::StatusCode;
|
||||
|
||||
pub struct PatreonReleases {}
|
||||
|
||||
/// The configuration for this release.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct PatreonConfig {
|
||||
repo: String,
|
||||
}
|
||||
|
||||
impl PatreonReleases {
|
||||
pub fn new() -> Self {
|
||||
PatreonReleases {}
|
||||
}
|
||||
}
|
||||
|
||||
impl ReleaseSource for PatreonReleases {
|
||||
fn get_current_releases(&self, _config: &TomlValue) -> Result<Vec<Release>, String> {
|
||||
let config: PatreonConfig = match _config.clone().try_into() {
|
||||
Ok(v) => v,
|
||||
Err(v) => return Err(format!("Failed to parse release config: {:?}", v)),
|
||||
};
|
||||
|
||||
let mut results: Vec<Release> = Vec::new();
|
||||
|
||||
// Build the HTTP client up
|
||||
let client = build_client()?;
|
||||
let mut response = client
|
||||
.get(&format!(
|
||||
"https://api.yuzu-emu.org/downloads/{}/",
|
||||
config.repo
|
||||
))
|
||||
.header(USER_AGENT, "liftinstall (j-selby)")
|
||||
.send()
|
||||
.map_err(|x| format!("Error while sending HTTP request: {:?}", x))?;
|
||||
|
||||
match response.status() {
|
||||
StatusCode::OK => {}
|
||||
StatusCode::FORBIDDEN => {
|
||||
return Err("You are not eligible to download this release".to_string());
|
||||
}
|
||||
_ => {
|
||||
return Err(format!("Bad status code: {:?}.", response.status()));
|
||||
}
|
||||
}
|
||||
|
||||
let body = response
|
||||
.text()
|
||||
.map_err(|x| format!("Failed to decode HTTP response body: {:?}", x))?;
|
||||
|
||||
let result: serde_json::Value = serde_json::from_str(&body)
|
||||
.map_err(|x| format!("Failed to parse response: {:?}", x))?;
|
||||
|
||||
// Parse JSON from server
|
||||
let mut files = Vec::new();
|
||||
|
||||
let id: u64 = match result["version"].as_u64() {
|
||||
Some(v) => v,
|
||||
None => return Err("JSON payload missing information about ID".to_string()),
|
||||
};
|
||||
|
||||
let downloads = match result["files"].as_array() {
|
||||
Some(v) => v,
|
||||
None => return Err("JSON payload not an array".to_string()),
|
||||
};
|
||||
|
||||
for file in downloads.iter() {
|
||||
let string = match file["name"].as_str() {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
return Err("JSON payload missing information about release name".to_string());
|
||||
}
|
||||
};
|
||||
|
||||
let url = match file["url"].as_str() {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
return Err("JSON payload missing information about release URL".to_string());
|
||||
}
|
||||
};
|
||||
|
||||
files.push(File {
|
||||
name: string.to_string(),
|
||||
url: url.to_string(),
|
||||
requires_authorization: true,
|
||||
});
|
||||
}
|
||||
|
||||
results.push(Release {
|
||||
version: Version::new_number(id),
|
||||
files,
|
||||
});
|
||||
Ok(results)
|
||||
}
|
||||
}
|
||||
|
|
@ -23,9 +23,7 @@ impl Version {
|
|||
fn coarse_into_semver(&self) -> SemverVersion {
|
||||
match *self {
|
||||
Version::Semver(ref version) => version.to_owned(),
|
||||
Version::Integer(ref version) => {
|
||||
SemverVersion::from((version.to_owned(), 0 as u64, 0 as u64))
|
||||
}
|
||||
Version::Integer(ref version) => SemverVersion::new(version.to_owned(), 0u64, 0u64),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -66,6 +64,7 @@ impl Ord for Version {
|
|||
pub struct File {
|
||||
pub name: String,
|
||||
pub url: String,
|
||||
pub requires_authorization: bool,
|
||||
}
|
||||
|
||||
impl File {}
|
||||
|
|
|
|||
88
src/tasks/check_authorization.rs
Normal file
88
src/tasks/check_authorization.rs
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
//! Validates that users have correct authorization to download packages.
|
||||
|
||||
use crate::frontend::rest::services::authentication;
|
||||
|
||||
use crate::installer::InstallerFramework;
|
||||
|
||||
use crate::logging::LoggingErrors;
|
||||
|
||||
use crate::tasks::resolver::ResolvePackageTask;
|
||||
use crate::tasks::{Task, TaskDependency, TaskMessage, TaskOrdering, TaskParamType};
|
||||
|
||||
pub struct CheckAuthorizationTask {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl Task for CheckAuthorizationTask {
|
||||
fn execute(
|
||||
&mut self,
|
||||
mut input: Vec<TaskParamType>,
|
||||
context: &mut InstallerFramework,
|
||||
_messenger: &dyn Fn(&TaskMessage),
|
||||
) -> Result<TaskParamType, String> {
|
||||
assert_eq!(input.len(), 1);
|
||||
|
||||
let params = input
|
||||
.pop()
|
||||
.log_expect("Check Authorization Task should have input from resolver!");
|
||||
let (version, file) = match params {
|
||||
TaskParamType::File(v, f) => Ok((v, f)),
|
||||
_ => Err("Unexpected TaskParamType in CheckAuthorization: {:?}"),
|
||||
}?;
|
||||
|
||||
if !file.requires_authorization {
|
||||
return Ok(TaskParamType::Authentication(version, file, None));
|
||||
}
|
||||
|
||||
let username = context.database.credentials.username.clone();
|
||||
let token = context.database.credentials.token.clone();
|
||||
let authentication = context
|
||||
.config
|
||||
.clone()
|
||||
.log_expect("In-memory configuration doesn't exist")
|
||||
.authentication
|
||||
.log_expect("No authentication configuration exists while checking authorization");
|
||||
|
||||
let auth_url = authentication.auth_url.clone();
|
||||
let pub_key_base64 = authentication.pub_key_base64.clone();
|
||||
let validation = authentication.validation.clone();
|
||||
|
||||
// Authorizaion is required for this package so post the username and token and get a jwt_token response
|
||||
let jwt_token = match authentication::authenticate_sync(auth_url, username, token) {
|
||||
Ok(jwt) => jwt,
|
||||
Err(_) => return Ok(TaskParamType::Authentication(version, file, None)),
|
||||
};
|
||||
|
||||
let claims =
|
||||
match authentication::validate_token(jwt_token.clone(), pub_key_base64, validation) {
|
||||
Ok(c) => c,
|
||||
Err(_) => return Ok(TaskParamType::Authentication(version, file, None)),
|
||||
};
|
||||
|
||||
// Validate that they are authorized
|
||||
if !claims.roles.contains(&"vip".to_string())
|
||||
&& !claims.channels.contains(&"early-access".to_string())
|
||||
{
|
||||
return Ok(TaskParamType::Authentication(version, file, None));
|
||||
}
|
||||
|
||||
Ok(TaskParamType::Authentication(
|
||||
version,
|
||||
file,
|
||||
Some(jwt_token),
|
||||
))
|
||||
}
|
||||
|
||||
fn dependencies(&self) -> Vec<TaskDependency> {
|
||||
vec![TaskDependency::build(
|
||||
TaskOrdering::Pre,
|
||||
Box::new(ResolvePackageTask {
|
||||
name: self.name.clone(),
|
||||
}),
|
||||
)]
|
||||
}
|
||||
|
||||
fn name(&self) -> String {
|
||||
format!("CheckAuthorizationTask (for {:?})", self.name)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +1,19 @@
|
|||
//! Downloads a package into memory.
|
||||
|
||||
use installer::InstallerFramework;
|
||||
use crate::installer::InstallerFramework;
|
||||
|
||||
use tasks::Task;
|
||||
use tasks::TaskDependency;
|
||||
use tasks::TaskMessage;
|
||||
use tasks::TaskOrdering;
|
||||
use tasks::TaskParamType;
|
||||
use crate::tasks::check_authorization::CheckAuthorizationTask;
|
||||
use crate::tasks::Task;
|
||||
use crate::tasks::TaskDependency;
|
||||
use crate::tasks::TaskMessage;
|
||||
use crate::tasks::TaskOrdering;
|
||||
use crate::tasks::TaskParamType;
|
||||
|
||||
use tasks::resolver::ResolvePackageTask;
|
||||
use crate::http::stream_file;
|
||||
|
||||
use http::stream_file;
|
||||
use number_prefix::NumberPrefix::{self, Prefixed, Standalone};
|
||||
|
||||
use number_prefix::{decimal_prefix, Prefixed, Standalone};
|
||||
|
||||
use logging::LoggingErrors;
|
||||
use crate::logging::LoggingErrors;
|
||||
|
||||
pub struct DownloadPackageTask {
|
||||
pub name: String,
|
||||
|
|
@ -25,16 +24,25 @@ impl Task for DownloadPackageTask {
|
|||
&mut self,
|
||||
mut input: Vec<TaskParamType>,
|
||||
context: &mut InstallerFramework,
|
||||
messenger: &Fn(&TaskMessage),
|
||||
messenger: &dyn Fn(&TaskMessage),
|
||||
) -> Result<TaskParamType, String> {
|
||||
assert_eq!(input.len(), 1);
|
||||
|
||||
let file = input.pop().log_expect("Should have input from resolver!");
|
||||
let (version, file) = match file {
|
||||
TaskParamType::File(v, f) => (v, f),
|
||||
let file = input
|
||||
.pop()
|
||||
.log_expect("Download Package Task should have input from resolver!");
|
||||
let (version, file, auth) = match file {
|
||||
TaskParamType::Authentication(v, f, auth) => (v, f, auth),
|
||||
_ => return Err("Unexpected param type to download package".to_string()),
|
||||
};
|
||||
|
||||
// TODO: move this back below checking for latest version after testing is done
|
||||
if file.requires_authorization && auth.is_none() {
|
||||
info!("Authorization required to update this package!");
|
||||
messenger(&TaskMessage::AuthorizationRequired("AuthorizationRequired"));
|
||||
return Ok(TaskParamType::Break);
|
||||
}
|
||||
|
||||
// Check to see if this is the newest file available already
|
||||
for element in &context.database.packages {
|
||||
if element.name == self.name {
|
||||
|
|
@ -54,7 +62,7 @@ impl Task for DownloadPackageTask {
|
|||
let mut downloaded = 0;
|
||||
let mut data_storage: Vec<u8> = Vec::new();
|
||||
|
||||
stream_file(&file.url, |data, size| {
|
||||
stream_file(&file.url, auth, |data, size| {
|
||||
{
|
||||
data_storage.extend_from_slice(&data);
|
||||
}
|
||||
|
|
@ -68,11 +76,11 @@ impl Task for DownloadPackageTask {
|
|||
};
|
||||
|
||||
// Pretty print data volumes
|
||||
let pretty_current = match decimal_prefix(downloaded as f64) {
|
||||
let pretty_current = match NumberPrefix::decimal(downloaded as f64) {
|
||||
Standalone(bytes) => format!("{} bytes", bytes),
|
||||
Prefixed(prefix, n) => format!("{:.0} {}B", n, prefix),
|
||||
};
|
||||
let pretty_total = match decimal_prefix(size as f64) {
|
||||
let pretty_total = match NumberPrefix::decimal(size as f64) {
|
||||
Standalone(bytes) => format!("{} bytes", bytes),
|
||||
Prefixed(prefix, n) => format!("{:.0} {}B", n, prefix),
|
||||
};
|
||||
|
|
@ -92,7 +100,7 @@ impl Task for DownloadPackageTask {
|
|||
fn dependencies(&self) -> Vec<TaskDependency> {
|
||||
vec![TaskDependency::build(
|
||||
TaskOrdering::Pre,
|
||||
Box::new(ResolvePackageTask {
|
||||
Box::new(CheckAuthorizationTask {
|
||||
name: self.name.clone(),
|
||||
}),
|
||||
)]
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
//! Verifies that this is the only running instance of the installer, and that no application is running.
|
||||
|
||||
use installer::InstallerFramework;
|
||||
use crate::installer::InstallerFramework;
|
||||
|
||||
use tasks::Task;
|
||||
use tasks::TaskDependency;
|
||||
use tasks::TaskMessage;
|
||||
use tasks::TaskParamType;
|
||||
use crate::tasks::Task;
|
||||
use crate::tasks::TaskDependency;
|
||||
use crate::tasks::TaskMessage;
|
||||
use crate::tasks::TaskParamType;
|
||||
|
||||
use sysinfo;
|
||||
use sysinfo::get_current_pid;
|
||||
use sysinfo::ProcessExt;
|
||||
use sysinfo::SystemExt;
|
||||
use crate::native::get_process_names;
|
||||
use crate::native::Process;
|
||||
|
||||
use std::process;
|
||||
|
||||
pub struct EnsureOnlyInstanceTask {}
|
||||
|
||||
|
|
@ -19,27 +19,26 @@ impl Task for EnsureOnlyInstanceTask {
|
|||
&mut self,
|
||||
input: Vec<TaskParamType>,
|
||||
context: &mut InstallerFramework,
|
||||
_messenger: &Fn(&TaskMessage),
|
||||
_messenger: &dyn Fn(&TaskMessage),
|
||||
) -> Result<TaskParamType, String> {
|
||||
assert_eq!(input.len(), 0);
|
||||
|
||||
let system = sysinfo::System::new();
|
||||
let current_pid = get_current_pid();
|
||||
for (pid, process) in system.get_process_list() {
|
||||
if pid == ¤t_pid {
|
||||
let current_pid = process::id() as usize;
|
||||
for Process { pid, name } in get_process_names() {
|
||||
if pid == current_pid {
|
||||
continue;
|
||||
}
|
||||
|
||||
let exe = process.exe();
|
||||
let exe = name;
|
||||
|
||||
if exe.ends_with("maintenancetool.exe") || exe.ends_with("maintenancetool") {
|
||||
return Err(format!("Maintenance tool is already running!"));
|
||||
return Err("Maintenance tool is already running!".to_string());
|
||||
}
|
||||
|
||||
for package in &context.database.packages {
|
||||
for file in &package.files {
|
||||
if exe.ends_with(file) {
|
||||
return Err(format!("The installed application is currently running!"));
|
||||
return Err("The installed application is currently running!".to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -53,6 +52,6 @@ impl Task for EnsureOnlyInstanceTask {
|
|||
}
|
||||
|
||||
fn name(&self) -> String {
|
||||
format!("EnsureOnlyInstanceTask")
|
||||
"EnsureOnlyInstanceTask".to_string()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,29 @@
|
|||
//! Overall hierarchy for installing a installation of the application.
|
||||
|
||||
use installer::InstallerFramework;
|
||||
use crate::installer::InstallerFramework;
|
||||
|
||||
use tasks::ensure_only_instance::EnsureOnlyInstanceTask;
|
||||
use tasks::install_dir::VerifyInstallDirTask;
|
||||
use tasks::install_global_shortcut::InstallGlobalShortcutsTask;
|
||||
use tasks::install_pkg::InstallPackageTask;
|
||||
use tasks::save_executable::SaveExecutableTask;
|
||||
use tasks::uninstall_pkg::UninstallPackageTask;
|
||||
use crate::tasks::ensure_only_instance::EnsureOnlyInstanceTask;
|
||||
use crate::tasks::install_dir::VerifyInstallDirTask;
|
||||
use crate::tasks::install_global_shortcut::InstallGlobalShortcutsTask;
|
||||
use crate::tasks::install_pkg::InstallPackageTask;
|
||||
use crate::tasks::launch_installed_on_exit::LaunchOnExitTask;
|
||||
use crate::tasks::remove_target_dir::RemoveTargetDirTask;
|
||||
use crate::tasks::save_executable::SaveExecutableTask;
|
||||
use crate::tasks::uninstall_pkg::UninstallPackageTask;
|
||||
|
||||
use tasks::Task;
|
||||
use tasks::TaskDependency;
|
||||
use tasks::TaskMessage;
|
||||
use tasks::TaskOrdering;
|
||||
use tasks::TaskParamType;
|
||||
use crate::tasks::Task;
|
||||
use crate::tasks::TaskDependency;
|
||||
use crate::tasks::TaskMessage;
|
||||
use crate::tasks::TaskOrdering;
|
||||
use crate::tasks::TaskParamType;
|
||||
|
||||
pub struct InstallTask {
|
||||
pub items: Vec<String>,
|
||||
pub uninstall_items: Vec<String>,
|
||||
pub fresh_install: bool,
|
||||
pub create_desktop_shortcuts: bool,
|
||||
// force_install: remove the target directory before installing
|
||||
pub force_install: bool,
|
||||
}
|
||||
|
||||
impl Task for InstallTask {
|
||||
|
|
@ -26,7 +31,7 @@ impl Task for InstallTask {
|
|||
&mut self,
|
||||
_: Vec<TaskParamType>,
|
||||
_: &mut InstallerFramework,
|
||||
messenger: &Fn(&TaskMessage),
|
||||
messenger: &dyn Fn(&TaskMessage),
|
||||
) -> Result<TaskParamType, String> {
|
||||
messenger(&TaskMessage::DisplayMessage("Wrapping up...", 0.0));
|
||||
Ok(TaskParamType::None)
|
||||
|
|
@ -40,6 +45,13 @@ impl Task for InstallTask {
|
|||
Box::new(EnsureOnlyInstanceTask {}),
|
||||
));
|
||||
|
||||
if self.force_install {
|
||||
elements.push(TaskDependency::build(
|
||||
TaskOrdering::Pre,
|
||||
Box::new(RemoveTargetDirTask {}),
|
||||
));
|
||||
}
|
||||
|
||||
elements.push(TaskDependency::build(
|
||||
TaskOrdering::Pre,
|
||||
Box::new(VerifyInstallDirTask {
|
||||
|
|
@ -47,13 +59,6 @@ impl Task for InstallTask {
|
|||
}),
|
||||
));
|
||||
|
||||
for item in &self.items {
|
||||
elements.push(TaskDependency::build(
|
||||
TaskOrdering::Pre,
|
||||
Box::new(InstallPackageTask { name: item.clone() }),
|
||||
));
|
||||
}
|
||||
|
||||
for item in &self.uninstall_items {
|
||||
elements.push(TaskDependency::build(
|
||||
TaskOrdering::Pre,
|
||||
|
|
@ -64,6 +69,16 @@ impl Task for InstallTask {
|
|||
));
|
||||
}
|
||||
|
||||
for item in &self.items {
|
||||
elements.push(TaskDependency::build(
|
||||
TaskOrdering::Pre,
|
||||
Box::new(InstallPackageTask {
|
||||
name: item.clone(),
|
||||
create_desktop_shortcuts: self.create_desktop_shortcuts,
|
||||
}),
|
||||
));
|
||||
}
|
||||
|
||||
if self.fresh_install {
|
||||
elements.push(TaskDependency::build(
|
||||
TaskOrdering::Pre,
|
||||
|
|
@ -74,6 +89,11 @@ impl Task for InstallTask {
|
|||
TaskOrdering::Pre,
|
||||
Box::new(InstallGlobalShortcutsTask {}),
|
||||
));
|
||||
|
||||
elements.push(TaskDependency::build(
|
||||
TaskOrdering::Post,
|
||||
Box::new(LaunchOnExitTask {}),
|
||||
))
|
||||
}
|
||||
|
||||
elements
|
||||
|
|
|
|||
133
src/tasks/install_desktop_shortcut.rs
Normal file
133
src/tasks/install_desktop_shortcut.rs
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
//! Generates shortcuts for a specified file.
|
||||
|
||||
use crate::installer::InstallerFramework;
|
||||
|
||||
use crate::tasks::Task;
|
||||
use crate::tasks::TaskDependency;
|
||||
use crate::tasks::TaskMessage;
|
||||
use crate::tasks::TaskParamType;
|
||||
|
||||
use crate::config::PackageDescription;
|
||||
|
||||
use crate::logging::LoggingErrors;
|
||||
|
||||
#[cfg(windows)]
|
||||
use crate::native::create_desktop_shortcut;
|
||||
#[cfg(target_os = "linux")]
|
||||
use crate::native::create_shortcut;
|
||||
|
||||
pub struct InstallDesktopShortcutTask {
|
||||
pub name: String,
|
||||
pub should_run: bool,
|
||||
}
|
||||
|
||||
impl Task for InstallDesktopShortcutTask {
|
||||
fn execute(
|
||||
&mut self,
|
||||
_: Vec<TaskParamType>,
|
||||
context: &mut InstallerFramework,
|
||||
messenger: &dyn Fn(&TaskMessage),
|
||||
) -> Result<TaskParamType, String> {
|
||||
if !self.should_run {
|
||||
return Ok(TaskParamType::GeneratedShortcuts(Vec::new()));
|
||||
}
|
||||
|
||||
messenger(&TaskMessage::DisplayMessage(
|
||||
&format!(
|
||||
"Generating desktop shortcuts for package {:?}...",
|
||||
self.name
|
||||
),
|
||||
0.0,
|
||||
));
|
||||
|
||||
let path = context
|
||||
.install_path
|
||||
.as_ref()
|
||||
.log_expect("No install path specified");
|
||||
|
||||
let starting_dir = path
|
||||
.to_str()
|
||||
.log_expect("Unable to build shortcut metadata (startingdir)");
|
||||
|
||||
let mut installed_files = Vec::new();
|
||||
|
||||
let mut metadata: Option<PackageDescription> = None;
|
||||
for description in &context
|
||||
.config
|
||||
.as_ref()
|
||||
.log_expect("Should have packages by now")
|
||||
.packages
|
||||
{
|
||||
if self.name == description.name {
|
||||
metadata = Some(description.clone());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let package = match metadata {
|
||||
Some(v) => v,
|
||||
None => return Err(format!("Package {:?} could not be found.", self.name)),
|
||||
};
|
||||
|
||||
// Generate installer path
|
||||
let platform_extension = if cfg!(windows) {
|
||||
"maintenancetool.exe"
|
||||
} else {
|
||||
"maintenancetool"
|
||||
};
|
||||
|
||||
for shortcut in package.shortcuts {
|
||||
let tool_path = path.join(platform_extension);
|
||||
let tool_path = tool_path
|
||||
.to_str()
|
||||
.log_expect("Unable to build shortcut metadata (tool)");
|
||||
|
||||
let exe_path = path.join(shortcut.relative_path);
|
||||
let exe_path = exe_path
|
||||
.to_str()
|
||||
.log_expect("Unable to build shortcut metadata (exe)");
|
||||
|
||||
#[cfg(windows)]
|
||||
installed_files.push(create_desktop_shortcut(
|
||||
&shortcut.name,
|
||||
&shortcut.description,
|
||||
tool_path,
|
||||
// TODO: Send by list
|
||||
&format!("--launcher \"{}\"", exe_path),
|
||||
&starting_dir,
|
||||
exe_path,
|
||||
)?);
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
installed_files.push(create_shortcut(
|
||||
&shortcut.name,
|
||||
&shortcut.description,
|
||||
tool_path,
|
||||
&format!("--launcher \"{}\"", exe_path),
|
||||
&starting_dir,
|
||||
exe_path,
|
||||
)?);
|
||||
}
|
||||
|
||||
// Update the installed packages shortcuts information in the database
|
||||
let packages = &mut context.database.packages;
|
||||
for pack in packages {
|
||||
if pack.name == self.name {
|
||||
pack.shortcuts.extend(installed_files.clone());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(TaskParamType::GeneratedShortcuts(installed_files))
|
||||
}
|
||||
|
||||
fn dependencies(&self) -> Vec<TaskDependency> {
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn name(&self) -> String {
|
||||
format!(
|
||||
"InstallDesktopShortcutTask (for {:?}, should_run = {:?})",
|
||||
self.name, self.should_run
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +1,16 @@
|
|||
//! Verifies properties about the installation directory.
|
||||
|
||||
use installer::InstallerFramework;
|
||||
use crate::installer::InstallerFramework;
|
||||
|
||||
use tasks::Task;
|
||||
use tasks::TaskDependency;
|
||||
use tasks::TaskMessage;
|
||||
use tasks::TaskParamType;
|
||||
use crate::tasks::Task;
|
||||
use crate::tasks::TaskDependency;
|
||||
use crate::tasks::TaskMessage;
|
||||
use crate::tasks::TaskParamType;
|
||||
|
||||
use std::fs::create_dir_all;
|
||||
use std::fs::read_dir;
|
||||
|
||||
use logging::LoggingErrors;
|
||||
use crate::logging::LoggingErrors;
|
||||
|
||||
pub struct VerifyInstallDirTask {
|
||||
pub clean_install: bool,
|
||||
|
|
@ -21,7 +21,7 @@ impl Task for VerifyInstallDirTask {
|
|||
&mut self,
|
||||
input: Vec<TaskParamType>,
|
||||
context: &mut InstallerFramework,
|
||||
messenger: &Fn(&TaskMessage),
|
||||
messenger: &dyn Fn(&TaskMessage),
|
||||
) -> Result<TaskParamType, String> {
|
||||
assert_eq!(input.len(), 0);
|
||||
messenger(&TaskMessage::DisplayMessage(
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
//! Generates the global shortcut for this application.
|
||||
|
||||
use installer::InstallerFramework;
|
||||
use crate::installer::InstallerFramework;
|
||||
|
||||
use tasks::Task;
|
||||
use tasks::TaskDependency;
|
||||
use tasks::TaskMessage;
|
||||
use tasks::TaskParamType;
|
||||
use crate::tasks::Task;
|
||||
use crate::tasks::TaskDependency;
|
||||
use crate::tasks::TaskMessage;
|
||||
use crate::tasks::TaskParamType;
|
||||
|
||||
use logging::LoggingErrors;
|
||||
use crate::logging::LoggingErrors;
|
||||
|
||||
use native::create_shortcut;
|
||||
use tasks::save_database::SaveDatabaseTask;
|
||||
use tasks::TaskOrdering;
|
||||
use crate::native::create_shortcut;
|
||||
use crate::tasks::save_database::SaveDatabaseTask;
|
||||
use crate::tasks::TaskOrdering;
|
||||
|
||||
pub struct InstallGlobalShortcutsTask {}
|
||||
|
||||
|
|
@ -20,7 +20,7 @@ impl Task for InstallGlobalShortcutsTask {
|
|||
&mut self,
|
||||
_: Vec<TaskParamType>,
|
||||
context: &mut InstallerFramework,
|
||||
messenger: &Fn(&TaskMessage),
|
||||
messenger: &dyn Fn(&TaskMessage),
|
||||
) -> Result<TaskParamType, String> {
|
||||
messenger(&TaskMessage::DisplayMessage(
|
||||
"Generating global shortcut...",
|
||||
|
|
@ -58,9 +58,10 @@ impl Task for InstallGlobalShortcutsTask {
|
|||
// TODO: Send by list
|
||||
"",
|
||||
&starting_dir,
|
||||
"",
|
||||
)?;
|
||||
|
||||
if !shortcut_file.is_empty() {
|
||||
if !shortcut_file.is_empty() && !context.database.shortcuts.contains(&shortcut_file) {
|
||||
context.database.shortcuts.push(shortcut_file);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,32 +1,35 @@
|
|||
//! Installs a specific package.
|
||||
|
||||
use installer::InstallerFramework;
|
||||
use crate::installer::InstallerFramework;
|
||||
|
||||
use tasks::download_pkg::DownloadPackageTask;
|
||||
use tasks::install_shortcuts::InstallShortcutsTask;
|
||||
use tasks::save_database::SaveDatabaseTask;
|
||||
use tasks::uninstall_pkg::UninstallPackageTask;
|
||||
use tasks::Task;
|
||||
use tasks::TaskDependency;
|
||||
use tasks::TaskMessage;
|
||||
use tasks::TaskOrdering;
|
||||
use tasks::TaskParamType;
|
||||
use crate::tasks::download_pkg::DownloadPackageTask;
|
||||
use crate::tasks::install_shortcuts::InstallShortcutsTask;
|
||||
use crate::tasks::save_database::SaveDatabaseTask;
|
||||
use crate::tasks::uninstall_pkg::UninstallPackageTask;
|
||||
use crate::tasks::Task;
|
||||
use crate::tasks::TaskDependency;
|
||||
use crate::tasks::TaskMessage;
|
||||
use crate::tasks::TaskOrdering;
|
||||
use crate::tasks::TaskParamType;
|
||||
|
||||
use config::PackageDescription;
|
||||
use installer::LocalInstallation;
|
||||
use crate::config::PackageDescription;
|
||||
use crate::installer::LocalInstallation;
|
||||
|
||||
use std::fs::create_dir_all;
|
||||
use std::io::copy;
|
||||
|
||||
use logging::LoggingErrors;
|
||||
use crate::logging::LoggingErrors;
|
||||
|
||||
use archives;
|
||||
use crate::archives;
|
||||
|
||||
use crate::tasks::install_desktop_shortcut::InstallDesktopShortcutTask;
|
||||
use std::collections::HashSet;
|
||||
use std::fs::OpenOptions;
|
||||
use std::path::Path;
|
||||
|
||||
pub struct InstallPackageTask {
|
||||
pub name: String,
|
||||
pub create_desktop_shortcuts: bool,
|
||||
}
|
||||
|
||||
impl Task for InstallPackageTask {
|
||||
|
|
@ -34,7 +37,7 @@ impl Task for InstallPackageTask {
|
|||
&mut self,
|
||||
mut input: Vec<TaskParamType>,
|
||||
context: &mut InstallerFramework,
|
||||
messenger: &Fn(&TaskMessage),
|
||||
messenger: &dyn Fn(&TaskMessage),
|
||||
) -> Result<TaskParamType, String> {
|
||||
messenger(&TaskMessage::DisplayMessage(
|
||||
&format!("Installing package {:?}...", self.name),
|
||||
|
|
@ -66,20 +69,20 @@ impl Task for InstallPackageTask {
|
|||
None => return Err(format!("Package {:?} could not be found.", self.name)),
|
||||
};
|
||||
|
||||
// Grab data from the shortcut generator
|
||||
let shortcuts = input.pop().log_expect("Should have input from resolver!");
|
||||
let shortcuts = match shortcuts {
|
||||
TaskParamType::GeneratedShortcuts(files) => files,
|
||||
// If the resolver returned early, we need to unwind
|
||||
// Ignore input from the uninstaller - no useful information passed
|
||||
// If a previous task Breaks, then just early exit
|
||||
match input
|
||||
.pop()
|
||||
.log_expect("Install Package Task should have guaranteed output!")
|
||||
{
|
||||
TaskParamType::Break => return Ok(TaskParamType::None),
|
||||
_ => return Err("Unexpected shortcuts param type to install package".to_string()),
|
||||
_ => (),
|
||||
};
|
||||
|
||||
// Ignore input from the uninstaller - no useful information passed
|
||||
input.pop();
|
||||
|
||||
// Grab data from the resolver
|
||||
let data = input.pop().log_expect("Should have input from resolver!");
|
||||
let data = input
|
||||
.pop()
|
||||
.log_expect("Install Package Task should have input from resolver!");
|
||||
let (version, file, data) = match data {
|
||||
TaskParamType::FileContents(version, file, data) => (version, file, data),
|
||||
_ => return Err("Unexpected file contents param type to install package".to_string()),
|
||||
|
|
@ -139,7 +142,7 @@ impl Task for InstallPackageTask {
|
|||
info!("Creating file: {:?}", string_name);
|
||||
|
||||
if !installed_files.contains(&string_name) {
|
||||
installed_files.push(string_name.to_string());
|
||||
installed_files.push(string_name);
|
||||
}
|
||||
|
||||
let mut file_metadata = OpenOptions::new();
|
||||
|
|
@ -168,9 +171,9 @@ impl Task for InstallPackageTask {
|
|||
|
||||
// Save metadata about this package
|
||||
context.database.packages.push(LocalInstallation {
|
||||
name: package.name.to_owned(),
|
||||
name: package.name,
|
||||
version,
|
||||
shortcuts,
|
||||
shortcuts: HashSet::new(),
|
||||
files: installed_files,
|
||||
});
|
||||
|
||||
|
|
@ -195,11 +198,18 @@ impl Task for InstallPackageTask {
|
|||
}),
|
||||
),
|
||||
TaskDependency::build(
|
||||
TaskOrdering::Pre,
|
||||
TaskOrdering::Post,
|
||||
Box::new(InstallShortcutsTask {
|
||||
name: self.name.clone(),
|
||||
}),
|
||||
),
|
||||
TaskDependency::build(
|
||||
TaskOrdering::Post,
|
||||
Box::new(InstallDesktopShortcutTask {
|
||||
name: self.name.clone(),
|
||||
should_run: self.create_desktop_shortcuts,
|
||||
}),
|
||||
),
|
||||
TaskDependency::build(TaskOrdering::Post, Box::new(SaveDatabaseTask {})),
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
//! Generates shortcuts for a specified file.
|
||||
|
||||
use installer::InstallerFramework;
|
||||
use crate::installer::InstallerFramework;
|
||||
|
||||
use tasks::Task;
|
||||
use tasks::TaskDependency;
|
||||
use tasks::TaskMessage;
|
||||
use tasks::TaskParamType;
|
||||
use crate::tasks::Task;
|
||||
use crate::tasks::TaskDependency;
|
||||
use crate::tasks::TaskMessage;
|
||||
use crate::tasks::TaskParamType;
|
||||
|
||||
use config::PackageDescription;
|
||||
use crate::config::PackageDescription;
|
||||
|
||||
use logging::LoggingErrors;
|
||||
use crate::logging::LoggingErrors;
|
||||
|
||||
use native::create_shortcut;
|
||||
use crate::native::create_shortcut;
|
||||
|
||||
pub struct InstallShortcutsTask {
|
||||
pub name: String,
|
||||
|
|
@ -22,7 +22,7 @@ impl Task for InstallShortcutsTask {
|
|||
&mut self,
|
||||
_: Vec<TaskParamType>,
|
||||
context: &mut InstallerFramework,
|
||||
messenger: &Fn(&TaskMessage),
|
||||
messenger: &dyn Fn(&TaskMessage),
|
||||
) -> Result<TaskParamType, String> {
|
||||
messenger(&TaskMessage::DisplayMessage(
|
||||
&format!("Generating shortcuts for package {:?}...", self.name),
|
||||
|
|
@ -83,9 +83,18 @@ impl Task for InstallShortcutsTask {
|
|||
// TODO: Send by list
|
||||
&format!("--launcher \"{}\"", exe_path),
|
||||
&starting_dir,
|
||||
exe_path,
|
||||
)?);
|
||||
}
|
||||
|
||||
// Update the installed packages shortcuts information in the database
|
||||
let packages = &mut context.database.packages;
|
||||
for pack in packages {
|
||||
if pack.name == self.name {
|
||||
pack.shortcuts.extend(installed_files.clone());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(TaskParamType::GeneratedShortcuts(installed_files))
|
||||
}
|
||||
|
||||
|
|
|
|||
76
src/tasks/launch_installed_on_exit.rs
Normal file
76
src/tasks/launch_installed_on_exit.rs
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
//! Configures lift to launch the new package on fresh install after its closed
|
||||
//! If theres multiple launchable packages, then choose the first listed in config
|
||||
//! If there are multiple shortcuts for the first package, then launch the first.
|
||||
|
||||
use crate::installer::InstallerFramework;
|
||||
|
||||
use crate::tasks::Task;
|
||||
use crate::tasks::TaskDependency;
|
||||
use crate::tasks::TaskMessage;
|
||||
use crate::tasks::TaskParamType;
|
||||
|
||||
use crate::config::PackageDescription;
|
||||
|
||||
use crate::logging::LoggingErrors;
|
||||
|
||||
pub struct LaunchOnExitTask {}
|
||||
|
||||
impl Task for LaunchOnExitTask {
|
||||
fn execute(
|
||||
&mut self,
|
||||
_: Vec<TaskParamType>,
|
||||
context: &mut InstallerFramework,
|
||||
_: &dyn Fn(&TaskMessage),
|
||||
) -> Result<TaskParamType, String> {
|
||||
let pkg = &context.database.packages.first();
|
||||
if pkg.is_none() {
|
||||
return Ok(TaskParamType::None);
|
||||
}
|
||||
let pkg = pkg.unwrap();
|
||||
|
||||
// look up the first shortcut for the first listed package in the database
|
||||
let path = context
|
||||
.install_path
|
||||
.as_ref()
|
||||
.log_expect("No install path specified");
|
||||
|
||||
let mut metadata: Option<PackageDescription> = None;
|
||||
for description in &context
|
||||
.config
|
||||
.as_ref()
|
||||
.log_expect("Should have packages by now")
|
||||
.packages
|
||||
{
|
||||
if pkg.name == description.name {
|
||||
metadata = Some(description.clone());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let package_desc = match metadata {
|
||||
Some(v) => v,
|
||||
// Package metadata is missing. Dunno what went wrong but we can skip this then
|
||||
None => return Ok(TaskParamType::None),
|
||||
};
|
||||
|
||||
let shortcut = package_desc.shortcuts.first();
|
||||
|
||||
// copy the path to the actual exe into launcher_path so it'll load it on exit
|
||||
context.launcher_path = shortcut.map(|s| {
|
||||
path.join(s.relative_path.clone())
|
||||
.to_str()
|
||||
.map(|t| t.to_string())
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
Ok(TaskParamType::None)
|
||||
}
|
||||
|
||||
fn dependencies(&self) -> Vec<TaskDependency> {
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn name(&self) -> String {
|
||||
"LaunchOnExitTask".to_string()
|
||||
}
|
||||
}
|
||||
|
|
@ -4,18 +4,22 @@
|
|||
use std::fmt;
|
||||
use std::fmt::Display;
|
||||
|
||||
use installer::InstallerFramework;
|
||||
use crate::installer::InstallerFramework;
|
||||
|
||||
use sources::types::File;
|
||||
use sources::types::Version;
|
||||
use crate::sources::types::File;
|
||||
use crate::sources::types::Version;
|
||||
|
||||
pub mod check_authorization;
|
||||
pub mod download_pkg;
|
||||
pub mod ensure_only_instance;
|
||||
pub mod install;
|
||||
pub mod install_desktop_shortcut;
|
||||
pub mod install_dir;
|
||||
pub mod install_global_shortcut;
|
||||
pub mod install_pkg;
|
||||
pub mod install_shortcuts;
|
||||
pub mod launch_installed_on_exit;
|
||||
pub mod remove_target_dir;
|
||||
pub mod resolver;
|
||||
pub mod save_database;
|
||||
pub mod save_executable;
|
||||
|
|
@ -29,6 +33,8 @@ pub enum TaskParamType {
|
|||
None,
|
||||
/// Metadata about a file
|
||||
File(Version, File),
|
||||
/// Authentication token for a package
|
||||
Authentication(Version, File, Option<String>),
|
||||
/// Downloaded contents of a file
|
||||
FileContents(Version, File, Vec<u8>),
|
||||
/// List of shortcuts that have been generated
|
||||
|
|
@ -49,12 +55,12 @@ pub enum TaskOrdering {
|
|||
/// A dependency of a task with various properties.
|
||||
pub struct TaskDependency {
|
||||
ordering: TaskOrdering,
|
||||
task: Box<Task>,
|
||||
task: Box<dyn Task>,
|
||||
}
|
||||
|
||||
impl TaskDependency {
|
||||
/// Builds a new dependency from the specified task.
|
||||
pub fn build(ordering: TaskOrdering, task: Box<Task>) -> TaskDependency {
|
||||
pub fn build(ordering: TaskOrdering, task: Box<dyn Task>) -> TaskDependency {
|
||||
TaskDependency { ordering, task }
|
||||
}
|
||||
}
|
||||
|
|
@ -62,6 +68,7 @@ impl TaskDependency {
|
|||
/// A message from a task.
|
||||
pub enum TaskMessage<'a> {
|
||||
DisplayMessage(&'a str, f64),
|
||||
AuthorizationRequired(&'a str),
|
||||
PackageInstalled,
|
||||
}
|
||||
|
||||
|
|
@ -74,7 +81,7 @@ pub trait Task {
|
|||
&mut self,
|
||||
input: Vec<TaskParamType>,
|
||||
context: &mut InstallerFramework,
|
||||
messenger: &Fn(&TaskMessage),
|
||||
messenger: &dyn Fn(&TaskMessage),
|
||||
) -> Result<TaskParamType, String>;
|
||||
|
||||
/// Returns a vector containing all dependencies that need to be executed
|
||||
|
|
@ -87,7 +94,7 @@ pub trait Task {
|
|||
|
||||
/// The dependency tree allows for smart iteration on a Task struct.
|
||||
pub struct DependencyTree {
|
||||
task: Box<Task>,
|
||||
task: Box<dyn Task>,
|
||||
dependencies: Vec<(TaskOrdering, DependencyTree)>,
|
||||
}
|
||||
|
||||
|
|
@ -120,7 +127,7 @@ impl DependencyTree {
|
|||
pub fn execute(
|
||||
&mut self,
|
||||
context: &mut InstallerFramework,
|
||||
messenger: &Fn(&TaskMessage),
|
||||
messenger: &dyn Fn(&TaskMessage),
|
||||
) -> Result<TaskParamType, String> {
|
||||
let total_tasks = (self.dependencies.len() + 1) as f64;
|
||||
|
||||
|
|
@ -133,8 +140,8 @@ impl DependencyTree {
|
|||
continue;
|
||||
}
|
||||
|
||||
let result = i.execute(context, &|msg: &TaskMessage| match msg {
|
||||
&TaskMessage::DisplayMessage(msg, progress) => {
|
||||
let result = i.execute(context, &|msg: &TaskMessage| match *msg {
|
||||
TaskMessage::DisplayMessage(msg, progress) => {
|
||||
messenger(&TaskMessage::DisplayMessage(
|
||||
msg,
|
||||
progress / total_tasks + (1.0 / total_tasks) * f64::from(count),
|
||||
|
|
@ -159,8 +166,8 @@ impl DependencyTree {
|
|||
|
||||
let task_result = self
|
||||
.task
|
||||
.execute(inputs, context, &|msg: &TaskMessage| match msg {
|
||||
&TaskMessage::DisplayMessage(msg, progress) => {
|
||||
.execute(inputs, context, &|msg: &TaskMessage| match *msg {
|
||||
TaskMessage::DisplayMessage(msg, progress) => {
|
||||
messenger(&TaskMessage::DisplayMessage(
|
||||
msg,
|
||||
progress / total_tasks + (1.0 / total_tasks) * f64::from(count),
|
||||
|
|
@ -179,8 +186,8 @@ impl DependencyTree {
|
|||
continue;
|
||||
}
|
||||
|
||||
let result = i.execute(context, &|msg: &TaskMessage| match msg {
|
||||
&TaskMessage::DisplayMessage(msg, progress) => {
|
||||
let result = i.execute(context, &|msg: &TaskMessage| match *msg {
|
||||
TaskMessage::DisplayMessage(msg, progress) => {
|
||||
messenger(&TaskMessage::DisplayMessage(
|
||||
msg,
|
||||
progress / total_tasks + (1.0 / total_tasks) * f64::from(count),
|
||||
|
|
@ -206,7 +213,7 @@ impl DependencyTree {
|
|||
}
|
||||
|
||||
/// Builds a new pipeline from the specified task, iterating on dependencies.
|
||||
pub fn build(task: Box<Task>) -> DependencyTree {
|
||||
pub fn build(task: Box<dyn Task>) -> DependencyTree {
|
||||
let dependencies = task
|
||||
.dependencies()
|
||||
.into_iter()
|
||||
|
|
|
|||
64
src/tasks/remove_target_dir.rs
Normal file
64
src/tasks/remove_target_dir.rs
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
//! remove the whole target directory from the existence
|
||||
|
||||
use crate::installer::InstallerFramework;
|
||||
|
||||
use crate::tasks::Task;
|
||||
use crate::tasks::TaskDependency;
|
||||
use crate::tasks::TaskMessage;
|
||||
use crate::tasks::TaskParamType;
|
||||
|
||||
pub struct RemoveTargetDirTask {}
|
||||
|
||||
impl Task for RemoveTargetDirTask {
|
||||
fn execute(
|
||||
&mut self,
|
||||
_: Vec<TaskParamType>,
|
||||
context: &mut InstallerFramework,
|
||||
messenger: &dyn Fn(&TaskMessage),
|
||||
) -> Result<TaskParamType, String> {
|
||||
messenger(&TaskMessage::DisplayMessage(
|
||||
"Removing previous install...",
|
||||
0.1,
|
||||
));
|
||||
// erase the database as well
|
||||
context.database.packages = Vec::new();
|
||||
if let Some(path) = context.install_path.as_ref() {
|
||||
let entries = std::fs::read_dir(path)
|
||||
.map_err(|e| format!("Error reading {}: {}", path.to_string_lossy(), e))?;
|
||||
// remove everything under the path
|
||||
if !context.preexisting_install {
|
||||
std::fs::remove_dir_all(path)
|
||||
.map_err(|e| format!("Error removing {}: {}", path.to_string_lossy(), e))?;
|
||||
return Ok(TaskParamType::None);
|
||||
}
|
||||
// remove everything except the maintenancetool if repairing
|
||||
for entry in entries {
|
||||
let path = entry
|
||||
.map_err(|e| format!("Error reading file: {}", e))?
|
||||
.path();
|
||||
if let Some(filename) = path.file_name() {
|
||||
if filename.to_string_lossy().starts_with("maintenancetool") {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if path.is_dir() {
|
||||
std::fs::remove_dir_all(&path)
|
||||
.map_err(|e| format!("Error removing {}: {}", path.to_string_lossy(), e))?;
|
||||
} else {
|
||||
std::fs::remove_file(&path)
|
||||
.map_err(|e| format!("Error removing {}: {}", path.to_string_lossy(), e))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(TaskParamType::None)
|
||||
}
|
||||
|
||||
fn dependencies(&self) -> Vec<TaskDependency> {
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn name(&self) -> String {
|
||||
"RemoveTargetDirTask".to_string()
|
||||
}
|
||||
}
|
||||
|
|
@ -2,18 +2,18 @@
|
|||
|
||||
use std::env::consts::OS;
|
||||
|
||||
use installer::InstallerFramework;
|
||||
use crate::installer::InstallerFramework;
|
||||
|
||||
use tasks::Task;
|
||||
use tasks::TaskDependency;
|
||||
use tasks::TaskMessage;
|
||||
use tasks::TaskParamType;
|
||||
use crate::tasks::Task;
|
||||
use crate::tasks::TaskDependency;
|
||||
use crate::tasks::TaskMessage;
|
||||
use crate::tasks::TaskParamType;
|
||||
|
||||
use config::PackageDescription;
|
||||
use crate::config::PackageDescription;
|
||||
|
||||
use regex::Regex;
|
||||
|
||||
use logging::LoggingErrors;
|
||||
use crate::logging::LoggingErrors;
|
||||
|
||||
pub struct ResolvePackageTask {
|
||||
pub name: String,
|
||||
|
|
@ -24,7 +24,7 @@ impl Task for ResolvePackageTask {
|
|||
&mut self,
|
||||
input: Vec<TaskParamType>,
|
||||
context: &mut InstallerFramework,
|
||||
messenger: &Fn(&TaskMessage),
|
||||
messenger: &dyn Fn(&TaskMessage),
|
||||
) -> Result<TaskParamType, String> {
|
||||
assert_eq!(input.len(), 0);
|
||||
let mut metadata: Option<PackageDescription> = None;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
//! Saves the main database into the installation directory.
|
||||
|
||||
use installer::InstallerFramework;
|
||||
use crate::installer::InstallerFramework;
|
||||
|
||||
use tasks::Task;
|
||||
use tasks::TaskDependency;
|
||||
use tasks::TaskMessage;
|
||||
use tasks::TaskParamType;
|
||||
use crate::tasks::Task;
|
||||
use crate::tasks::TaskDependency;
|
||||
use crate::tasks::TaskMessage;
|
||||
use crate::tasks::TaskParamType;
|
||||
|
||||
pub struct SaveDatabaseTask {}
|
||||
|
||||
|
|
@ -14,7 +14,7 @@ impl Task for SaveDatabaseTask {
|
|||
&mut self,
|
||||
input: Vec<TaskParamType>,
|
||||
context: &mut InstallerFramework,
|
||||
messenger: &Fn(&TaskMessage),
|
||||
messenger: &dyn Fn(&TaskMessage),
|
||||
) -> Result<TaskParamType, String> {
|
||||
assert_eq!(input.len(), 0);
|
||||
messenger(&TaskMessage::DisplayMessage(
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
//! Saves the installer executable into the install directory.
|
||||
|
||||
use installer::InstallerFramework;
|
||||
use crate::installer::InstallerFramework;
|
||||
|
||||
use tasks::Task;
|
||||
use tasks::TaskDependency;
|
||||
use tasks::TaskMessage;
|
||||
use tasks::TaskParamType;
|
||||
use crate::tasks::Task;
|
||||
use crate::tasks::TaskDependency;
|
||||
use crate::tasks::TaskMessage;
|
||||
use crate::tasks::TaskParamType;
|
||||
|
||||
use std::fs::File;
|
||||
use std::fs::OpenOptions;
|
||||
|
|
@ -14,7 +14,7 @@ use std::io::copy;
|
|||
|
||||
use std::env::current_exe;
|
||||
|
||||
use logging::LoggingErrors;
|
||||
use crate::logging::LoggingErrors;
|
||||
|
||||
pub struct SaveExecutableTask {}
|
||||
|
||||
|
|
@ -23,7 +23,7 @@ impl Task for SaveExecutableTask {
|
|||
&mut self,
|
||||
input: Vec<TaskParamType>,
|
||||
context: &mut InstallerFramework,
|
||||
messenger: &Fn(&TaskMessage),
|
||||
messenger: &dyn Fn(&TaskMessage),
|
||||
) -> Result<TaskParamType, String> {
|
||||
assert_eq!(input.len(), 0);
|
||||
messenger(&TaskMessage::DisplayMessage(
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
//! Uninstalls a set of packages.
|
||||
|
||||
use installer::InstallerFramework;
|
||||
use crate::installer::InstallerFramework;
|
||||
|
||||
use tasks::Task;
|
||||
use tasks::TaskParamType;
|
||||
use crate::tasks::Task;
|
||||
use crate::tasks::TaskParamType;
|
||||
|
||||
use tasks::uninstall_pkg::UninstallPackageTask;
|
||||
use tasks::TaskDependency;
|
||||
use tasks::TaskMessage;
|
||||
use tasks::TaskOrdering;
|
||||
use crate::tasks::uninstall_pkg::UninstallPackageTask;
|
||||
use crate::tasks::TaskDependency;
|
||||
use crate::tasks::TaskMessage;
|
||||
use crate::tasks::TaskOrdering;
|
||||
|
||||
pub struct UninstallTask {
|
||||
pub items: Vec<String>,
|
||||
|
|
@ -19,7 +19,7 @@ impl Task for UninstallTask {
|
|||
&mut self,
|
||||
_: Vec<TaskParamType>,
|
||||
_: &mut InstallerFramework,
|
||||
messenger: &Fn(&TaskMessage),
|
||||
messenger: &dyn Fn(&TaskMessage),
|
||||
) -> Result<TaskParamType, String> {
|
||||
messenger(&TaskMessage::DisplayMessage("Wrapping up...", 0.0));
|
||||
Ok(TaskParamType::None)
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
//! Uninstalls a specific package.
|
||||
|
||||
use installer::InstallerFramework;
|
||||
use crate::installer::InstallerFramework;
|
||||
|
||||
use tasks::Task;
|
||||
use tasks::TaskDependency;
|
||||
use tasks::TaskMessage;
|
||||
use tasks::TaskParamType;
|
||||
use crate::tasks::Task;
|
||||
use crate::tasks::TaskDependency;
|
||||
use crate::tasks::TaskMessage;
|
||||
use crate::tasks::TaskParamType;
|
||||
|
||||
use crate::tasks::save_database::SaveDatabaseTask;
|
||||
use crate::tasks::TaskOrdering;
|
||||
use std::fs::remove_file;
|
||||
use tasks::save_database::SaveDatabaseTask;
|
||||
use tasks::TaskOrdering;
|
||||
|
||||
pub struct UninstallGlobalShortcutsTask {}
|
||||
|
||||
|
|
@ -18,7 +18,7 @@ impl Task for UninstallGlobalShortcutsTask {
|
|||
&mut self,
|
||||
input: Vec<TaskParamType>,
|
||||
context: &mut InstallerFramework,
|
||||
messenger: &Fn(&TaskMessage),
|
||||
messenger: &dyn Fn(&TaskMessage),
|
||||
) -> Result<TaskParamType, String> {
|
||||
assert_eq!(input.len(), 0);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,21 +1,21 @@
|
|||
//! Uninstalls a specific package.
|
||||
|
||||
use installer::InstallerFramework;
|
||||
use crate::installer::InstallerFramework;
|
||||
|
||||
use tasks::save_database::SaveDatabaseTask;
|
||||
use tasks::Task;
|
||||
use tasks::TaskDependency;
|
||||
use tasks::TaskMessage;
|
||||
use tasks::TaskOrdering;
|
||||
use tasks::TaskParamType;
|
||||
use crate::tasks::save_database::SaveDatabaseTask;
|
||||
use crate::tasks::Task;
|
||||
use crate::tasks::TaskDependency;
|
||||
use crate::tasks::TaskMessage;
|
||||
use crate::tasks::TaskOrdering;
|
||||
use crate::tasks::TaskParamType;
|
||||
|
||||
use installer::LocalInstallation;
|
||||
use crate::installer::LocalInstallation;
|
||||
|
||||
use std::fs::remove_dir;
|
||||
use std::fs::remove_file;
|
||||
|
||||
use logging::LoggingErrors;
|
||||
use tasks::uninstall_shortcuts::UninstallShortcutsTask;
|
||||
use crate::logging::LoggingErrors;
|
||||
use crate::tasks::uninstall_shortcuts::UninstallShortcutsTask;
|
||||
|
||||
pub struct UninstallPackageTask {
|
||||
pub name: String,
|
||||
|
|
@ -27,7 +27,7 @@ impl Task for UninstallPackageTask {
|
|||
&mut self,
|
||||
input: Vec<TaskParamType>,
|
||||
context: &mut InstallerFramework,
|
||||
messenger: &Fn(&TaskMessage),
|
||||
messenger: &dyn Fn(&TaskMessage),
|
||||
) -> Result<TaskParamType, String> {
|
||||
assert_eq!(input.len(), 1);
|
||||
|
||||
|
|
@ -44,7 +44,7 @@ impl Task for UninstallPackageTask {
|
|||
}
|
||||
}
|
||||
|
||||
let mut package = match metadata {
|
||||
let package = match metadata {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
if self.optional {
|
||||
|
|
@ -63,8 +63,7 @@ impl Task for UninstallPackageTask {
|
|||
0.0,
|
||||
));
|
||||
|
||||
// Reverse, as to delete directories last
|
||||
package.files.reverse();
|
||||
let mut directories = Vec::new();
|
||||
|
||||
let max = package.files.len();
|
||||
for (i, file) in package.files.iter().enumerate() {
|
||||
|
|
@ -78,7 +77,9 @@ impl Task for UninstallPackageTask {
|
|||
));
|
||||
|
||||
let result = if file.is_dir() {
|
||||
remove_dir(file)
|
||||
// we don't delete directory just yet
|
||||
directories.push(file);
|
||||
Ok(())
|
||||
} else {
|
||||
remove_file(file)
|
||||
};
|
||||
|
|
@ -88,6 +89,17 @@ impl Task for UninstallPackageTask {
|
|||
}
|
||||
}
|
||||
|
||||
// sort directories by reverse depth order
|
||||
directories.sort_by(|a, b| {
|
||||
let depth_a = a.components().fold(0usize, |acc, _| acc + 1);
|
||||
let depth_b = b.components().fold(0usize, |acc, _| acc + 1);
|
||||
depth_b.cmp(&depth_a)
|
||||
});
|
||||
for i in directories.iter() {
|
||||
info!("Deleting directory: {:?}", i);
|
||||
remove_dir(i).ok();
|
||||
}
|
||||
|
||||
Ok(TaskParamType::None)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
//! Uninstalls a specific package.
|
||||
|
||||
use installer::InstallerFramework;
|
||||
use crate::installer::InstallerFramework;
|
||||
|
||||
use tasks::Task;
|
||||
use tasks::TaskDependency;
|
||||
use tasks::TaskMessage;
|
||||
use tasks::TaskParamType;
|
||||
use crate::tasks::Task;
|
||||
use crate::tasks::TaskDependency;
|
||||
use crate::tasks::TaskMessage;
|
||||
use crate::tasks::TaskParamType;
|
||||
|
||||
use installer::LocalInstallation;
|
||||
use crate::installer::LocalInstallation;
|
||||
|
||||
use std::fs::remove_dir;
|
||||
use std::fs::remove_file;
|
||||
|
||||
use logging::LoggingErrors;
|
||||
use crate::logging::LoggingErrors;
|
||||
|
||||
pub struct UninstallShortcutsTask {
|
||||
pub name: String,
|
||||
|
|
@ -24,7 +24,7 @@ impl Task for UninstallShortcutsTask {
|
|||
&mut self,
|
||||
input: Vec<TaskParamType>,
|
||||
context: &mut InstallerFramework,
|
||||
messenger: &Fn(&TaskMessage),
|
||||
messenger: &dyn Fn(&TaskMessage),
|
||||
) -> Result<TaskParamType, String> {
|
||||
assert_eq!(input.len(), 0);
|
||||
|
||||
|
|
|
|||
1
static/css/bulma.min.css
vendored
1
static/css/bulma.min.css
vendored
File diff suppressed because one or more lines are too long
|
|
@ -1,88 +0,0 @@
|
|||
/* roboto-regular - latin */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url('../fonts/roboto-v18-latin-regular.eot'); /* IE9 Compat Modes */
|
||||
src: local('Roboto'), local('Roboto-Regular'),
|
||||
url('../fonts/roboto-v18-latin-regular.woff2') format('woff2'), /* Super Modern Browsers */
|
||||
url('../fonts/roboto-v18-latin-regular.woff') format('woff');
|
||||
}
|
||||
|
||||
html, body {
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body, div, span, h1, h2, h3, h4, h5, h6 {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
pre {
|
||||
-webkit-user-select: text;
|
||||
-moz-user-select: text;
|
||||
-ms-user-select: text;
|
||||
user-select: text;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.tile.is-child > .box {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.has-padding {
|
||||
padding: 2rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.clickable-box {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.clickable-box label {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.is-max-height {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.is-bottom-floating {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.is-right-floating {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.has-padding .is-right-floating {
|
||||
right: 1rem;
|
||||
}
|
||||
|
||||
.is-left-floating {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.has-padding .is-left-floating {
|
||||
left: 1rem;
|
||||
}
|
||||
|
||||
.fullscreen {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 9999;
|
||||
padding: 20px;
|
||||
background: #fff;
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 47 KiB |
|
|
@ -1,67 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport"
|
||||
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title id="window-title">... Installer</title>
|
||||
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
|
||||
|
||||
<link rel="stylesheet" href="/css/bulma.min.css" type="text/css">
|
||||
<link rel="stylesheet" href="/css/main.css" type="text/css">
|
||||
</head>
|
||||
<body class="is-max-height">
|
||||
<div class="fullscreen" id="ie-blackout" style="display: none">
|
||||
<div class="title">Your computer is out of date.</div>
|
||||
<div class="subtitle">
|
||||
Make sure that your computer is up to date, and that you have Internet Explorer 11 installed.
|
||||
</div>
|
||||
<div class="subtitle">
|
||||
Please note we do not support pirated or unsupported versions of Windows.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
if (!document.__proto__) {
|
||||
document.getElementById("ie-blackout").style.display = "block";
|
||||
}
|
||||
</script>
|
||||
|
||||
<div id="app" class="is-max-height">
|
||||
<section class="section is-max-height">
|
||||
<div class="container is-max-height">
|
||||
<div class="columns is-max-height">
|
||||
<div class="column is-one-third has-padding" v-if="!metadata.is_launcher">
|
||||
<img src="/logo.png" width="60%" />
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<h2 class="subtitle" v-if="!metadata.preexisting_install">
|
||||
Welcome to the {{ attrs.name }} installer!
|
||||
</h2>
|
||||
<h2 class="subtitle" v-if="!metadata.preexisting_install">
|
||||
We will have you up and running in just a few moments.
|
||||
</h2>
|
||||
|
||||
<h2 class="subtitle" v-if="metadata.preexisting_install">
|
||||
Welcome to the {{ attrs.name }} Maintenance Tool.
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<router-view></router-view>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script src="/js/vue.min.js" type="text/javascript"></script>
|
||||
<script src="/js/vue-router.min.js" type="text/javascript"></script>
|
||||
<script src="/api/attrs" type="text/javascript"></script>
|
||||
<script src="/js/helpers.js" type="text/javascript"></script>
|
||||
<script src="/js/views.js" type="text/javascript"></script>
|
||||
<script src="/js/main.js" type="text/javascript"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,144 +0,0 @@
|
|||
/**
|
||||
* helpers.js
|
||||
*
|
||||
* Additional state-less helper methods.
|
||||
*/
|
||||
|
||||
var request_id = 0;
|
||||
|
||||
/**
|
||||
* Makes a AJAX request.
|
||||
*
|
||||
* @param path The path to connect to.
|
||||
* @param successCallback A callback with a JSON payload.
|
||||
* @param failCallback A fail callback. Optional.
|
||||
* @param data POST data. Optional.
|
||||
*/
|
||||
function ajax(path, successCallback, failCallback, data) {
|
||||
if (failCallback === undefined) {
|
||||
failCallback = defaultFailHandler;
|
||||
}
|
||||
|
||||
console.log("Making HTTP request to " + path);
|
||||
|
||||
var req = new XMLHttpRequest();
|
||||
|
||||
req.addEventListener("load", function() {
|
||||
// The server can sometimes return a string error. Make sure we handle this.
|
||||
if (this.status === 200 && this.getResponseHeader('Content-Type').indexOf("application/json") !== -1) {
|
||||
successCallback(JSON.parse(this.responseText));
|
||||
} else {
|
||||
failCallback(this.responseText);
|
||||
}
|
||||
});
|
||||
req.addEventListener("error", failCallback);
|
||||
|
||||
req.open(data == null ? "GET" : "POST", path + "?nocache=" + request_id++, true);
|
||||
// Rocket only currently supports URL encoded forms.
|
||||
req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
|
||||
|
||||
if (data != null) {
|
||||
var form = "";
|
||||
|
||||
for (var key in data) {
|
||||
if (!data.hasOwnProperty(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (form !== "") {
|
||||
form += "&";
|
||||
}
|
||||
|
||||
form += encodeURIComponent(key) + "=" + encodeURIComponent(data[key]);
|
||||
}
|
||||
|
||||
req.send(form);
|
||||
} else {
|
||||
req.send();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a AJAX request, streaming each line as it arrives. Type should be text/plain,
|
||||
* each line will be interpreted as JSON separately.
|
||||
*
|
||||
* @param path The path to connect to.
|
||||
* @param callback A callback with a JSON payload. Called for every line as it comes.
|
||||
* @param successCallback A callback with a raw text payload.
|
||||
* @param failCallback A fail callback. Optional.
|
||||
* @param data POST data. Optional.
|
||||
*/
|
||||
function stream_ajax(path, callback, successCallback, failCallback, data) {
|
||||
var req = new XMLHttpRequest();
|
||||
|
||||
console.log("Making streaming HTTP request to " + path);
|
||||
|
||||
req.addEventListener("load", function() {
|
||||
// The server can sometimes return a string error. Make sure we handle this.
|
||||
if (this.status === 200) {
|
||||
successCallback(this.responseText);
|
||||
} else {
|
||||
failCallback(this.responseText);
|
||||
}
|
||||
});
|
||||
|
||||
var buffer = "";
|
||||
var seenBytes = 0;
|
||||
|
||||
req.onreadystatechange = function() {
|
||||
if(req.readyState > 2) {
|
||||
buffer += req.responseText.substr(seenBytes);
|
||||
|
||||
var pointer;
|
||||
while ((pointer = buffer.indexOf("\n")) >= 0) {
|
||||
var line = buffer.substring(0, pointer).trim();
|
||||
buffer = buffer.substring(pointer + 1);
|
||||
|
||||
if (line.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var contents = JSON.parse(line);
|
||||
callback(contents);
|
||||
}
|
||||
|
||||
seenBytes = req.responseText.length;
|
||||
}
|
||||
};
|
||||
|
||||
req.addEventListener("error", failCallback);
|
||||
|
||||
req.open(data == null ? "GET" : "POST", path + "?nocache=" + request_id++, true);
|
||||
// Rocket only currently supports URL encoded forms.
|
||||
req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
|
||||
|
||||
if (data != null) {
|
||||
var form = "";
|
||||
|
||||
for (var key in data) {
|
||||
if (!data.hasOwnProperty(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (form !== "") {
|
||||
form += "&";
|
||||
}
|
||||
|
||||
form += encodeURIComponent(key) + "=" + encodeURIComponent(data[key]);
|
||||
}
|
||||
|
||||
req.send(form);
|
||||
} else {
|
||||
req.send();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The default handler if a AJAX request fails. Not to be used directly.
|
||||
*
|
||||
* @param e The XMLHttpRequest that failed.
|
||||
*/
|
||||
function defaultFailHandler(e) {
|
||||
console.error("A AJAX request failed, and was not caught:");
|
||||
console.error(e);
|
||||
}
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
// Overwrite loggers with the logging backend
|
||||
if (window.external !== undefined && window.external.invoke !== undefined) {
|
||||
window.onerror = function(msg, url, line) {
|
||||
old_onerror(msg, url, line);
|
||||
window.external.invoke(JSON.stringify({
|
||||
Log: {
|
||||
kind: "error",
|
||||
msg: msg + " @ " + url + ":" + line
|
||||
}
|
||||
}));
|
||||
};
|
||||
|
||||
// Borrowed from http://tobyho.com/2012/07/27/taking-over-console-log/
|
||||
function intercept(method){
|
||||
console[method] = function(){
|
||||
var message = Array.prototype.slice.apply(arguments).join(' ');
|
||||
window.external.invoke(JSON.stringify({
|
||||
Log: {
|
||||
kind: method,
|
||||
msg: message
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
var methods = ['log', 'warn', 'error'];
|
||||
for (var i = 0; i < methods.length; i++) {
|
||||
intercept(methods[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Disable F5
|
||||
function disable_shortcuts(e) {
|
||||
switch (e.keyCode) {
|
||||
case 116: // F5
|
||||
e.preventDefault();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("keydown", disable_shortcuts);
|
||||
|
||||
document.getElementById("window-title").innerText = base_attributes.name + " Installer";
|
||||
|
||||
function selectFileCallback(name) {
|
||||
app.install_location = name;
|
||||
}
|
||||
|
||||
var app = new Vue({
|
||||
router: router,
|
||||
data: {
|
||||
attrs: base_attributes,
|
||||
config : {},
|
||||
install_location : "",
|
||||
// If the option to pick an install location should be provided
|
||||
show_install_location : true,
|
||||
metadata : {
|
||||
database : [],
|
||||
install_path : "",
|
||||
preexisting_install : false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
"exit": function() {
|
||||
ajax("/api/exit", function() {});
|
||||
}
|
||||
}
|
||||
}).$mount("#app");
|
||||
|
|
@ -1,460 +0,0 @@
|
|||
const DownloadConfig = {
|
||||
template: `
|
||||
<div class="column has-padding">
|
||||
<h4 class="subtitle">Downloading config...</h4>
|
||||
|
||||
<br />
|
||||
<progress class="progress is-info is-medium" value="0" max="100">
|
||||
0%
|
||||
</progress>
|
||||
</div>
|
||||
`,
|
||||
created: function() {
|
||||
this.download_install_status();
|
||||
},
|
||||
methods: {
|
||||
download_install_status: function() {
|
||||
var that = this; // IE workaround
|
||||
|
||||
ajax("/api/installation-status", function(e) {
|
||||
app.metadata = e;
|
||||
|
||||
that.download_config();
|
||||
});
|
||||
},
|
||||
download_config: function() {
|
||||
var that = this; // IE workaround
|
||||
|
||||
ajax("/api/config", function(e) {
|
||||
app.config = e;
|
||||
|
||||
that.choose_next_state();
|
||||
|
||||
}, function(e) {
|
||||
console.error("Got error while downloading config: "
|
||||
+ e);
|
||||
|
||||
if (app.metadata.is_launcher) {
|
||||
// Just launch the target application
|
||||
app.exit();
|
||||
} else {
|
||||
router.replace({name: 'showerr', params: {msg: "Got error while downloading config: "
|
||||
+ e}});
|
||||
}
|
||||
});
|
||||
},
|
||||
choose_next_state: function() {
|
||||
// Update the updater if needed
|
||||
if (app.config.new_tool) {
|
||||
router.push("/install/updater");
|
||||
return;
|
||||
}
|
||||
|
||||
if (app.metadata.preexisting_install) {
|
||||
app.install_location = app.metadata.install_path;
|
||||
|
||||
// Copy over installed packages
|
||||
for (var x = 0; x < app.config.packages.length; x++) {
|
||||
app.config.packages[x].default = false;
|
||||
app.config.packages[x].installed = false;
|
||||
}
|
||||
|
||||
for (var i = 0; i < app.metadata.database.packages.length; i++) {
|
||||
// Find this config package
|
||||
for (var x = 0; x < app.config.packages.length; x++) {
|
||||
if (app.config.packages[x].name === app.metadata.database.packages[i].name) {
|
||||
app.config.packages[x].default = true;
|
||||
app.config.packages[x].installed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (app.metadata.is_launcher) {
|
||||
router.replace("/install/regular");
|
||||
} else {
|
||||
router.replace("/modify");
|
||||
}
|
||||
} else {
|
||||
for (var x = 0; x < app.config.packages.length; x++) {
|
||||
app.config.packages[x].installed = false;
|
||||
}
|
||||
|
||||
// Need to do a bit more digging to get at the
|
||||
// install location.
|
||||
ajax("/api/default-path", function(e) {
|
||||
if (e.path != null) {
|
||||
app.install_location = e.path;
|
||||
}
|
||||
});
|
||||
|
||||
router.replace("/packages");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const SelectPackages = {
|
||||
template: `
|
||||
<div class="column has-padding">
|
||||
<h4 class="subtitle">Select which packages you want to install:</h4>
|
||||
|
||||
<!-- Build options -->
|
||||
<div class="tile is-ancestor">
|
||||
<div class="tile is-parent" v-for="package in $root.$data.config.packages" :index="package.name">
|
||||
<div class="tile is-child">
|
||||
<div class="box clickable-box" v-on:click.capture.stop="package.default = !package.default">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" v-model="package.default" />
|
||||
{{ package.name }}
|
||||
<span v-if="package.installed"><i>(installed)</i></span>
|
||||
</label>
|
||||
<p>
|
||||
{{ package.description }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="subtitle is-6" v-if="!$root.$data.metadata.preexisting_install && advanced">Install Location</div>
|
||||
<div class="field has-addons" v-if="!$root.$data.metadata.preexisting_install && advanced">
|
||||
<div class="control is-expanded">
|
||||
<input class="input" type="text" v-model="$root.$data.install_location"
|
||||
placeholder="Enter a install path here">
|
||||
</div>
|
||||
<div class="control">
|
||||
<a class="button is-dark" v-on:click="select_file">
|
||||
Select
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="is-right-floating is-bottom-floating">
|
||||
<div class="field is-grouped">
|
||||
<p class="control">
|
||||
<a class="button is-medium" v-if="!$root.$data.config.hide_advanced && !$root.$data.metadata.preexisting_install && !advanced"
|
||||
v-on:click="advanced = true">Advanced...</a>
|
||||
</p>
|
||||
<p class="control">
|
||||
<a class="button is-dark is-medium" v-if="!$root.$data.metadata.preexisting_install"
|
||||
v-on:click="install">Install</a>
|
||||
</p>
|
||||
<p class="control">
|
||||
<a class="button is-dark is-medium" v-if="$root.$data.metadata.preexisting_install"
|
||||
v-on:click="install">Modify</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field is-grouped is-left-floating is-bottom-floating">
|
||||
<p class="control">
|
||||
<a class="button is-medium" v-if="$root.$data.metadata.preexisting_install"
|
||||
v-on:click="go_back">Back</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
data: function() {
|
||||
return {
|
||||
advanced: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
select_file: function() {
|
||||
window.external.invoke(JSON.stringify({
|
||||
SelectInstallDir: {
|
||||
callback_name: "selectFileCallback"
|
||||
}
|
||||
}));
|
||||
},
|
||||
install: function() {
|
||||
router.push("/install/regular");
|
||||
},
|
||||
go_back: function() {
|
||||
router.go(-1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const InstallPackages = {
|
||||
template: `
|
||||
<div class="column has-padding">
|
||||
<h4 class="subtitle" v-if="$root.$data.metadata.is_launcher || is_update">Checking for updates...</h4>
|
||||
<h4 class="subtitle" v-else-if="is_uninstall">Uninstalling...</h4>
|
||||
<h4 class="subtitle" v-else-if="is_updater_update">Downloading self-update...</h4>
|
||||
<h4 class="subtitle" v-else>Installing...</h4>
|
||||
<div v-html="$root.$data.config.installing_message"></div>
|
||||
<br />
|
||||
|
||||
<div v-html="progress_message"></div>
|
||||
<progress class="progress is-info is-medium" v-bind:value="progress" max="100">
|
||||
{{ progress }}%
|
||||
</progress>
|
||||
</div>
|
||||
`,
|
||||
data: function() {
|
||||
return {
|
||||
progress: 0.0,
|
||||
progress_message: "Please wait...",
|
||||
is_uninstall: false,
|
||||
is_updater_update: false,
|
||||
is_update: false,
|
||||
failed_with_error: false,
|
||||
packages_installed: 0
|
||||
}
|
||||
},
|
||||
created: function() {
|
||||
this.is_uninstall = this.$route.params.kind === "uninstall";
|
||||
this.is_updater_update = this.$route.params.kind === "updater";
|
||||
this.is_update = this.$route.params.kind === "update";
|
||||
console.log("Installer kind: " + this.$route.params.kind);
|
||||
this.install();
|
||||
},
|
||||
methods: {
|
||||
install: function() {
|
||||
var results = {};
|
||||
|
||||
for (var package_index = 0; package_index < app.config.packages.length; package_index++) {
|
||||
var current_package = app.config.packages[package_index];
|
||||
if (current_package.default != null) {
|
||||
results[current_package.name] = current_package.default;
|
||||
}
|
||||
}
|
||||
|
||||
results["path"] = app.install_location;
|
||||
|
||||
var that = this; // IE workaround
|
||||
|
||||
var targetUrl = "/api/start-install";
|
||||
if (this.is_uninstall) {
|
||||
targetUrl = "/api/uninstall";
|
||||
}
|
||||
if (this.is_updater_update) {
|
||||
targetUrl = "/api/update-updater";
|
||||
}
|
||||
|
||||
stream_ajax(targetUrl, function(line) {
|
||||
if (line.hasOwnProperty("Status")) {
|
||||
that.progress_message = line.Status[0];
|
||||
that.progress = line.Status[1] * 100;
|
||||
}
|
||||
|
||||
if (line.hasOwnProperty("PackageInstalled")) {
|
||||
that.packages_installed += 1;
|
||||
}
|
||||
|
||||
if (line.hasOwnProperty("Error")) {
|
||||
if (app.metadata.is_launcher) {
|
||||
app.exit();
|
||||
} else {
|
||||
that.failed_with_error = true;
|
||||
router.replace({name: 'showerr', params: {msg: line.Error}});
|
||||
}
|
||||
}
|
||||
}, function(e) {
|
||||
if (that.is_updater_update) {
|
||||
// Continue with what we were doing
|
||||
if (app.metadata.is_launcher) {
|
||||
router.replace("/install/regular");
|
||||
} else {
|
||||
if (app.metadata.preexisting_install) {
|
||||
router.replace("/modify");
|
||||
} else {
|
||||
router.replace("/packages");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (app.metadata.is_launcher) {
|
||||
app.exit();
|
||||
} else if (!that.failed_with_error) {
|
||||
if (that.is_uninstall) {
|
||||
router.replace({name: 'complete', params: {
|
||||
uninstall: true,
|
||||
update: that.is_update,
|
||||
installed: that.packages_installed
|
||||
}});
|
||||
} else {
|
||||
router.replace({name: 'complete', params: {
|
||||
uninstall: false,
|
||||
update: that.is_update,
|
||||
installed: that.packages_installed
|
||||
}});
|
||||
}
|
||||
}
|
||||
}
|
||||
}, undefined, results);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const ErrorView = {
|
||||
template: `
|
||||
<div class="column has-padding">
|
||||
<h4 class="subtitle">An error occurred:</h4>
|
||||
|
||||
<pre>{{ msg }}</pre>
|
||||
|
||||
<div class="field is-grouped is-right-floating is-bottom-floating">
|
||||
<p class="control">
|
||||
<a class="button is-primary is-medium" v-if="remaining" v-on:click="go_back">Back</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
data: function() {
|
||||
return {
|
||||
msg: this.$route.params.msg,
|
||||
remaining: window.history.length > 1
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
go_back: function() {
|
||||
router.go(-1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const CompleteView = {
|
||||
template: `
|
||||
<div class="column has-padding">
|
||||
<div v-if="was_update">
|
||||
<div v-if="has_installed">
|
||||
<h4 class="subtitle">{{ $root.$data.attrs.name }} has been updated.</h4>
|
||||
|
||||
<p>You can find your installed applications in your start menu.</p>
|
||||
</div>
|
||||
<div v-else>
|
||||
<h4 class="subtitle">{{ $root.$data.attrs.name }} is already up to date!</h4>
|
||||
|
||||
<p>You can find your installed applications in your start menu.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="was_install">
|
||||
<h4 class="subtitle">Thanks for installing {{ $root.$data.attrs.name }}!</h4>
|
||||
|
||||
<p>You can find your installed applications in your start menu.</p>
|
||||
|
||||
<img src="/how-to-open.png" />
|
||||
</div>
|
||||
<div v-else>
|
||||
<h4 class="subtitle">{{ $root.$data.attrs.name }} has been uninstalled.</h4>
|
||||
</div>
|
||||
|
||||
<div class="field is-grouped is-right-floating is-bottom-floating">
|
||||
<p class="control">
|
||||
<a class="button is-dark is-medium" v-on:click="exit">Exit</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
data: function() {
|
||||
return {
|
||||
was_install: !this.$route.params.uninstall,
|
||||
was_update: this.$route.params.update,
|
||||
has_installed: this.$route.params.packages_installed > 0
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
exit: function() {
|
||||
app.exit();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const ModifyView = {
|
||||
template: `
|
||||
<div class="column has-padding">
|
||||
<h4 class="subtitle">Choose an option:</h4>
|
||||
|
||||
<a class="button is-dark is-medium" v-on:click="update">
|
||||
Update
|
||||
</a>
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<a class="button is-dark is-medium" v-on:click="modify_packages">
|
||||
Modify
|
||||
</a>
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<a class="button is-dark is-medium" v-on:click="prepare_uninstall">
|
||||
Uninstall
|
||||
</a>
|
||||
|
||||
<div class="modal is-active" v-if="show_uninstall">
|
||||
<div class="modal-background"></div>
|
||||
<div class="modal-card">
|
||||
<header class="modal-card-head">
|
||||
<p class="modal-card-title">Are you sure you want to uninstall {{ $root.$data.attrs.name }}?</p>
|
||||
</header>
|
||||
<footer class="modal-card-foot">
|
||||
<button class="button is-danger" v-on:click="uninstall">Yes</button>
|
||||
<button class="button" v-on:click="cancel_uninstall">No</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
data: function() {
|
||||
return {
|
||||
show_uninstall: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
update: function() {
|
||||
router.push("/install/update");
|
||||
},
|
||||
modify_packages: function() {
|
||||
router.push("/packages");
|
||||
},
|
||||
prepare_uninstall: function() {
|
||||
this.show_uninstall = true;
|
||||
},
|
||||
cancel_uninstall: function() {
|
||||
this.show_uninstall = false;
|
||||
},
|
||||
uninstall: function() {
|
||||
router.push("/install/uninstall");
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
const router = new VueRouter({
|
||||
routes: [
|
||||
{
|
||||
path: '/config',
|
||||
name: 'config',
|
||||
component: DownloadConfig
|
||||
},
|
||||
{
|
||||
path: '/packages',
|
||||
name: 'packages',
|
||||
component: SelectPackages
|
||||
},
|
||||
{
|
||||
path: '/install/:kind',
|
||||
name: 'install',
|
||||
component: InstallPackages
|
||||
},
|
||||
{
|
||||
path: '/showerr',
|
||||
name: 'showerr',
|
||||
component: ErrorView
|
||||
},
|
||||
{
|
||||
path: '/complete/:uninstall/:update/:packages_installed',
|
||||
name: 'complete',
|
||||
component: CompleteView
|
||||
},
|
||||
{
|
||||
path: '/modify',
|
||||
name: 'modify',
|
||||
component: ModifyView
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
redirect: '/config'
|
||||
}
|
||||
]
|
||||
});
|
||||
6
static/js/vue-router.min.js
vendored
6
static/js/vue-router.min.js
vendored
File diff suppressed because one or more lines are too long
6
static/js/vue.min.js
vendored
6
static/js/vue.min.js
vendored
File diff suppressed because one or more lines are too long
1
ui/.browserslistrc
Normal file
1
ui/.browserslistrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
> 1%
|
||||
5
ui/.editorconfig
Normal file
5
ui/.editorconfig
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
[*.{js,jsx,ts,tsx,vue}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
16
ui/.eslintrc.js
Normal file
16
ui/.eslintrc.js
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true
|
||||
},
|
||||
extends: [
|
||||
'plugin:vue/recommended',
|
||||
'@vue/standard'
|
||||
],
|
||||
rules: {
|
||||
'no-console': 'off',
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||
'no-redeclare': 'off',
|
||||
camelcase: 'off'
|
||||
}
|
||||
}
|
||||
21
ui/.gitignore
vendored
Normal file
21
ui/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
/dist
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw*
|
||||
29
ui/README.md
Normal file
29
ui/README.md
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# ui
|
||||
|
||||
## Project setup
|
||||
```
|
||||
yarn install
|
||||
```
|
||||
|
||||
### Compiles and hot-reloads for development
|
||||
```
|
||||
yarn run serve
|
||||
```
|
||||
|
||||
### Compiles and minifies for production
|
||||
```
|
||||
yarn run build
|
||||
```
|
||||
|
||||
### Run your tests
|
||||
```
|
||||
yarn run test
|
||||
```
|
||||
|
||||
### Lints and fixes files
|
||||
```
|
||||
yarn run lint
|
||||
```
|
||||
|
||||
### Customize configuration
|
||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
||||
5
ui/babel.config.js
Normal file
5
ui/babel.config.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
module.exports = {
|
||||
presets: [
|
||||
'@vue/app'
|
||||
]
|
||||
}
|
||||
20
ui/merge-strings.js
Executable file
20
ui/merge-strings.js
Executable file
|
|
@ -0,0 +1,20 @@
|
|||
#!/bin/env node
|
||||
const fs = require('fs')
|
||||
const merge = require('deepmerge')
|
||||
const glob = require('glob')
|
||||
|
||||
glob('src/locales/!(messages).json', {}, (e, files) => {
|
||||
let messages = []
|
||||
for (const file of files) {
|
||||
console.log(`Loading ${file}...`)
|
||||
const locale_messages = require(`./${file}`)
|
||||
messages.push(locale_messages)
|
||||
}
|
||||
console.log('Merging messages...')
|
||||
if (messages && messages.length > 1) {
|
||||
messages = merge.all(messages)
|
||||
} else {
|
||||
messages = messages[0] // single locale mode
|
||||
}
|
||||
fs.writeFileSync('src/locales/messages.json', JSON.stringify(messages), {})
|
||||
})
|
||||
173
ui/mock-server.js
Normal file
173
ui/mock-server.js
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
'use strict'
|
||||
|
||||
const express = require('express')
|
||||
const app = express()
|
||||
const port = 3000
|
||||
|
||||
let showError = false
|
||||
let showConfigError = false
|
||||
let maintenance = false
|
||||
let launcher = false
|
||||
let fileExists = false
|
||||
let darkMode = false
|
||||
let recoveryMode = false
|
||||
|
||||
function progressSimulation (res) {
|
||||
if (showError) {
|
||||
const resp = JSON.stringify({ Error: 'Simulated error.' }) + '\n'
|
||||
res.write(resp)
|
||||
res.status(200).end()
|
||||
return
|
||||
}
|
||||
let progress = 0.0
|
||||
const timer = setInterval(() => {
|
||||
const resp = JSON.stringify({ Status: ['Processing...', progress] }) + '\n'
|
||||
progress += 0.1
|
||||
res.write(resp)
|
||||
if (progress >= 1) {
|
||||
res.status(200).end()
|
||||
clearInterval(timer)
|
||||
}
|
||||
}, 500)
|
||||
}
|
||||
|
||||
function returnConfig (res) {
|
||||
if (showConfigError) {
|
||||
res.status(500).json({})
|
||||
return
|
||||
}
|
||||
res.json({
|
||||
installing_message:
|
||||
'Test Banner <strong>Bold</strong> <pre>Code block</pre> <i>Italic</i> <del>Strike</del>',
|
||||
new_tool: null,
|
||||
packages: [
|
||||
{
|
||||
name: 'Test 1',
|
||||
description: 'LiftInstall GUI Test 1',
|
||||
default: true,
|
||||
source: {
|
||||
name: 'github',
|
||||
match: '^test$',
|
||||
config: { repo: 'j-selby/liftinstall' }
|
||||
},
|
||||
shortcuts: []
|
||||
},
|
||||
{
|
||||
name: 'Test 2',
|
||||
description:
|
||||
'Different Banner <strong>Bold</strong> <pre>Code block</pre> <i>Italic</i> <del>Strike</del>',
|
||||
default: null,
|
||||
source: {
|
||||
name: 'github',
|
||||
match: '^test2$',
|
||||
config: { repo: 'j-selby/liftinstall' }
|
||||
},
|
||||
shortcuts: []
|
||||
}
|
||||
],
|
||||
hide_advanced: false
|
||||
})
|
||||
}
|
||||
|
||||
app.get('/api/attrs', (req, res) => {
|
||||
console.log('-- Get attrs')
|
||||
res.send(
|
||||
{ name: 'yuzu', recovery: recoveryMode, target_url: 'https://raw.githubusercontent.com/j-selby/test-installer/master/config.linux.v2.toml' }
|
||||
)
|
||||
})
|
||||
|
||||
app.get('/api/dark-mode', (req, res) => {
|
||||
res.json(darkMode)
|
||||
})
|
||||
|
||||
app.get('/api/installation-status', (req, res) => {
|
||||
res.json({
|
||||
database: { packages: [], shortcuts: [] },
|
||||
install_path: null,
|
||||
preexisting_install: maintenance,
|
||||
is_launcher: launcher,
|
||||
launcher_path: null
|
||||
})
|
||||
})
|
||||
|
||||
app.get('/api/default-path', (req, res) => {
|
||||
res.json({ path: '/tmp/test/' })
|
||||
})
|
||||
|
||||
app.get('/api/config', (req, res) => {
|
||||
setTimeout(() => {
|
||||
returnConfig(res)
|
||||
}, 3000)
|
||||
})
|
||||
|
||||
app.post('/api/start-install', (req, res) => {
|
||||
console.log('-- Install:')
|
||||
console.log(req.body)
|
||||
progressSimulation(res)
|
||||
})
|
||||
|
||||
app.get('/api/exit', (req, res) => {
|
||||
console.log('-- Exit')
|
||||
if (showError) {
|
||||
res.status(500).send('Simulated error: Nothing to see here.')
|
||||
return
|
||||
}
|
||||
res.status(204).send()
|
||||
})
|
||||
|
||||
app.post('/api/verify-path', (req, res) => {
|
||||
console.log('-- Verify Path')
|
||||
res.send({
|
||||
exists: fileExists
|
||||
})
|
||||
})
|
||||
|
||||
app.post('/api/check-auth', (req, res) => {
|
||||
console.log('-- Check Authorization')
|
||||
res.send({
|
||||
username: 'test1',
|
||||
token: 'token',
|
||||
jwt_token: {
|
||||
isPatreonAccountLinked: true,
|
||||
isPatreonSubscriptionActive: true,
|
||||
releaseChannels: ['early-access']
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
process.argv.forEach((val, index) => {
|
||||
switch (val) {
|
||||
case 'maintenance':
|
||||
maintenance = true
|
||||
console.log('Simulating maintenance mode')
|
||||
break
|
||||
case 'launcher':
|
||||
maintenance = true
|
||||
launcher = true
|
||||
console.log('Simulating launcher mode')
|
||||
break
|
||||
case 'exists':
|
||||
fileExists = true
|
||||
console.log('Simulating file exists situation')
|
||||
break
|
||||
case 'dark':
|
||||
darkMode = true
|
||||
console.log('Simulating dark mode')
|
||||
break
|
||||
case 'config-error':
|
||||
showConfigError = true
|
||||
console.log('Simulating configuration errors')
|
||||
break
|
||||
case 'error':
|
||||
showError = true
|
||||
console.log('Simulating errors')
|
||||
break
|
||||
case 'recovery':
|
||||
recoveryMode = true
|
||||
console.log('Simulating recovery mode')
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
console.log(`Listening on ${port}...`)
|
||||
app.listen(port)
|
||||
36
ui/package.json
Normal file
36
ui/package.json
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
{
|
||||
"name": "ui",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"lint": "vue-cli-service lint",
|
||||
"postinstall": "node merge-strings.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mdi/font": "^7.1.96",
|
||||
"axios": "^1.2.1",
|
||||
"buefy": "^0.9.22",
|
||||
"canvas-confetti": "^1.6.0",
|
||||
"vue": "^2.6.14",
|
||||
"vue-axios": "^3.5.2",
|
||||
"vue-i18n": "^8.26.5",
|
||||
"vue-router": "^3.5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "^5.0.8",
|
||||
"@vue/cli-plugin-eslint": "^5.0.8",
|
||||
"@vue/cli-service": "^5.0.8",
|
||||
"@vue/eslint-config-standard": "^6.1.0",
|
||||
"eslint": "^8.30.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"eslint-plugin-standard": "^4.1.0",
|
||||
"eslint-plugin-vue": "^9.8.0",
|
||||
"express": "^4.18.2",
|
||||
"http-proxy-middleware": "^2.0.6",
|
||||
"vue-template-compiler": "^2.7.14"
|
||||
}
|
||||
}
|
||||
5
ui/postcss.config.js
Normal file
5
ui/postcss.config.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
autoprefixer: {}
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue