X-Git-Url: https://code.grnet.gr/git/pithos-ms-client/blobdiff_plain/2dc6f7651023305cfc0660752b642e47eac9a578..a026e07af954df4bc312c164a7d606c9b5f3bdf9:/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 1660224..897d7d7 100644 --- a/trunk/Pithos.Client.WPF/Shell/ShellViewModel.cs +++ b/trunk/Pithos.Client.WPF/Shell/ShellViewModel.cs @@ -1,4 +1,45 @@ -using System.Collections.Concurrent; +#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; @@ -6,9 +47,12 @@ using System.Net; 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; @@ -29,7 +73,27 @@ 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 @@ -42,19 +106,17 @@ namespace Pithos.Client.WPF { /// * 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(IShell)), Export(typeof(ShellViewModel)),Export(typeof(IStatusNotification))] 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(); + private readonly ConcurrentDictionary _monitors = new ConcurrentDictionary(); /// /// Dictionary of account monitors, keyed by account /// @@ -64,44 +126,81 @@ namespace Pithos.Client.WPF { /// // 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 + 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; + /// + /// 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"); + private static readonly log4net.ILog Log = log4net.LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); - /// + [Import] + private PollAgent _pollAgent; + + [Import] + private NetworkAgent _networkAgent; + + [Import] + public Selectives Selectives { get; set; } + + + 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, IStatusChecker statusChecker, PithosSettings settings) + public ShellViewModel(IWindowManager windowManager, IEventAggregator events, PithosSettings settings/*,PollAgent pollAgent,NetworkAgent networkAgent*/) { try { _windowManager = windowManager; //CHECK: Caliburn doesn't need explicit command construction - //OpenPithosFolderCommand = new PithosCommand(OpenPithosFolder); - _statusChecker = statusChecker; + //CurrentSyncStatusCommand = new PithosCommand(OpenPithosFolder); //The event subst _events = events; _events.Subscribe(this); +/* + _pollAgent = pollAgent; + _networkAgent = networkAgent; +*/ Settings = settings; - StatusMessage = "In Synch"; + Proxy.SetFromSettings(settings); + + StatusMessage = Settings.Accounts.Count==0 + ? "No Accounts added\r\nPlease add an account" + : "Starting"; _accounts.CollectionChanged += (sender, e) => { @@ -109,38 +208,118 @@ namespace Pithos.Client.WPF { 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 CurrentSyncStatus() + { + if (Accounts.Count == 0) + { + ShowPreferences("AccountTab"); + } + else + { + if (!_statusVisible) + { + _windowManager.ShowWindow(MiniStatus); + _statusVisible = true; + } + else + { + if (MiniStatus.IsActive) + MiniStatus.TryClose(); + _statusVisible = false; + } - protected override void OnActivate() + 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 async void 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); + await TaskEx.WhenAll(accounts).ConfigureAwait(false); _statusService = StatusService.Start(); -/* - foreach (var account in Settings.Accounts) - { - await MonitorAccount(account); - } -*/ - } catch (AggregateException exc) { @@ -170,22 +349,33 @@ namespace Pithos.Client.WPF { PithosMonitor monitor; var accountName = account.AccountName; - if (_monitors.TryGetValue(accountName, out monitor)) + MigrateFolders(account); + + Selectives.SetIsSelectiveEnabled(account.AccountKey, account.SelectiveSyncEnabled); + + if (Monitors.TryGetValue(account.AccountKey, 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); - } + 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 { @@ -197,9 +387,9 @@ namespace Pithos.Client.WPF { //PithosMonitor uses MEF so we need to resolve it IoC.BuildUp(monitor); - monitor.AuthenticationUrl = account.ServerUrl; + monitor.AuthenticationUrl = account.ServerUrl; - _monitors[accountName] = monitor; + Monitors[account.AccountKey] = monitor; if (account.IsActive) { @@ -212,8 +402,21 @@ namespace Pithos.Client.WPF { }); } + 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) + + protected override void OnViewLoaded(object view) { UpdateStatus(); var window = (Window)view; @@ -232,10 +435,37 @@ namespace Pithos.Client.WPF { { _statusMessage = value; NotifyOfPropertyChange(() => StatusMessage); + NotifyOfPropertyChange(() => TooltipMessage); } } - private readonly ObservableConcurrentCollection _accounts = new ObservableConcurrentCollection(); + 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; } @@ -281,7 +511,7 @@ namespace Pithos.Client.WPF { get { return _statusIcon; } set { - //TODO: Ensure all status icons use the Pithos logo + //TODO: Ensure all status icons use the Pithos logo _statusIcon = value; NotifyOfPropertyChange(() => StatusIcon); } @@ -291,17 +521,33 @@ namespace Pithos.Client.WPF { #region Commands - public void ShowPreferences() + public void CancelCurrentOperation() + { + _networkAgent.CancelCurrentOperation(); + } + + public void ShowPreferences() + { + ShowPreferences(null); + } + + public void ShowPreferences(string currentTab) { - Settings.Reload(); - var preferences = new PreferencesViewModel(_windowManager,_events, this,Settings); - _windowManager.ShowDialog(preferences); - + //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 = new AboutViewModel(); + var about = IoC.Get(); + about.LatestVersion=_sparkle.LatestVersion; _windowManager.ShowWindow(about); } @@ -327,23 +573,60 @@ namespace Pithos.Client.WPF { } -/* + public void GoToSite() { - var site = Properties.Settings.Default.PithosSite; + var site = Properties.Settings.Default.ProductionServer; 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); + var uri = account.SiteUri.Replace("http://","https://"); + Process.Start(uri); } - public void ShowFileProperties() + 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"); @@ -364,7 +647,7 @@ namespace Pithos.Client.WPF { var pair=(from monitor in Monitors where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase) select monitor).FirstOrDefault(); - var accountMonitor = pair.Value; + var accountMonitor = pair.Value; if (accountMonitor == null) return; @@ -398,7 +681,7 @@ namespace Pithos.Client.WPF { var pair=(from monitor in Monitors where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase) select monitor).FirstOrDefault(); - var accountMonitor = pair.Value; + var accountMonitor = pair.Value; var info = accountMonitor.GetContainerInfo(filePath); @@ -407,19 +690,17 @@ namespace Pithos.Client.WPF { _windowManager.ShowWindow(containerProperties); } - public void SynchNow() - { - var agent = IoC.Get(); - agent.SynchNow(); - } + public void SynchNow() + { + _pollAgent.SynchNow(); + } public ObjectInfo RefreshObjectInfo(ObjectInfo currentInfo) { if (currentInfo==null) throw new ArgumentNullException("currentInfo"); - Contract.EndContractBlock(); - - var monitor = Monitors[currentInfo.Account]; + Contract.EndContractBlock(); + var monitor = Monitors[currentInfo.AccountKey]; var newInfo=monitor.CloudClient.GetObjectInfo(currentInfo.Account, currentInfo.Container, currentInfo.Name); return newInfo; } @@ -430,70 +711,127 @@ namespace Pithos.Client.WPF { throw new ArgumentNullException("container"); Contract.EndContractBlock(); - var monitor = Monitors[container.Account]; + var monitor = Monitors[container.AccountKey]; var newInfo = monitor.CloudClient.GetContainerInfo(container.Account, container.Name); 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() + public void ToggleSynching() { - bool isPaused=false; - foreach (var pair in Monitors) + IsPaused=!IsPaused; + foreach (var monitor in Monitors.Values) { - var monitor = pair.Value; - monitor.Pause = !monitor.Pause; - isPaused = monitor.Pause; + monitor.Pause = IsPaused ; } + _pollAgent.Pause = IsPaused; + _networkAgent.Pause = IsPaused; + - 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(); - } + public void ExitPithos() + { + try + { - ((Window)GetView()).Close(); - } - #endregion + 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.Syncing, "Syncing Files", "TraySynching"), + 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, params object[] args) + { + return new Notifier(this, + new StatusNotification(String.Format(startMessage,args)), + new StatusNotification(String.Format(endMessage,args))); + } + + + /// /// 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)) + if (_iconNames.ContainsKey(_pithosStatus)) { - var info = _iconNames[pithosStatus]; + var info = _iconNames[_pithosStatus]; StatusIcon = String.Format(@"../Images/{0}.ico", info.IconName); - - Assembly assembly = Assembly.GetExecutingAssembly(); - var fileVersion = FileVersionInfo.GetVersionInfo(assembly.Location); - - - StatusMessage = String.Format("Pithos {0}\r\n{1}", fileVersion.FileVersion,info.StatusText); } - - //_events.Publish(new Notification { Title = "Start", Message = "Start Monitoring", Level = TraceLevel.Info}); + + if (_pithosStatus == PithosStatus.InSynch) + StatusMessage = "All files up to date"; } @@ -515,26 +853,29 @@ namespace Pithos.Client.WPF { 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, retries).Wait(); - break; - case HttpStatusCode.ProxyAuthenticationRequired: - TryAuthenticateProxy(monitor,retries); - break; - default: - TryLater(monitor, exc, retries); - break; - } + 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); + var account = Settings.Accounts.Find(acc => acc.AccountKey == new Uri(new Uri(monitor.AuthenticationUrl), monitor.UserName)); + account.IsExpired = true; + 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) { @@ -547,23 +888,22 @@ namespace Pithos.Client.WPF { }); } - private void TryAuthenticateProxy(PithosMonitor monitor,int retries) - { - Execute.OnUIThread(() => - { - var proxyAccount = new ProxyAccountViewModel(this.Settings); - if (true == _windowManager.ShowDialog(proxyAccount)) - { - - StartMonitor(monitor, retries); - NotifyOfPropertyChange(() => Accounts); - } - }); - } + 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) + private bool AbandonRetry(PithosMonitor monitor, int retries) { - if (retries > 1) + if (retries > 3) { var message = String.Format("Monitoring of account {0} has failed too many times. Will not retry", monitor.UserName); @@ -575,51 +915,7 @@ namespace Pithos.Client.WPF { } - private async Task TryAuthorize(PithosMonitor monitor,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); - 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", monitor.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", monitor.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) + 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)); @@ -633,16 +929,19 @@ namespace Pithos.Client.WPF { { StatusMessage = status; - _events.Publish(new Notification { Title = "Pithos", Message = status, Level = level }); + _events.Publish(new Notification { Title = "Pithos+", Message = status, Level = level }); } public void NotifyChangedFile(string filePath) { - var entry = new FileEntry {FullPath=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); } @@ -653,56 +952,65 @@ namespace Pithos.Client.WPF { //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)); + Uri.EscapeDataString(account.UserName)); - if (Accounts.All(item => item.UserName != 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; - if (!conflictFiles.Any()) - return; + 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(conflictFiles,message,TraceLevel.Warning); + 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; + public void NotifyForFiles(IEnumerable files, string message,TraceLevel level=TraceLevel.Info) + { + if (files == null) + return; + if (!files.Any()) + return; - StatusMessage = message; + StatusMessage = message; - _events.Publish(new Notification { Title = "Pithos", Message = message, Level = level}); - } + _events.Publish(new Notification { Title = "Pithos+", Message = message, Level = level}); + } - public void Notify(Notification notification) - { - _events.Publish(notification); - } + public void Notify(Notification notification) + { + TaskEx.Run(()=> _events.Publish(notification)); + } - public void RemoveMonitor(string accountName) + public void RemoveMonitor(string serverUrl,string accountName) { if (String.IsNullOrWhiteSpace(accountName)) return; - var accountInfo=_accounts.FirstOrDefault(account => account.UserName == accountName); - _accounts.TryRemove(accountInfo); + var accountInfo=_accounts.FirstOrDefault(account => account.UserName == accountName && account.StorageUri.ToString().StartsWith(serverUrl)); + if (accountInfo != null) + { + _accounts.TryRemove(accountInfo); + _pollAgent.RemoveAccount(accountInfo); + } - PithosMonitor monitor; - if (Monitors.TryRemove(accountName, out monitor)) + var accountKey = new Uri(new Uri(serverUrl),accountName); + PithosMonitor monitor; + if (Monitors.TryRemove(accountKey, out monitor)) { monitor.Stop(); + //TODO: Also remove any pending actions for this account + //from the network queue } } @@ -739,76 +1047,131 @@ namespace Pithos.Client.WPF { public void Handle(SelectiveSynchChanges message) { - var accountName = message.Account.AccountName; - PithosMonitor monitor; - if (_monitors.TryGetValue(accountName, out monitor)) - { - monitor.AddSelectivePaths(message.Added); - monitor.RemoveSelectivePaths(message.Removed); + 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 = false; + 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 - // + //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(); + UpdateStatus(); if (!Settings.ShowDesktopNotifications) return; - if (notification is PollNotification) + 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) { - _pollStarted = true; + StatusMessage = String.Format("Deleted {0}", deleteNotification.Data.Name); return; } - if (notification is CloudNotification) + + var progress = notification as ProgressNotification; + + + if (progress != null) + { + StatusMessage = String.Format("{0} {1:p2} of {2} - {3}", + progress.Action, + (progress.Block + progress.BlockPercentage/100.0)/(double)progress.TotalBlocks, + progress.FileSize.ToByteSize(), + progress.FileName); + return; + } + + var info = notification as StatusNotification; + if (info != null) { - if (!_pollStarted) - return; - _pollStarted= false; - notification.Title = "Pithos"; - notification.Message = "Start Synchronisation"; + StatusMessage = info.Title; + return; } + if (String.IsNullOrWhiteSpace(notification.Message) && String.IsNullOrWhiteSpace(notification.Title)) + return; - if (String.IsNullOrWhiteSpace(notification.Message) && String.IsNullOrWhiteSpace(notification.Title)) - return; + if (notification.Level <= TraceLevel.Warning) + ShowBalloonFor(notification); + } - 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; - } + private void ShowBalloonFor(Notification notification) + { + Contract.Requires(notification!=null); - 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 + 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) { @@ -819,39 +1182,45 @@ namespace Pithos.Client.WPF { Contract.EndContractBlock(); var fileName = message.FileName; - //TODO: Display file properties for non-container folders + //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)); + //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 + //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); + 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(); - } + 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 FileStatus GetFileStatus(string localFileName) + public void RemoveAccountFromDatabase(AccountSettings account) { - if (String.IsNullOrWhiteSpace(localFileName)) - throw new ArgumentNullException("localFileName"); - Contract.EndContractBlock(); - - var statusKeeper = IoC.Get(); - var status=statusKeeper.GetFileStatus(localFileName); - return status; + var statusKeeper = IoC.Get(); + statusKeeper.ClearFolderStatus(account.RootPath); } } }