2 /* -----------------------------------------------------------------------
\r
3 * <copyright file="ShellViewModel.cs" company="GRNet">
\r
5 * Copyright 2011-2012 GRNET S.A. All rights reserved.
\r
7 * Redistribution and use in source and binary forms, with or
\r
8 * without modification, are permitted provided that the following
\r
9 * conditions are met:
\r
11 * 1. Redistributions of source code must retain the above
\r
12 * copyright notice, this list of conditions and the following
\r
15 * 2. Redistributions in binary form must reproduce the above
\r
16 * copyright notice, this list of conditions and the following
\r
17 * disclaimer in the documentation and/or other materials
\r
18 * provided with the distribution.
\r
21 * THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
\r
22 * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
\r
23 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
\r
24 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
\r
25 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
\r
26 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
\r
27 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
\r
28 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
\r
29 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
\r
30 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
\r
31 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
\r
32 * POSSIBILITY OF SUCH DAMAGE.
\r
34 * The views and conclusions contained in the software and
\r
35 * documentation are those of the authors and should not be
\r
36 * interpreted as representing official policies, either expressed
\r
37 * or implied, of GRNET S.A.
\r
39 * -----------------------------------------------------------------------
\r
42 using System.Collections.Concurrent;
\r
43 using System.Diagnostics;
\r
44 using System.Diagnostics.Contracts;
\r
47 using System.Reflection;
\r
48 using System.Runtime.InteropServices;
\r
49 using System.ServiceModel;
\r
50 using System.Threading;
\r
51 using System.Threading.Tasks;
\r
52 using System.Windows;
\r
53 using System.Windows.Controls.Primitives;
\r
54 using System.Windows.Input;
\r
55 using AppLimit.NetSparkle;
\r
56 using Caliburn.Micro;
\r
57 using Hardcodet.Wpf.TaskbarNotification;
\r
58 using Pithos.Client.WPF.Configuration;
\r
59 using Pithos.Client.WPF.FileProperties;
\r
60 using Pithos.Client.WPF.Preferences;
\r
61 using Pithos.Client.WPF.SelectiveSynch;
\r
62 using Pithos.Client.WPF.Services;
\r
63 using Pithos.Client.WPF.Shell;
\r
65 using Pithos.Core.Agents;
\r
66 using Pithos.Interfaces;
\r
68 using System.Collections.Generic;
\r
70 using Pithos.Network;
\r
71 using StatusService = Pithos.Client.WPF.Services.StatusService;
\r
73 namespace Pithos.Client.WPF {
\r
74 using System.ComponentModel.Composition;
\r
76 public class ToggleStatusCommand:ICommand
\r
78 private readonly ShellViewModel _model;
\r
79 public ToggleStatusCommand(ShellViewModel model)
\r
83 public void Execute(object parameter)
\r
85 _model.CurrentSyncStatus();
\r
88 public bool CanExecute(object parameter)
\r
93 public event EventHandler CanExecuteChanged;
\r
98 /// The "shell" of the Pithos application displays the taskbar icon, menu and notifications.
\r
99 /// The shell also hosts the status service called by shell extensions to retrieve file info
\r
102 /// It is a strange "shell" as its main visible element is an icon instead of a window
\r
103 /// The shell subscribes to the following events:
\r
104 /// * Notification: Raised by components that want to notify the user. Usually displayed in a balloon
\r
105 /// * 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
\r
106 /// * ShowFilePropertiesEvent: Raised when a shell command requests the display of the file/container properties dialog
\r
108 //TODO: CODE SMELL Why does the shell handle the SelectiveSynchChanges?
\r
109 [Export(typeof(IShell)), Export(typeof(ShellViewModel)),Export(typeof(IStatusNotification))]
\r
110 public class ShellViewModel : Screen, IStatusNotification, IShell,
\r
111 IHandle<Notification>, IHandle<SelectiveSynchChanges>, IHandle<ShowFilePropertiesEvent>
\r
114 private readonly IEventAggregator _events;
\r
116 public PithosSettings Settings { get; private set; }
\r
119 private readonly ConcurrentDictionary<Uri, PithosMonitor> _monitors = new ConcurrentDictionary<Uri, PithosMonitor>();
\r
121 /// Dictionary of account monitors, keyed by account
\r
124 /// One monitor class is created for each account. The Shell needs access to the monitors to execute start/stop/pause commands,
\r
125 /// retrieve account and boject info
\r
127 // TODO: Does the Shell REALLY need access to the monitors? Could we achieve the same results with a better design?
\r
128 // TODO: The monitors should be internal to Pithos.Core, even though exposing them makes coding of the Object and Container windows easier
\r
129 public ConcurrentDictionary<Uri, PithosMonitor> Monitors
\r
131 get { return _monitors; }
\r
136 /// The status service is used by Shell extensions to retrieve file status information
\r
138 //TODO: CODE SMELL! This is the shell! While hosting in the shell makes executing start/stop commands easier, it is still a smell
\r
139 private ServiceHost _statusService;
\r
141 //Logging in the Pithos client is provided by log4net
\r
142 private static readonly log4net.ILog Log = log4net.LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
\r
144 #pragma warning disable 649
\r
146 private PollAgent _pollAgent;
\r
149 private NetworkAgent _networkAgent;
\r
152 public Selectives Selectives { get; set; }
\r
154 #pragma warning restore 649
\r
156 public ToggleStatusCommand ToggleMiniStatusCommand { get; set; }
\r
158 private MiniStatusViewModel _miniStatus;
\r
161 public MiniStatusViewModel MiniStatus
\r
163 get { return _miniStatus; }
\r
166 _miniStatus = value;
\r
167 _miniStatus.Shell = this;
\r
168 _miniStatus.Deactivated += (sender, arg) =>
\r
170 _statusVisible = false;
\r
171 NotifyOfPropertyChange(()=>MiniStatusCaption);
\r
177 /// The Shell depends on MEF to provide implementations for windowManager, events, the status checker service and the settings
\r
180 /// The PithosSettings class encapsulates the app's settings to abstract their storage mechanism (App settings, a database or registry)
\r
182 [ImportingConstructor]
\r
183 public ShellViewModel(IWindowManager windowManager, IEventAggregator events, PithosSettings settings/*,PollAgent pollAgent,NetworkAgent networkAgent*/)
\r
188 _windowManager = windowManager;
\r
189 //CHECK: Caliburn doesn't need explicit command construction
\r
190 //CurrentSyncStatusCommand = new PithosCommand(OpenPithosFolder);
\r
193 _events.Subscribe(this);
\r
196 _pollAgent = pollAgent;
\r
197 _networkAgent = networkAgent;
\r
199 Settings = settings;
\r
201 Proxy.SetFromSettings(settings);
\r
203 StatusMessage = Settings.Accounts.Count==0
\r
204 ? "No Accounts added\r\nPlease add an account"
\r
207 _accounts.CollectionChanged += (sender, e) =>
\r
209 NotifyOfPropertyChange(() => OpenFolderCaption);
\r
210 NotifyOfPropertyChange(() => HasAccounts);
\r
213 SetVersionMessage();
\r
215 ToggleMiniStatusCommand=new ToggleStatusCommand(this);
\r
217 catch (Exception exc)
\r
219 Log.Error("Error while starting the ShellViewModel",exc);
\r
225 private void SetVersionMessage()
\r
227 Assembly assembly = Assembly.GetExecutingAssembly();
\r
228 var fileVersion = FileVersionInfo.GetVersionInfo(assembly.Location);
\r
229 VersionMessage = String.Format("Pithos+ {0}", fileVersion.FileVersion);
\r
232 public void CurrentSyncStatus()
\r
234 if (Accounts.Count == 0)
\r
236 ShowPreferences("AccountTab");
\r
240 if (!_statusVisible)
\r
242 _windowManager.ShowWindow(MiniStatus);
\r
243 _statusVisible = true;
\r
247 if (MiniStatus.IsActive)
\r
248 MiniStatus.TryClose();
\r
249 _statusVisible = false;
\r
252 NotifyOfPropertyChange(() => MiniStatusCaption);
\r
256 protected override void OnActivate()
\r
260 InitializeSparkle();
\r
262 //Must delay opening the upgrade window
\r
263 //to avoid Windows Messages sent by the TaskbarIcon
\r
264 TaskEx.Delay(5000).ContinueWith(_=>
\r
265 Execute.OnUIThread(()=> _sparkle.StartLoop(true,Settings.UpdateForceCheck,Settings.UpdateCheckInterval)));
\r
268 StartMonitoring();
\r
272 private void OnCheckFinished(object sender, bool updaterequired)
\r
275 Log.InfoFormat("Upgrade check finished. Need Upgrade: {0}", updaterequired);
\r
276 if (_manualUpgradeCheck)
\r
278 _manualUpgradeCheck = false;
\r
279 if (!updaterequired)
\r
280 //Sparkle raises events on a background thread
\r
281 Execute.OnUIThread(()=>
\r
282 ShowBalloonFor(new Notification{Title="Pithos+ is up to date",Message="You have the latest Pithos+ version. No update is required"}));
\r
286 private void OnUpgradeDetected(object sender, UpdateDetectedEventArgs e)
\r
288 Log.InfoFormat("Update detected {0}",e.LatestVersion);
\r
291 public void CheckForUpgrade()
\r
293 ShowBalloonFor(new Notification{Title="Checking for upgrades",Message="Contacting the server to retrieve the latest Pithos+ version."});
\r
294 _sparkle.StopLoop();
\r
295 _sparkle.updateDetected -= OnUpgradeDetected;
\r
296 _sparkle.checkLoopFinished -= OnCheckFinished;
\r
297 _sparkle.Dispose();
\r
299 _manualUpgradeCheck = true;
\r
300 InitializeSparkle();
\r
301 _sparkle.StartLoop(true,true,Settings.UpdateCheckInterval);
\r
304 private void InitializeSparkle()
\r
306 _sparkle = new Sparkle(Settings.UpdateUrl);
\r
307 _sparkle.updateDetected += OnUpgradeDetected;
\r
308 _sparkle.checkLoopFinished += OnCheckFinished;
\r
309 _sparkle.ShowDiagnosticWindow = Settings.UpdateDiagnostics;
\r
312 private async void StartMonitoring()
\r
316 if (Settings.IgnoreCertificateErrors)
\r
318 ServicePointManager.ServerCertificateValidationCallback = (sender, certificate, chain, errors) => true;
\r
321 var accounts = Settings.Accounts.Select(MonitorAccount);
\r
322 await TaskEx.WhenAll(accounts).ConfigureAwait(false);
\r
323 _statusService = StatusService.Start();
\r
326 catch (AggregateException exc)
\r
330 Log.Error("Error while starting monitoring", e);
\r
337 protected override void OnDeactivate(bool close)
\r
339 base.OnDeactivate(close);
\r
342 StatusService.Stop(_statusService);
\r
343 _statusService = null;
\r
347 public Task MonitorAccount(AccountSettings account)
\r
349 return Task.Factory.StartNew(() =>
\r
351 PithosMonitor monitor;
\r
352 var accountName = account.AccountName;
\r
354 MigrateFolders(account);
\r
356 Selectives.SetIsSelectiveEnabled(account.AccountKey, account.SelectiveSyncEnabled);
\r
358 if (Monitors.TryGetValue(account.AccountKey, out monitor))
\r
360 //If the account is active
\r
361 if (account.IsActive)
\r
363 //The Api Key may have changed throuth the Preferences dialog
\r
364 monitor.ApiKey = account.ApiKey;
\r
365 Debug.Assert(monitor.StatusNotification == this,"An existing monitor should already have a StatusNotification service object");
\r
366 monitor.RootPath = account.RootPath;
\r
367 //Start the monitor. It's OK to start an already started monitor,
\r
368 //it will just ignore the call
\r
369 StartMonitor(monitor).Wait();
\r
373 //If the account is inactive
\r
374 //Stop and remove the monitor
\r
375 RemoveMonitor(account.ServerUrl,accountName);
\r
381 //Create a new monitor/ Can't use MEF here, it would return a single instance for all monitors
\r
382 monitor = new PithosMonitor
\r
384 UserName = accountName,
\r
385 ApiKey = account.ApiKey,
\r
386 StatusNotification = this,
\r
387 RootPath = account.RootPath
\r
389 //PithosMonitor uses MEF so we need to resolve it
\r
390 IoC.BuildUp(monitor);
\r
392 monitor.AuthenticationUrl = account.ServerUrl;
\r
394 Monitors[account.AccountKey] = monitor;
\r
396 if (!Directory.Exists(account.RootPath))
\r
398 account.IsActive = false;
\r
400 Notify(new Notification
\r
402 Level = TraceLevel.Error,
\r
403 Title = "Missing account folder",
\r
404 Message = String.Format("Can't find the root folder for account {0} at {1}. The account was deactivated.\r" +
\r
405 "If the account's files were stored in a removable disk, please connect it and reactivate the account", account.AccountName, account.RootPath)
\r
410 if (account.IsActive)
\r
412 //Don't start a monitor if it doesn't have an account and ApiKey
\r
413 if (String.IsNullOrWhiteSpace(monitor.UserName) ||
\r
414 String.IsNullOrWhiteSpace(monitor.ApiKey))
\r
416 StartMonitor(monitor);
\r
421 private void MigrateFolders(AccountSettings account)
\r
423 var oldOthersFolder=Path.Combine(account.RootPath, FolderConstants.OldOthersFolder);
\r
424 var newOthersFolder = Path.Combine(account.RootPath, FolderConstants.OthersFolder);
\r
425 var oldFolder = new DirectoryInfo(oldOthersFolder);
\r
426 var newFolder = new DirectoryInfo(newOthersFolder);
\r
428 if (oldFolder.Exists && !newFolder.Exists)
\r
430 oldFolder.MoveTo(newOthersFolder);
\r
435 protected override void OnViewLoaded(object view)
\r
438 var window = (Window)view;
\r
439 TaskEx.Delay(1000).ContinueWith(t => Execute.OnUIThread(window.Hide));
\r
440 base.OnViewLoaded(view);
\r
444 #region Status Properties
\r
446 private string _statusMessage;
\r
447 public string StatusMessage
\r
449 get { return _statusMessage; }
\r
452 _statusMessage = value;
\r
453 NotifyOfPropertyChange(() => StatusMessage);
\r
454 NotifyOfPropertyChange(() => TooltipMessage);
\r
458 public string VersionMessage { get; set; }
\r
460 public string TooltipMessage
\r
464 return String.Format("{0}\r\n{1}",VersionMessage,StatusMessage);
\r
468 public string TooltipMiniStatus
\r
472 return String.Format("{0}\r\n{1}", "Status Window", "Enable / Disable the status window");
\r
476 /*public string ToggleStatusWindowMessage
\r
480 return String.Format("{0}" + Environment.NewLine + "{1} Toggle Mini Status");
\r
484 private readonly ObservableConcurrentCollection<AccountInfo> _accounts = new ObservableConcurrentCollection<AccountInfo>();
\r
485 public ObservableConcurrentCollection<AccountInfo> Accounts
\r
487 get { return _accounts; }
\r
490 public bool HasAccounts
\r
492 get { return _accounts.Count > 0; }
\r
496 public string OpenFolderCaption
\r
500 return (_accounts.Count == 0)
\r
501 ? "No Accounts Defined"
\r
502 : "Open Pithos Folder";
\r
506 private string _pauseSyncCaption="Pause Synching";
\r
507 public string PauseSyncCaption
\r
509 get { return _pauseSyncCaption; }
\r
512 _pauseSyncCaption = value;
\r
513 NotifyOfPropertyChange(() => PauseSyncCaption);
\r
517 private readonly ObservableConcurrentCollection<FileEntry> _recentFiles = new ObservableConcurrentCollection<FileEntry>();
\r
518 public ObservableConcurrentCollection<FileEntry> RecentFiles
\r
520 get { return _recentFiles; }
\r
524 private string _statusIcon="../Images/Pithos.ico";
\r
525 public string StatusIcon
\r
527 get { return _statusIcon; }
\r
530 //TODO: Ensure all status icons use the Pithos logo
\r
531 _statusIcon = value;
\r
532 NotifyOfPropertyChange(() => StatusIcon);
\r
540 public void CancelCurrentOperation()
\r
542 _pollAgent.CancelCurrentOperation();
\r
545 public void ShowPreferences()
\r
547 ShowPreferences(null);
\r
550 public void ShowPreferences(string currentTab)
\r
552 //Settings.Reload();
\r
554 var preferences = IoC.Get<PreferencesViewModel>();//??new PreferencesViewModel(_windowManager, _events, this, Settings,currentTab);
\r
555 if (!String.IsNullOrWhiteSpace(currentTab))
\r
556 preferences.SelectedTab = currentTab;
\r
557 if (!preferences.IsActive)
\r
558 _windowManager.ShowWindow(preferences);
\r
559 var view = (Window)preferences.GetView();
\r
560 view.NullSafe(v=>v.Activate());
\r
563 public void AboutPithos()
\r
565 var about = IoC.Get<AboutViewModel>();
\r
566 about.LatestVersion=_sparkle.LatestVersion;
\r
567 _windowManager.ShowWindow(about);
\r
570 public void SendFeedback()
\r
572 var feedBack = IoC.Get<FeedbackViewModel>();
\r
573 _windowManager.ShowWindow(feedBack);
\r
576 //public PithosCommand OpenPithosFolderCommand { get; private set; }
\r
578 public void OpenPithosFolder()
\r
580 var account = Settings.Accounts.FirstOrDefault(acc => acc.IsActive);
\r
581 if (account == null)
\r
583 Process.Start(account.RootPath);
\r
586 public void OpenPithosFolder(AccountInfo account)
\r
588 Process.Start(account.AccountPath);
\r
593 public void GoToSite()
\r
595 var site = Properties.Settings.Default.ProductionServer;
\r
596 Process.Start(site);
\r
600 public void GoToSite(AccountInfo account)
\r
602 var uri = account.SiteUri.Replace("http://","https://");
\r
603 Process.Start(uri);
\r
606 private bool _statusVisible;
\r
608 public string MiniStatusCaption
\r
612 return _statusVisible ? "Hide Status Window" : "Show Status Window";
\r
616 public bool HasConflicts
\r
618 get { return true; }
\r
620 public void ShowConflicts()
\r
622 _windowManager.ShowWindow(IoC.Get<ConflictsViewModel>());
\r
626 /// Open an explorer window to the target path's directory
\r
627 /// and select the file
\r
629 /// <param name="entry"></param>
\r
630 public void GoToFile(FileEntry entry)
\r
632 var fullPath = entry.FullPath;
\r
633 if (!File.Exists(fullPath) && !Directory.Exists(fullPath))
\r
635 Process.Start("explorer.exe","/select, " + fullPath);
\r
638 public void OpenLogPath()
\r
640 var pithosDataPath = PithosSettings.PithosDataPath;
\r
642 Process.Start(pithosDataPath);
\r
645 public void ShowFileProperties()
\r
647 var account = Settings.Accounts.First(acc => acc.IsActive);
\r
648 var dir = new DirectoryInfo(account.RootPath + @"\pithos");
\r
649 var files=dir.GetFiles();
\r
650 var r=new Random();
\r
651 var idx=r.Next(0, files.Length);
\r
652 ShowFileProperties(files[idx].FullName);
\r
655 public void ShowFileProperties(string filePath)
\r
657 if (String.IsNullOrWhiteSpace(filePath))
\r
658 throw new ArgumentNullException("filePath");
\r
659 if (!File.Exists(filePath) && !Directory.Exists(filePath))
\r
660 throw new ArgumentException(String.Format("Non existent file {0}",filePath),"filePath");
\r
661 Contract.EndContractBlock();
\r
663 var pair=(from monitor in Monitors
\r
664 where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase)
\r
665 select monitor).FirstOrDefault();
\r
666 var accountMonitor = pair.Value;
\r
668 if (accountMonitor == null)
\r
671 var infoTask=accountMonitor.GetObjectInfo(filePath);
\r
675 var fileProperties = new FilePropertiesViewModel(this, infoTask,filePath);
\r
676 _windowManager.ShowWindow(fileProperties);
\r
679 public void ShowContainerProperties()
\r
681 var account = Settings.Accounts.First(acc => acc.IsActive);
\r
682 var dir = new DirectoryInfo(account.RootPath);
\r
683 var fullName = (from folder in dir.EnumerateDirectories()
\r
684 where (folder.Attributes & FileAttributes.Hidden) == 0
\r
685 select folder.FullName).First();
\r
686 ShowContainerProperties(fullName);
\r
689 public void ShowContainerProperties(string filePath)
\r
691 if (String.IsNullOrWhiteSpace(filePath))
\r
692 throw new ArgumentNullException("filePath");
\r
693 if (!Directory.Exists(filePath))
\r
694 throw new ArgumentException(String.Format("Non existent file {0}",filePath),"filePath");
\r
695 Contract.EndContractBlock();
\r
697 var pair=(from monitor in Monitors
\r
698 where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase)
\r
699 select monitor).FirstOrDefault();
\r
700 var accountMonitor = pair.Value;
\r
701 var info = accountMonitor.GetContainerInfo(filePath);
\r
705 var containerProperties = new ContainerPropertiesViewModel(this, info,filePath);
\r
706 _windowManager.ShowWindow(containerProperties);
\r
709 public void SynchNow()
\r
711 _pollAgent.SynchNow();
\r
714 public async Task<ObjectInfo> RefreshObjectInfo(ObjectInfo currentInfo)
\r
716 if (currentInfo==null)
\r
717 throw new ArgumentNullException("currentInfo");
\r
718 Contract.EndContractBlock();
\r
719 var monitor = Monitors[currentInfo.AccountKey];
\r
720 var newInfo=await monitor.CloudClient.GetObjectInfo(currentInfo.Account, currentInfo.Container, currentInfo.Name).ConfigureAwait(false);
\r
724 public async Task<ContainerInfo> RefreshContainerInfo(ContainerInfo container)
\r
726 if (container == null)
\r
727 throw new ArgumentNullException("container");
\r
728 Contract.EndContractBlock();
\r
730 var monitor = Monitors[container.AccountKey];
\r
731 var newInfo = await monitor.CloudClient.GetContainerInfo(container.Account, container.Name).ConfigureAwait(false);
\r
735 private bool _isPaused;
\r
736 public bool IsPaused
\r
738 get { return _isPaused; }
\r
742 PauseSyncCaption = IsPaused ? "Resume syncing" : "Pause syncing";
\r
743 var iconKey = IsPaused ? "TraySyncPaused" : "TrayInSynch";
\r
744 StatusIcon = String.Format(@"../Images/{0}.ico", iconKey);
\r
746 NotifyOfPropertyChange(() => IsPaused);
\r
750 public void ToggleSynching()
\r
752 IsPaused=!IsPaused;
\r
753 foreach (var monitor in Monitors.Values)
\r
755 monitor.Pause = IsPaused ;
\r
757 _pollAgent.Pause = IsPaused;
\r
758 _networkAgent.Pause = IsPaused;
\r
763 public void ExitPithos()
\r
768 foreach (var monitor in Monitors.Select(pair => pair.Value))
\r
773 var view = GetView() as Window;
\r
777 catch (Exception exc)
\r
779 Log.Info("Exception while exiting", exc);
\r
783 Application.Current.Shutdown();
\r
790 private readonly Dictionary<PithosStatus, StatusInfo> _iconNames = new List<StatusInfo>
\r
792 new StatusInfo(PithosStatus.InSynch, "All files up to date", "TrayInSynch"),
\r
793 new StatusInfo(PithosStatus.PollSyncing, "Polling Files", "TraySynching"),
\r
794 new StatusInfo(PithosStatus.LocalSyncing, "Syncing Files", "TraySynching"),
\r
795 new StatusInfo(PithosStatus.SyncPaused, "Sync Paused", "TraySyncPaused")
\r
796 }.ToDictionary(s => s.Status);
\r
798 readonly IWindowManager _windowManager;
\r
800 //private int _syncCount=0;
\r
803 private PithosStatus _pithosStatus = PithosStatus.Disconnected;
\r
805 public void SetPithosStatus(PithosStatus status)
\r
807 if (_pithosStatus == PithosStatus.LocalSyncing && status == PithosStatus.PollComplete)
\r
809 if (_pithosStatus == PithosStatus.PollSyncing && status == PithosStatus.LocalComplete)
\r
811 if (status == PithosStatus.LocalComplete || status == PithosStatus.PollComplete)
\r
812 _pithosStatus = PithosStatus.InSynch;
\r
814 _pithosStatus = status;
\r
818 public void SetPithosStatus(PithosStatus status,string message)
\r
820 StatusMessage = message;
\r
821 SetPithosStatus(status);
\r
824 /* public Notifier GetNotifier(Notification startNotification, Notification endNotification)
\r
826 return new Notifier(this, startNotification, endNotification);
\r
829 public Notifier GetNotifier(string startMessage, string endMessage, bool isActive=true,params object[] args)
\r
831 return isActive?new Notifier(this,
\r
832 new StatusNotification(String.Format(startMessage,args)),
\r
833 new StatusNotification(String.Format(endMessage,args)))
\r
834 :new Notifier(this,(Notification) null,null);
\r
839 /// Updates the visual status indicators of the application depending on status changes, e.g. icon, stat
\r
841 public void UpdateStatus()
\r
844 if (_iconNames.ContainsKey(_pithosStatus))
\r
846 var info = _iconNames[_pithosStatus];
\r
847 StatusIcon = String.Format(@"../Images/{0}.ico", info.IconName);
\r
850 if (_pithosStatus == PithosStatus.InSynch)
\r
851 StatusMessage = "All files up to date";
\r
856 private Task StartMonitor(PithosMonitor monitor,int retries=0)
\r
858 return Task.Factory.StartNew(() =>
\r
860 using (log4net.ThreadContext.Stacks["Monitor"].Push("Start"))
\r
864 Log.InfoFormat("Start Monitoring {0}", monitor.UserName);
\r
868 catch (WebException exc)
\r
870 if (AbandonRetry(monitor, retries))
\r
873 HttpStatusCode statusCode =HttpStatusCode.OK;
\r
874 var response = exc.Response as HttpWebResponse;
\r
876 statusCode = response.StatusCode;
\r
878 switch (statusCode)
\r
880 case HttpStatusCode.Unauthorized:
\r
881 var message = String.Format("API Key Expired for {0}. Starting Renewal",
\r
883 Log.Error(message, exc);
\r
884 var account = Settings.Accounts.Find(acc => acc.AccountKey == new Uri(monitor.AuthenticationUrl).Combine(monitor.UserName));
\r
885 account.IsExpired = true;
\r
886 Notify(new ExpirationNotification(account));
\r
887 //TryAuthorize(monitor.UserName, retries).Wait();
\r
889 case HttpStatusCode.ProxyAuthenticationRequired:
\r
890 TryAuthenticateProxy(monitor,retries);
\r
893 TryLater(monitor, exc, retries);
\r
897 catch (Exception exc)
\r
899 if (AbandonRetry(monitor, retries))
\r
902 TryLater(monitor,exc,retries);
\r
908 private void TryAuthenticateProxy(PithosMonitor monitor,int retries)
\r
910 Execute.OnUIThread(() =>
\r
912 var proxyAccount = IoC.Get<ProxyAccountViewModel>();
\r
913 proxyAccount.Settings = Settings;
\r
914 if (true != _windowManager.ShowDialog(proxyAccount))
\r
916 StartMonitor(monitor, retries);
\r
917 NotifyOfPropertyChange(() => Accounts);
\r
921 private bool AbandonRetry(PithosMonitor monitor, int retries)
\r
925 var message = String.Format("Monitoring of account {0} has failed too many times. Will not retry",
\r
927 _events.Publish(new Notification
\r
928 {Title = "Account monitoring failed", Message = message, Level = TraceLevel.Error});
\r
935 private void TryLater(PithosMonitor monitor, Exception exc,int retries)
\r
937 var message = String.Format("An exception occured. Can't start monitoring\nWill retry in 10 seconds");
\r
938 Task.Factory.StartNewDelayed(10000, () => StartMonitor(monitor,retries+1));
\r
939 _events.Publish(new Notification
\r
940 {Title = "Error", Message = message, Level = TraceLevel.Error});
\r
941 Log.Error(message, exc);
\r
945 public void NotifyChange(string status, TraceLevel level=TraceLevel.Info)
\r
947 StatusMessage = status;
\r
949 _events.Publish(new Notification { Title = "Pithos+", Message = status, Level = level });
\r
952 public void NotifyChangedFile(string filePath)
\r
954 if (RecentFiles.Any(e => e.FullPath == filePath))
\r
957 IProducerConsumerCollection<FileEntry> files=RecentFiles;
\r
959 while (files.Count > 5)
\r
960 files.TryTake(out popped);
\r
961 var entry = new FileEntry { FullPath = filePath };
\r
962 files.TryAdd(entry);
\r
965 public void NotifyAccount(AccountInfo account)
\r
967 if (account== null)
\r
969 //TODO: What happens to an existing account whose Token has changed?
\r
970 account.SiteUri= String.Format("{0}/ui/?token={1}&user={2}",
\r
971 account.SiteUri, Uri.EscapeDataString(account.Token),
\r
972 Uri.EscapeDataString(account.UserName));
\r
974 if (!Accounts.Any(item => item.UserName == account.UserName && item.SiteUri == account.SiteUri))
\r
975 Accounts.TryAdd(account);
\r
979 public void NotifyConflicts(IEnumerable<FileSystemInfo> conflictFiles, string message)
\r
981 if (conflictFiles == null)
\r
983 //Convert to list to avoid multiple iterations
\r
984 var files = conflictFiles.ToList();
\r
985 if (files.Count==0)
\r
989 //TODO: Create a more specific message. For now, just show a warning
\r
990 NotifyForFiles(files,message,TraceLevel.Warning);
\r
994 public void NotifyForFiles(IEnumerable<FileSystemInfo> files, string message,TraceLevel level=TraceLevel.Info)
\r
1001 StatusMessage = message;
\r
1003 _events.Publish(new Notification { Title = "Pithos+", Message = message, Level = level});
\r
1006 public void Notify(Notification notification)
\r
1008 TaskEx.Run(()=> _events.Publish(notification));
\r
1012 public void RemoveMonitor(string serverUrl,string accountName)
\r
1014 if (String.IsNullOrWhiteSpace(accountName))
\r
1017 var accountInfo=_accounts.FirstOrDefault(account => account.UserName == accountName && account.StorageUri.ToString().StartsWith(serverUrl));
\r
1018 if (accountInfo != null)
\r
1020 _accounts.TryRemove(accountInfo);
\r
1021 _pollAgent.RemoveAccount(accountInfo);
\r
1024 var accountKey = new Uri(serverUrl).Combine(accountName);
\r
1025 PithosMonitor monitor;
\r
1026 if (Monitors.TryRemove(accountKey, out monitor))
\r
1029 //TODO: Also remove any pending actions for this account
\r
1030 //from the network queue
\r
1034 public void RefreshOverlays()
\r
1036 foreach (var pair in Monitors)
\r
1038 var monitor = pair.Value;
\r
1040 var path = monitor.RootPath;
\r
1042 if (String.IsNullOrWhiteSpace(path))
\r
1045 if (!Directory.Exists(path) && !File.Exists(path))
\r
1048 IntPtr pathPointer = Marshal.StringToCoTaskMemAuto(path);
\r
1052 NativeMethods.SHChangeNotify(HChangeNotifyEventID.SHCNE_UPDATEITEM,
\r
1053 HChangeNotifyFlags.SHCNF_PATHW | HChangeNotifyFlags.SHCNF_FLUSHNOWAIT,
\r
1054 pathPointer, IntPtr.Zero);
\r
1058 Marshal.FreeHGlobal(pathPointer);
\r
1063 #region Event Handlers
\r
1065 public void Handle(SelectiveSynchChanges message)
\r
1069 PithosMonitor monitor;
\r
1070 if (Monitors.TryGetValue(message.Account.AccountKey, out monitor))
\r
1072 Selectives.SetIsSelectiveEnabled(message.Account.AccountKey, message.Enabled);
\r
1073 monitor.SetSelectivePaths(message.Uris, message.Added, message.Removed);
\r
1076 var account = Accounts.FirstOrDefault(acc => acc.AccountKey == message.Account.AccountKey);
\r
1077 if (account != null)
\r
1079 var added=monitor.UrisToFilePaths(message.Added);
\r
1080 _pollAgent.SynchNow(added);
\r
1087 private bool _pollStarted;
\r
1088 private Sparkle _sparkle;
\r
1089 private bool _manualUpgradeCheck;
\r
1091 //SMELL: Doing so much work for notifications in the shell is wrong
\r
1092 //The notifications should be moved to their own view/viewmodel pair
\r
1093 //and different templates should be used for different message types
\r
1094 //This will also allow the addition of extra functionality, eg. actions
\r
1096 public void Handle(Notification notification)
\r
1100 if (!Settings.ShowDesktopNotifications)
\r
1103 if (notification is PollNotification)
\r
1105 _pollStarted = true;
\r
1108 if (notification is CloudNotification)
\r
1110 if (!_pollStarted)
\r
1112 _pollStarted= false;
\r
1113 notification.Title = "Pithos+";
\r
1114 notification.Message = "Start Synchronisation";
\r
1117 var deleteNotification = notification as CloudDeleteNotification;
\r
1118 if (deleteNotification != null)
\r
1120 StatusMessage = String.Format("Deleted {0}", deleteNotification.Data.Name);
\r
1124 var progress = notification as ProgressNotification;
\r
1127 if (progress != null)
\r
1129 double percentage = (progress.TotalBlocks == progress.Block) ? 1
\r
1130 :(progress.Block + progress.BlockPercentage / 100.0) / (double)progress.TotalBlocks;
\r
1131 StatusMessage = String.Format("{0} {1:p2} of {2} - {3}",
\r
1134 progress.FileSize.ToByteSize(),
\r
1135 progress.FileName);
\r
1139 var info = notification as StatusNotification;
\r
1142 StatusMessage = info.Title;
\r
1145 if (String.IsNullOrWhiteSpace(notification.Message) && String.IsNullOrWhiteSpace(notification.Title))
\r
1148 if (notification.Level <= TraceLevel.Warning)
\r
1149 ShowBalloonFor(notification);
\r
1152 private void ShowBalloonFor(Notification notification)
\r
1154 Contract.Requires(notification!=null);
\r
1156 if (!Settings.ShowDesktopNotifications)
\r
1160 switch (notification.Level)
\r
1162 case TraceLevel.Verbose:
\r
1164 case TraceLevel.Info:
\r
1165 icon = BalloonIcon.Info;
\r
1167 case TraceLevel.Error:
\r
1168 icon = BalloonIcon.Error;
\r
1170 case TraceLevel.Warning:
\r
1171 icon = BalloonIcon.Warning;
\r
1177 var tv = (ShellView) GetView();
\r
1178 System.Action clickAction = null;
\r
1179 if (notification is ExpirationNotification)
\r
1181 clickAction = () => ShowPreferences("AccountTab");
\r
1183 var balloon = new PithosBalloon
\r
1185 Title = notification.Title,
\r
1186 Message = notification.Message,
\r
1188 ClickAction = clickAction
\r
1190 tv.TaskbarView.ShowCustomBalloon(balloon, PopupAnimation.Fade, 4000);
\r
1195 public void Handle(ShowFilePropertiesEvent message)
\r
1197 if (message == null)
\r
1198 throw new ArgumentNullException("message");
\r
1199 if (String.IsNullOrWhiteSpace(message.FileName) )
\r
1200 throw new ArgumentException("message");
\r
1201 Contract.EndContractBlock();
\r
1203 var fileName = message.FileName;
\r
1204 //TODO: Display file properties for non-container folders
\r
1205 if (File.Exists(fileName))
\r
1206 //Retrieve the full name with exact casing. Pithos names are case sensitive
\r
1207 ShowFileProperties(FileInfoExtensions.GetProperFilePathCapitalization(fileName));
\r
1208 else if (Directory.Exists(fileName))
\r
1209 //Retrieve the full name with exact casing. Pithos names are case sensitive
\r
1211 var path = FileInfoExtensions.GetProperDirectoryCapitalization(fileName);
\r
1212 if (IsContainer(path))
\r
1213 ShowContainerProperties(path);
\r
1215 ShowFileProperties(path);
\r
1219 private bool IsContainer(string path)
\r
1221 var matchingFolders = from account in _accounts
\r
1222 from rootFolder in Directory.GetDirectories(account.AccountPath)
\r
1223 where rootFolder.Equals(path, StringComparison.InvariantCultureIgnoreCase)
\r
1224 select rootFolder;
\r
1225 return matchingFolders.Any();
\r
1228 public FileStatus GetFileStatus(string localFileName)
\r
1230 if (String.IsNullOrWhiteSpace(localFileName))
\r
1231 throw new ArgumentNullException("localFileName");
\r
1232 Contract.EndContractBlock();
\r
1234 var statusKeeper = IoC.Get<IStatusKeeper>();
\r
1235 var status=statusKeeper.GetFileStatus(localFileName);
\r
1239 public void RemoveAccountFromDatabase(AccountSettings account)
\r
1241 var statusKeeper = IoC.Get<IStatusKeeper>();
\r
1242 statusKeeper.ClearFolderStatus(account.RootPath);
\r