X-Git-Url: https://code.grnet.gr/git/pithos-ms-client/blobdiff_plain/2e3aee00fb1040ff2e876396974c92653c023ebb..88e2300a132f903fe892721360f12ec0ebb0824a:/trunk/Pithos.Client.WPF/Shell/ShellViewModel.cs diff --git a/trunk/Pithos.Client.WPF/Shell/ShellViewModel.cs b/trunk/Pithos.Client.WPF/Shell/ShellViewModel.cs index 8f27113..4a2a4c9 100644 --- a/trunk/Pithos.Client.WPF/Shell/ShellViewModel.cs +++ b/trunk/Pithos.Client.WPF/Shell/ShellViewModel.cs @@ -1,911 +1,1274 @@ -#region -/* ----------------------------------------------------------------------- - * - * - * Copyright 2011-2012 GRNET S.A. All rights reserved. - * - * Redistribution and use in source and binary forms, with or - * without modification, are permitted provided that the following - * conditions are met: - * - * 1. Redistributions of source code must retain the above - * copyright notice, this list of conditions and the following - * disclaimer. - * - * 2. 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. - * - * - * THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``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 GRNET S.A 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. - * - * The views and conclusions contained in the software and - * documentation are those of the authors and should not be - * interpreted as representing official policies, either expressed - * or implied, of GRNET S.A. - * - * ----------------------------------------------------------------------- - */ -#endregion -using System.Collections.Concurrent; -using System.Diagnostics; -using System.Diagnostics.Contracts; -using System.IO; -using System.Net; -using System.Reflection; -using System.Runtime.InteropServices; -using System.ServiceModel; -using System.Threading.Tasks; -using System.Windows; -using System.Windows.Controls.Primitives; -using Caliburn.Micro; -using Hardcodet.Wpf.TaskbarNotification; -using Pithos.Client.WPF.Configuration; -using Pithos.Client.WPF.FileProperties; -using Pithos.Client.WPF.Preferences; -using Pithos.Client.WPF.SelectiveSynch; -using Pithos.Client.WPF.Services; -using Pithos.Client.WPF.Shell; -using Pithos.Core; -using Pithos.Core.Agents; -using Pithos.Interfaces; -using System; -using System.Collections.Generic; -using System.Linq; -using Pithos.Network; -using StatusService = Pithos.Client.WPF.Services.StatusService; - -namespace Pithos.Client.WPF { - using System.ComponentModel.Composition; - - - /// - /// The "shell" of the Pithos application displays the taskbar icon, menu and notifications. - /// The shell also hosts the status service called by shell extensions to retrieve file info - /// - /// - /// It is a strange "shell" as its main visible element is an icon instead of a window - /// The shell subscribes to the following events: - /// * Notification: Raised by components that want to notify the user. Usually displayed in a balloon - /// * SelectiveSynchChanges: Notifies that the user made changes to the selective synch folders for an account. Raised by the Selective Synch dialog. Located here because the monitors are here - /// * ShowFilePropertiesEvent: Raised when a shell command requests the display of the file/container properties dialog - /// - //TODO: CODE SMELL Why does the shell handle the SelectiveSynchChanges? - [Export(typeof(IShell))] - public class ShellViewModel : Screen, IStatusNotification, IShell, - IHandle, IHandle, IHandle - { - //The Status Checker provides the current synch state - //TODO: Could we remove the status checker and use events in its place? - private readonly IStatusChecker _statusChecker; - private readonly IEventAggregator _events; - - public PithosSettings Settings { get; private set; } - - - private readonly ConcurrentDictionary _monitors = new ConcurrentDictionary(); - /// - /// Dictionary of account monitors, keyed by account - /// - /// - /// One monitor class is created for each account. The Shell needs access to the monitors to execute start/stop/pause commands, - /// retrieve account and boject info - /// - // TODO: Does the Shell REALLY need access to the monitors? Could we achieve the same results with a better design? - // TODO: The monitors should be internal to Pithos.Core, even though exposing them makes coding of the Object and Container windows easier - public ConcurrentDictionary Monitors - { - get { return _monitors; } - } - - - /// - /// The status service is used by Shell extensions to retrieve file status information - /// - //TODO: CODE SMELL! This is the shell! While hosting in the shell makes executing start/stop commands easier, it is still a smell - private ServiceHost _statusService; - - //Logging in the Pithos client is provided by log4net - private static readonly log4net.ILog Log = log4net.LogManager.GetLogger("Pithos"); - - //Lazily initialized File Version info. This is done once and lazily to avoid blocking the UI - private Lazy _fileVersion; - - /// - /// The Shell depends on MEF to provide implementations for windowManager, events, the status checker service and the settings - /// - /// - /// The PithosSettings class encapsulates the app's settings to abstract their storage mechanism (App settings, a database or registry) - /// - [ImportingConstructor] - public ShellViewModel(IWindowManager windowManager, IEventAggregator events, IStatusChecker statusChecker, PithosSettings settings) - { - try - { - - _windowManager = windowManager; - //CHECK: Caliburn doesn't need explicit command construction - //OpenPithosFolderCommand = new PithosCommand(OpenPithosFolder); - _statusChecker = statusChecker; - //The event subst - _events = events; - _events.Subscribe(this); - - Settings = settings; - - Proxy.SetFromSettings(settings); - - StatusMessage = "In Synch"; - - _fileVersion= new Lazy(() => - { - Assembly assembly = Assembly.GetExecutingAssembly(); - var fileVersion = FileVersionInfo.GetVersionInfo(assembly.Location); - return fileVersion; - }); - _accounts.CollectionChanged += (sender, e) => - { - NotifyOfPropertyChange(() => OpenFolderCaption); - NotifyOfPropertyChange(() => HasAccounts); - }; - - } - catch (Exception exc) - { - Log.Error("Error while starting the ShellViewModel",exc); - throw; - } - } - - - protected override void OnActivate() - { - base.OnActivate(); - - - - StartMonitoring(); - } - - - - private async void StartMonitoring() - { - try - { - var accounts = Settings.Accounts.Select(MonitorAccount); - await TaskEx.WhenAll(accounts); - _statusService = StatusService.Start(); - -/* - foreach (var account in Settings.Accounts) - { - await MonitorAccount(account); - } -*/ - - } - catch (AggregateException exc) - { - exc.Handle(e => - { - Log.Error("Error while starting monitoring", e); - return true; - }); - throw; - } - } - - protected override void OnDeactivate(bool close) - { - base.OnDeactivate(close); - if (close) - { - StatusService.Stop(_statusService); - _statusService = null; - } - } - - public Task MonitorAccount(AccountSettings account) - { - return Task.Factory.StartNew(() => - { - PithosMonitor monitor; - var accountName = account.AccountName; - - if (_monitors.TryGetValue(accountName, out monitor)) - { - //If the account is active - if (account.IsActive) - //Start the monitor. It's OK to start an already started monitor, - //it will just ignore the call - StartMonitor(monitor).Wait(); - else - { - //If the account is inactive - //Stop and remove the monitor - RemoveMonitor(accountName); - } - return; - } - - - //Create a new monitor/ Can't use MEF here, it would return a single instance for all monitors - monitor = new PithosMonitor - { - UserName = accountName, - ApiKey = account.ApiKey, - StatusNotification = this, - RootPath = account.RootPath - }; - //PithosMonitor uses MEF so we need to resolve it - IoC.BuildUp(monitor); - - monitor.AuthenticationUrl = account.ServerUrl; - - _monitors[accountName] = monitor; - - if (account.IsActive) - { - //Don't start a monitor if it doesn't have an account and ApiKey - if (String.IsNullOrWhiteSpace(monitor.UserName) || - String.IsNullOrWhiteSpace(monitor.ApiKey)) - return; - StartMonitor(monitor); - } - }); - } - - - protected override void OnViewLoaded(object view) - { - UpdateStatus(); - var window = (Window)view; - TaskEx.Delay(1000).ContinueWith(t => Execute.OnUIThread(window.Hide)); - base.OnViewLoaded(view); - } - - - #region Status Properties - - private string _statusMessage; - public string StatusMessage - { - get { return _statusMessage; } - set - { - _statusMessage = value; - NotifyOfPropertyChange(() => StatusMessage); - } - } - - private readonly ObservableConcurrentCollection _accounts = new ObservableConcurrentCollection(); - public ObservableConcurrentCollection Accounts - { - get { return _accounts; } - } - - public bool HasAccounts - { - get { return _accounts.Count > 0; } - } - - - public string OpenFolderCaption - { - get - { - return (_accounts.Count == 0) - ? "No Accounts Defined" - : "Open Pithos Folder"; - } - } - - private string _pauseSyncCaption="Pause Synching"; - public string PauseSyncCaption - { - get { return _pauseSyncCaption; } - set - { - _pauseSyncCaption = value; - NotifyOfPropertyChange(() => PauseSyncCaption); - } - } - - private readonly ObservableConcurrentCollection _recentFiles = new ObservableConcurrentCollection(); - public ObservableConcurrentCollection RecentFiles - { - get { return _recentFiles; } - } - - - private string _statusIcon="../Images/Pithos.ico"; - public string StatusIcon - { - get { return _statusIcon; } - set - { - //TODO: Ensure all status icons use the Pithos logo - _statusIcon = value; - NotifyOfPropertyChange(() => StatusIcon); - } - } - - #endregion - - #region Commands - - public void ShowPreferences() - { - Settings.Reload(); - var preferences = new PreferencesViewModel(_windowManager,_events, this,Settings); - _windowManager.ShowDialog(preferences); - - } - - public void AboutPithos() - { - var about = new AboutViewModel(); - _windowManager.ShowWindow(about); - } - - public void SendFeedback() - { - var feedBack = IoC.Get(); - _windowManager.ShowWindow(feedBack); - } - - //public PithosCommand OpenPithosFolderCommand { get; private set; } - - public void OpenPithosFolder() - { - var account = Settings.Accounts.FirstOrDefault(acc => acc.IsActive); - if (account == null) - return; - Process.Start(account.RootPath); - } - - public void OpenPithosFolder(AccountInfo account) - { - Process.Start(account.AccountPath); - } - - -/* - public void GoToSite() - { - var site = Properties.Settings.Default.PithosSite; - Process.Start(site); - } -*/ - - public void GoToSite(AccountInfo account) - { - /*var site = String.Format("{0}/ui/?token={1}&user={2}", - account.SiteUri,account.Token, - account.UserName);*/ - Process.Start(account.SiteUri); - } - - public void ShowFileProperties() - { - var account = Settings.Accounts.First(acc => acc.IsActive); - var dir = new DirectoryInfo(account.RootPath + @"\pithos"); - var files=dir.GetFiles(); - var r=new Random(); - var idx=r.Next(0, files.Length); - ShowFileProperties(files[idx].FullName); - } - - public void ShowFileProperties(string filePath) - { - if (String.IsNullOrWhiteSpace(filePath)) - throw new ArgumentNullException("filePath"); - if (!File.Exists(filePath) && !Directory.Exists(filePath)) - throw new ArgumentException(String.Format("Non existent file {0}",filePath),"filePath"); - Contract.EndContractBlock(); - - var pair=(from monitor in Monitors - where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase) - select monitor).FirstOrDefault(); - var accountMonitor = pair.Value; - - if (accountMonitor == null) - return; - - var infoTask=Task.Factory.StartNew(()=>accountMonitor.GetObjectInfo(filePath)); - - - - var fileProperties = new FilePropertiesViewModel(this, infoTask,filePath); - _windowManager.ShowWindow(fileProperties); - } - - public void ShowContainerProperties() - { - var account = Settings.Accounts.First(acc => acc.IsActive); - var dir = new DirectoryInfo(account.RootPath); - var fullName = (from folder in dir.EnumerateDirectories() - where (folder.Attributes & FileAttributes.Hidden) == 0 - select folder.FullName).First(); - ShowContainerProperties(fullName); - } - - public void ShowContainerProperties(string filePath) - { - if (String.IsNullOrWhiteSpace(filePath)) - throw new ArgumentNullException("filePath"); - if (!Directory.Exists(filePath)) - throw new ArgumentException(String.Format("Non existent file {0}",filePath),"filePath"); - Contract.EndContractBlock(); - - var pair=(from monitor in Monitors - where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase) - select monitor).FirstOrDefault(); - var accountMonitor = pair.Value; - var info = accountMonitor.GetContainerInfo(filePath); - - - - var containerProperties = new ContainerPropertiesViewModel(this, info,filePath); - _windowManager.ShowWindow(containerProperties); - } - - public void SynchNow() - { - var agent = IoC.Get(); - agent.SynchNow(); - } - - public ObjectInfo RefreshObjectInfo(ObjectInfo currentInfo) - { - if (currentInfo==null) - throw new ArgumentNullException("currentInfo"); - Contract.EndContractBlock(); - - var monitor = Monitors[currentInfo.Account]; - var newInfo=monitor.CloudClient.GetObjectInfo(currentInfo.Account, currentInfo.Container, currentInfo.Name); - return newInfo; - } - - public ContainerInfo RefreshContainerInfo(ContainerInfo container) - { - if (container == null) - throw new ArgumentNullException("container"); - Contract.EndContractBlock(); - - var monitor = Monitors[container.Account]; - var newInfo = monitor.CloudClient.GetContainerInfo(container.Account, container.Name); - return newInfo; - } - - - public void ToggleSynching() - { - bool isPaused=false; - foreach (var pair in Monitors) - { - var monitor = pair.Value; - monitor.Pause = !monitor.Pause; - isPaused = monitor.Pause; - } - - PauseSyncCaption = isPaused ? "Resume syncing" : "Pause syncing"; - var iconKey = isPaused? "TraySyncPaused" : "TrayInSynch"; - StatusIcon = String.Format(@"../Images/{0}.ico", iconKey); - } - - public void ExitPithos() - { - foreach (var pair in Monitors) - { - var monitor = pair.Value; - monitor.Stop(); - } - - ((Window)GetView()).Close(); - } - #endregion - - - private readonly Dictionary _iconNames = new List - { - new StatusInfo(PithosStatus.InSynch, "All files up to date", "TrayInSynch"), - new StatusInfo(PithosStatus.Syncing, "Syncing Files", "TraySynching"), - new StatusInfo(PithosStatus.SyncPaused, "Sync Paused", "TraySyncPaused") - }.ToDictionary(s => s.Status); - - readonly IWindowManager _windowManager; - - - /// - /// Updates the visual status indicators of the application depending on status changes, e.g. icon, stat - /// - public void UpdateStatus() - { - var pithosStatus = _statusChecker.GetPithosStatus(); - - if (_iconNames.ContainsKey(pithosStatus)) - { - var info = _iconNames[pithosStatus]; - StatusIcon = String.Format(@"../Images/{0}.ico", info.IconName); - - - - StatusMessage = String.Format("Pithos {0}\r\n{1}", _fileVersion.Value.FileVersion,info.StatusText); - } - - //_events.Publish(new Notification { Title = "Start", Message = "Start Monitoring", Level = TraceLevel.Info}); - } - - - - private Task StartMonitor(PithosMonitor monitor,int retries=0) - { - return Task.Factory.StartNew(() => - { - using (log4net.ThreadContext.Stacks["Monitor"].Push("Start")) - { - try - { - Log.InfoFormat("Start Monitoring {0}", monitor.UserName); - - monitor.Start(); - } - catch (WebException exc) - { - if (AbandonRetry(monitor, retries)) - return; - - HttpStatusCode statusCode =HttpStatusCode.OK; - var response = exc.Response as HttpWebResponse; - if(response!=null) - statusCode = response.StatusCode; - - switch (statusCode) - { - case HttpStatusCode.Unauthorized: - var message = String.Format("API Key Expired for {0}. Starting Renewal", - monitor.UserName); - Log.Error(message, exc); - TryAuthorize(monitor.UserName, retries).Wait(); - break; - case HttpStatusCode.ProxyAuthenticationRequired: - TryAuthenticateProxy(monitor,retries); - break; - default: - TryLater(monitor, exc, retries); - break; - } - } - catch (Exception exc) - { - if (AbandonRetry(monitor, retries)) - return; - - TryLater(monitor,exc,retries); - } - } - }); - } - - private void TryAuthenticateProxy(PithosMonitor monitor,int retries) - { - Execute.OnUIThread(() => - { - var proxyAccount = IoC.Get(); - proxyAccount.Settings = this.Settings; - if (true != _windowManager.ShowDialog(proxyAccount)) - return; - StartMonitor(monitor, retries); - NotifyOfPropertyChange(() => Accounts); - }); - } - - private bool AbandonRetry(PithosMonitor monitor, int retries) - { - if (retries > 1) - { - var message = String.Format("Monitoring of account {0} has failed too many times. Will not retry", - monitor.UserName); - _events.Publish(new Notification - {Title = "Account monitoring failed", Message = message, Level = TraceLevel.Error}); - return true; - } - return false; - } - - - public async Task TryAuthorize(string userName, int retries) - { - _events.Publish(new Notification { Title = "Authorization failed", Message = "Your API Key has probably expired. You will be directed to a page where you can renew it", Level = TraceLevel.Error }); - - try - { - - var credentials = await PithosAccount.RetrieveCredentials(Settings.PithosLoginUrl); - - var account = Settings.Accounts.First(act => act.AccountName == credentials.UserName); - //The server may return credentials for a different account - var monitor = _monitors[account.AccountName]; - account.ApiKey = credentials.Password; - monitor.ApiKey = credentials.Password; - Settings.Save(); - await TaskEx.Delay(10000); - StartMonitor(monitor, retries + 1); - NotifyOfPropertyChange(()=>Accounts); - } - catch (AggregateException exc) - { - string message = String.Format("API Key retrieval for {0} failed", userName); - Log.Error(message, exc.InnerException); - _events.Publish(new Notification { Title = "Authorization failed", Message = message, Level = TraceLevel.Error }); - } - catch (Exception exc) - { - string message = String.Format("API Key retrieval for {0} failed", userName); - Log.Error(message, exc); - _events.Publish(new Notification { Title = "Authorization failed", Message = message, Level = TraceLevel.Error }); - } - - } - - private static bool IsUnauthorized(WebException exc) - { - if (exc==null) - throw new ArgumentNullException("exc"); - Contract.EndContractBlock(); - - var response = exc.Response as HttpWebResponse; - if (response == null) - return false; - return (response.StatusCode == HttpStatusCode.Unauthorized); - } - - private void TryLater(PithosMonitor monitor, Exception exc,int retries) - { - var message = String.Format("An exception occured. Can't start monitoring\nWill retry in 10 seconds"); - Task.Factory.StartNewDelayed(10000, () => StartMonitor(monitor,retries+1)); - _events.Publish(new Notification - {Title = "Error", Message = message, Level = TraceLevel.Error}); - Log.Error(message, exc); - } - - - public void NotifyChange(string status, TraceLevel level=TraceLevel.Info) - { - StatusMessage = status; - - _events.Publish(new Notification { Title = "Pithos", Message = status, Level = level }); - } - - public void NotifyChangedFile(string filePath) - { - var entry = new FileEntry {FullPath=filePath}; - IProducerConsumerCollection files=RecentFiles; - FileEntry popped; - while (files.Count > 5) - files.TryTake(out popped); - files.TryAdd(entry); - } - - public void NotifyAccount(AccountInfo account) - { - if (account== null) - return; - //TODO: What happens to an existing account whose Token has changed? - account.SiteUri= String.Format("{0}/ui/?token={1}&user={2}", - account.SiteUri, Uri.EscapeDataString(account.Token), - Uri.EscapeDataString(account.UserName)); - - if (Accounts.All(item => item.UserName != account.UserName)) - Accounts.TryAdd(account); - - } - - public void NotifyConflicts(IEnumerable conflictFiles, string message) - { - if (conflictFiles == null) - return; - if (!conflictFiles.Any()) - return; - - UpdateStatus(); - //TODO: Create a more specific message. For now, just show a warning - NotifyForFiles(conflictFiles,message,TraceLevel.Warning); - - } - - public void NotifyForFiles(IEnumerable files, string message,TraceLevel level=TraceLevel.Info) - { - if (files == null) - return; - if (!files.Any()) - return; - - StatusMessage = message; - - _events.Publish(new Notification { Title = "Pithos", Message = message, Level = level}); - } - - public void Notify(Notification notification) - { - _events.Publish(notification); - } - - - public void RemoveMonitor(string accountName) - { - if (String.IsNullOrWhiteSpace(accountName)) - return; - - var accountInfo=_accounts.FirstOrDefault(account => account.UserName == accountName); - _accounts.TryRemove(accountInfo); - - PithosMonitor monitor; - if (Monitors.TryRemove(accountName, out monitor)) - { - monitor.Stop(); - } - } - - public void RefreshOverlays() - { - foreach (var pair in Monitors) - { - var monitor = pair.Value; - - var path = monitor.RootPath; - - if (String.IsNullOrWhiteSpace(path)) - continue; - - if (!Directory.Exists(path) && !File.Exists(path)) - continue; - - IntPtr pathPointer = Marshal.StringToCoTaskMemAuto(path); - - try - { - NativeMethods.SHChangeNotify(HChangeNotifyEventID.SHCNE_UPDATEITEM, - HChangeNotifyFlags.SHCNF_PATHW | HChangeNotifyFlags.SHCNF_FLUSHNOWAIT, - pathPointer, IntPtr.Zero); - } - finally - { - Marshal.FreeHGlobal(pathPointer); - } - } - } - - #region Event Handlers - - public void Handle(SelectiveSynchChanges message) - { - var accountName = message.Account.AccountName; - PithosMonitor monitor; - if (_monitors.TryGetValue(accountName, out monitor)) - { - monitor.SetSelectivePaths(message.Uris,message.Added,message.Removed); - - } - - } - - - private bool _pollStarted = false; - - //SMELL: Doing so much work for notifications in the shell is wrong - //The notifications should be moved to their own view/viewmodel pair - //and different templates should be used for different message types - //This will also allow the addition of extra functionality, eg. actions - // - public void Handle(Notification notification) - { - UpdateStatus(); - - if (!Settings.ShowDesktopNotifications) - return; - - if (notification is PollNotification) - { - _pollStarted = true; - return; - } - if (notification is CloudNotification) - { - if (!_pollStarted) - return; - _pollStarted= false; - notification.Title = "Pithos"; - notification.Message = "Start Synchronisation"; - } - - if (String.IsNullOrWhiteSpace(notification.Message) && String.IsNullOrWhiteSpace(notification.Title)) - return; - - BalloonIcon icon; - switch (notification.Level) - { - case TraceLevel.Error: - icon = BalloonIcon.Error; - break; - case TraceLevel.Info: - case TraceLevel.Verbose: - icon = BalloonIcon.Info; - break; - case TraceLevel.Warning: - icon = BalloonIcon.Warning; - break; - default: - icon = BalloonIcon.None; - break; - } - - if (Settings.ShowDesktopNotifications) - { - var tv = (ShellView) GetView(); - var balloon=new PithosBalloon{Title=notification.Title,Message=notification.Message,Icon=icon}; - tv.TaskbarView.ShowCustomBalloon(balloon,PopupAnimation.Fade,4000); -// tv.TaskbarView.ShowBalloonTip(notification.Title, notification.Message, icon); - } - } - #endregion - - public void Handle(ShowFilePropertiesEvent message) - { - if (message == null) - throw new ArgumentNullException("message"); - if (String.IsNullOrWhiteSpace(message.FileName) ) - throw new ArgumentException("message"); - Contract.EndContractBlock(); - - var fileName = message.FileName; - //TODO: Display file properties for non-container folders - if (File.Exists(fileName)) - //Retrieve the full name with exact casing. Pithos names are case sensitive - ShowFileProperties(FileInfoExtensions.GetProperFilePathCapitalization(fileName)); - else if (Directory.Exists(fileName)) - //Retrieve the full name with exact casing. Pithos names are case sensitive - { - var path = FileInfoExtensions.GetProperDirectoryCapitalization(fileName); - if (IsContainer(path)) - ShowContainerProperties(path); - else - ShowFileProperties(path); - } - } - - private bool IsContainer(string path) - { - var matchingFolders = from account in _accounts - from rootFolder in Directory.GetDirectories(account.AccountPath) - where rootFolder.Equals(path, StringComparison.InvariantCultureIgnoreCase) - select rootFolder; - return matchingFolders.Any(); - } - - public FileStatus GetFileStatus(string localFileName) - { - if (String.IsNullOrWhiteSpace(localFileName)) - throw new ArgumentNullException("localFileName"); - Contract.EndContractBlock(); - - var statusKeeper = IoC.Get(); - var status=statusKeeper.GetFileStatus(localFileName); - return status; - } - } -} +#region +/* ----------------------------------------------------------------------- + * + * + * Copyright 2011-2012 GRNET S.A. All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * 1. Redistributions of source code must retain the above + * copyright notice, this list of conditions and the following + * disclaimer. + * + * 2. 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. + * + * + * THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``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 GRNET S.A 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. + * + * The views and conclusions contained in the software and + * documentation are those of the authors and should not be + * interpreted as representing official policies, either expressed + * or implied, of GRNET S.A. + * + * ----------------------------------------------------------------------- + */ +#endregion +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Diagnostics.Contracts; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Reflection; +using System.Runtime.InteropServices; +using System.ServiceModel; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls.Primitives; +using System.Windows.Input; +using AppLimit.NetSparkle; +using Caliburn.Micro; +using Hardcodet.Wpf.TaskbarNotification; +using Pithos.Client.WPF.Configuration; +using Pithos.Client.WPF.FileProperties; +using Pithos.Client.WPF.Preferences; +using Pithos.Client.WPF.SelectiveSynch; +using Pithos.Client.WPF.Services; +using Pithos.Client.WPF.Shell; +using Pithos.Core; +using Pithos.Core.Agents; +using Pithos.Interfaces; +using System; +using System.Collections.Generic; +using System.Linq; +using Pithos.Network; +using Pithos.OFM; +using StatusService = Pithos.Client.WPF.Services.StatusService; + + +namespace Pithos.Client.WPF { + using System.ComponentModel.Composition; + + public class ToggleStatusCommand:ICommand + { + private readonly ShellViewModel _model; + public ToggleStatusCommand(ShellViewModel model) + { + _model = model; + } + public void Execute(object parameter) + { + _model.CurrentSyncStatus(); + } + + public bool CanExecute(object parameter) + { + return true; + } + + public event EventHandler CanExecuteChanged; + } + + + /// + /// The "shell" of the Pithos application displays the taskbar icon, menu and notifications. + /// The shell also hosts the status service called by shell extensions to retrieve file info + /// + /// + /// It is a strange "shell" as its main visible element is an icon instead of a window + /// The shell subscribes to the following events: + /// * Notification: Raised by components that want to notify the user. Usually displayed in a balloon + /// * SelectiveSynchChanges: Notifies that the user made changes to the selective synch folders for an account. Raised by the Selective Synch dialog. Located here because the monitors are here + /// * ShowFilePropertiesEvent: Raised when a shell command requests the display of the file/container properties dialog + /// + //TODO: CODE SMELL Why does the shell handle the SelectiveSynchChanges? + [Export(typeof(IShell)), Export(typeof(ShellViewModel)),Export(typeof(IStatusNotification))] + public class ShellViewModel : Screen, IStatusNotification, IShell, + IHandle, IHandle, IHandle + { + + private readonly IEventAggregator _events; + + + public PithosSettings Settings { get; private set; } + + + private readonly ConcurrentDictionary _monitors = new ConcurrentDictionary(); + /// + /// Dictionary of account monitors, keyed by account + /// + /// + /// One monitor class is created for each account. The Shell needs access to the monitors to execute start/stop/pause commands, + /// retrieve account and boject info + /// + // TODO: Does the Shell REALLY need access to the monitors? Could we achieve the same results with a better design? + // TODO: The monitors should be internal to Pithos.Core, even though exposing them makes coding of the Object and Container windows easier + public ConcurrentDictionary Monitors + { + get { return _monitors; } + } + + + /// + /// The status service is used by Shell extensions to retrieve file status information + /// + //TODO: CODE SMELL! This is the shell! While hosting in the shell makes executing start/stop commands easier, it is still a smell + private ServiceHost _statusService; + + //Logging in the Pithos client is provided by log4net + private static readonly log4net.ILog Log = log4net.LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + + #pragma warning disable 649 + [Import] + private PollAgent _pollAgent; + + [Import] + private NetworkAgent _networkAgent; + + [Import] + public Selectives Selectives { get; set; } + + #pragma warning restore 649 + + public ToggleStatusCommand ToggleMiniStatusCommand { get; set; } + + private MiniStatusViewModel _miniStatus; + + [Import] + public MiniStatusViewModel MiniStatus + { + get { return _miniStatus; } + set + { + _miniStatus = value; + _miniStatus.Shell = this; + _miniStatus.Deactivated += (sender, arg) => + { + _statusVisible = false; + NotifyOfPropertyChange(()=>MiniStatusCaption); + }; + } + } + + /// + /// The Shell depends on MEF to provide implementations for windowManager, events, the status checker service and the settings + /// + /// + /// The PithosSettings class encapsulates the app's settings to abstract their storage mechanism (App settings, a database or registry) + /// + [ImportingConstructor] + public ShellViewModel(IWindowManager windowManager, IEventAggregator events, PithosSettings settings/*,PollAgent pollAgent,NetworkAgent networkAgent*/) + { + try + { + + _windowManager = windowManager; + //CHECK: Caliburn doesn't need explicit command construction + //CurrentSyncStatusCommand = new PithosCommand(OpenPithosFolder); + //The event subst + _events = events; + _events.Subscribe(this); + +/* + _pollAgent = pollAgent; + _networkAgent = networkAgent; +*/ + Settings = settings; + + Proxy.SetFromSettings(settings); + + StatusMessage = Settings.Accounts.Count==0 + ? "No Accounts added\r\nPlease add an account" + : "Starting"; + + _accounts.CollectionChanged += (sender, e) => + { + NotifyOfPropertyChange(() => OpenFolderCaption); + NotifyOfPropertyChange(() => HasAccounts); + }; + + SetVersionMessage(); + + ToggleMiniStatusCommand=new ToggleStatusCommand(this); + } + catch (Exception exc) + { + Log.Error("Error while starting the ShellViewModel",exc); + throw; + } + + } + + private void SetVersionMessage() + { + Assembly assembly = Assembly.GetExecutingAssembly(); + var fileVersion = FileVersionInfo.GetVersionInfo(assembly.Location); + VersionMessage = String.Format("Pithos+ {0}", fileVersion.FileVersion); + } + + + public void openOFM() + { + var fileManager = IoC.Get(); + fileManager.Accounts = Settings.Accounts; + fileManager.CurrentAccount = fileManager.Accounts.First();// Settings.Accounts.First(); + _windowManager.ShowWindow(fileManager); + //var ofm = new OFM.GUI.OFM(); + //ofm.Show(); + } + + public void CurrentSyncStatus() + { + if (Accounts.Count == 0) + { + ShowPreferences("AccountTab"); + } + else + { + if (!_statusVisible) + { + _windowManager.ShowWindow(MiniStatus); + _statusVisible = true; + } + else + { + if (MiniStatus.IsActive) + MiniStatus.TryClose(); + _statusVisible = false; + } + + NotifyOfPropertyChange(() => MiniStatusCaption); + } + } + + protected override void OnActivate() + { + base.OnActivate(); + + InitializeSparkle(); + + //Must delay opening the upgrade window + //to avoid Windows Messages sent by the TaskbarIcon + TaskEx.Delay(5000).ContinueWith(_=> + Execute.OnUIThread(()=> _sparkle.StartLoop(true,Settings.UpdateForceCheck,Settings.UpdateCheckInterval))); + + + StartMonitoring(); + } + + + private void OnCheckFinished(object sender, bool updaterequired) + { + + Log.InfoFormat("Upgrade check finished. Need Upgrade: {0}", updaterequired); + if (_manualUpgradeCheck) + { + _manualUpgradeCheck = false; + if (!updaterequired) + //Sparkle raises events on a background thread + Execute.OnUIThread(()=> + ShowBalloonFor(new Notification{Title="Pithos+ is up to date",Message="You have the latest Pithos+ version. No update is required"})); + } + } + + private void OnUpgradeDetected(object sender, UpdateDetectedEventArgs e) + { + Log.InfoFormat("Update detected {0}",e.LatestVersion); + } + + public void CheckForUpgrade() + { + ShowBalloonFor(new Notification{Title="Checking for upgrades",Message="Contacting the server to retrieve the latest Pithos+ version."}); + _sparkle.StopLoop(); + _sparkle.updateDetected -= OnUpgradeDetected; + _sparkle.checkLoopFinished -= OnCheckFinished; + _sparkle.Dispose(); + + _manualUpgradeCheck = true; + InitializeSparkle(); + _sparkle.StartLoop(true,true,Settings.UpdateCheckInterval); + } + + private void InitializeSparkle() + { + _sparkle = new Sparkle(Settings.UpdateUrl); + _sparkle.updateDetected += OnUpgradeDetected; + _sparkle.checkLoopFinished += OnCheckFinished; + _sparkle.ShowDiagnosticWindow = Settings.UpdateDiagnostics; + } + + private async void StartMonitoring() + { + try + { + if (Settings.IgnoreCertificateErrors) + { + ServicePointManager.ServerCertificateValidationCallback = (sender, certificate, chain, errors) => true; + } + + var accounts = Settings.Accounts.Select(MonitorAccount); + await TaskEx.WhenAll(accounts).ConfigureAwait(false); + _statusService = StatusService.Start(); + + } + catch (AggregateException exc) + { + exc.Handle(e => + { + Log.Error("Error while starting monitoring", e); + return true; + }); + throw; + } + } + + protected override void OnDeactivate(bool close) + { + base.OnDeactivate(close); + if (close) + { + StatusService.Stop(_statusService); + _statusService = null; + } + } + + public Task MonitorAccount(AccountSettings account) + { + return Task.Factory.StartNew(() => + { + PithosMonitor monitor; + var accountName = account.AccountName; + + MigrateFolders(account); + + Selectives.SetIsSelectiveEnabled(account.AccountKey, account.SelectiveSyncEnabled); + + if (Monitors.TryGetValue(account.AccountKey, out monitor)) + { + //If the account is active + if (account.IsActive) + { + //The Api Key may have changed throuth the Preferences dialog + monitor.ApiKey = account.ApiKey; + Debug.Assert(monitor.StatusNotification == this,"An existing monitor should already have a StatusNotification service object"); + monitor.RootPath = account.RootPath; + //Start the monitor. It's OK to start an already started monitor, + //it will just ignore the call + StartMonitor(monitor).Wait(); + } + else + { + //If the account is inactive + //Stop and remove the monitor + RemoveMonitor(account.ServerUrl,accountName); + } + return; + } + + + //Create a new monitor/ Can't use MEF here, it would return a single instance for all monitors + monitor = new PithosMonitor + { + UserName = accountName, + ApiKey = account.ApiKey, + StatusNotification = this, + RootPath = account.RootPath + }; + //PithosMonitor uses MEF so we need to resolve it + IoC.BuildUp(monitor); + + monitor.AuthenticationUrl = account.ServerUrl; + + Monitors[account.AccountKey] = monitor; + + if (!Directory.Exists(account.RootPath)) + { + account.IsActive = false; + Settings.Save(); + Notify(new Notification + { + Level = TraceLevel.Error, + Title = "Missing account folder", + Message = String.Format("Can't find the root folder for account {0} at {1}. The account was deactivated.\r" + + "If the account's files were stored in a removable disk, please connect it and reactivate the account", account.AccountName, account.RootPath) + }); + } + + + if (account.IsActive) + { + //Don't start a monitor if it doesn't have an account and ApiKey + if (String.IsNullOrWhiteSpace(monitor.UserName) || + String.IsNullOrWhiteSpace(monitor.ApiKey)) + return; + StartMonitor(monitor); + } + }); + } + + private void MigrateFolders(AccountSettings account) + { + var oldOthersFolder=Path.Combine(account.RootPath, FolderConstants.OldOthersFolder); + var newOthersFolder = Path.Combine(account.RootPath, FolderConstants.OthersFolder); + var oldFolder = new DirectoryInfo(oldOthersFolder); + var newFolder = new DirectoryInfo(newOthersFolder); + + if (oldFolder.Exists && !newFolder.Exists) + { + oldFolder.MoveTo(newOthersFolder); + } + } + + + protected override void OnViewLoaded(object view) + { + UpdateStatus(); + var window = (Window)view; + TaskEx.Delay(1000).ContinueWith(t => Execute.OnUIThread(window.Hide)); + base.OnViewLoaded(view); + } + + + #region Status Properties + + private string _statusMessage; + public string StatusMessage + { + get { return _statusMessage; } + set + { + _statusMessage = value; + NotifyOfPropertyChange(() => StatusMessage); + NotifyOfPropertyChange(() => TooltipMessage); + } + } + + private double _ProgressBar; + public double ProgressBar + { + get { return _ProgressBar; } + set + { + _ProgressBar = value; + NotifyOfPropertyChange(() => ProgressBar); + } + } + + + public string VersionMessage { get; set; } + + public string TooltipMessage + { + get + { + return String.Format("{0}\r\n{1}",VersionMessage,StatusMessage); + } + } + + public string TooltipMiniStatus + { + get + { + return String.Format("{0}\r\n{1}", "Status Window", "Enable / Disable the status window"); + } + } + + /*public string ToggleStatusWindowMessage + { + get + { + return String.Format("{0}" + Environment.NewLine + "{1} Toggle Mini Status"); + } + }*/ + + private readonly ObservableConcurrentCollection _accounts = new ObservableConcurrentCollection(); + public ObservableConcurrentCollection Accounts + { + get { return _accounts; } + } + + public bool HasAccounts + { + get { return _accounts.Count > 0; } + } + + + public string OpenFolderCaption + { + get + { + return (_accounts.Count == 0) + ? "No Accounts Defined" + : "Open Pithos Folder"; + } + } + + private string _pauseSyncCaption="Pause Synching"; + public string PauseSyncCaption + { + get { return _pauseSyncCaption; } + set + { + _pauseSyncCaption = value; + NotifyOfPropertyChange(() => PauseSyncCaption); + } + } + + private readonly ObservableConcurrentCollection _recentFiles = new ObservableConcurrentCollection(); + public ObservableConcurrentCollection RecentFiles + { + get { return _recentFiles; } + } + + + private string _statusIcon="../Images/Pithos.ico"; + public string StatusIcon + { + get { return _statusIcon; } + set + { + //TODO: Ensure all status icons use the Pithos logo + _statusIcon = value; + NotifyOfPropertyChange(() => StatusIcon); + } + } + + #endregion + + #region Commands + + public void CancelCurrentOperation() + { + _pollAgent.CancelCurrentOperation(); + } + + public void ShowPreferences() + { + ShowPreferences(null); + } + + public void ShowPreferences(string currentTab) + { + //Settings.Reload(); + + var preferences = IoC.Get();//??new PreferencesViewModel(_windowManager, _events, this, Settings,currentTab); + if (!String.IsNullOrWhiteSpace(currentTab)) + preferences.SelectedTab = currentTab; + if (!preferences.IsActive) + _windowManager.ShowWindow(preferences); + var view = (Window)preferences.GetView(); + view.NullSafe(v=>v.Activate()); + } + + public void AboutPithos() + { + var about = IoC.Get(); + about.LatestVersion=_sparkle.LatestVersion; + _windowManager.ShowWindow(about); + } + + public void SendFeedback() + { + var feedBack = IoC.Get(); + _windowManager.ShowWindow(feedBack); + } + + //public PithosCommand OpenPithosFolderCommand { get; private set; } + + public void OpenPithosFolder() + { + var account = Settings.Accounts.FirstOrDefault(acc => acc.IsActive); + if (account == null) + return; + Process.Start(account.RootPath); + } + + public void OpenPithosFolder(AccountInfo account) + { + Process.Start(account.AccountPath); + } + + + + public void GoToSite() + { + var site = Properties.Settings.Default.ProductionServer; + Process.Start(site); + } + + + public void GoToSite(AccountInfo account) + { + var uri = account.SiteUri.Replace("http://","https://"); + Process.Start(uri); + } + + private bool _statusVisible; + + public string MiniStatusCaption + { + get + { + return _statusVisible ? "Hide Status Window" : "Show Status Window"; + } + } + + public bool HasConflicts + { + get { return true; } + } + public void ShowConflicts() + { + _windowManager.ShowWindow(IoC.Get()); + } + + /// + /// Open an explorer window to the target path's directory + /// and select the file + /// + /// + public void GoToFile(FileEntry entry) + { + var fullPath = entry.FullPath; + if (!File.Exists(fullPath) && !Directory.Exists(fullPath)) + return; + Process.Start("explorer.exe","/select, " + fullPath); + } + + public void OpenLogPath() + { + var pithosDataPath = PithosSettings.PithosDataPath; + + Process.Start(pithosDataPath); + } + + public void ShowFileProperties() + { + var account = Settings.Accounts.First(acc => acc.IsActive); + var dir = new DirectoryInfo(account.RootPath + @"\pithos"); + var files=dir.GetFiles(); + var r=new Random(); + var idx=r.Next(0, files.Length); + ShowFileProperties(files[idx].FullName); + } + + public void ShowFileProperties(string filePath) + { + if (String.IsNullOrWhiteSpace(filePath)) + throw new ArgumentNullException("filePath"); + if (!File.Exists(filePath) && !Directory.Exists(filePath)) + throw new ArgumentException(String.Format("Non existent file {0}",filePath),"filePath"); + Contract.EndContractBlock(); + + var pair=(from monitor in Monitors + where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase) + select monitor).FirstOrDefault(); + var accountMonitor = pair.Value; + + if (accountMonitor == null) + return; + + var infoTask=accountMonitor.GetObjectInfo(filePath); + + + + var fileProperties = new FilePropertiesViewModel(this, infoTask,filePath); + _windowManager.ShowWindow(fileProperties); + } + + public void ShowContainerProperties() + { + var account = Settings.Accounts.First(acc => acc.IsActive); + var dir = new DirectoryInfo(account.RootPath); + var fullName = (from folder in dir.EnumerateDirectories() + where (folder.Attributes & FileAttributes.Hidden) == 0 + select folder.FullName).First(); + ShowContainerProperties(fullName); + } + + public void ShowContainerProperties(string filePath) + { + if (String.IsNullOrWhiteSpace(filePath)) + throw new ArgumentNullException("filePath"); + if (!Directory.Exists(filePath)) + throw new ArgumentException(String.Format("Non existent file {0}",filePath),"filePath"); + Contract.EndContractBlock(); + + var pair=(from monitor in Monitors + where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase) + select monitor).FirstOrDefault(); + var accountMonitor = pair.Value; + var info = accountMonitor.GetContainerInfo(filePath); + + + + var containerProperties = new ContainerPropertiesViewModel(this, info,filePath); + _windowManager.ShowWindow(containerProperties); + } + + public void SynchNow() + { + _pollAgent.SynchNow(); + } + + public async Task RefreshObjectInfo(ObjectInfo currentInfo) + { + if (currentInfo==null) + throw new ArgumentNullException("currentInfo"); + Contract.EndContractBlock(); + var monitor = Monitors[currentInfo.AccountKey]; + var newInfo=await monitor.CloudClient.GetObjectInfo(currentInfo.Account, currentInfo.Container, currentInfo.Name).ConfigureAwait(false); + return newInfo; + } + + public async Task RefreshContainerInfo(ContainerInfo container) + { + if (container == null) + throw new ArgumentNullException("container"); + Contract.EndContractBlock(); + + var monitor = Monitors[container.AccountKey]; + var newInfo = await monitor.CloudClient.GetContainerInfo(container.Account, container.Name).ConfigureAwait(false); + return newInfo; + } + + private bool _isPaused; + public bool IsPaused + { + get { return _isPaused; } + set + { + _isPaused = value; + PauseSyncCaption = IsPaused ? "Resume syncing" : "Pause syncing"; + var iconKey = IsPaused ? "TraySyncPaused" : "TrayInSynch"; + StatusIcon = String.Format(@"../Images/{0}.ico", iconKey); + + NotifyOfPropertyChange(() => IsPaused); + } + } + + public void ToggleSynching() + { + IsPaused=!IsPaused; + foreach (var monitor in Monitors.Values) + { + monitor.Pause = IsPaused ; + } + _pollAgent.Pause = IsPaused; + _networkAgent.Pause = IsPaused; + + + } + + public void ExitPithos() + { + try + { + + foreach (var monitor in Monitors.Select(pair => pair.Value)) + { + monitor.Stop(); + } + + var view = GetView() as Window; + if (view != null) + view.Close(); + } + catch (Exception exc) + { + Log.Info("Exception while exiting", exc); + } + finally + { + Application.Current.Shutdown(); + } + } + + #endregion + + + private readonly Dictionary _iconNames = new List + { + new StatusInfo(PithosStatus.InSynch, "All files up to date", "TrayInSynch"), + new StatusInfo(PithosStatus.PollSyncing, "Polling Files", "TraySynching"), + new StatusInfo(PithosStatus.LocalSyncing, "Syncing Files", "TraySynching"), + new StatusInfo(PithosStatus.SyncPaused, "Sync Paused", "TraySyncPaused") + }.ToDictionary(s => s.Status); + + readonly IWindowManager _windowManager; + + //private int _syncCount=0; + + + private PithosStatus _pithosStatus = PithosStatus.Disconnected; + + public void SetPithosStatus(PithosStatus status) + { + if (_pithosStatus == PithosStatus.LocalSyncing && status == PithosStatus.PollComplete) + return; + if (_pithosStatus == PithosStatus.PollSyncing && status == PithosStatus.LocalComplete) + return; + if (status == PithosStatus.LocalComplete || status == PithosStatus.PollComplete) + _pithosStatus = PithosStatus.InSynch; + else + _pithosStatus = status; + UpdateStatus(); + } + + public void SetPithosStatus(PithosStatus status,string message) + { + StatusMessage = message; + SetPithosStatus(status); + } + + /* public Notifier GetNotifier(Notification startNotification, Notification endNotification) + { + return new Notifier(this, startNotification, endNotification); + }*/ + + public Notifier GetNotifier(string startMessage, string endMessage, bool isActive=true,params object[] args) + { + return isActive?new Notifier(this, + new StatusNotification(String.Format(startMessage,args)), + new StatusNotification(String.Format(endMessage,args))) + :new Notifier(this,(Notification) null,null); + } + + + /// + /// Updates the visual status indicators of the application depending on status changes, e.g. icon, stat + /// + public void UpdateStatus() + { + + if (_iconNames.ContainsKey(_pithosStatus)) + { + var info = _iconNames[_pithosStatus]; + StatusIcon = String.Format(@"../Images/{0}.ico", info.IconName); + } + + if (_pithosStatus == PithosStatus.InSynch) + StatusMessage = "All files up to date"; + } + + + + public async Task StartMonitor(PithosMonitor monitor,int retries=0) + { + using (log4net.ThreadContext.Stacks["Monitor"].Push("Start")) + { + try + { + Log.InfoFormat("Start Monitoring {0}", monitor.UserName); + + await monitor.Start(); + } + catch (HttpRequestWithStatusException exc) + { + if (AbandonRetry(monitor, retries)) + return; + + //HttpStatusCode statusCode = HttpStatusCode.OK; + //var response = as HttpWebResponse; + //if (response != null) + var statusCode = exc.StatusCode;//response.StatusCode; + + switch (statusCode) + { + case HttpStatusCode.Unauthorized: + var message = String.Format("API Key Expired for {0}. Starting Renewal", + monitor.UserName); + Log.Error(message, exc); + var account = + Settings.Accounts.Find( + acc => acc.AccountKey == new Uri(monitor.AuthenticationUrl).Combine(monitor.UserName)); + account.IsExpired = true; + Settings.Save(); + Notify(new ExpirationNotification(account)); + //TryAuthorize(monitor.UserName, retries).Wait(); + break; + case HttpStatusCode.ProxyAuthenticationRequired: + TryAuthenticateProxy(monitor, retries); + break; + default: + TryLater(monitor, exc, retries); + break; + } + } + catch (Exception exc) + { + if (AbandonRetry(monitor, retries)) + return; + + TryLater(monitor, exc, retries); + } + } + + } + + private void TryAuthenticateProxy(PithosMonitor monitor,int retries) + { + Execute.OnUIThread(() => + { + var proxyAccount = IoC.Get(); + proxyAccount.Settings = Settings; + if (true != _windowManager.ShowDialog(proxyAccount)) + return; + StartMonitor(monitor, retries); + NotifyOfPropertyChange(() => Accounts); + }); + } + + private bool AbandonRetry(PithosMonitor monitor, int retries) + { + if (retries > 3) + { + var message = String.Format("Monitoring of account {0} has failed too many times. Will not retry", + monitor.UserName); + _events.Publish(new Notification + {Title = "Account monitoring failed", Message = message, Level = TraceLevel.Error}); + return true; + } + return false; + } + + + private void TryLater(PithosMonitor monitor, Exception exc,int retries) + { + var message = String.Format("An exception occured. Can't start monitoring\nWill retry in 10 seconds"); + Task.Factory.StartNewDelayed(10000, () => StartMonitor(monitor,retries+1)); + _events.Publish(new Notification + {Title = "Error", Message = message, Level = TraceLevel.Error}); + Log.Error(message, exc); + } + + + public void NotifyChange(string status, TraceLevel level=TraceLevel.Info) + { + StatusMessage = status; + + _events.Publish(new Notification { Title = "Pithos+", Message = status, Level = level }); + } + + public void NotifyChangedFile(string filePath) + { + if (RecentFiles.Any(e => e.FullPath == filePath)) + return; + + IProducerConsumerCollection files=RecentFiles; + FileEntry popped; + while (files.Count > 5) + files.TryTake(out popped); + var entry = new FileEntry { FullPath = filePath }; + files.TryAdd(entry); + } + + public void NotifyAccount(AccountInfo account) + { + if (account== null) + return; + //TODO: What happens to an existing account whose Token has changed? + account.SiteUri= String.Format("{0}/ui/?token={1}&user={2}", + account.SiteUri, Uri.EscapeDataString(account.Token), + Uri.EscapeDataString(account.UserName)); + + if (!Accounts.Any(item => item.UserName == account.UserName && item.SiteUri == account.SiteUri)) + Accounts.TryAdd(account); + + } + + public void NotifyConflicts(IEnumerable conflictFiles, string message) + { + if (conflictFiles == null) + return; + //Convert to list to avoid multiple iterations + var files = conflictFiles.ToList(); + if (files.Count==0) + return; + + UpdateStatus(); + //TODO: Create a more specific message. For now, just show a warning + NotifyForFiles(files,message,TraceLevel.Warning); + + } + + public void NotifyForFiles(IEnumerable files, string message,TraceLevel level=TraceLevel.Info) + { + if (files == null) + return; + if (!files.Any()) + return; + + StatusMessage = message; + + _events.Publish(new Notification { Title = "Pithos+", Message = message, Level = level}); + } + + public void Notify(Notification notification) + { + TaskEx.Run(()=> _events.Publish(notification)); + } + + + public void RemoveMonitor(string serverUrl,string accountName) + { + if (String.IsNullOrWhiteSpace(accountName)) + return; + + var accountInfo=_accounts.FirstOrDefault(account => account.UserName == accountName && account.StorageUri.ToString().StartsWith(serverUrl)); + if (accountInfo != null) + { + _accounts.TryRemove(accountInfo); + _pollAgent.RemoveAccount(accountInfo); + } + + var accountKey = new Uri(serverUrl).Combine(accountName); + PithosMonitor monitor; + if (Monitors.TryRemove(accountKey, out monitor)) + { + monitor.Stop(); + //TODO: Also remove any pending actions for this account + //from the network queue + } + } + + public void RefreshOverlays() + { + foreach (var pair in Monitors) + { + var monitor = pair.Value; + + var path = monitor.RootPath; + + if (String.IsNullOrWhiteSpace(path)) + continue; + + if (!Directory.Exists(path) && !File.Exists(path)) + continue; + + IntPtr pathPointer = Marshal.StringToCoTaskMemAuto(path); + + try + { + NativeMethods.SHChangeNotify(HChangeNotifyEventID.SHCNE_UPDATEITEM, + HChangeNotifyFlags.SHCNF_PATHW | HChangeNotifyFlags.SHCNF_FLUSHNOWAIT, + pathPointer, IntPtr.Zero); + } + finally + { + Marshal.FreeHGlobal(pathPointer); + } + } + } + + #region Event Handlers + + public void Handle(SelectiveSynchChanges message) + { + TaskEx.Run(() => + { + PithosMonitor monitor; + if (Monitors.TryGetValue(message.Account.AccountKey, out monitor)) + { + Selectives.SetIsSelectiveEnabled(message.Account.AccountKey, message.Enabled); + monitor.SetSelectivePaths(message.Uris, message.Added, message.Removed); + } + + var account = Accounts.FirstOrDefault(acc => acc.AccountKey == message.Account.AccountKey); + if (account != null) + { + var added=monitor.UrisToFilePaths(message.Added); + _pollAgent.SynchNow(added); + } + }); + + } + + + private bool _pollStarted; + private Sparkle _sparkle; + private bool _manualUpgradeCheck; + + //SMELL: Doing so much work for notifications in the shell is wrong + //The notifications should be moved to their own view/viewmodel pair + //and different templates should be used for different message types + //This will also allow the addition of extra functionality, eg. actions + // + public void Handle(Notification notification) + { + UpdateStatus(); + + if (!Settings.ShowDesktopNotifications) + return; + + if (notification is PollNotification) + { + _pollStarted = true; + return; + } + if (notification is CloudNotification) + { + if (!_pollStarted) + return; + _pollStarted= false; + notification.Title = "Pithos+"; + notification.Message = "Start Synchronisation"; + } + + var deleteNotification = notification as CloudDeleteNotification; + if (deleteNotification != null) + { + StatusMessage = String.Format("Deleted {0}", deleteNotification.Data.Name); + return; + } + + var progress = notification as ProgressNotification; + + + if (progress != null) + { + double percentage = (progress.TotalBlocks == progress.Block) ? 1 + :(progress.Block + progress.BlockPercentage / 100.0) / (double)progress.TotalBlocks; + StatusMessage = String.Format("{0} {1:p2} of {2} - {3}", + progress.Action, + percentage, + progress.FileSize.ToByteSize(), + progress.FileName); + ProgressBar = percentage; + return; + } + + var info = notification as StatusNotification; + if (info != null) + { + StatusMessage = info.Title; + return; + } + if (String.IsNullOrWhiteSpace(notification.Message) && String.IsNullOrWhiteSpace(notification.Title)) + return; + + if (notification.Level <= TraceLevel.Warning) + ShowBalloonFor(notification); + } + + private void ShowBalloonFor(Notification notification) + { + Contract.Requires(notification!=null); + + if (!Settings.ShowDesktopNotifications) + return; + + BalloonIcon icon; + switch (notification.Level) + { + case TraceLevel.Verbose: + return; + case TraceLevel.Info: + icon = BalloonIcon.Info; + break; + case TraceLevel.Error: + icon = BalloonIcon.Error; + break; + case TraceLevel.Warning: + icon = BalloonIcon.Warning; + break; + default: + return; + } + + var tv = (ShellView) GetView(); + System.Action clickAction = null; + if (notification is ExpirationNotification) + { + clickAction = () => ShowPreferences("AccountTab"); + } + var balloon = new PithosBalloon + { + Title = notification.Title, + Message = notification.Message, + Icon = icon, + ClickAction = clickAction + }; + tv.TaskbarView.ShowCustomBalloon(balloon, PopupAnimation.Fade, 4000); + } + + #endregion + + public void Handle(ShowFilePropertiesEvent message) + { + if (message == null) + throw new ArgumentNullException("message"); + if (String.IsNullOrWhiteSpace(message.FileName) ) + throw new ArgumentException("message"); + Contract.EndContractBlock(); + + var fileName = message.FileName; + //TODO: Display file properties for non-container folders + if (File.Exists(fileName)) + //Retrieve the full name with exact casing. Pithos names are case sensitive + ShowFileProperties(FileInfoExtensions.GetProperFilePathCapitalization(fileName)); + else if (Directory.Exists(fileName)) + //Retrieve the full name with exact casing. Pithos names are case sensitive + { + var path = FileInfoExtensions.GetProperDirectoryCapitalization(fileName); + if (IsContainer(path)) + ShowContainerProperties(path); + else + ShowFileProperties(path); + } + } + + private bool IsContainer(string path) + { + var matchingFolders = from account in _accounts + from rootFolder in Directory.GetDirectories(account.AccountPath) + where rootFolder.Equals(path, StringComparison.InvariantCultureIgnoreCase) + select rootFolder; + return matchingFolders.Any(); + } + + public FileStatus GetFileStatus(string localFileName) + { + if (String.IsNullOrWhiteSpace(localFileName)) + throw new ArgumentNullException("localFileName"); + Contract.EndContractBlock(); + + var statusKeeper = IoC.Get(); + var status=statusKeeper.GetFileStatus(localFileName); + return status; + } + + public void RemoveAccountFromDatabase(AccountSettings account) + { + var statusKeeper = IoC.Get(); + statusKeeper.ClearFolderStatus(account.RootPath); + } + } +}