浏览代码

Implement automatic download

Antony Male 10 年之前
父节点
当前提交
5bfcaa938e

+ 8 - 1
src/SyncTrayzor/Bootstrapper.cs

@@ -37,6 +37,7 @@ namespace SyncTrayzor
             builder.Bind<IApplicationState>().ToInstance(new ApplicationState(this.Application));
             builder.Bind<IApplicationWindowState>().ToFactory(c => new ApplicationWindowState((IScreenState)this.RootViewModel)).InSingletonScope();
             builder.Bind<IConfigurationProvider>().To<ConfigurationProvider>().InSingletonScope();
+            builder.Bind<IApplicationPathsProvider>().To<ApplicationPathsProvider>().InSingletonScope();
             builder.Bind<IAssemblyProvider>().To<AssemblyProvider>().InSingletonScope();
             builder.Bind<IAutostartProvider>().To<AutostartProvider>().InSingletonScope();
             builder.Bind<ConfigurationApplicator>().ToSelf().InSingletonScope();
@@ -48,10 +49,14 @@ namespace SyncTrayzor
             builder.Bind<INotifyIconManager>().To<NotifyIconManager>().InSingletonScope();
             builder.Bind<IWatchedFolderMonitor>().To<WatchedFolderMonitor>().InSingletonScope();
             builder.Bind<IUpdateManager>().To<UpdateManager>().InSingletonScope();
+            builder.Bind<IUpdateDownloader>().To<UpdateDownloader>().InSingletonScope();
             builder.Bind<IUpdateCheckerFactory>().To<UpdateCheckerFactory>();
             builder.Bind<IUpdatePromptProvider>().To<UpdatePromptProvider>();
             builder.Bind<IUpdateNotificationClientFactory>().To<UpdateNotificationClientFactory>();
             builder.Bind<IProcessStartProvider>().To<ProcessStartProvider>().InSingletonScope();
+            builder.Bind<IFilesystemProvider>().To<FilesystemProvider>().InSingletonScope();
+
+            builder.Bind<IUpdateVariantHandler>().To<InstalledUpdateVariantHandler>();
 
             builder.Bind(typeof(IModelValidator<>)).To(typeof(FluentModelValidator<>));
             builder.Bind(typeof(IValidator<>)).ToAllImplementations(this.Assemblies);
@@ -63,8 +68,10 @@ namespace SyncTrayzor
             pathConfiguration.Transform(EnvVarTransformer.Transform);
             GlobalDiagnosticsContext.Set("LogFilePath", pathConfiguration.LogFilePath);
 
+            this.Container.Get<IApplicationPathsProvider>().Initialize(pathConfiguration);
+
             var configurationProvider = this.Container.Get<IConfigurationProvider>();
-            configurationProvider.Initialize(pathConfiguration, Settings.Default.DefaultUserConfiguration);
+            configurationProvider.Initialize(Settings.Default.DefaultUserConfiguration);
             var configuration = this.Container.Get<IConfigurationProvider>().Load();
 
             // Has to be done before the VMs are fetched from the container

+ 2 - 2
src/SyncTrayzor/Pages/UnhandledExceptionViewModel.cs

@@ -27,10 +27,10 @@ namespace SyncTrayzor.Pages
             get { return SystemIcons.Error; }
         }
 
-        public UnhandledExceptionViewModel(IConfigurationProvider configurationProvider)
+        public UnhandledExceptionViewModel(IApplicationPathsProvider applicationPathsProvider)
         {
             this.IssuesUrl = Settings.Default.IssuesUrl;
-            this.LogFilePath = configurationProvider.LogFilePath;
+            this.LogFilePath = applicationPathsProvider.LogFilePath;
         }
 
         public void ShowIssues()

+ 82 - 0
src/SyncTrayzor/Services/Config/ApplicationPathsProvider.cs

