// Copyright (c) 2012, Outercurve Foundation. // All rights reserved. // // Redistribution and use in source and binary forms, with or without modification, // are permitted provided that the following conditions are met: // // - Redistributions of source code must retain the above copyright notice, this // list of conditions and the following disclaimer. // // - Redistributions in binary form must reproduce the above copyright notice, // this list of conditions and the following disclaimer in the documentation // and/or other materials provided with the distribution. // // - Neither the name of the Outercurve Foundation nor the names of its // contributors may be used to endorse or promote products derived from this // software without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR // ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON // ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. using System; using System.IO; using System.Threading; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Text; using Ionic.Zip; using WebsitePanel.Installer.Common; using System.Net; using System.Text.RegularExpressions; using System.Collections; using System.Threading.Tasks; using System.Reflection; using System.Diagnostics; namespace WebsitePanel.Installer.Core { public class LoaderEventArgs : EventArgs { public string StatusMessage { get; set; } public T EventData { get; set; } public bool Cancellable { get; set; } } public static class LoaderFactory { /// /// Instantiates either CodeplexLoader or InstallerServiceLoader based on remote file format. /// /// /// public static Loader CreateFileLoader(string remoteFile) { Debug.Assert(!String.IsNullOrEmpty(remoteFile), "Remote file is empty"); if (remoteFile.StartsWith("http://websitepanel.codeplex.com/")) { return new CodeplexLoader(remoteFile); } else { return new Loader(remoteFile); } } } public class CodeplexLoader : Loader { public const string WEB_PI_USER_AGENT_HEADER = "PI-Integrator/3.0.0.0({0})"; private WebClient fileLoader; internal CodeplexLoader(string remoteFile) : base(remoteFile) { InitFileLoader(); } private void InitFileLoader() { fileLoader = new WebClient(); // Set HTTP header for Codeplex to allow direct downloads fileLoader.Headers.Add("User-Agent", String.Format(WEB_PI_USER_AGENT_HEADER, Assembly.GetExecutingAssembly().FullName)); } protected override Task GetDownloadFileTask(string remoteFile, string tmpFile, CancellationToken ct) { var downloadFileTask = new Task(() => { if (!File.Exists(tmpFile)) { // Mimic synchronous file download operation because we need to track the download progress // and be able to cancel the operation in progress AutoResetEvent autoEvent = new AutoResetEvent(false); if (fileLoader.IsBusy.Equals(true)) { return; } ct.Register(() => { fileLoader.CancelAsync(); }); Log.WriteStart("Downloading file"); Log.WriteInfo("Downloading file \"{0}\" to \"{1}\"", remoteFile, tmpFile); // Attach event handlers to track status of the download process fileLoader.DownloadProgressChanged += (obj, e) => { if (ct.IsCancellationRequested) return; RaiseOnProgressChangedEvent(e.ProgressPercentage); RaiseOnStatusChangedEvent(DownloadingSetupFilesMessage, String.Format(DownloadProgressMessage, e.BytesReceived / 1024, e.TotalBytesToReceive / 1024)); }; fileLoader.DownloadFileCompleted += (obj, e) => { if (ct.IsCancellationRequested == false) { RaiseOnProgressChangedEvent(100); RaiseOnStatusChangedEvent(DownloadingSetupFilesMessage, "100%"); } if (e.Cancelled) { CancelDownload(tmpFile); } autoEvent.Set(); }; fileLoader.DownloadFileAsync(new Uri(remoteFile), tmpFile); RaiseOnStatusChangedEvent(DownloadingSetupFilesMessage); autoEvent.WaitOne(); } }, ct); return downloadFileTask; } } /// /// Loader form. /// public class Loader { public const string ConnectingRemotServiceMessage = "Connecting..."; public const string DownloadingSetupFilesMessage = "Downloading setup files..."; public const string CopyingSetupFilesMessage = "Copying setup files..."; public const string PreparingSetupFilesMessage = "Please wait while Setup prepares the necessary files..."; public const string DownloadProgressMessage = "{0} KB of {1} KB"; public const string PrepareSetupProgressMessage = "{0}%"; private const int ChunkSize = 262144; private string remoteFile; private CancellationTokenSource cts; public event EventHandler> StatusChanged; public event EventHandler> OperationFailed; public event EventHandler> ProgressChanged; public event EventHandler OperationCompleted; internal Loader(string remoteFile) { this.remoteFile = remoteFile; } public void LoadAppDistributive() { ThreadPool.QueueUserWorkItem(q => LoadAppDistributiveInternal()); } protected void RaiseOnStatusChangedEvent(string statusMessage) { RaiseOnStatusChangedEvent(statusMessage, String.Empty); } protected void RaiseOnStatusChangedEvent(string statusMessage, string eventData) { RaiseOnStatusChangedEvent(statusMessage, eventData, true); } protected void RaiseOnStatusChangedEvent(string statusMessage, string eventData, bool cancellable) { if (StatusChanged == null) { return; } // No event data for status updates StatusChanged(this, new LoaderEventArgs { StatusMessage = statusMessage, EventData = eventData, Cancellable = cancellable }); } protected void RaiseOnProgressChangedEvent(int eventData) { RaiseOnProgressChangedEvent(eventData, true); } protected void RaiseOnProgressChangedEvent(int eventData, bool cancellable) { if (ProgressChanged == null) { return; } // ProgressChanged(this, new LoaderEventArgs { EventData = eventData, Cancellable = cancellable }); } protected void RaiseOnOperationFailedEvent(Exception ex) { if (OperationFailed == null) { return; } // OperationFailed(this, new LoaderEventArgs { EventData = ex }); } protected void RaiseOnOperationCompletedEvent() { if (OperationCompleted == null) { return; } // OperationCompleted(this, EventArgs.Empty); } /// /// Executes a file download request asynchronously /// private void LoadAppDistributiveInternal() { try { string dataFolder; string tmpFolder; // Retrieve local storage configuration GetLocalStorageInfo(out dataFolder, out tmpFolder); // Initialize storage InitializeLocalStorage(dataFolder, tmpFolder); string fileToDownload = Path.GetFileName(remoteFile); string destinationFile = Path.Combine(dataFolder, fileToDownload); string tmpFile = Path.Combine(tmpFolder, fileToDownload); cts = new CancellationTokenSource(); CancellationToken token = cts.Token; try { // Download the file requested Task downloadFileTask = GetDownloadFileTask(remoteFile, tmpFile, token); // Move the file downloaded from temporary location to Data folder var moveFileTask = downloadFileTask.ContinueWith((t) => { if (File.Exists(tmpFile)) { // copy downloaded file to data folder RaiseOnStatusChangedEvent(CopyingSetupFilesMessage); // RaiseOnProgressChangedEvent(0); // Ensure that the target does not exist. if (File.Exists(destinationFile)) FileUtils.DeleteFile(destinationFile); File.Move(tmpFile, destinationFile); // RaiseOnProgressChangedEvent(100); } }, TaskContinuationOptions.NotOnCanceled); // Unzip file downloaded var unzipFileTask = moveFileTask.ContinueWith((t) => { if (File.Exists(destinationFile)) { RaiseOnStatusChangedEvent(PreparingSetupFilesMessage); // RaiseOnProgressChangedEvent(0); // UnzipFile(destinationFile, tmpFolder); // RaiseOnProgressChangedEvent(100); } }, token); // var notifyCompletionTask = unzipFileTask.ContinueWith((t) => { RaiseOnOperationCompletedEvent(); }, token); downloadFileTask.Start(); downloadFileTask.Wait(); } catch (AggregateException ae) { ae.Handle((e) => { // We handle cancellation requests if (e is OperationCanceledException) { CancelDownload(tmpFile); Log.WriteInfo("Download has been cancelled by the user"); return true; } // But other issues just being logged Log.WriteError("Could not download the file", e); return false; }); } } catch (Exception ex) { if (Utils.IsThreadAbortException(ex)) return; Log.WriteError("Loader module error", ex); // RaiseOnOperationFailedEvent(ex); } } protected virtual Task GetDownloadFileTask(string sourceFile, string tmpFile, CancellationToken ct) { var downloadFileTask = new Task(() => { if (!File.Exists(tmpFile)) { var service = ServiceProviderProxy.GetInstallerWebService(); RaiseOnProgressChangedEvent(0); RaiseOnStatusChangedEvent(DownloadingSetupFilesMessage); Log.WriteStart("Downloading file"); Log.WriteInfo(string.Format("Downloading file \"{0}\" to \"{1}\"", sourceFile, tmpFile)); long downloaded = 0; long fileSize = service.GetFileSize(sourceFile); if (fileSize == 0) { throw new FileNotFoundException("Service returned empty file.", sourceFile); } byte[] content; while (downloaded < fileSize) { // Throw OperationCancelledException if there is an incoming cancel request ct.ThrowIfCancellationRequested(); content = service.GetFileChunk(sourceFile, (int)downloaded, ChunkSize); if (content == null) { throw new FileNotFoundException("Service returned NULL file content.", sourceFile); } FileUtils.AppendFileContent(tmpFile, content); downloaded += content.Length; // Update download progress RaiseOnStatusChangedEvent(DownloadingSetupFilesMessage, string.Format(DownloadProgressMessage, downloaded / 1024, fileSize / 1024)); RaiseOnProgressChangedEvent(Convert.ToInt32((downloaded * 100) / fileSize)); if (content.Length < ChunkSize) break; } RaiseOnStatusChangedEvent(DownloadingSetupFilesMessage, "100%"); Log.WriteEnd(string.Format("Downloaded {0} bytes", downloaded)); } }, ct); return downloadFileTask; } private static void InitializeLocalStorage(string dataFolder, string tmpFolder) { if (!Directory.Exists(dataFolder)) { Directory.CreateDirectory(dataFolder); Log.WriteInfo("Data directory created"); } if (Directory.Exists(tmpFolder)) { Directory.Delete(tmpFolder, true); } if (!Directory.Exists(tmpFolder)) { Directory.CreateDirectory(tmpFolder); Log.WriteInfo("Tmp directory created"); } } private static void GetLocalStorageInfo(out string dataFolder, out string tmpFolder) { dataFolder = FileUtils.GetDataDirectory(); tmpFolder = FileUtils.GetTempDirectory(); } private void UnzipFile(string zipFile, string destFolder) { try { int val = 0; // Negative value means no progress made yet int progress = -1; // Log.WriteStart("Unzipping file"); Log.WriteInfo(string.Format("Unzipping file \"{0}\" to the folder \"{1}\"", zipFile, destFolder)); long zipSize = 0; var zipInfo = ZipFile.Read(zipFile); try { foreach (ZipEntry entry in zipInfo) { if (!entry.IsDirectory) zipSize += entry.UncompressedSize; } } finally { if (zipInfo != null) { zipInfo.Dispose(); } } long unzipped = 0; // var zip = ZipFile.Read(zipFile); // try { foreach (ZipEntry entry in zip) { // entry.Extract(destFolder, ExtractExistingFileAction.OverwriteSilently); // if (!entry.IsDirectory) unzipped += entry.UncompressedSize; if (zipSize != 0) { val = Convert.ToInt32(unzipped * 100 / zipSize); // Skip to raise the progress event change when calculated progress // and the current progress value are even if (val == progress) { continue; } // RaiseOnStatusChangedEvent( PreparingSetupFilesMessage, String.Format(PrepareSetupProgressMessage, val), false); // RaiseOnProgressChangedEvent(val, false); } } // Notify client the operation can be cancelled at this time RaiseOnProgressChangedEvent(100); // Log.WriteEnd("Unzipped file"); } finally { if (zip != null) { zip.Dispose(); } } } catch (Exception ex) { if (Utils.IsThreadAbortException(ex)) return; // RaiseOnOperationFailedEvent(ex); } } /// /// Cleans up temporary file if the download process has been cancelled. /// /// Path to the temporary file to cleanup protected virtual void CancelDownload(string tmpFile) { if (File.Exists(tmpFile)) { File.Delete(tmpFile); } } public void AbortOperation() { // Make sure we are in business if (cts != null) { cts.Cancel(); } } } }