diff --git a/src/installer.rs b/src/installer.rs index 3323c84..c769921 100644 --- a/src/installer.rs +++ b/src/installer.rs @@ -30,6 +30,7 @@ use tasks::install::InstallTask; use tasks::uninstall::UninstallTask; use tasks::uninstall_global_shortcut::UninstallGlobalShortcutsTask; use tasks::DependencyTree; +use tasks::TaskMessage; use logging::LoggingErrors; @@ -45,6 +46,7 @@ use number_prefix::{decimal_prefix, Prefixed, Standalone}; #[derive(Serialize)] pub enum InstallMessage { Status(String, f64), + PackageInstalled, Error(String), EOF, } @@ -101,6 +103,24 @@ pub struct LocalInstallation { pub shortcuts: Vec, } +macro_rules! declare_messenger_callback { + ($target:expr) => { + &|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 => { + if let Err(v) = $target.send(InstallMessage::PackageInstalled) { + error!("Failed to submit queue message: {:?}", v); + } + } + } + }; +} + impl InstallerFramework { /// Returns a copy of the configuration. pub fn get_config(&self) -> Option { @@ -170,11 +190,8 @@ impl InstallerFramework { info!("Dependency tree:\n{}", tree); - tree.execute(self, &|msg: &str, progress: f64| { - if let Err(v) = messages.send(InstallMessage::Status(msg.to_string(), progress as _)) { - error!("Failed to submit queue message: {:?}", v); - } - }).map(|_x| ()) + tree.execute(self, declare_messenger_callback!(messages)) + .map(|_x| ()) } /// Sends a request for everything to be uninstalled. @@ -192,22 +209,16 @@ impl InstallerFramework { info!("Dependency tree:\n{}", tree); - tree.execute(self, &|msg: &str, progress: f64| { - if let Err(v) = messages.send(InstallMessage::Status(msg.to_string(), progress as _)) { - error!("Failed to submit queue message: {:?}", v); - } - }).map(|_x| ())?; + tree.execute(self, declare_messenger_callback!(messages)) + .map(|_x| ())?; // Uninstall shortcuts let task = Box::new(UninstallGlobalShortcutsTask {}); let mut tree = DependencyTree::build(task); - tree.execute(self, &|msg: &str, progress: f64| { - if let Err(v) = messages.send(InstallMessage::Status(msg.to_string(), progress as _)) { - error!("Failed to submit queue message: {:?}", v); - } - }).map(|_x| ())?; + tree.execute(self, declare_messenger_callback!(messages)) + .map(|_x| ())?; // Delete the metadata file let path = self diff --git a/src/logging.rs b/src/logging.rs index e64a9a0..26acba0 100644 --- a/src/logging.rs +++ b/src/logging.rs @@ -7,7 +7,7 @@ use log; use std::fmt::Debug; use std::io; -pub fn setup_logger(file_name : String) -> Result<(), fern::InitError> { +pub fn setup_logger(file_name: String) -> Result<(), fern::InitError> { fern::Dispatch::new() .format(|out, message, record| { out.finish(format_args!( diff --git a/src/main.rs b/src/main.rs index b27cee4..468c7c5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -93,10 +93,10 @@ enum CallbackType { } fn main() { - let config = - BaseAttributes::from_toml_str(RAW_CONFIG).expect("Config file could not be read"); + let config = BaseAttributes::from_toml_str(RAW_CONFIG).expect("Config file could not be read"); - logging::setup_logger(format!("{}_installer.log", config.name)).expect("Unable to setup logging!"); + logging::setup_logger(format!("{}_installer.log", config.name)) + .expect("Unable to setup logging!"); let app_name = config.name.clone(); diff --git a/src/native/mod.rs b/src/native/mod.rs index c16245f..2c2569c 100644 --- a/src/native/mod.rs +++ b/src/native/mod.rs @@ -70,7 +70,7 @@ mod natives { } /// Cleans up the installer - pub fn burn_on_exit(app_name : &str) { + pub fn burn_on_exit(app_name: &str) { let current_exe = env::current_exe().log_expect("Current executable could not be found"); let path = current_exe .parent() @@ -123,7 +123,7 @@ mod natives { } /// Cleans up the installer - pub fn burn_on_exit(app_name : &str) { + pub fn burn_on_exit(app_name: &str) { let current_exe = env::current_exe().log_expect("Current executable could not be found"); // Thank god for *nix platforms diff --git a/src/sources/github/mod.rs b/src/sources/github/mod.rs index 09a1c6b..76f95ec 100644 --- a/src/sources/github/mod.rs +++ b/src/sources/github/mod.rs @@ -45,8 +45,17 @@ impl ReleaseSource for GithubReleases { .send() .map_err(|x| format!("Error while sending HTTP request: {:?}", x))?; - if response.status() != StatusCode::OK { - return Err(format!("Bad status code: {:?}", response.status())); + match response.status() { + StatusCode::OK => {} + StatusCode::FORBIDDEN => { + return Err(format!( + "GitHub is rate limiting you. Try moving to a internet connection \ + that isn't shared, and/or disabling VPNs." + )); + } + _ => { + return Err(format!("Bad status code: {:?}.", response.status())); + } } let body = response diff --git a/src/tasks/download_pkg.rs b/src/tasks/download_pkg.rs index 3470184..fe1279a 100644 --- a/src/tasks/download_pkg.rs +++ b/src/tasks/download_pkg.rs @@ -4,6 +4,7 @@ use installer::InstallerFramework; use tasks::Task; use tasks::TaskDependency; +use tasks::TaskMessage; use tasks::TaskOrdering; use tasks::TaskParamType; @@ -24,7 +25,7 @@ impl Task for DownloadPackageTask { &mut self, mut input: Vec, context: &mut InstallerFramework, - messenger: &Fn(&str, f64), + messenger: &Fn(&TaskMessage), ) -> Result { assert_eq!(input.len(), 1); @@ -45,7 +46,10 @@ impl Task for DownloadPackageTask { } } - messenger(&format!("Downloading package {:?}...", self.name), 0.0); + messenger(&TaskMessage::DisplayMessage( + &format!("Downloading package {:?}...", self.name), + 0.0, + )); let mut downloaded = 0; let mut data_storage: Vec = Vec::new(); @@ -73,13 +77,13 @@ impl Task for DownloadPackageTask { Prefixed(prefix, n) => format!("{:.0} {}B", n, prefix), }; - messenger( + messenger(&TaskMessage::DisplayMessage( &format!( "Downloading {} ({} of {})...", self.name, pretty_current, pretty_total ), percentage, - ); + )); })?; Ok(TaskParamType::FileContents(version, file, data_storage)) diff --git a/src/tasks/install.rs b/src/tasks/install.rs index e70adba..6f1574e 100644 --- a/src/tasks/install.rs +++ b/src/tasks/install.rs @@ -10,6 +10,7 @@ use tasks::uninstall_pkg::UninstallPackageTask; use tasks::install_global_shortcut::InstallGlobalShortcutsTask; use tasks::Task; use tasks::TaskDependency; +use tasks::TaskMessage; use tasks::TaskOrdering; use tasks::TaskParamType; @@ -24,9 +25,9 @@ impl Task for InstallTask { &mut self, _: Vec, _: &mut InstallerFramework, - messenger: &Fn(&str, f64), + messenger: &Fn(&TaskMessage), ) -> Result { - messenger("Wrapping up...", 0.0); + messenger(&TaskMessage::DisplayMessage("Wrapping up...", 0.0)); Ok(TaskParamType::None) } diff --git a/src/tasks/install_dir.rs b/src/tasks/install_dir.rs index 503329d..be11deb 100644 --- a/src/tasks/install_dir.rs +++ b/src/tasks/install_dir.rs @@ -4,6 +4,7 @@ use installer::InstallerFramework; use tasks::Task; use tasks::TaskDependency; +use tasks::TaskMessage; use tasks::TaskParamType; use std::fs::create_dir_all; @@ -20,10 +21,13 @@ impl Task for VerifyInstallDirTask { &mut self, input: Vec, context: &mut InstallerFramework, - messenger: &Fn(&str, f64), + messenger: &Fn(&TaskMessage), ) -> Result { assert_eq!(input.len(), 0); - messenger("Polling installation directory...", 0.0); + messenger(&TaskMessage::DisplayMessage( + "Polling installation directory...", + 0.0, + )); let path = context .install_path diff --git a/src/tasks/install_global_shortcut.rs b/src/tasks/install_global_shortcut.rs index 633324c..64ffe8e 100644 --- a/src/tasks/install_global_shortcut.rs +++ b/src/tasks/install_global_shortcut.rs @@ -4,6 +4,7 @@ use installer::InstallerFramework; use tasks::Task; use tasks::TaskDependency; +use tasks::TaskMessage; use tasks::TaskParamType; use logging::LoggingErrors; @@ -19,9 +20,12 @@ impl Task for InstallGlobalShortcutsTask { &mut self, _: Vec, context: &mut InstallerFramework, - messenger: &Fn(&str, f64), + messenger: &Fn(&TaskMessage), ) -> Result { - messenger("Generating global shortcut...", 0.0); + messenger(&TaskMessage::DisplayMessage( + "Generating global shortcut...", + 0.0, + )); let path = context .install_path @@ -45,7 +49,7 @@ impl Task for InstallGlobalShortcutsTask { .log_expect("Unable to build shortcut metadata (tool)"); let shortcut_file = create_shortcut( - &format!("{} maintenance tool", context.base_attributes.name), + &format!("{} Maintenance Tool", context.base_attributes.name), &format!( "Launch the {} Maintenance Tool to update, modify and uninstall the application.", context.base_attributes.name diff --git a/src/tasks/install_pkg.rs b/src/tasks/install_pkg.rs index f2ce893..9f42d21 100644 --- a/src/tasks/install_pkg.rs +++ b/src/tasks/install_pkg.rs @@ -8,6 +8,7 @@ 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; @@ -33,9 +34,12 @@ impl Task for InstallPackageTask { &mut self, mut input: Vec, context: &mut InstallerFramework, - messenger: &Fn(&str, f64), + messenger: &Fn(&TaskMessage), ) -> Result { - messenger(&format!("Installing package {:?}...", self.name), 0.0); + messenger(&TaskMessage::DisplayMessage( + &format!("Installing package {:?}...", self.name), + 0.0, + )); let path = context .install_path @@ -91,16 +95,16 @@ impl Task for InstallPackageTask { match &archive_size { Some(size) => { - messenger( + messenger(&TaskMessage::DisplayMessage( &format!("Extracting {} ({} of {})", string_name, i + 1, size), (i as f64) / (*size as f64), - ); + )); } _ => { - messenger( + messenger(&TaskMessage::DisplayMessage( &format!("Extracting {} ({} of ??)", string_name, i + 1), 0.0, - ); + )); } } @@ -170,6 +174,8 @@ impl Task for InstallPackageTask { files: installed_files, }); + messenger(&TaskMessage::PackageInstalled); + Ok(TaskParamType::None) } diff --git a/src/tasks/install_shortcuts.rs b/src/tasks/install_shortcuts.rs index dae4967..c2f03e6 100644 --- a/src/tasks/install_shortcuts.rs +++ b/src/tasks/install_shortcuts.rs @@ -4,6 +4,7 @@ use installer::InstallerFramework; use tasks::Task; use tasks::TaskDependency; +use tasks::TaskMessage; use tasks::TaskParamType; use config::PackageDescription; @@ -21,12 +22,12 @@ impl Task for InstallShortcutsTask { &mut self, _: Vec, context: &mut InstallerFramework, - messenger: &Fn(&str, f64), + messenger: &Fn(&TaskMessage), ) -> Result { - messenger( + messenger(&TaskMessage::DisplayMessage( &format!("Generating shortcuts for package {:?}...", self.name), 0.0, - ); + )); let path = context .install_path diff --git a/src/tasks/mod.rs b/src/tasks/mod.rs index c587202..51c399e 100644 --- a/src/tasks/mod.rs +++ b/src/tasks/mod.rs @@ -58,6 +58,12 @@ impl TaskDependency { } } +/// A message from a task. +pub enum TaskMessage<'a> { + DisplayMessage(&'a str, f64), + PackageInstalled, +} + /// A Task is a small, async task conforming to a fixed set of inputs/outputs. pub trait Task { /// Executes this individual task, evaluating to the given Output result. @@ -67,7 +73,7 @@ pub trait Task { &mut self, input: Vec, context: &mut InstallerFramework, - messenger: &Fn(&str, f64), + messenger: &Fn(&TaskMessage), ) -> Result; /// Returns a vector containing all dependencies that need to be executed @@ -113,7 +119,7 @@ impl DependencyTree { pub fn execute( &mut self, context: &mut InstallerFramework, - messenger: &Fn(&str, f64), + messenger: &Fn(&TaskMessage), ) -> Result { let total_tasks = (self.dependencies.len() + 1) as f64; @@ -126,11 +132,14 @@ impl DependencyTree { continue; } - let result = i.execute(context, &|msg: &str, progress: f64| { - messenger( - msg, - progress / total_tasks + (1.0 / total_tasks) * f64::from(count), - ) + 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), + )) + } + _ => messenger(msg), })?; // Check to see if we skip matching other dependencies @@ -149,11 +158,14 @@ impl DependencyTree { let task_result = self .task - .execute(inputs, context, &|msg: &str, progress: f64| { - messenger( - msg, - progress / total_tasks + (1.0 / total_tasks) * f64::from(count), - ) + .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), + )) + } + _ => messenger(msg), })?; if let TaskParamType::Break = task_result { @@ -166,11 +178,14 @@ impl DependencyTree { continue; } - let result = i.execute(context, &|msg: &str, progress: f64| { - messenger( - msg, - progress / total_tasks + (1.0 / total_tasks) * f64::from(count), - ) + 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), + )) + } + _ => messenger(msg), })?; // Check to see if we skip matching other dependencies diff --git a/src/tasks/resolver.rs b/src/tasks/resolver.rs index 1034bb7..b94cf09 100644 --- a/src/tasks/resolver.rs +++ b/src/tasks/resolver.rs @@ -6,6 +6,7 @@ use installer::InstallerFramework; use tasks::Task; use tasks::TaskDependency; +use tasks::TaskMessage; use tasks::TaskParamType; use config::PackageDescription; @@ -23,7 +24,7 @@ impl Task for ResolvePackageTask { &mut self, input: Vec, context: &mut InstallerFramework, - messenger: &Fn(&str, f64), + messenger: &Fn(&TaskMessage), ) -> Result { assert_eq!(input.len(), 0); let mut metadata: Option = None; @@ -44,20 +45,20 @@ impl Task for ResolvePackageTask { None => return Err(format!("Package {:?} could not be found.", self.name)), }; - messenger( + messenger(&TaskMessage::DisplayMessage( &format!( "Polling {} for latest version of {:?}...", package.source.name, package.name ), 0.0, - ); + )); let results = package.source.get_current_releases()?; - messenger( + messenger(&TaskMessage::DisplayMessage( &format!("Resolving dependency for {:?}...", package.name), 0.5, - ); + )); let filtered_regex = package.source.match_regex.replace("#PLATFORM#", OS); let regex = match Regex::new(&filtered_regex) { diff --git a/src/tasks/save_database.rs b/src/tasks/save_database.rs index b030345..ef23137 100644 --- a/src/tasks/save_database.rs +++ b/src/tasks/save_database.rs @@ -4,6 +4,7 @@ use installer::InstallerFramework; use tasks::Task; use tasks::TaskDependency; +use tasks::TaskMessage; use tasks::TaskParamType; pub struct SaveDatabaseTask {} @@ -13,10 +14,13 @@ impl Task for SaveDatabaseTask { &mut self, input: Vec, context: &mut InstallerFramework, - messenger: &Fn(&str, f64), + messenger: &Fn(&TaskMessage), ) -> Result { assert_eq!(input.len(), 0); - messenger("Saving application database...", 0.0); + messenger(&TaskMessage::DisplayMessage( + "Saving application database...", + 0.0, + )); context.save_database()?; diff --git a/src/tasks/save_executable.rs b/src/tasks/save_executable.rs index ef8d593..6401b07 100644 --- a/src/tasks/save_executable.rs +++ b/src/tasks/save_executable.rs @@ -4,6 +4,7 @@ use installer::InstallerFramework; use tasks::Task; use tasks::TaskDependency; +use tasks::TaskMessage; use tasks::TaskParamType; use std::fs::File; @@ -22,10 +23,13 @@ impl Task for SaveExecutableTask { &mut self, input: Vec, context: &mut InstallerFramework, - messenger: &Fn(&str, f64), + messenger: &Fn(&TaskMessage), ) -> Result { assert_eq!(input.len(), 0); - messenger("Copying installer binary...", 0.0); + messenger(&TaskMessage::DisplayMessage( + "Copying installer binary...", + 0.0, + )); let path = context .install_path diff --git a/src/tasks/uninstall.rs b/src/tasks/uninstall.rs index 006068f..5e166bf 100644 --- a/src/tasks/uninstall.rs +++ b/src/tasks/uninstall.rs @@ -7,6 +7,7 @@ use tasks::TaskParamType; use tasks::uninstall_pkg::UninstallPackageTask; use tasks::TaskDependency; +use tasks::TaskMessage; use tasks::TaskOrdering; pub struct UninstallTask { @@ -18,9 +19,9 @@ impl Task for UninstallTask { &mut self, _: Vec, _: &mut InstallerFramework, - messenger: &Fn(&str, f64), + messenger: &Fn(&TaskMessage), ) -> Result { - messenger("Wrapping up...", 0.0); + messenger(&TaskMessage::DisplayMessage("Wrapping up...", 0.0)); Ok(TaskParamType::None) } diff --git a/src/tasks/uninstall_global_shortcut.rs b/src/tasks/uninstall_global_shortcut.rs index fc7d901..d5d4733 100644 --- a/src/tasks/uninstall_global_shortcut.rs +++ b/src/tasks/uninstall_global_shortcut.rs @@ -4,6 +4,7 @@ use installer::InstallerFramework; use tasks::Task; use tasks::TaskDependency; +use tasks::TaskMessage; use tasks::TaskParamType; use std::fs::remove_file; @@ -17,11 +18,14 @@ impl Task for UninstallGlobalShortcutsTask { &mut self, input: Vec, context: &mut InstallerFramework, - messenger: &Fn(&str, f64), + messenger: &Fn(&TaskMessage), ) -> Result { assert_eq!(input.len(), 0); - messenger("Uninstalling global shortcut...", 0.0); + messenger(&TaskMessage::DisplayMessage( + "Uninstalling global shortcut...", + 0.0, + )); while let Some(file) = context.database.shortcuts.pop() { info!("Deleting shortcut {:?}", file); diff --git a/src/tasks/uninstall_pkg.rs b/src/tasks/uninstall_pkg.rs index bf16ac7..391df0c 100644 --- a/src/tasks/uninstall_pkg.rs +++ b/src/tasks/uninstall_pkg.rs @@ -5,6 +5,7 @@ use installer::InstallerFramework; use tasks::save_database::SaveDatabaseTask; use tasks::Task; use tasks::TaskDependency; +use tasks::TaskMessage; use tasks::TaskOrdering; use tasks::TaskParamType; @@ -26,7 +27,7 @@ impl Task for UninstallPackageTask { &mut self, input: Vec, context: &mut InstallerFramework, - messenger: &Fn(&str, f64), + messenger: &Fn(&TaskMessage), ) -> Result { assert_eq!(input.len(), 1); @@ -57,7 +58,10 @@ impl Task for UninstallPackageTask { } }; - messenger(&format!("Uninstalling package {:?}...", self.name), 0.0); + messenger(&TaskMessage::DisplayMessage( + &format!("Uninstalling package {:?}...", self.name), + 0.0, + )); // Reverse, as to delete directories last package.files.reverse(); @@ -68,10 +72,10 @@ impl Task for UninstallPackageTask { let file = path.join(file); info!("Deleting {:?}", file); - messenger( + messenger(&TaskMessage::DisplayMessage( &format!("Deleting {} ({} of {})", name, i + 1, max), (i as f64) / (max as f64), - ); + )); let result = if file.is_dir() { remove_dir(file) diff --git a/src/tasks/uninstall_shortcuts.rs b/src/tasks/uninstall_shortcuts.rs index a7f378f..4dfe70c 100644 --- a/src/tasks/uninstall_shortcuts.rs +++ b/src/tasks/uninstall_shortcuts.rs @@ -4,6 +4,7 @@ use installer::InstallerFramework; use tasks::Task; use tasks::TaskDependency; +use tasks::TaskMessage; use tasks::TaskParamType; use installer::LocalInstallation; @@ -23,7 +24,7 @@ impl Task for UninstallShortcutsTask { &mut self, input: Vec, context: &mut InstallerFramework, - messenger: &Fn(&str, f64), + messenger: &Fn(&TaskMessage), ) -> Result { assert_eq!(input.len(), 0); @@ -54,10 +55,10 @@ impl Task for UninstallShortcutsTask { } }; - messenger( + messenger(&TaskMessage::DisplayMessage( &format!("Uninstalling shortcuts for package {:?}...", self.name), 0.0, - ); + )); // Reverse, as to delete directories last package.files.reverse(); @@ -68,10 +69,10 @@ impl Task for UninstallShortcutsTask { let file = path.join(file); info!("Deleting shortcut {:?}", file); - messenger( + messenger(&TaskMessage::DisplayMessage( &format!("Deleting shortcut {} ({} of {})", name, i + 1, max), (i as f64) / (max as f64), - ); + )); let result = if file.is_dir() { remove_dir(file) diff --git a/static/js/views.js b/static/js/views.js index 70e1323..ecbb793 100644 --- a/static/js/views.js +++ b/static/js/views.js @@ -199,7 +199,8 @@ const InstallPackages = { is_uninstall: false, is_updater_update: false, is_update: false, - failed_with_error: false + failed_with_error: false, + packages_installed: 0 } }, created: function() { @@ -238,6 +239,10 @@ const InstallPackages = { 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(); @@ -263,9 +268,17 @@ const InstallPackages = { app.exit(); } else if (!that.failed_with_error) { if (that.is_uninstall) { - router.replace({name: 'complete', params: {uninstall: true, update: that.is_update}}); + 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}}); + router.replace({name: 'complete', params: { + uninstall: false, + update: that.is_update, + installed: that.packages_installed + }}); } } } @@ -305,9 +318,16 @@ const CompleteView = { template: `
-

{{ $root.$data.attrs.name }} has been updated.

- -

You can find your installed applications in your start menu.

+
+

{{ $root.$data.attrs.name }} has been updated.

+ +

You can find your installed applications in your start menu.

+
+
+

{{ $root.$data.attrs.name }} is already up to date!

+ +

You can find your installed applications in your start menu.

+

Thanks for installing {{ $root.$data.attrs.name }}!

@@ -330,7 +350,8 @@ const CompleteView = { data: function() { return { was_install: !this.$route.params.uninstall, - was_update: this.$route.params.update + was_update: this.$route.params.update, + has_installed: this.$route.params.packages_installed > 0 } }, methods: { @@ -422,7 +443,7 @@ const router = new VueRouter({ component: ErrorView }, { - path: '/complete/:uninstall/:update', + path: '/complete/:uninstall/:update/:packages_installed', name: 'complete', component: CompleteView },