@@ -0,0 +1,82 @@
+using NLog;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace SyncTrayzor.Services.Config
+{
+    public interface IApplicationPathsProvider
+    {
+        string LogFilePath { get; }
+        string SyncthingPath { get; }
+        string SyncthingCustomHomePath { get; }
+        string SyncthingBackupPath { get; }
+        string ConfigurationFilePath { get; }
+        string UpdatesDownloadPath { get; }
+
+        void Initialize(PathConfiguration pathConfiguration);
+    }
+
+    public class ApplicationPathsProvider : IApplicationPathsProvider
+    {
+        private static readonly Logger logger = LogManager.GetCurrentClassLogger();
+
+        private PathConfiguration pathConfiguration;
+
+        public ApplicationPathsProvider(IAssemblyProvider assemblyProvider)
+        {
+            this.ExePath = Path.GetDirectoryName(assemblyProvider.Location);
+        }
+
+        public void Initialize(PathConfiguration pathConfiguration)
+        {
+            if (pathConfiguration == null)
+                throw new ArgumentNullException("pathConfiguration");
+
+            this.pathConfiguration = pathConfiguration;
+
+            logger.Debug("ExePath: {0}", this.ExePath);
+            logger.Debug("LogFilePath: {0}", this.LogFilePath);
+            logger.Debug("SyncthingCustomHomePath: {0}", this.SyncthingCustomHomePath);
+            logger.Debug("SyncThingPath: {0}", this.SyncthingPath);
+            logger.Debug("SyncThingBackupPath: {0}", this.SyncthingBackupPath);
+            logger.Debug("ConfigurationFilePath: {0}", this.ConfigurationFilePath);
+        }
+
+        public string ExePath { get; set; }
+
+        public string LogFilePath
+        {
+            get { return this.pathConfiguration.LogFilePath; }
+        }
+
+        public string SyncthingCustomHomePath
+        {
+            get { return this.pathConfiguration.SyncthingCustomHomePath; }
+        }
+
+        public string SyncthingPath
+        {
+            get { return this.pathConfiguration.SyncthingPath; }
+        }
+
+        public string SyncthingBackupPath
+        {
+            get { return Path.Combine(this.ExePath, "syncthing.exe"); }
+        }
+
+        public string ConfigurationFilePath
+        {
+            get { return this.pathConfiguration.ConfigurationFilePath; }
+        }
+
+
+        public string UpdatesDownloadPath
+        {
+            get { return Path.Combine(Path.GetTempPath(), "SyncTrayzor"); }
+        }
+    }
+}

+ 23 - 66
src/SyncTrayzor/Services/Config/ConfigurationProvider.cs

@@ -34,11 +34,8 @@ namespace SyncTrayzor.Services.Config
         event EventHandler<ConfigurationChangedEventArgs> ConfigurationChanged;
 
         bool HadToCreateConfiguration { get; }
-        string LogFilePath { get; }
-        string SyncthingPath { get; }
-        string SyncthingCustomHomePath { get; }
 
-        void Initialize(PathConfiguration pathConfiguration, Configuration defaultConfiguration);
+        void Initialize(Configuration defaultConfiguration);
         Configuration Load();
         void Save(Configuration config);
         void AtomicLoadAndSave(Action<Configuration> setter);
@@ -52,7 +49,7 @@ namespace SyncTrayzor.Services.Config
         private readonly Logger logger = LogManager.GetCurrentClassLogger();
         private readonly SynchronizedEventDispatcher eventDispatcher;
         private readonly XmlSerializer serializer = new XmlSerializer(typeof(Configuration));
-        private PathConfiguration pathConfiguration;
+        private readonly IApplicationPathsProvider paths;
 
         private readonly object currentConfigLock = new object();
         private Configuration currentConfig;
@@ -61,76 +58,36 @@ namespace SyncTrayzor.Services.Config
 
         public bool HadToCreateConfiguration { get; private set; }
 
