diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1ff0c42 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/Documentation/Icon/BlueUpdate Icon.png b/Documentation/Icon/BlueUpdate Icon.png new file mode 100644 index 0000000..59d1864 Binary files /dev/null and b/Documentation/Icon/BlueUpdate Icon.png differ diff --git a/Documentation/Icon/BlueUpdate Icon.psd b/Documentation/Icon/BlueUpdate Icon.psd new file mode 100644 index 0000000..ff34a02 Binary files /dev/null and b/Documentation/Icon/BlueUpdate Icon.psd differ diff --git a/Documentation/Icon/BlueUpdate Updater.ico b/Documentation/Icon/BlueUpdate Updater.ico new file mode 100644 index 0000000..368da24 Binary files /dev/null and b/Documentation/Icon/BlueUpdate Updater.ico differ diff --git a/Documentation/Screenshots/Updater Screenshot.png b/Documentation/Screenshots/Updater Screenshot.png new file mode 100644 index 0000000..d905d60 Binary files /dev/null and b/Documentation/Screenshots/Updater Screenshot.png differ diff --git a/README.md b/README.md index 7110e26..c317e07 100644 --- a/README.md +++ b/README.md @@ -1 +1,128 @@ -# BlueUpdate \ No newline at end of file +# BlueUpdate +A toolkit for automatic updating of .NET applications from the web. Includes it's own executable updater, which is automatically downloaded the first time the application starts. + +Latest release: [v1.0](https://github.com/GregaMohorko/BlueUpdate/releases/latest) + +## Usage +```C# +// 1st, define the currently running application +// you can use a method from ReflectionUtility class +// that gets the assembly information of the current application +var assembly = ReflectionUtility.GetAssemblyInformation(ReflectionUtility.AssemblyType.Application); +UpdatableApp.Current = new UpdatableApp( + assembly.Title, // name + assembly.Version, // current version + latestVersion, // latest version (connect to your server to check it) + "http://install.myapp.com" // address to the app directory on your server with the zip files +); + +// now we can check and possibly update +if(Update.Check()) { + Update.Run(); + Shutdown(); // shuts down the current application +} +``` + +The above example is very raw. Please read the notes below and check the recommended usage for WPF applications below. + +## How it works +Please read the notes below carefully. `BlueUpdate` toolkit is not at all complicated, but it will not work if it is not set up correctly. That is why I will try to explain it in great detail. + +In order to use the `BlueUpdate` toolkit, you must have your own web directory in which you will put the applications zip files. The directory hierarchy inside of it must be in the format: *[version]/[appName] [version].zip*. For example, a possible location of a zip file for the case from above could be *`http://install.myapp.com/1.0.4.2/My App 1.0.4.2.zip`*. This zip file would obviously represent the application *My App* of version *1.0.4.2*. + +Before using the `Update` class, you must set the `UpdatableApp.Current`, which represents the currently running application. This must be done in order to check if the currently running application was installed correctly, to set the root directory, etc.. Every application must be inside the directory with the same name as the application itself. The recommended install path format is: *`/MyCompany/My App/My App.exe`*. The root directory of this example is *`/MyCompany`*. This way, you can have multiple applications inside your companies root directory. + +When you use the `Update` class for the first time, its static initializer will check if the updater is installed inside the root directory. If I follow the previous path example, the updater would be installed in *`/MyCompany/Updater`*. If the updater is not yet installed, it will install it. The updater is very small (~24 KB). + +The `Update.Check()` simply checks if the latest version of the application is greater than the current. If it is, then it needs to be updated. +The `Update.Run()` starts the updater application, which will update the currently running application. That is why the currently running application must close immediately after that, otherwise the updater will not be able to update/modify the files of the application. + +You can also specify which application you want to update if you want to update some other application, in which case you don't need to close the one that is currently running. + +The updater will download the latest version and update the files inside the applications folder. When it is done, it will try to run the updated application. You can modify this behavior by setting the method parameter of `Update.Run()` accordingly. + +![Updater screenshot](/Documentation/Screenshots/Updater Screenshot.png?raw=true "Screenshot of the updater while updating 'My App' example application") + +By default, the updater deletes everything inside the applications directory when it updates it, but you can specify which directories inside to leave as they are (for example *Resources*). + +### Checksum check +`BlueUpdate` can also automatically do a [checksum](https://en.wikipedia.org/wiki/Checksum) check when the zip is downloaded. If you want to enable this feature, you have to add an XML file in the same directory as the zip and with the same name. If the zip file is at *`http://install.myapp.com/1.0.4.2/My App 1.0.4.2.zip`*, then the XML must be at *`http://install.myapp.com/1.0.4.2/My App 1.0.4.2.xml`*. The checksum of the zip file must be calculated using the SHA-256 algorithm. + +The format inside the XML must be like this (replace the hash with the hash of your zip file): +```XML + + 3928C72D4F3296092DA892DFBC8D83B32880D1472CFFC9598C62825DB47C3B4F + +``` + +## Recommended usage for WPF applications +OK, so now that you understand in greater detail how the `BlueUpdate` toolkit works, I will show you the recommended usage for WPF applications. I think that the update check should only be done at the startup of the application, so I will show you how to do it. + +Every WPF application has the `App.xaml` and `App.xaml.cs` files. This represents your application. By default, the `StartupUri` attribute in the `App.xaml` is set to `MainWindow.xaml`. Delete it, because you will manually show the `MainWindow` window later. + +Because you deleted the `StartupUri`, the application doesn't know what to do at startup. That is why you must now override the `OnStartup` method inside the `App.xaml.cs`. + +Here is an example of `OnStartup` method implementation inside the `App.xaml.cs`: + +```C# +protected override void OnStartup(StartupEventArgs e) +{ + // make sure that updating is not done during development + string dir = Directory.GetCurrentDirectory(); + if(!Debugger.IsAttached && !dir.EndsWith("Debug") && !dir.EndsWith("Release")) { + try{ + // check the latest version + Version latestVersion; + // this is an example of how to connect to an example server + // and check the version written inside the 'latest.version' file + using(WebClient webClient = new WebClient()){ + string latest = webClient.DownloadString("http://install.myapp.com/latest.version"); + latestVersion = Version.Parse(latest); + } + + // set the current application + var assembly = ReflectionUtility.GetAssemblyInformation(ReflectionUtility.AssemblyType.Application); + UpdatableApp = new UpdatableApp( + assembly.Title, + assembly.Version, + latestVersion, + "http://install.myapp.com" + ); + + // check if it needs to be updated + if(Update.Check()) { + // start the updater ... + Update.Run(); + // ... and close this application + Shutdown(); + return; + } + } catch(WebException) { + // there is no internet ... + // do nothing + } catch(Exception ex) { + MessageBox.Show("Error: " + ex.Message); + Shutdown(); + return; + } + } + + // show your main window + try { + MainWindow = new MainWindow(); + MainWindow.ShowDialog(); + } catch(Exception ex) { + MessageBox.Show("Error: " + ex.Message); + } +} +``` + +## Requirements +.NET Framework 4.6.1 + +## Author and License +Grega Mohorko ([www.mohorko.info](http://www.mohorko.info)) + +Copyright (c) 2017 Grega Mohorko + +[Apache License 2.0](./LICENSE) diff --git a/build/BlueUpdate.XML b/build/BlueUpdate.XML new file mode 100644 index 0000000..56c514b --- /dev/null +++ b/build/BlueUpdate.XML @@ -0,0 +1,461 @@ + + + + BlueUpdate + + + + + Represents an application that can be updated. + + + + + Gets or sets the currently running application. + + + + + Gets the name of this application. + + + + + Gets the current version of this application. + + + + + Gets the latest version of this application. + + + + + Gets the name of this applications directory in the root directory. + + + + + Gets the web address of the directory where this application can be downloaded. + + + + + A collection of directories inside this applications directory to ignore while updating (to be left as they are). + + + + + Creates a new instance of updatable application. + + The name of the application. + The current version of the application. + The latest version of the application. You should connect to the internet to check the latest version. + The web address of the directory where this application can be downloaded. + The name of the applications directory in the root directory. If null, will be set to the same value as the name of the application. + A collection of directories inside this applications directory to ignore while updating (to be left as they are). + + + + Provides utility functions for working with files. + + + + + Gets the executable file of the specified application. + + The application of which to get the executable file. + + + + Provides utility functions for working with directories. + + + + + Deletes the BlueUpdate temporary folder. + + + + + Provides utility functions for calculations using the SHA-256 algorithm. + + + + + Calculates the checksum for the specified file. + + The path of the file for which to calculate the checksum. + + + + Root directory of all applications. + + + + + Directory used for temporary files while installing/updating. + + + + + Gets information about the currently installed BlueUpdate executable. + + + + + Checks and determines whether the current application needs to update. + + + + + Checks and determines whether the specified application needs to update. + + The application to check. + + + + Starts the updater and attempts to update the current application. + + The application should be shut down right after calling this method. + + + Determines whether to run the application after the update. + + + + Starts the updater and attempts to update the specified application. + + If the specified application is the same as the current, it should be shut down right after calling this method. + + + The application to update. + Determines whether to run the application after the update. + + + + Provides utility functions for the update process. Do not use methods from this class unless you really know what you are doing. + + + + + Installs the specified application. + + The application to be installed. + If provided, the download process will be asynchronous and this action will be called upon completion. + When doing an asynchronous call, this action will be caled upon any changes of the download progress. + + + + Updates the specified application. + + The application to be updated. + If provided, the download process will be asynchronous and this action will be called upon completion. + When doing an asynchronous call, this action will be caled upon any changes of the download progress. + + + + Prepares the string so that it can be used as an argument for a process. + + The text to be prepared. + + + + Creates the actual text from the process argument. + + The argument to be converted to the actual text. + + + + Downloads the specified application to the temporary folder and returns the file path of the downloaded zip. + + + + + Adds the specified suffix to all directories and files in the specified directory, including subdirectories. + + The directory in which to add the suffix to all directories and files. + The suffix to add to all directories and files. + + + + Adds the specified suffix to all directories and files in the specified directory, using the specified search option. + + The directory in which to add the suffix to all directories and files. + The suffix to add to all directories and files. + Specifies whether to include only the current directory or all subdirectories. + + + + Adds the specified suffix to all directories and files in the specified directory, using the specified search option. If a directory matches any of the values in the provided ignore list, the suffix will not be added to it. + + The directory in which to add the suffix to all directories and files. + The suffix to add to all directories and files. + Specifies whether to include only the current directory or all subdirectories. + A collection of directory names to ignore when adding suffix. + + + + Adds the specified suffix to all directories in the specified directory, including subdirectories. + + The directory in which to add the suffix to all directories. + The suffix to add to all directories. + + + + Adds the specified suffix to all directories in the specified directory, using the specified search option. + + The directory in which to add the suffix to all directories. + The suffix to add to all directories. + Specifies whether to include only the current directory or all subdirectories. + + + + Adds the specified suffix to all directories in the specified directory, using the specified search option. If a directory matches any of the values in the provided ignore list, the suffix will not be added to it. + + The directory in which to add the suffix to all directories. + The suffix to add to all directories. + Specifies whether to include only the current directory or all subdirectories. + A collection of directory names to ignore when adding suffix. + + + + Adds the specified suffix to all files inside the specified directory, including subdirectories. + + The directory in which to add the suffix to all files. + The suffix to add to all files. + + + + Adds the specified suffix to all files inside the specified directory, using the specified search option. + + The directory in which to add the suffix to all files. + The suffix to add to all files. + Specifies whether to include only the current directory or all subdirectories. + + + + Renames the specified directory (adds the specified suffix to the end of the name). + + The directory to add the suffix to. + The suffix to add to the directory. + + + + Renames the specified file (adds the specified suffix to the end of the name). + + The file to add the suffix to. + The suffix to add to the file. + + + + Removes the specified suffix from all directories and files in the specified directory, including subdirectories. + + The directory in which to remove the suffix from all directories and files. + The suffix to remove from all directories and files. + + + + Removes the specified suffix from all directories and files in the specified directory, using the specified search option. + + The directory in which to remove the suffix from all directories and files. + The suffix to remove from all directories and files. + Specifies whether to include only the current directory or all subdirectories. + + + + Removes the specified suffix from all directories in the specified directory, including subdirectories. + + The directory in which to remove the suffix from all directories. + The suffix to remove from all directories. + + + + Removes the specified suffix from all directories in the specified directory, using the specified search option. + + The directory in which to remove the suffix from all directories. + The suffix to remove from all directories. + Specifies whether to include only the current directory or all subdirectories. + + + + Removes the specified suffix from all files inside the specified directory, including subdirectories. + + The directory in which to remove the suffix from all directories. + The suffix to remove from all directories. + + + + Removes the specified suffix from all files inside the specified directory, using the specified search option. + + The directory in which to remove the suffix from all directories. + The suffix to remove from all directories. + Specifies whether to include only the current directory or all subdirectories. + + + + Renames the specified directory (removes the specified suffix from the end of the name, if present). + + The directory to remove the suffix from. + The suffix to remove from the directory. + + + + Renames the specified file (removes the specified suffix from the end of the name, if present). + + The file to remove the suffix from. + The suffix to remove from the file. + + + + Deletes all files and directories in the specified directory, whose name is ended by the specified suffix, including subdirectories. + + The directory in which to delete all files and directories. + The suffix that files and directories must contain in order to be deleted. + + + + Deletes all files and directories in the specified directory, whose name is ended by the specified suffix, using the specified search option. + + The directory in which to delete all files and directories. + The suffix that files and directories must contain in order to be deleted. + Specifies whether to include only the current directory or all subdirectories. + + + + Deletes all directories in the specified directory, whose name is ended by the specified suffix, including subdirectories. + + The directory in which to delete all directories. + The suffix that directories must contain in order to be deleted. + + + + Deletes all directories in the specified directory, whose name is ended by the specified suffix, using the specified search option. + + The directory in which to delete all directories. + The suffix that directories must contain in order to be deleted. + Specifies whether to include only the current directory or all subdirectories. + + + + Deletes all files in the specified directory, whose name is ended by the specified suffix, including subdirectories. + + The directory in which to delete all files. + The suffix that files must contain in order to be deleted. + + + + Deletes all files in the specified directory, whose name is ended by the specified suffix, using the specified search option. + + The directory in which to delete all files. + The suffix that files must contain in order to be deleted. + Specifies whether to include only the current directory or all subdirectories. + + + + Deletes all files and directories in the specified directory, whose name is not ended by the specified suffix, including subdirectories. + + The directory in which to delete all files and directories. + The suffix that files and directories must not contain in order to be deleted. + + + + Deletes all files and directories in the specified directory, whose name is not ended by the specified suffix, using the specified search option. + + The directory in which to delete all files and directories. + The suffix that files and directories must not contain in order to be deleted. + Specifies whether to include only the current directory or all subdirectories. + + + + Deletes all files and directories in the specified directory, whose name is not ended by the specified suffix, using the specified search option. If a directory matches any of the values in the provided ignore list, it will not be deleted. + + The directory in which to delete all files and directories. + The suffix that files and directories must not contain in order to be deleted. + Specifies whether to include only the current directory or all subdirectories. + A collection of directory names to ignore. + + + + Deletes all directories in the specified directory, whose name is not ended by the specified suffix, including subdirectories. + + The directory in which to delete all directories. + The suffix that directories must not contain in order to be deleted. + + + + Deletes all directories in the specified directory, whose name is not ended by the specified suffix, using the specified search option. + + The directory in which to delete all directories. + The suffix that directories must not contain in order to be deleted. + Specifies whether to include only the current directory or all subdirectories. + + + + Deletes all directories in the specified directory, whose name is not ended by the specified suffix, using the specified search option. If a directory matches any of the values in the provided ignore list, it will not be deleted. + + The directory in which to delete all directories. + The suffix that directories must not contain in order to be deleted. + Specifies whether to include only the current directory or all subdirectories. + A collection of directory names to ignore. + + + + Deletes all files in the specified directory, whose name is not ended by the specified suffix, including subdirectories. + + The directory in which to delete all files. + The suffix that files must not contain in order to be deleted. + + + + Deletes all files in the specified directory, whose name is not ended by the specified suffix, using the specified search option. + + The directory in which to delete all files. + The suffix that files must not contain in order to be deleted. + Specifies whether to include only the current directory or all subdirectories. + + + + Specifies the type of the assembly. + + + + + Represents the entry assembly, which is usually the assembly of the application. + + + + + Represents the assembly of the library where this enum is defined. + + + + + Represents the currently running assembly. + + + + + Structure with assembly information. + + + + + Gets the assembly of the specified type. + + The type of the assembly to look for. + + + + Gets the assembly information of the specified type. + + The type of the assembly to look for. + + + + Gets the assembly information of the specified type. + + The assembly from which to extract the information from. + + + diff --git a/build/BlueUpdate.dll b/build/BlueUpdate.dll new file mode 100644 index 0000000..281448b Binary files /dev/null and b/build/BlueUpdate.dll differ diff --git a/src/.gitignore b/src/.gitignore new file mode 100644 index 0000000..1c9a181 --- /dev/null +++ b/src/.gitignore @@ -0,0 +1,242 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +[Xx]64/ +[Xx]86/ +[Bb]uild/ +bld/ +[Bb]in/ +[Oo]bj/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml + +# TODO: Un-comment the next line if you do not want to checkin +# your web deploy settings because they may include unencrypted +# passwords +#*.pubxml +*.publishproj + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directory +AppPackages/ +BundleArtifacts/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +[Ss]tyle[Cc]op.* +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# LightSwitch generated files +GeneratedArtifacts/ +ModelManifest.xml + +# Paket dependency manager +.paket/paket.exe + +# FAKE - F# Make +.fake/ diff --git a/src/BlueUpdate/BlueUpdate Updater/App.config b/src/BlueUpdate/BlueUpdate Updater/App.config new file mode 100644 index 0000000..731f6de --- /dev/null +++ b/src/BlueUpdate/BlueUpdate Updater/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/BlueUpdate/BlueUpdate Updater/App.xaml b/src/BlueUpdate/BlueUpdate Updater/App.xaml new file mode 100644 index 0000000..3ec8d39 --- /dev/null +++ b/src/BlueUpdate/BlueUpdate Updater/App.xaml @@ -0,0 +1,7 @@ + + diff --git a/src/BlueUpdate/BlueUpdate Updater/App.xaml.cs b/src/BlueUpdate/BlueUpdate Updater/App.xaml.cs new file mode 100644 index 0000000..7ed71fd --- /dev/null +++ b/src/BlueUpdate/BlueUpdate Updater/App.xaml.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.Generic; +using System.Configuration; +using System.Data; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using System.Windows; +using BlueUpdate; +using BlueUpdate.Common; +using BlueUpdate.IO; +using BlueUpdate.Utility; +using BlueUpdate_Updater.Presentation; + +namespace BlueUpdate_Updater +{ + /// + /// Interaction logic for App.xaml + /// + public partial class App : Application + { + protected override void OnStartup(StartupEventArgs e) + { + ShutdownMode = ShutdownMode.OnExplicitShutdown; + + UpdatableApp appToUpdate; + bool runAfterUpdate; + + try { + // set the current application as the updater + var assembly =ReflectionUtility.GetAssemblyInformation(ReflectionUtility.AssemblyType.Application); + UpdatableApp.Current = new UpdatableApp(BlueUpdateConstants.UpdaterName, assembly.Version, null, null, BlueUpdateConstants.UpdaterDirectoryName); + + // check arguments + if(e.Args.Length != 6) { + throw new Exception("Code 10."); + } + + if(!bool.TryParse(UpdateUtility.FromProcessArgument(e.Args[5]), out runAfterUpdate)) { + throw new Exception("Code 20."); + } + + appToUpdate = AppFromArgs(e); + + if(appToUpdate.Name == "Updater") { + throw new Exception("Application must not be named 'Updater'."); + } + } catch(Exception ex) { + HandleError("Error", ex); + Shutdown(); + return; + } + + try { + // show updater window and wait for it to close + MainWindow mainWindow = new MainWindow(appToUpdate); + mainWindow.ShowDialog(); + if(mainWindow.Error != null) { + throw mainWindow.Error; + } + }catch(WebException ex) { + HandleError($"There was a problem with the internet connection while updating {appToUpdate.Name}", ex); + Shutdown(); + return; + }catch(Exception ex) { + HandleError($"There was an error while updating {appToUpdate.Name}", ex); + Shutdown(); + return; + } + + if(runAfterUpdate) { + // startup the updated application + try { + FileInfo executable = Files.GetExecutable(appToUpdate); + if(executable == null) { + throw new Exception($"The executable file '{appToUpdate.Name}.exe' could not be found."); + } + + Process.Start(executable.FullName); + } catch(Exception ex) { + HandleError($"There was an error while trying to run the updated version of {appToUpdate.Name}", ex); + } + }else { + MessageBox.Show($"{appToUpdate.Name} was successfully updated!","Update finished"); + } + + Shutdown(); + } + + private void HandleError(string initialMessage,Exception error) + { + string message = $"{initialMessage}:"; + while(error != null) { + message += $"{Environment.NewLine}{Environment.NewLine}{error.Message}"; + error = error.InnerException; + } + MessageBox.Show(message, "Error"); + + } + + private UpdatableApp AppFromArgs(StartupEventArgs e) + { + string name; + Version latestVersion; + string directoryName; + string address; + string[] ignoredDirectories; + { + name = UpdateUtility.FromProcessArgument(e.Args[0]); + if(!Version.TryParse(UpdateUtility.FromProcessArgument(e.Args[1]), out latestVersion)) { + throw new Exception("Code 30."); + } + directoryName = UpdateUtility.FromProcessArgument(e.Args[2]); + address = UpdateUtility.FromProcessArgument(e.Args[3]); + + // ignored directories format: {dir1:dir2:...:dirn} + if(!e.Args[4].StartsWith("{")) { + throw new Exception("Code 40"); + } + if(!e.Args[4].EndsWith("}")) { + throw new Exception("Code 50"); + } + string inside = e.Args[4].Substring(1, e.Args[4].Length - 2); + ignoredDirectories = (inside == string.Empty) ? null : inside.Split(':'); + } + + return new UpdatableApp(name, null, latestVersion, address, directoryName,ignoredDirectories); + } + } +} diff --git a/src/BlueUpdate/BlueUpdate Updater/BlueUpdate Updater.csproj b/src/BlueUpdate/BlueUpdate Updater/BlueUpdate Updater.csproj new file mode 100644 index 0000000..edce47e --- /dev/null +++ b/src/BlueUpdate/BlueUpdate Updater/BlueUpdate Updater.csproj @@ -0,0 +1,123 @@ + + + + + Debug + AnyCPU + {A7F835C7-9F74-4C26-8A8C-07E5A2E7DEED} + WinExe + Properties + BlueUpdate_Updater + BlueUpdate Updater + v4.6.1 + 512 + {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 4 + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + BlueUpdate Updater.ico + + + + ..\packages\ByteSize.1.3.0\lib\net45\ByteSize.dll + True + + + + + + + + + + + 4.0 + + + + + + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + App.xaml + Code + + + MainWindow.xaml + Code + + + + + Code + + + True + True + Resources.resx + + + True + Settings.settings + True + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + + + + + + + + + + {D9B2471C-9048-4795-BEAB-BC099A3518B6} + BlueUpdate + + + + + \ No newline at end of file diff --git a/src/BlueUpdate/BlueUpdate Updater/BlueUpdate Updater.ico b/src/BlueUpdate/BlueUpdate Updater/BlueUpdate Updater.ico new file mode 100644 index 0000000..368da24 Binary files /dev/null and b/src/BlueUpdate/BlueUpdate Updater/BlueUpdate Updater.ico differ diff --git a/src/BlueUpdate/BlueUpdate Updater/Presentation/MainWindow.xaml b/src/BlueUpdate/BlueUpdate Updater/Presentation/MainWindow.xaml new file mode 100644 index 0000000..5f55cbe --- /dev/null +++ b/src/BlueUpdate/BlueUpdate Updater/Presentation/MainWindow.xaml @@ -0,0 +1,26 @@ + + + + + + + + + diff --git a/src/BlueUpdate/BlueUpdate Updater/Presentation/MainWindow.xaml.cs b/src/BlueUpdate/BlueUpdate Updater/Presentation/MainWindow.xaml.cs new file mode 100644 index 0000000..e4e3a33 --- /dev/null +++ b/src/BlueUpdate/BlueUpdate Updater/Presentation/MainWindow.xaml.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; +using BlueUpdate; +using ByteSizeLib; + +namespace BlueUpdate_Updater.Presentation +{ + /// + /// Interaction logic for MainWindow.xaml + /// + public partial class MainWindow : Window + { + /// + /// Gets the exception that was thrown in case of a unsuccessful update. + /// + public Exception Error { get; private set; } + + private readonly UpdatableApp appToUpdate; + + private bool isFinished; + + public MainWindow(UpdatableApp appToUpdate) + { + InitializeComponent(); + + this.appToUpdate = appToUpdate; + + _Label_Title.Content = $"Updating {appToUpdate.Name}"; + } + + protected override void OnContentRendered(EventArgs e) + { + base.OnContentRendered(e); + + Update(); + } + + private void Update() + { + string total = null; + + Action updateProgressChanged = (e) => + { + ByteSize received = ByteSize.FromBytes(e.BytesReceived); + if(total == null) { + ByteSize totalBS = ByteSize.FromBytes(e.TotalBytesToReceive); + total = totalBS.ToString(); + } + _TextBlock_Status.Text = $"Downloading: {received.ToString()} / {total}"; + _ProgressBar.IsIndeterminate = false; + _ProgressBar.Value = e.ProgressPercentage; + }; + + Action updateCompleted = (e) => + { + if(e.Error != null) { + Error = e.Error; + }else if(e.Cancelled) { + Error = new Exception("The download process was cancelled."); + } + isFinished = true; + Close(); + }; + + try { + UpdateUtility.Update(appToUpdate, updateCompleted, updateProgressChanged); + }catch(Exception e) { + updateCompleted(new AsyncCompletedEventArgs(e, false, null)); + } + } + + protected override void OnClosing(CancelEventArgs e) + { + if(!isFinished) { + // do not allow the user to close this window if the download is not finished yet ... + e.Cancel = true; + }else { + base.OnClosing(e); + } + } + } +} diff --git a/src/BlueUpdate/BlueUpdate Updater/Properties/AssemblyInfo.cs b/src/BlueUpdate/BlueUpdate Updater/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..4ad9cd4 --- /dev/null +++ b/src/BlueUpdate/BlueUpdate Updater/Properties/AssemblyInfo.cs @@ -0,0 +1,55 @@ +using System.Reflection; +using System.Resources; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Windows; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("BlueUpdate Updater")] +[assembly: AssemblyDescription("A toolkit for automatic updating of .NET applications from the web.")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Grega Mohorko")] +[assembly: AssemblyProduct("BlueUpdate Updater")] +[assembly: AssemblyCopyright("Copyright © Grega Mohorko 2017")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +//In order to begin building localizable applications, set +//CultureYouAreCodingWith in your .csproj file +//inside a . For example, if you are using US english +//in your source files, set the to en-US. Then uncomment +//the NeutralResourceLanguage attribute below. Update the "en-US" in +//the line below to match the UICulture setting in the project file. + +//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)] + + +[assembly: ThemeInfo( + ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located + //(used if a resource is not found in the page, + // or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located + //(used if a resource is not found in the page, + // app, or any theme specific resource dictionaries) +)] + + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/BlueUpdate/BlueUpdate Updater/Properties/Resources.Designer.cs b/src/BlueUpdate/BlueUpdate Updater/Properties/Resources.Designer.cs new file mode 100644 index 0000000..fea9c46 --- /dev/null +++ b/src/BlueUpdate/BlueUpdate Updater/Properties/Resources.Designer.cs @@ -0,0 +1,70 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace BlueUpdate_Updater.Properties +{ + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources + { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() + { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager + { + get + { + if((resourceMan == null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("BlueUpdate_Updater.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture + { + get + { + return resourceCulture; + } + set + { + resourceCulture = value; + } + } + } +} diff --git a/src/BlueUpdate/BlueUpdate Updater/Properties/Resources.resx b/src/BlueUpdate/BlueUpdate Updater/Properties/Resources.resx new file mode 100644 index 0000000..af7dbeb --- /dev/null +++ b/src/BlueUpdate/BlueUpdate Updater/Properties/Resources.resx @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/src/BlueUpdate/BlueUpdate Updater/Properties/Settings.Designer.cs b/src/BlueUpdate/BlueUpdate Updater/Properties/Settings.Designer.cs new file mode 100644 index 0000000..f7a798d --- /dev/null +++ b/src/BlueUpdate/BlueUpdate Updater/Properties/Settings.Designer.cs @@ -0,0 +1,30 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace BlueUpdate_Updater.Properties +{ + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase + { + + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); + + public static Settings Default + { + get + { + return defaultInstance; + } + } + } +} diff --git a/src/BlueUpdate/BlueUpdate Updater/Properties/Settings.settings b/src/BlueUpdate/BlueUpdate Updater/Properties/Settings.settings new file mode 100644 index 0000000..033d7a5 --- /dev/null +++ b/src/BlueUpdate/BlueUpdate Updater/Properties/Settings.settings @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/BlueUpdate/BlueUpdate Updater/packages.config b/src/BlueUpdate/BlueUpdate Updater/packages.config new file mode 100644 index 0000000..05313c4 --- /dev/null +++ b/src/BlueUpdate/BlueUpdate Updater/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/BlueUpdate/BlueUpdate.sln b/src/BlueUpdate/BlueUpdate.sln new file mode 100644 index 0000000..fa9d073 --- /dev/null +++ b/src/BlueUpdate/BlueUpdate.sln @@ -0,0 +1,28 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 14 +VisualStudioVersion = 14.0.25420.1 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlueUpdate", "BlueUpdate\BlueUpdate.csproj", "{D9B2471C-9048-4795-BEAB-BC099A3518B6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlueUpdate Updater", "BlueUpdate Updater\BlueUpdate Updater.csproj", "{A7F835C7-9F74-4C26-8A8C-07E5A2E7DEED}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D9B2471C-9048-4795-BEAB-BC099A3518B6}.Debug|Any CPU.ActiveCfg = Release|Any CPU + {D9B2471C-9048-4795-BEAB-BC099A3518B6}.Debug|Any CPU.Build.0 = Release|Any CPU + {D9B2471C-9048-4795-BEAB-BC099A3518B6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D9B2471C-9048-4795-BEAB-BC099A3518B6}.Release|Any CPU.Build.0 = Release|Any CPU + {A7F835C7-9F74-4C26-8A8C-07E5A2E7DEED}.Debug|Any CPU.ActiveCfg = Release|Any CPU + {A7F835C7-9F74-4C26-8A8C-07E5A2E7DEED}.Debug|Any CPU.Build.0 = Release|Any CPU + {A7F835C7-9F74-4C26-8A8C-07E5A2E7DEED}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A7F835C7-9F74-4C26-8A8C-07E5A2E7DEED}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/src/BlueUpdate/BlueUpdate/BlueUpdate.csproj b/src/BlueUpdate/BlueUpdate/BlueUpdate.csproj new file mode 100644 index 0000000..341bac7 --- /dev/null +++ b/src/BlueUpdate/BlueUpdate/BlueUpdate.csproj @@ -0,0 +1,69 @@ + + + + + Debug + AnyCPU + {D9B2471C-9048-4795-BEAB-BC099A3518B6} + Library + Properties + BlueUpdate + BlueUpdate + v4.6.1 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + bin\Debug\BlueUpdate.XML + 1591 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + bin\Release\BlueUpdate.XML + 1591 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/BlueUpdate/BlueUpdate/Common/BlueUpdateConstants.cs b/src/BlueUpdate/BlueUpdate/Common/BlueUpdateConstants.cs new file mode 100644 index 0000000..a150ad0 --- /dev/null +++ b/src/BlueUpdate/BlueUpdate/Common/BlueUpdateConstants.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BlueUpdate.Common +{ + public static class BlueUpdateConstants + { + public const string UpdaterName = "BlueUpdate Updater"; + public const string UpdaterDirectoryName = "Updater"; + public const string TempDirectoryName = "Updater tmp"; + + internal const string UpdaterAddress = "http://blueupdate.mohorko.info/"; + + internal const string BackupSuffix = "_BUbackup"; + } +} diff --git a/src/BlueUpdate/BlueUpdate/IO/Directories.cs b/src/BlueUpdate/BlueUpdate/IO/Directories.cs new file mode 100644 index 0000000..5d3fd04 --- /dev/null +++ b/src/BlueUpdate/BlueUpdate/IO/Directories.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BlueUpdate.IO +{ + /// + /// Provides utility functions for working with directories. + /// + public static class Directories + { + internal static DirectoryInfo GetDirectory(string name,bool createIfNotPresent=false) + { + DirectoryInfo[] siblingDirectories = Update.RootDirectory.GetDirectories(name, SearchOption.TopDirectoryOnly); + + if(siblingDirectories.Length == 1) { + return siblingDirectories[0]; + } + + if(siblingDirectories.Length > 1) { + // more than 1 folder for one application? + throw new Exception($"There are multiple directories that are returned for the search pattern '{name}'."); + } + + if(!createIfNotPresent) { + return null; + } + + return Directory.CreateDirectory(Path.Combine(Update.RootDirectory.FullName, name)); + } + + /// + /// Deletes the BlueUpdate temporary folder. + /// + public static void DeleteTemp() + { + if(Directory.Exists(Update.TempDirectoryPath)) { + Directory.Delete(Update.TempDirectoryPath, true); + } + } + } +} diff --git a/src/BlueUpdate/BlueUpdate/IO/Files.cs b/src/BlueUpdate/BlueUpdate/IO/Files.cs new file mode 100644 index 0000000..5a08533 --- /dev/null +++ b/src/BlueUpdate/BlueUpdate/IO/Files.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BlueUpdate.IO +{ + /// + /// Provides utility functions for working with files. + /// + public static class Files + { + /// + /// Gets the executable file of the specified application. + /// + /// The application of which to get the executable file. + public static FileInfo GetExecutable(UpdatableApp application) + { + DirectoryInfo appDirectory = Directories.GetDirectory(application.DirectoryName, false); + if(appDirectory == null) { + return null; + } + + string appExecutableName = application.Name + ".exe"; + FileInfo[] files = appDirectory.GetFiles(appExecutableName, SearchOption.TopDirectoryOnly); + if(files.Length == 1) { + return files[0]; + } + if(files.Length == 0) { + return null; + } + + throw new Exception($"There are multiple files that are returned for the search pattern '{appExecutableName}' for application '{application.Name}'."); + } + } +} diff --git a/src/BlueUpdate/BlueUpdate/Properties/AssemblyInfo.cs b/src/BlueUpdate/BlueUpdate/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..1bb15f4 --- /dev/null +++ b/src/BlueUpdate/BlueUpdate/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("BlueUpdate")] +[assembly: AssemblyDescription("A toolkit for automatic updating of .NET applications from the web.")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Grega Mohorko")] +[assembly: AssemblyProduct("BlueUpdate")] +[assembly: AssemblyCopyright("Copyright © Grega Mohorko 2017")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("d9b2471c-9048-4795-beab-bc099a3518b6")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/BlueUpdate/BlueUpdate/Security/Cryptography/Sha256.cs b/src/BlueUpdate/BlueUpdate/Security/Cryptography/Sha256.cs new file mode 100644 index 0000000..8e2675d --- /dev/null +++ b/src/BlueUpdate/BlueUpdate/Security/Cryptography/Sha256.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; + +namespace BlueUpdate.Security.Cryptography +{ + /// + /// Provides utility functions for calculations using the SHA-256 algorithm. + /// + public static class Sha256 + { + /// + /// Calculates the checksum for the specified file. + /// + /// The path of the file for which to calculate the checksum. + public static string GetChecksum(string file) + { + var sha256 = new SHA256Managed(); + byte[] checksumBytes; + + using(FileStream fileStream = File.OpenRead(file)) { + checksumBytes = sha256.ComputeHash(fileStream); + } + + // remove the hyphen + return BitConverter.ToString(checksumBytes).Replace("-", string.Empty); + } + } +} diff --git a/src/BlueUpdate/BlueUpdate/UpdatableApp.cs b/src/BlueUpdate/BlueUpdate/UpdatableApp.cs new file mode 100644 index 0000000..c3bfec5 --- /dev/null +++ b/src/BlueUpdate/BlueUpdate/UpdatableApp.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BlueUpdate +{ + /// + /// Represents an application that can be updated. + /// + public class UpdatableApp + { + /// + /// Gets or sets the currently running application. + /// + public static UpdatableApp Current + { + get { return _current; } + set + { + if(_current != null) { + throw new InvalidOperationException("Current application is already set."); + } + _current = value; + } + } + private static UpdatableApp _current; + + /// + /// Gets the name of this application. + /// + public string Name { get; private set; } + + /// + /// Gets the current version of this application. + /// + public Version Version { get; private set; } + + /// + /// Gets the latest version of this application. + /// + public Version VersionLatest { get; private set; } + + /// + /// Gets the name of this applications directory in the root directory. + /// + public string DirectoryName { get; private set; } + + /// + /// Gets the web address of the directory where this application can be downloaded. + /// + public string Address { get; private set; } + + /// + /// A collection of directories inside this applications directory to ignore while updating (to be left as they are). + /// + public ReadOnlyCollection IgnoredDirectories { get; private set; } + + /// + /// Creates a new instance of updatable application. + /// + /// The name of the application. + /// The current version of the application. + /// The latest version of the application. You should connect to the internet to check the latest version. + /// The web address of the directory where this application can be downloaded. + /// The name of the applications directory in the root directory. If null, will be set to the same value as the name of the application. + /// A collection of directories inside this applications directory to ignore while updating (to be left as they are). + public UpdatableApp(string name,Version version,Version latestVersion,string address,string directoryName=null,IEnumerable ignoredDirectories=null) + { + Name = name; + Version = version; + VersionLatest = latestVersion; + Address = address; + DirectoryName = directoryName ?? Name; + IgnoredDirectories = ignoredDirectories?.ToList().AsReadOnly(); + } + } +} diff --git a/src/BlueUpdate/BlueUpdate/Update.cs b/src/BlueUpdate/BlueUpdate/Update.cs new file mode 100644 index 0000000..b918189 --- /dev/null +++ b/src/BlueUpdate/BlueUpdate/Update.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using BlueUpdate.Common; +using BlueUpdate.IO; +using BlueUpdate.Utility; + +namespace BlueUpdate +{ + public static class Update + { + /// + /// Root directory of all applications. + /// + public static readonly DirectoryInfo RootDirectory; + + /// + /// Directory used for temporary files while installing/updating. + /// + public static readonly string TempDirectoryPath; + + /// + /// Gets information about the currently installed BlueUpdate executable. + /// + public static readonly UpdatableApp Updater; + + static Update() + { + try { + if(UpdatableApp.Current == null) { + throw new InvalidOperationException("You must first set BlueUpdate.Application.Current before using the BlueUpdate.Update class."); + } + + // check/set root folder + string currentDirectory = Directory.GetParent(AppDomain.CurrentDomain.BaseDirectory).FullName; + if(Path.GetFileName(currentDirectory) != UpdatableApp.Current.DirectoryName) { + throw new Exception($"The folder name of the application is incorrect: '{Path.GetFileName(currentDirectory)}' should be '{UpdatableApp.Current.DirectoryName}'."); + } + RootDirectory = Directory.GetParent(currentDirectory); + TempDirectoryPath = Path.Combine(RootDirectory.FullName, BlueUpdateConstants.TempDirectoryName); + + // check updater executable + FileVersionInfo updaterFileInfo = UpdateUtility.GetUpdaterExecutableFileInfo(); + if(updaterFileInfo != null) { + ReflectionUtility.AssemblyInformation assembly = ReflectionUtility.GetAssemblyInformation(ReflectionUtility.AssemblyType.Current); + + if(updaterFileInfo.CompanyName != assembly.Company || updaterFileInfo.ProductName != BlueUpdateConstants.UpdaterName) { + throw new Exception("Updater executable is not legit."); + } + Version currentVersion = Version.Parse(updaterFileInfo.FileVersion); + Version latestVersion = assembly.Version; + + Updater = new UpdatableApp(BlueUpdateConstants.UpdaterName, currentVersion, latestVersion, BlueUpdateConstants.UpdaterAddress, BlueUpdateConstants.UpdaterDirectoryName); + } + + // should install/update the updater executable? + if(Updater == null) { + // install + UpdatableApp updater = UpdateUtility.UpdaterAppDummy; + UpdateUtility.Install(updater); + Updater = updater; + } else if(Updater.Version != Updater.VersionLatest) { + // the version of the executable should be the same as the version of this library + UpdateUtility.Update(Updater); + } + }catch(Exception e) { + MessageBox.Show($"Error while initializing BlueUpdate:{Environment.NewLine}{Environment.NewLine}{e.Message}"); + throw e; + } + } + + /// + /// Checks and determines whether the current application needs to update. + /// + public static bool Check() + { + return Check(UpdatableApp.Current); + } + + /// + /// Checks and determines whether the specified application needs to update. + /// + /// The application to check. + public static bool Check(UpdatableApp application) + { + return application.VersionLatest > application.Version; + } + + /// + /// Starts the updater and attempts to update the current application. + /// + /// The application should be shut down right after calling this method. + /// + /// + /// Determines whether to run the application after the update. + public static void Run(bool runAfterUpdate=true) + { + Run(UpdatableApp.Current, runAfterUpdate); + } + + /// + /// Starts the updater and attempts to update the specified application. + /// + /// If the specified application is the same as the current, it should be shut down right after calling this method. + /// + /// + /// The application to update. + /// Determines whether to run the application after the update. + public static void Run(UpdatableApp application, bool runAfterUpdate=true) + { + if(Updater == null) { + throw new InvalidOperationException("The Updater was not successfully installed."); + } + + string ignoredDirectories; + if(application.IgnoredDirectories == null || application.IgnoredDirectories.Count==0) { + ignoredDirectories = "{}"; + }else { + ignoredDirectories = $"{{{string.Join(":",application.IgnoredDirectories)}}}"; + } + + // prepare the arguments + string[] arguments = { + application.Name, + application.VersionLatest.ToString(), + application.DirectoryName, + application.Address, + ignoredDirectories, + runAfterUpdate.ToString() + }; + + // prepare the process + FileInfo updaterExecutable = Files.GetExecutable(Updater); + Process updaterProcess = new Process(); + updaterProcess.StartInfo.FileName = updaterExecutable.FullName; + updaterProcess.StartInfo.Arguments = string.Join(" ", arguments.Select(arg => UpdateUtility.ToProcessArgument(arg))); + + // start the process + updaterProcess.Start(); + } + } +} diff --git a/src/BlueUpdate/BlueUpdate/UpdateUtility.cs b/src/BlueUpdate/BlueUpdate/UpdateUtility.cs new file mode 100644 index 0000000..e8a7dd1 --- /dev/null +++ b/src/BlueUpdate/BlueUpdate/UpdateUtility.cs @@ -0,0 +1,295 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using System.Xml.Linq; +using BlueUpdate.Common; +using BlueUpdate.IO; +using BlueUpdate.Security.Cryptography; +using BlueUpdate.Utility; + +namespace BlueUpdate +{ + /// + /// Provides utility functions for the update process. Do not use methods from this class unless you really know what you are doing. + /// + public static class UpdateUtility + { + internal static UpdatableApp UpdaterAppDummy + { + get + { + var assembly = ReflectionUtility.GetAssemblyInformation(ReflectionUtility.AssemblyType.Library); + return new UpdatableApp(BlueUpdateConstants.UpdaterName, assembly.Version, assembly.Version, BlueUpdateConstants.UpdaterAddress, BlueUpdateConstants.UpdaterDirectoryName); + } + } + + internal static FileVersionInfo GetUpdaterExecutableFileInfo() + { + UpdatableApp updaterAppDummy = UpdaterAppDummy; + FileInfo updaterExecutable = Files.GetExecutable(updaterAppDummy); + if(updaterExecutable == null) { + return null; + } + + return FileVersionInfo.GetVersionInfo(updaterExecutable.FullName); + } + + /// + /// Installs the specified application. + /// + /// The application to be installed. + /// If provided, the download process will be asynchronous and this action will be called upon completion. + /// When doing an asynchronous call, this action will be caled upon any changes of the download progress. + public static void Install(UpdatableApp application, Action completed = null, Action progressChanged = null) + { + DirectoryInfo appDirectory = Directories.GetDirectory(application.DirectoryName, true); + + bool exceptionRethrown = false; + + Action afterDownload = (downloadedZip) => + { + // extract to the app directory + try { + ZipFile.ExtractToDirectory(downloadedZip, appDirectory.FullName); + } catch(Exception e) { + // something went wrong + + // delete the directory and any files inside that were possibly left from extracting the zip + Directory.Delete(appDirectory.FullName); + + exceptionRethrown = true; + throw e; + } finally { + Directories.DeleteTemp(); + } + }; + + Directories.DeleteTemp(); + + try { + // download the zip and then call the afterDownload + if(completed == null) { + // synchronous + string filePath = Download(application); + afterDownload(filePath); + } else { + // asynchronous + Action downloadCompleted = (filePath, e) => + { + Exception error = null; + try { + if(e.Error != null) { + throw e.Error; + } + if(e.Cancelled) { + throw new WebException("The download process was cancelled."); + } + afterDownload(filePath); + } catch(Exception ex) { + if(!exceptionRethrown) { + // temp was already deleted when the exception was rethrown + Directories.DeleteTemp(); + } + error = ex; + } + + completed(new AsyncCompletedEventArgs(error, false, null)); + }; + Download(application, downloadCompleted, progressChanged); + } + } catch(Exception e) { + if(!exceptionRethrown) { + // temp was already deleted when the exception was rethrown + Directories.DeleteTemp(); + } + throw e; + } + } + + /// + /// Updates the specified application. + /// + /// The application to be updated. + /// If provided, the download process will be asynchronous and this action will be called upon completion. + /// When doing an asynchronous call, this action will be caled upon any changes of the download progress. + public static void Update(UpdatableApp application, Action completed = null, Action progressChanged = null) + { + DirectoryInfo appDirectory = Directories.GetDirectory(application.DirectoryName, true); + string backupSuffix = BlueUpdateConstants.BackupSuffix; + + bool exceptionRethrown = false; + + Action afterDownload = (downloadedZip) => + { + // add the backup suffix to all current files + IOUtility.AddSuffixToAll(appDirectory, backupSuffix, SearchOption.TopDirectoryOnly,application.IgnoredDirectories); + + // extract the downloaded zip file to the app directory + try { + ZipFile.ExtractToDirectory(downloadedZip, appDirectory.FullName); + } catch(Exception e) { + // something went wrong + + // reroll back: + // delete all non-backup files that were possibly left from extracting the zip + IOUtility.DeleteAllWithoutSuffix(appDirectory, backupSuffix, SearchOption.TopDirectoryOnly, application.IgnoredDirectories); + // reverse the backup files + IOUtility.RemoveSuffixFromAll(appDirectory, backupSuffix, SearchOption.TopDirectoryOnly); + + exceptionRethrown=true; + throw e; + } finally { + // delete the backup files + IOUtility.DeleteAllWithSuffix(appDirectory, backupSuffix, SearchOption.TopDirectoryOnly); + + // delete the temp folder + Directories.DeleteTemp(); + } + }; + + Directories.DeleteTemp(); + try { + // clear any backup files that possibly remained from previous update attempts + IOUtility.DeleteAllWithSuffix(appDirectory, backupSuffix, SearchOption.TopDirectoryOnly); + + // download the zip and then call the afterDownload + if(completed == null) { + // synchronous + string filePath = Download(application); + afterDownload(filePath); + }else { + // asynchronous + Action downloadCompleted = (filePath, e) => + { + Exception error = null; + try { + if(e.Error != null) { + throw e.Error; + } + if(e.Cancelled) { + throw new WebException("The download process was cancelled."); + } + afterDownload(filePath); + } catch(Exception ex) { + if(!exceptionRethrown) { + // temp was already deleted when the exception was rethrown + Directories.DeleteTemp(); + } + error = ex; + } + + completed(new AsyncCompletedEventArgs(error, false, null)); + }; + Download(application, downloadCompleted, progressChanged); + } + } catch(Exception e) { + if(!exceptionRethrown) { + // temp was already deleted when the exception was rethrown + Directories.DeleteTemp(); + } + throw e; + } + } + + /// + /// Prepares the string so that it can be used as an argument for a process. + /// + /// The text to be prepared. + public static string ToProcessArgument(string argument) + { + return argument.Replace(" ", "%20"); + } + + /// + /// Creates the actual text from the process argument. + /// + /// The argument to be converted to the actual text. + public static string FromProcessArgument(string argument) + { + return argument.Replace("%20", " "); + } + + /// + /// Downloads the specified application to the temporary folder and returns the file path of the downloaded zip. + /// + private static string Download(UpdatableApp application, Action completed = null, Action progressChanged=null) + { + string fileName = $"{application.Name} {application.VersionLatest}"; + string zipFileName = $"{fileName}.zip"; + string xmlFileName = $"{fileName}.xml"; + string downloadDirectoryAddress = string.Format("{0}/{1}", application.Address, application.VersionLatest); + string downloadFileAddress = string.Format("{0}/{1}", downloadDirectoryAddress, zipFileName); + string downloadXMLAddress = string.Format("{0}/{1}", downloadDirectoryAddress, xmlFileName); + DirectoryInfo tempDirectory = Directories.GetDirectory(BlueUpdateConstants.TempDirectoryName, true); + string downloadFilePath = Path.Combine(tempDirectory.FullName, zipFileName); + + Action afterDownload = delegate + { + // download checksum if it exists + XElement xml; + try { + xml = XElement.Load(downloadXMLAddress); + }catch(WebException) { + // no xml with checksum is present + return; + } + + var sha256 = xml.Element("SHA256"); + if(sha256 == null) { + throw new Exception("No SHA256 element inside the XML could be found."); + } + + string statedChecksum = sha256.Value; + + // get checksum of the downloaded file + string fileChecksum = Sha256.GetChecksum(downloadFilePath); + + if(statedChecksum != fileChecksum) { + throw new Exception("Checksum of the downloaded zip file did not match the one specified in the XML file."); + } + }; + + // download file to the temp folder + using(WebClient webClient=new WebClient()) { + if(completed==null) { + // synchronous call + webClient.DownloadFile(downloadFileAddress, downloadFilePath); + afterDownload(); + return downloadFilePath; + } else { + // asynchronous call + if(progressChanged != null) { + webClient.DownloadProgressChanged += (sender, e) => + { + progressChanged(e); + }; + } + webClient.DownloadFileCompleted += (sender, e) => + { + if(!e.Cancelled && e.Error == null) { + try { + afterDownload(); + } catch(Exception ex) { + downloadFilePath = null; + e = new AsyncCompletedEventArgs(ex, false, e.UserState); + } + }else { + downloadFilePath = null; + } + + completed(downloadFilePath, e); + }; + webClient.DownloadFileAsync(new Uri(downloadFileAddress), downloadFilePath); + return null; + } + } + } + } +} diff --git a/src/BlueUpdate/BlueUpdate/Utility/IOUtility.cs b/src/BlueUpdate/BlueUpdate/Utility/IOUtility.cs new file mode 100644 index 0000000..f87dc71 --- /dev/null +++ b/src/BlueUpdate/BlueUpdate/Utility/IOUtility.cs @@ -0,0 +1,463 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BlueUpdate.Utility +{ + public static class IOUtility + { + /// + /// Adds the specified suffix to all directories and files in the specified directory, including subdirectories. + /// + /// The directory in which to add the suffix to all directories and files. + /// The suffix to add to all directories and files. + public static void AddSuffixToAll(DirectoryInfo directory, string suffix) + { + AddSuffixToAll(directory, suffix, SearchOption.AllDirectories); + } + + /// + /// Adds the specified suffix to all directories and files in the specified directory, using the specified search option. + /// + /// The directory in which to add the suffix to all directories and files. + /// The suffix to add to all directories and files. + /// Specifies whether to include only the current directory or all subdirectories. + public static void AddSuffixToAll(DirectoryInfo directory, string suffix, SearchOption searchOption) + { + AddSuffixToAll(directory, suffix, searchOption, null); + } + + /// + /// Adds the specified suffix to all directories and files in the specified directory, using the specified search option. If a directory matches any of the values in the provided ignore list, the suffix will not be added to it. + /// + /// The directory in which to add the suffix to all directories and files. + /// The suffix to add to all directories and files. + /// Specifies whether to include only the current directory or all subdirectories. + /// A collection of directory names to ignore when adding suffix. + public static void AddSuffixToAll(DirectoryInfo directory,string suffix,SearchOption searchOption,IEnumerable ignoredDirectories) + { + AddSuffixToAllFiles(directory, suffix, SearchOption.TopDirectoryOnly); + AddSuffixToAllDirectories(directory, suffix, SearchOption.TopDirectoryOnly,ignoredDirectories); + + if(searchOption == SearchOption.AllDirectories) { + DirectoryInfo[] allSubdirectories = directory.GetDirectories("*", SearchOption.TopDirectoryOnly); + foreach(DirectoryInfo subdirectory in allSubdirectories) { + AddSuffixToAll(subdirectory, suffix, SearchOption.AllDirectories,ignoredDirectories); + } + } + } + + /// + /// Adds the specified suffix to all directories in the specified directory, including subdirectories. + /// + /// The directory in which to add the suffix to all directories. + /// The suffix to add to all directories. + public static void AddSuffixToAllDirectories(DirectoryInfo directory, string suffix) + { + AddSuffixToAllDirectories(directory, suffix, SearchOption.AllDirectories); + } + + /// + /// Adds the specified suffix to all directories in the specified directory, using the specified search option. + /// + /// The directory in which to add the suffix to all directories. + /// The suffix to add to all directories. + /// Specifies whether to include only the current directory or all subdirectories. + public static void AddSuffixToAllDirectories(DirectoryInfo directory, string suffix, SearchOption searchOption) + { + AddSuffixToAllDirectories(directory, suffix, searchOption, null); + } + + /// + /// Adds the specified suffix to all directories in the specified directory, using the specified search option. If a directory matches any of the values in the provided ignore list, the suffix will not be added to it. + /// + /// The directory in which to add the suffix to all directories. + /// The suffix to add to all directories. + /// Specifies whether to include only the current directory or all subdirectories. + /// A collection of directory names to ignore when adding suffix. + public static void AddSuffixToAllDirectories(DirectoryInfo directory, string suffix, SearchOption searchOption, IEnumerable ignoredDirectories) + { + DirectoryInfo[] allDirectories = directory.GetDirectories("*", SearchOption.TopDirectoryOnly); + + foreach(DirectoryInfo subdirectory in allDirectories) { + if(ignoredDirectories!=null && ignoredDirectories.Contains(subdirectory.Name)) { + continue; + } + + if(searchOption == SearchOption.AllDirectories) { + AddSuffixToAllDirectories(subdirectory, suffix, SearchOption.AllDirectories,ignoredDirectories); + } + AddSuffix(subdirectory, suffix); + } + } + + /// + /// Adds the specified suffix to all files inside the specified directory, including subdirectories. + /// + /// The directory in which to add the suffix to all files. + /// The suffix to add to all files. + public static void AddSuffixToAllFiles(DirectoryInfo directory, string suffix) + { + AddSuffixToAllFiles(directory, suffix, SearchOption.AllDirectories); + } + + /// + /// Adds the specified suffix to all files inside the specified directory, using the specified search option. + /// + /// The directory in which to add the suffix to all files. + /// The suffix to add to all files. + /// Specifies whether to include only the current directory or all subdirectories. + public static void AddSuffixToAllFiles(DirectoryInfo directory, string suffix, SearchOption searchOption) + { + FileInfo[] allFiles = directory.GetFiles("*", searchOption); + foreach(FileInfo file in allFiles) { + AddSuffix(file, suffix); + } + } + + /// + /// Renames the specified directory (adds the specified suffix to the end of the name). + /// + /// The directory to add the suffix to. + /// The suffix to add to the directory. + public static void AddSuffix(DirectoryInfo directory, string suffix) + { + string newName = directory.Name + suffix; + string absolutePath = Path.Combine(Path.GetDirectoryName(directory.FullName), newName); + directory.MoveTo(absolutePath); + } + + /// + /// Renames the specified file (adds the specified suffix to the end of the name). + /// + /// The file to add the suffix to. + /// The suffix to add to the file. + public static void AddSuffix(FileInfo file, string suffix) + { + string newName = file.Name + suffix; + string absolutePath = Path.Combine(Path.GetDirectoryName(file.FullName), newName); + file.MoveTo(absolutePath); + } + + /// + /// Removes the specified suffix from all directories and files in the specified directory, including subdirectories. + /// + /// The directory in which to remove the suffix from all directories and files. + /// The suffix to remove from all directories and files. + public static void RemoveSuffixFromAll(DirectoryInfo directory, string suffix) + { + RemoveSuffixFromAll(directory, suffix, SearchOption.AllDirectories); + } + + /// + /// Removes the specified suffix from all directories and files in the specified directory, using the specified search option. + /// + /// The directory in which to remove the suffix from all directories and files. + /// The suffix to remove from all directories and files. + /// Specifies whether to include only the current directory or all subdirectories. + public static void RemoveSuffixFromAll(DirectoryInfo directory, string suffix, SearchOption searchOption) + { + RemoveSuffixFromAllFiles(directory, suffix, SearchOption.TopDirectoryOnly); + RemoveSuffixFromAllDirectories(directory, suffix, SearchOption.TopDirectoryOnly); + + if(searchOption == SearchOption.AllDirectories) { + DirectoryInfo[] allSubdirectories = directory.GetDirectories("*", SearchOption.TopDirectoryOnly); + foreach(DirectoryInfo subdirectory in allSubdirectories) { + RemoveSuffixFromAll(subdirectory, suffix, SearchOption.AllDirectories); + } + } + } + + /// + /// Removes the specified suffix from all directories in the specified directory, including subdirectories. + /// + /// The directory in which to remove the suffix from all directories. + /// The suffix to remove from all directories. + public static void RemoveSuffixFromAllDirectories(DirectoryInfo directory, string suffix) + { + RemoveSuffixFromAllDirectories(directory, suffix, SearchOption.AllDirectories); + } + + /// + /// Removes the specified suffix from all directories in the specified directory, using the specified search option. + /// + /// The directory in which to remove the suffix from all directories. + /// The suffix to remove from all directories. + /// Specifies whether to include only the current directory or all subdirectories. + public static void RemoveSuffixFromAllDirectories(DirectoryInfo directory, string suffix, SearchOption searchOption) + { + DirectoryInfo[] allDirectories = directory.GetDirectories("*", SearchOption.TopDirectoryOnly); + + foreach(DirectoryInfo subdirectory in allDirectories) { + if(searchOption == SearchOption.AllDirectories) { + RemoveSuffixFromAllDirectories(subdirectory, suffix, SearchOption.AllDirectories); + } + RemoveSuffix(subdirectory, suffix); + } + } + + /// + /// Removes the specified suffix from all files inside the specified directory, including subdirectories. + /// + /// The directory in which to remove the suffix from all directories. + /// The suffix to remove from all directories. + public static void RemoveSuffixFromAllFiles(DirectoryInfo directory, string suffix) + { + RemoveSuffixFromAllFiles(directory, suffix, SearchOption.AllDirectories); + } + + /// + /// Removes the specified suffix from all files inside the specified directory, using the specified search option. + /// + /// The directory in which to remove the suffix from all directories. + /// The suffix to remove from all directories. + /// Specifies whether to include only the current directory or all subdirectories. + public static void RemoveSuffixFromAllFiles(DirectoryInfo directory, string suffix, SearchOption searchOption) + { + FileInfo[] allFiles = directory.GetFiles("*"+suffix, searchOption); + foreach(FileInfo file in allFiles) { + RemoveSuffix(file, suffix); + } + } + + /// + /// Renames the specified directory (removes the specified suffix from the end of the name, if present). + /// + /// The directory to remove the suffix from. + /// The suffix to remove from the directory. + public static void RemoveSuffix(DirectoryInfo directory, string suffix) + { + if(!directory.Name.EndsWith(suffix)) { + return; + } + + string newName = directory.Name.Substring(0, directory.Name.Length - suffix.Length); + string absolutePath = Path.Combine(Path.GetDirectoryName(directory.FullName), newName); + directory.MoveTo(absolutePath); + } + + /// + /// Renames the specified file (removes the specified suffix from the end of the name, if present). + /// + /// The file to remove the suffix from. + /// The suffix to remove from the file. + public static void RemoveSuffix(FileInfo file, string suffix) + { + if(!file.Name.EndsWith(suffix)) { + return; + } + + string newName = file.Name.Substring(0,file.Name.Length-suffix.Length); + string absolutePath = Path.Combine(Path.GetDirectoryName(file.FullName), newName); + file.MoveTo(absolutePath); + } + + /// + /// Deletes all files and directories in the specified directory, whose name is ended by the specified suffix, including subdirectories. + /// + /// The directory in which to delete all files and directories. + /// The suffix that files and directories must contain in order to be deleted. + public static void DeleteAllWithSuffix(DirectoryInfo directory, string suffix) + { + DeleteAllWithSuffix(directory, suffix, SearchOption.AllDirectories); + } + + /// + /// Deletes all files and directories in the specified directory, whose name is ended by the specified suffix, using the specified search option. + /// + /// The directory in which to delete all files and directories. + /// The suffix that files and directories must contain in order to be deleted. + /// Specifies whether to include only the current directory or all subdirectories. + public static void DeleteAllWithSuffix(DirectoryInfo directory, string suffix, SearchOption searchOption) + { + DeleteAllFilesWithSuffix(directory, suffix, SearchOption.TopDirectoryOnly); + DeleteAllDirectoriesWithSuffix(directory, suffix, SearchOption.TopDirectoryOnly); + + if(searchOption == SearchOption.AllDirectories) { + DirectoryInfo[] allSubdirectories = directory.GetDirectories("*", SearchOption.TopDirectoryOnly); + + foreach(DirectoryInfo subdirectory in allSubdirectories) { + DeleteAllWithSuffix(subdirectory, suffix, SearchOption.AllDirectories); + } + } + } + + /// + /// Deletes all directories in the specified directory, whose name is ended by the specified suffix, including subdirectories. + /// + /// The directory in which to delete all directories. + /// The suffix that directories must contain in order to be deleted. + public static void DeleteAllDirectoriesWithSuffix(DirectoryInfo directory, string suffix) + { + DeleteAllDirectoriesWithSuffix(directory, suffix, SearchOption.AllDirectories); + } + + /// + /// Deletes all directories in the specified directory, whose name is ended by the specified suffix, using the specified search option. + /// + /// The directory in which to delete all directories. + /// The suffix that directories must contain in order to be deleted. + /// Specifies whether to include only the current directory or all subdirectories. + public static void DeleteAllDirectoriesWithSuffix(DirectoryInfo directory, string suffix, SearchOption searchOption) + { + DirectoryInfo[] allSubdirectories = directory.GetDirectories("*", SearchOption.TopDirectoryOnly); + for(int i = allSubdirectories.Length - 1; i >= 0; i--) { + DirectoryInfo subdirectory = allSubdirectories[i]; + + if(subdirectory.Name.EndsWith(suffix)) { + allSubdirectories[i].Delete(true); + continue; + } + + if(searchOption == SearchOption.AllDirectories) { + DeleteAllDirectoriesWithSuffix(subdirectory, suffix, SearchOption.AllDirectories); + } + } + } + + /// + /// Deletes all files in the specified directory, whose name is ended by the specified suffix, including subdirectories. + /// + /// The directory in which to delete all files. + /// The suffix that files must contain in order to be deleted. + public static void DeleteAllFilesWithSuffix(DirectoryInfo directory, string suffix) + { + DeleteAllFilesWithSuffix(directory, suffix, SearchOption.AllDirectories); + } + + /// + /// Deletes all files in the specified directory, whose name is ended by the specified suffix, using the specified search option. + /// + /// The directory in which to delete all files. + /// The suffix that files must contain in order to be deleted. + /// Specifies whether to include only the current directory or all subdirectories. + public static void DeleteAllFilesWithSuffix(DirectoryInfo directory, string suffix, SearchOption searchOption) + { + FileInfo[] allFiles = directory.GetFiles("*" + suffix, searchOption); + foreach(FileInfo file in allFiles) { + file.Delete(); + } + } + + /// + /// Deletes all files and directories in the specified directory, whose name is not ended by the specified suffix, including subdirectories. + /// + /// The directory in which to delete all files and directories. + /// The suffix that files and directories must not contain in order to be deleted. + public static void DeleteAllWithoutSuffix(DirectoryInfo directory, string suffix) + { + DeleteAllWithoutSuffix(directory, suffix, SearchOption.AllDirectories); + } + + /// + /// Deletes all files and directories in the specified directory, whose name is not ended by the specified suffix, using the specified search option. + /// + /// The directory in which to delete all files and directories. + /// The suffix that files and directories must not contain in order to be deleted. + /// Specifies whether to include only the current directory or all subdirectories. + public static void DeleteAllWithoutSuffix(DirectoryInfo directory, string suffix, SearchOption searchOption) + { + DeleteAllWithoutSuffix(directory, suffix, searchOption, null); + } + + /// + /// Deletes all files and directories in the specified directory, whose name is not ended by the specified suffix, using the specified search option. If a directory matches any of the values in the provided ignore list, it will not be deleted. + /// + /// The directory in which to delete all files and directories. + /// The suffix that files and directories must not contain in order to be deleted. + /// Specifies whether to include only the current directory or all subdirectories. + /// A collection of directory names to ignore. + public static void DeleteAllWithoutSuffix(DirectoryInfo directory, string suffix, SearchOption searchOption,IEnumerable ignoredDirectories) + { + DeleteAllFilesWithoutSuffix(directory, suffix, SearchOption.TopDirectoryOnly); + DeleteAllDirectoriesWithoutSuffix(directory, suffix, SearchOption.TopDirectoryOnly,ignoredDirectories); + + if(searchOption == SearchOption.AllDirectories) { + DirectoryInfo[] allSubdirectories = directory.GetDirectories("*", SearchOption.TopDirectoryOnly); + + foreach(DirectoryInfo subdirectory in allSubdirectories) { + DeleteAllWithoutSuffix(subdirectory, suffix, SearchOption.AllDirectories,ignoredDirectories); + } + } + } + + /// + /// Deletes all directories in the specified directory, whose name is not ended by the specified suffix, including subdirectories. + /// + /// The directory in which to delete all directories. + /// The suffix that directories must not contain in order to be deleted. + public static void DeleteAllDirectoriesWithoutSuffix(DirectoryInfo directory, string suffix) + { + DeleteAllDirectoriesWithoutSuffix(directory, suffix, SearchOption.AllDirectories); + } + + /// + /// Deletes all directories in the specified directory, whose name is not ended by the specified suffix, using the specified search option. + /// + /// The directory in which to delete all directories. + /// The suffix that directories must not contain in order to be deleted. + /// Specifies whether to include only the current directory or all subdirectories. + public static void DeleteAllDirectoriesWithoutSuffix(DirectoryInfo directory, string suffix, SearchOption searchOption) + { + DeleteAllDirectoriesWithoutSuffix(directory, suffix, searchOption, null); + } + + /// + /// Deletes all directories in the specified directory, whose name is not ended by the specified suffix, using the specified search option. If a directory matches any of the values in the provided ignore list, it will not be deleted. + /// + /// The directory in which to delete all directories. + /// The suffix that directories must not contain in order to be deleted. + /// Specifies whether to include only the current directory or all subdirectories. + /// A collection of directory names to ignore. + public static void DeleteAllDirectoriesWithoutSuffix(DirectoryInfo directory, string suffix, SearchOption searchOption,IEnumerable ignoredDirectories) + { + DirectoryInfo[] allSubdirectories = directory.GetDirectories("*", SearchOption.TopDirectoryOnly); + for(int i = allSubdirectories.Length - 1; i >= 0; --i) { + DirectoryInfo subdirectory = allSubdirectories[i]; + + if(ignoredDirectories!=null && ignoredDirectories.Contains(subdirectory.Name)) { + continue; + } + + if(!subdirectory.Name.EndsWith(suffix)) { + allSubdirectories[i].Delete(true); + continue; + } + + if(searchOption == SearchOption.AllDirectories) { + DeleteAllDirectoriesWithoutSuffix(subdirectory, suffix, SearchOption.AllDirectories,ignoredDirectories); + } + } + } + + /// + /// Deletes all files in the specified directory, whose name is not ended by the specified suffix, including subdirectories. + /// + /// The directory in which to delete all files. + /// The suffix that files must not contain in order to be deleted. + public static void DeleteAllFilesWithoutSuffix(DirectoryInfo directory, string suffix) + { + DeleteAllFilesWithoutSuffix(directory, suffix, SearchOption.AllDirectories); + } + + /// + /// Deletes all files in the specified directory, whose name is not ended by the specified suffix, using the specified search option. + /// + /// The directory in which to delete all files. + /// The suffix that files must not contain in order to be deleted. + /// Specifies whether to include only the current directory or all subdirectories. + public static void DeleteAllFilesWithoutSuffix(DirectoryInfo directory, string suffix, SearchOption searchOption) + { + FileInfo[] allFiles = directory.GetFiles("*", searchOption); + foreach(FileInfo file in allFiles) { + if(!file.Name.EndsWith(suffix)) { + file.Delete(); + } + } + } + } +} diff --git a/src/BlueUpdate/BlueUpdate/Utility/ReflectionUtility.cs b/src/BlueUpdate/BlueUpdate/Utility/ReflectionUtility.cs new file mode 100644 index 0000000..2a5c716 --- /dev/null +++ b/src/BlueUpdate/BlueUpdate/Utility/ReflectionUtility.cs @@ -0,0 +1,128 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace BlueUpdate.Utility +{ + public static class ReflectionUtility + { + /// + /// Specifies the type of the assembly. + /// + public enum AssemblyType + { + Unknown = 0, + /// + /// Represents the entry assembly, which is usually the assembly of the application. + /// + Application = 1, + /// + /// Represents the assembly of the library where this enum is defined. + /// + Library = 2, + /// + /// Represents the currently running assembly. + /// + Current = 3 + } + + /// + /// Structure with assembly information. + /// + public struct AssemblyInformation + { + public readonly string Title; + public readonly string Description; + public readonly string Company; + public readonly string Product; + public readonly string Copyright; + public readonly string Trademark; + public readonly Version Version; + + public AssemblyInformation(string title, string description, string company, string product, string copyright, string trademark, Version version) + { + Title = title; + Description = description; + Company = company; + Product = product; + Copyright = copyright; + Trademark = trademark; + Version = version; + } + } + + /// + /// Gets the assembly of the specified type. + /// + /// The type of the assembly to look for. + public static Assembly GetAssembly(AssemblyType assemblyType) + { + Assembly assembly; + + switch(assemblyType) { + case AssemblyType.Application: + assembly = Assembly.GetEntryAssembly(); + break; + case AssemblyType.Library: + assembly = Assembly.GetExecutingAssembly(); + break; + case AssemblyType.Current: + assembly = null; + break; + default: + throw new ArgumentException($"Unsupported assembly type '{assemblyType}'."); + } + + if(assembly == null) { + assembly = Assembly.GetCallingAssembly(); + } + if(assembly == null) { + throw new Exception("Assembly information could not be found."); + } + + return assembly; + } + + /// + /// Gets the assembly information of the specified type. + /// + /// The type of the assembly to look for. + public static AssemblyInformation GetAssemblyInformation(AssemblyType assemblyType) + { + var assembly = GetAssembly(assemblyType); + return GetAssemblyInformation(assembly); + } + + /// + /// Gets the assembly information of the specified type. + /// + /// The assembly from which to extract the information from. + public static AssemblyInformation GetAssemblyInformation(Assembly assembly) + { + string title = assembly.GetCustomAttribute().Title; + string description = assembly.GetCustomAttribute().Description; + string company = assembly.GetCustomAttribute().Company; + string product = assembly.GetCustomAttribute().Product; + string copyright = assembly.GetCustomAttribute().Copyright; + string trademark = assembly.GetCustomAttribute().Trademark; + + string versionS; + AssemblyVersionAttribute assemblyVersion = assembly.GetCustomAttribute(); + if(assemblyVersion != null) { + versionS = assemblyVersion.Version; + } else { + AssemblyFileVersionAttribute fileVersion = assembly.GetCustomAttribute(); + if(fileVersion == null) { + throw new Exception("Assembly version could not be found."); + } + versionS = fileVersion.Version; + } + Version version = Version.Parse(versionS); + + return new AssemblyInformation(title, description, company, product, copyright, trademark, version); + } + } +}