-        public string ExePath
-        {
-            get { return Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); }
-        }
-
-        public string LogFilePath
-        {
-            get { return this.pathConfiguration.LogFilePath; }
-        }
-
-        public string SyncthingCustomHomePath
-        {
-            get { return this.pathConfiguration.SyncthingCustomHomePath; }
-        }
-        
-        public string SyncthingPath
-        {
-            get { return this.pathConfiguration.SyncthingPath; }
-        }
-
-        public string SyncthingBackupPath
-        {
-            get { return Path.Combine(this.ExePath, "syncthing.exe"); }
-        }
-
-        public string ConfigurationFilePath
-        {
-            get { return this.pathConfiguration.ConfigurationFilePath; }
-        }
-
-        public ConfigurationProvider()
+        public ConfigurationProvider(IApplicationPathsProvider paths)
         {
+            this.paths = paths;
             this.eventDispatcher = new SynchronizedEventDispatcher(this);
         }
 
-        public void Initialize(PathConfiguration pathConfiguration, Configuration defaultConfiguration)
+        public void Initialize(Configuration defaultConfiguration)
         {
-            if (pathConfiguration == null)
-                throw new ArgumentNullException("pathConfiguration");
             if (defaultConfiguration == null)
                 throw new ArgumentNullException("defaultConfiguration");
 
-            this.pathConfiguration = pathConfiguration;
-
-            logger.Debug("ExePath: {0}", this.ExePath);
-            logger.Debug("LogFilePath: {0}", this.LogFilePath);
-            logger.Debug("SyncthingCustomHomePath: {0}", this.SyncthingCustomHomePath);
-            logger.Debug("SyncThingPath: {0}", this.SyncthingPath);
-            logger.Debug("SyncThingBackupPath: {0}", this.SyncthingBackupPath);
-            logger.Debug("ConfigurationFilePath: {0}", this.ConfigurationFilePath);
-
-            if (!File.Exists(Path.GetDirectoryName(this.ConfigurationFilePath)))
-                Directory.CreateDirectory(Path.GetDirectoryName(this.ConfigurationFilePath));
+            if (!File.Exists(Path.GetDirectoryName(this.paths.ConfigurationFilePath)))
+                Directory.CreateDirectory(Path.GetDirectoryName(this.paths.ConfigurationFilePath));
 
-            if (!File.Exists(this.SyncthingPath))
+            if (!File.Exists(this.paths.SyncthingPath))
             {
-                if (File.Exists(this.SyncthingBackupPath))
+                if (File.Exists(this.paths.SyncthingBackupPath))
                 {
-                    logger.Info("Syncthing doesn't exist at {0}, so copying from {1}", this.SyncthingPath, this.SyncthingBackupPath);
-                    File.Copy(this.SyncthingBackupPath, this.SyncthingPath);
+                    logger.Info("Syncthing doesn't exist at {0}, so copying from {1}", this.paths.SyncthingPath, this.paths.SyncthingBackupPath);
+                    File.Copy(this.paths.SyncthingBackupPath, this.paths.SyncthingPath);
                 }
                 else
-                    throw new Exception(String.Format("Unable to find Syncthing at {0} or {1}", this.SyncthingPath, this.SyncthingBackupPath));
+                    throw new Exception(String.Format("Unable to find Syncthing at {0} or {1}", this.paths.SyncthingPath, this.paths.SyncthingBackupPath));
             }
-            else if (this.SyncthingPath != this.SyncthingBackupPath && File.Exists(this.SyncthingBackupPath) &&
-                File.GetLastWriteTimeUtc(this.SyncthingPath) < File.GetLastWriteTimeUtc(this.SyncthingBackupPath))
+            else if (this.paths.SyncthingPath != this.paths.SyncthingBackupPath && File.Exists(this.paths.SyncthingBackupPath) &&
+                File.GetLastWriteTimeUtc(this.paths.SyncthingPath) < File.GetLastWriteTimeUtc(this.paths.SyncthingBackupPath))
             {
                 logger.Info("Syncthing at {0} is older ({1}) than at {2} ({3}, so overwriting from backup",
-                    this.SyncthingPath, File.GetLastWriteTimeUtc(this.SyncthingPath), this.SyncthingBackupPath, File.GetLastWriteTimeUtc(this.SyncthingBackupPath));
-                File.Copy(this.SyncthingBackupPath, this.SyncthingPath, true);
+                    this.paths.SyncthingPath, File.GetLastWriteTimeUtc(this.paths.SyncthingPath), this.paths.SyncthingBackupPath, File.GetLastWriteTimeUtc(this.paths.SyncthingBackupPath));
+                File.Copy(this.paths.SyncthingBackupPath, this.paths.SyncthingPath, true);
             }
 
             this.currentConfig = this.LoadFromDisk(defaultConfiguration);
@@ -149,21 +106,21 @@ namespace SyncTrayzor.Services.Config
                 defaultConfig = XDocument.Load(ms);
             }
 
-            if (File.Exists(this.ConfigurationFilePath))
+            if (File.Exists(this.paths.ConfigurationFilePath))
             {
-                logger.Debug("Found existing configuration at {0}", this.ConfigurationFilePath);
-                var loadedConfig = XDocument.Load(this.ConfigurationFilePath);
+                logger.Debug("Found existing configuration at {0}", this.paths.ConfigurationFilePath);
+                var loadedConfig = XDocument.Load(this.paths.ConfigurationFilePath);
                 var merged = loadedConfig.Root.Elements().Union(defaultConfig.Root.Elements(), new XmlNodeComparer());
                 loadedConfig.Root.ReplaceNodes(merged);
-                loadedConfig.Save(this.ConfigurationFilePath);
+                loadedConfig.Save(this.paths.ConfigurationFilePath);
             }
             else
             {
-                defaultConfig.Save(this.ConfigurationFilePath);
+                defaultConfig.Save(this.paths.ConfigurationFilePath);
             }
             
             Configuration configuration;
-            using (var stream = File.OpenRead(this.ConfigurationFilePath))
+            using (var stream = File.OpenRead(this.paths.ConfigurationFilePath))
             {
                 configuration = (Configuration)this.serializer.Deserialize(stream);
                 logger.Info("Loaded configuration: {0}", configuration);
@@ -235,7 +192,7 @@ namespace SyncTrayzor.Services.Config
 
         private void SaveToFile(Configuration config)
         {
-            using (var stream = File.Open(this.ConfigurationFilePath, FileMode.Create))
+            using (var stream = File.Open(this.paths.ConfigurationFilePath, FileMode.Create))
             {
                 this.serializer.Serialize(stream, config);
             }

+ 5 - 2
src/SyncTrayzor/Services/ConfigurationApplicator.cs

@@ -15,6 +15,7 @@ namespace SyncTrayzor.Services
     {
         private readonly IConfigurationProvider configurationProvider;
 
+        private readonly IApplicationPathsProvider pathsProvider;
         private readonly INotifyIconManager notifyIconManager;
         private readonly ISyncThingManager syncThingManager;
         private readonly IAutostartProvider autostartProvider;
@@ -23,6 +24,7 @@ namespace SyncTrayzor.Services
 
         public ConfigurationApplicator(
             IConfigurationProvider configurationProvider,
+            IApplicationPathsProvider pathsProvider,
             INotifyIconManager notifyIconManager,
             ISyncThingManager syncThingManager,
             IAutostartProvider autostartProvider,
@@ -32,6 +34,7 @@ namespace SyncTrayzor.Services
             this.configurationProvider = configurationProvider;
             this.configurationProvider.ConfigurationChanged += (o, e) => this.ApplyNewConfiguration(e.NewConfiguration);
 
+            this.pathsProvider = pathsProvider;
             this.notifyIconManager = notifyIconManager;
             this.syncThingManager = syncThingManager;
             this.autostartProvider = autostartProvider;
@@ -47,7 +50,7 @@ namespace SyncTrayzor.Services
             this.watchedFolderMonitor.BackoffInterval = TimeSpan.FromMilliseconds(Settings.Default.DirectoryWatcherBackoffMilliseconds);
             this.watchedFolderMonitor.FolderExistenceCheckingInterval = TimeSpan.FromMilliseconds(Settings.Default.DirectoryWatcherFolderExistenceCheckMilliseconds);
 
-            this.syncThingManager.ExecutablePath = this.configurationProvider.SyncthingPath;
+            this.syncThingManager.ExecutablePath = this.pathsProvider.SyncthingPath;
 
             this.updateManager.UpdateCheckApiUrl = Settings.Default.UpdateApiUrl;
             this.updateManager.Variant = Settings.Default.Variant;
@@ -66,7 +69,7 @@ namespace SyncTrayzor.Services
             this.syncThingManager.Address = new Uri("https://" + configuration.SyncthingAddress);
             this.syncThingManager.ApiKey = configuration.SyncthingApiKey;
             this.syncThingManager.SyncthingTraceFacilities = configuration.SyncthingTraceFacilities;
-            this.syncThingManager.SyncthingCustomHomeDir = configuration.SyncthingUseCustomHome ? this.configurationProvider.SyncthingCustomHomePath : null;
+            this.syncThingManager.SyncthingCustomHomeDir = configuration.SyncthingUseCustomHome ? this.pathsProvider.SyncthingCustomHomePath : null;
             this.syncThingManager.SyncthingDenyUpgrade = configuration.SyncthingDenyUpgrade;
             this.syncThingManager.SyncthingRunLowPriority = configuration.SyncthingRunLowPriority;
             this.syncThingManager.SyncthingHideDeviceIds = configuration.ObfuscateDeviceIDs;

+ 46 - 0
src/SyncTrayzor/Services/FilesystemProvider.cs

@@ -0,0 +1,46 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace SyncTrayzor.Services
+{
+    public interface IFilesystemProvider
+    {
+        bool Exists(string path);
+        Stream Open(string path, FileMode fileMode, FileAccess fileAccess, FileShare fileShare);
+        void Move(string from, string to);
+        void CreateDirectory(string path);
+        void Delete(string path);
+    }
+
+    public class FilesystemProvider : IFilesystemProvider
+    {
+        public bool Exists(string path)
+        {
+            return File.Exists(path);
+        }
+
+        public Stream Open(string path, FileMode fileMode, FileAccess fileAccess, FileShare fileShare)
+        {
+            return new FileStream(path, fileMode, fileAccess, fileShare);
+        }
+
+        public void Move(string from, string to)
+        {
+            File.Move(from, to);
+        }
+
+        public void CreateDirectory(string path)
+        {
+            Directory.CreateDirectory(path);
+        }
+
+        public void Delete(string path)
+        {
+            File.Delete(path);
+        }
+    }
+}

+ 3 - 15
src/SyncTrayzor/Services/MemoryUsageLogger.cs

@@ -1,4 +1,5 @@
 using NLog;
+using SyncTrayzor.Utils;
 using System;
 using System.Collections.Generic;
 using System.Diagnostics;
@@ -12,7 +13,6 @@ namespace SyncTrayzor.Services
     public class MemoryUsageLogger
     {
         private static readonly Logger logger = LogManager.GetCurrentClassLogger();
-        private static readonly string[] sizes = { "B", "KB", "MB", "GB" };
         private static readonly TimeSpan pollInterval = TimeSpan.FromMinutes(5);
 
         private readonly Timer timer;
@@ -36,21 +36,9 @@ namespace SyncTrayzor.Services
             this.timer.Elapsed += (o, e) =>
             {
                 logger.Debug("Working Set: {0}. Private Memory Size: {1}. GC Total Memory: {2}",
-                    this.BytesToHuman(this.process.WorkingSet64), this.BytesToHuman(this.process.PrivateMemorySize64),
-                    this.BytesToHuman(GC.GetTotalMemory(false)));
+                    FormatUtils.BytesToHuman(this.process.WorkingSet64), FormatUtils.BytesToHuman(this.process.PrivateMemorySize64),
+                    FormatUtils.BytesToHuman(GC.GetTotalMemory(false)));
             };
         }
-
-        private string BytesToHuman(long bytes)
-        {
-            // http://stackoverflow.com/a/281679/1086121
-            int order = 0;
-            while (bytes >= 1024 && order + 1 < sizes.Length)
-            {
-                order++;
-                bytes = bytes / 1024;
-            }
-            return String.Format("{0:0.#}{1}", bytes, sizes[order]);
-        }
     }
 }

+ 13 - 0
src/SyncTrayzor/Services/UpdateManagement/IUpdateVariantHandler.cs

@@ -0,0 +1,13 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace SyncTrayzor.Services.UpdateManagement
+{
+    public interface IUpdateVariantHandler
+    {
+        Task<bool> TryHandleUpdateAvailableAsync(VersionCheckResults checkResult);
+    }
+}

+ 26 - 0
src/SyncTrayzor/Services/UpdateManagement/InstalledUpdateVariantHandler.cs

@@ -0,0 +1,26 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace SyncTrayzor.Services.UpdateManagement
+{
+    public class InstalledUpdateVariantHandler : IUpdateVariantHandler
+    {
+        private readonly IUpdateDownloader updateDownloader;
+
+        private string installerPath;
+
+        public InstalledUpdateVariantHandler(IUpdateDownloader updateDownloader)
+        {
+            this.updateDownloader = updateDownloader;
+        }
+
+        public async Task<bool> TryHandleUpdateAvailableAsync(VersionCheckResults checkResult)
+        {
+            this.installerPath = await this.updateDownloader.DownloadUpdateAsync(checkResult.DownloadUrl, checkResult.NewVersion);
+            return this.installerPath != null;
+        }
+    }
+}

+ 102 - 0
src/SyncTrayzor/Services/UpdateManagement/UpdateDownloader.cs

@@ -0,0 +1,102 @@
+using NLog;
+using SyncTrayzor.Services.Config;
+using SyncTrayzor.Utils;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace SyncTrayzor.Services.UpdateManagement
+{
+    public interface IUpdateDownloader
+    {
+        Task<string> DownloadUpdateAsync(string url, Version version);
+    }
+
+    public class UpdateDownloader : IUpdateDownloader
+    {
+        private static readonly Logger logger = LogManager.GetCurrentClassLogger();
+
+        private const string downloadFileTempName = "SyncTrayzorUpdate-{0}.exe.temp";
+        private const string downloadFileName = "SyncTrayzorUpdate-{0}.exe";
+
+        private readonly string downloadsDir;
+        private readonly IFilesystemProvider filesystemProvider;
+
+        public UpdateDownloader(IApplicationPathsProvider pathsProvider, IFilesystemProvider filesystemProvider)
+        {
+            this.downloadsDir = pathsProvider.UpdatesDownloadPath;
+            this.filesystemProvider = filesystemProvider;
+        }
+
+        public async Task<string> DownloadUpdateAsync(string url, Version version)
+        {
+            var tempPath = Path.Combine(this.downloadsDir, String.Format(downloadFileTempName, version.ToString(3)));
+            var finalPath = Path.Combine(this.downloadsDir, String.Format(downloadFileName, version.ToString(3)));
+
+            // Someone downloaded it already? Oh good.
+            if (this.filesystemProvider.Exists(finalPath))
+            {
+                logger.Info("Skipping download as final file {0} already exists", finalPath);
+                return finalPath;
+            }
+
+            // Just in case...
+            this.filesystemProvider.CreateDirectory(this.downloadsDir);
+
+            // Temp file exists? Either a previous download was aborted, or there's another copy of us running somewhere
+            // The difference depends on whether or not it's locked...
+            try
+            {
+                var webClient = new WebClient();
+
+                logger.Info("Downloading to {0}", tempPath);
+                using (var fileStream = this.filesystemProvider.Open(tempPath, FileMode.Create, FileAccess.ReadWrite, FileShare.None))
+                using (var downloadStream = await webClient.OpenReadTaskAsync(url))
+                {
+                    var responseLength = Int64.Parse(webClient.ResponseHeaders["Content-Length"]);
+                    var previousDownloadProgressString = String.Empty;
+
+                    var progress = new Progress<CopyToAsyncProgress>(p =>
+                    {
+                        var downloadProgressString = String.Format("Downloaded {0}/{1} ({2}%)",
+                            FormatUtils.BytesToHuman(p.BytesRead), FormatUtils.BytesToHuman(responseLength), (p.BytesRead * 100) / responseLength);
+                        if (downloadProgressString != previousDownloadProgressString)
+                        {
+                            logger.Info(downloadProgressString);
+                            previousDownloadProgressString = downloadProgressString;
+                        }
+                    });
+
+                    await downloadStream.CopyToAsync(fileStream, progress);
+                }
+            }
+            catch (IOException e)
+            {
+                logger.Warn(String.Format("Failed to initiate download to temp file {0}", tempPath), e);
+                return null;
+            }
+
+            // Possible, I guess, that the finalPath now exists. If it does, that's fine
+            try
+            {
+                logger.Info("Copying temp file {0} to {1}", tempPath, finalPath);
+                this.filesystemProvider.Move(tempPath, finalPath);
+            }
+            catch (IOException e)
+            {
+                logger.Warn(String.Format("Failed to move temp file {0} to final file {1}", tempPath, finalPath), e);
+                return null;
+            }
+
+            this.filesystemProvider.Delete(tempPath);
+
+            logger.Info("Done");
+
+            return finalPath;
+        }
+    }
+}

+ 8 - 1
src/SyncTrayzor/Services/UpdateManagement/UpdateManager.cs

@@ -43,6 +43,7 @@ namespace SyncTrayzor.Services.UpdateManagement
         private readonly IUpdateCheckerFactory updateCheckerFactory;
         private readonly IProcessStartProvider processStartProvider;
         private readonly IUpdatePromptProvider updatePromptProvider;
+        private readonly Func<IUpdateVariantHandler> updateVariantHandlerFactory;
         private readonly System.Timers.Timer promptTimer;
 
         private readonly SemaphoreSlim versionCheckLock = new SemaphoreSlim(1, 1);
@@ -72,13 +73,15 @@ namespace SyncTrayzor.Services.UpdateManagement
             IApplicationWindowState applicationWindowState,
             IUpdateCheckerFactory updateCheckerFactory,
             IProcessStartProvider processStartProvider,
-            IUpdatePromptProvider updatePromptProvider)
+            IUpdatePromptProvider updatePromptProvider,
+            Func<IUpdateVariantHandler> updateVariantHandlerFactory)
         {
             this.applicationState = applicationState;
             this.applicationWindowState = applicationWindowState;
             this.updateCheckerFactory = updateCheckerFactory;
             this.processStartProvider = processStartProvider;
             this.updatePromptProvider = updatePromptProvider;
+            this.updateVariantHandlerFactory = updateVariantHandlerFactory;
 
             this.promptTimer = new System.Timers.Timer();
             this.promptTimer.Elapsed += this.PromptTimerElapsed;
@@ -166,6 +169,10 @@ namespace SyncTrayzor.Services.UpdateManagement
                 if (checkResult == null)
                     return;
 
+                var variantHandler = this.updateVariantHandlerFactory();
+                if (!await variantHandler.TryHandleUpdateAvailableAsync(checkResult))
+                    return;
+
                 VersionPromptResult promptResult;
                 if (this.applicationState.HasMainWindow)
                 {

+ 7 - 0
src/SyncTrayzor/SyncTrayzor.csproj

@@ -122,9 +122,14 @@
     </Compile>
     <Compile Include="Services\ApplicationWindowState.cs" />
     <Compile Include="Services\AssemblyProvider.cs" />
+    <Compile Include="Services\Config\ApplicationPathsProvider.cs" />
+    <Compile Include="Services\FilesystemProvider.cs" />
     <Compile Include="Services\ProcessStartProvider.cs" />
+    <Compile Include="Services\UpdateManagement\InstalledUpdateVariantHandler.cs" />
     <Compile Include="Services\UpdateManagement\IUpdateNotificationApi.cs" />
+    <Compile Include="Services\UpdateManagement\IUpdateVariantHandler.cs" />
     <Compile Include="Services\UpdateManagement\UpdateCheckerFactory.cs" />
+    <Compile Include="Services\UpdateManagement\UpdateDownloader.cs" />
     <Compile Include="Services\UpdateManagement\UpdateManager.cs" />
     <Compile Include="Services\UpdateManagement\UpdateNotificationClient.cs" />
     <Compile Include="Services\UpdateManagement\UpdateNotificationClientFactory.cs" />
@@ -225,9 +230,11 @@
     <Compile Include="Utils\Buffer.cs" />
     <Compile Include="Utils\EnvVarTransformer.cs" />
     <Compile Include="Utils\FluentModelValidator.cs" />
+    <Compile Include="Utils\FormatUtils.cs" />
     <Compile Include="Utils\ObservableQueue.cs" />
     <Compile Include="Utils\SafeSyncthingExtensions.cs" />
     <Compile Include="Utils\SemaphoreSlimExtensions.cs" />
+    <Compile Include="Utils\StreamExtensions.cs" />
     <Compile Include="Utils\SynchronizedEventDispatcher.cs" />
     <Compile Include="Utils\UriExtensions.cs" />
     <Compile Include="Xaml\CollapsingRowDefinitionBehaviour.cs" />

+ 25 - 0
src/SyncTrayzor/Utils/FormatUtils.cs

@@ -0,0 +1,25 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace SyncTrayzor.Utils
+{
+    public static class FormatUtils
+    {
+        private static readonly string[] sizes = { "B", "KB", "MB", "GB" };
+
+        public static string BytesToHuman(long bytes)
+        {
+            // http://stackoverflow.com/a/281679/1086121
+            int order = 0;
+            while (bytes >= 1024 && order + 1 < sizes.Length)
+            {
+                order++;
+                bytes = bytes / 1024;
+            }
+            return String.Format("{0:0.#}{1}", bytes, sizes[order]);
+        }
+    }
+}

+ 46 - 0
src/SyncTrayzor/Utils/StreamExtensions.cs

@@ -0,0 +1,46 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace SyncTrayzor.Utils
+{
+    public struct CopyToAsyncProgress
+    {
+        public long BytesRead { get; private set; }
+        public long TotalBytesToRead { get; private set; }
+        public int ProgressPercent { get; private set; }
+
+        public CopyToAsyncProgress(long bytesRead, long totalBytesToRead)
+            : this()
+        {
+            this.BytesRead = bytesRead;
+            this.TotalBytesToRead = totalBytesToRead;
+
+            if (this.TotalBytesToRead > 0)
+                this.ProgressPercent = (int)((this.BytesRead * 100) / this.TotalBytesToRead);
+            else
+                this.ProgressPercent = -1;
+        }
+    }
+
+    public static class StreamExtensions
+    {
+        public static async Task CopyToAsync(this Stream source, Stream destination, IProgress<CopyToAsyncProgress> progress)
+        {
+            var buffer = new byte[81920];
+            var totalBytesToRead = source.CanSeek ? source.Length : -1;
+            long totalBytesRead = 0;
+            int bytesRead;
+
+            while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false)) != 0)
+            {
+                await destination.WriteAsync(buffer, 0, bytesRead).ConfigureAwait(false);
+                totalBytesRead += bytesRead;
+                progress.Report(new CopyToAsyncProgress(totalBytesRead, totalBytesToRead));
+            }
+        }
+    }
+}