1 using System.Collections.Concurrent;
2 using System.ComponentModel.Composition;
3 using System.Diagnostics;
4 using System.Diagnostics.Contracts;
7 using System.Runtime.InteropServices;
8 using System.ServiceModel;
9 using System.ServiceModel.Description;
10 using System.Threading.Tasks;
13 using Hardcodet.Wpf.TaskbarNotification;
14 using Pithos.Client.WPF.Configuration;
15 using Pithos.Client.WPF.Properties;
16 using Pithos.Client.WPF.SelectiveSynch;
18 using Pithos.Interfaces;
20 using System.Collections.Generic;
24 using StatusService = Pithos.Client.WPF.Services.StatusService;
26 namespace Pithos.Client.WPF {
27 using System.ComponentModel.Composition;
29 [Export(typeof(IShell))]
30 public class ShellViewModel : Screen, IStatusNotification, IShell,
31 IHandle<Notification>, IHandle<SelectiveSynchChanges>
34 private IStatusChecker _statusChecker;
35 private IEventAggregator _events;
37 public PithosSettings Settings { get; private set; }
39 public IScreen Parent { get; set; }
42 private Dictionary<string, PithosMonitor> _monitors = new Dictionary<string, PithosMonitor>();
43 public Dictionary<string, PithosMonitor> Monitors
45 get { return _monitors; }
48 private ServiceHost _statusService { get; set; }
50 private static readonly log4net.ILog Log = log4net.LogManager.GetLogger("Pithos");
52 [ImportingConstructor]
53 public ShellViewModel(IWindowManager windowManager, IEventAggregator events, IStatusChecker statusChecker, PithosSettings settings)
58 _windowManager = windowManager;
59 OpenPithosFolderCommand = new PithosCommand(OpenPithosFolder);
60 _statusChecker = statusChecker;
62 _events.Subscribe(this);
66 StatusMessage = "In Synch";
71 Log.Error("Error while starting the ShellViewModel",exc);
76 protected override void OnActivate()
79 foreach (var account in Settings.Accounts)
82 MonitorAccount(account);
88 public void MonitorAccount(AccountSettings account)
90 Task.Factory.StartNew(() =>
92 PithosMonitor monitor = null;
93 var accountName = account.AccountName;
95 if (_monitors.TryGetValue(accountName, out monitor))
97 //If the account is active
99 //Start the monitor. It's OK to start an already started monitor,
100 //it will just ignore the call
104 //If the account is inactive
105 //Stop and remove the monitor
106 RemoveMonitor(accountName);
111 //Create a new monitor/ Can't use MEF here, it would return a single instance for all monitors
112 monitor = new PithosMonitor
114 UserName = accountName,
115 ApiKey = account.ApiKey,
116 UsePithos = account.UsePithos,
117 StatusNotification = this,
118 RootPath = account.RootPath
120 //PithosMonitor uses MEF so we need to resolve it
121 IoC.BuildUp(monitor);
123 var appSettings = Properties.Settings.Default;
124 monitor.AuthenticationUrl = account.UsePithos
125 ? appSettings.PithosAuthenticationUrl
126 : appSettings.CloudfilesAuthenticationUrl;
128 _monitors[accountName] = monitor;
130 if (account.IsActive)
132 //Don't start a monitor if it doesn't have an account and ApiKey
133 if (String.IsNullOrWhiteSpace(monitor.UserName) ||
134 String.IsNullOrWhiteSpace(monitor.ApiKey))
136 StartMonitor(monitor);
142 protected override void OnViewLoaded(object view)
144 var window = (Window)view;
147 base.OnViewLoaded(view);
151 #region Status Properties
153 private string _statusMessage;
154 public string StatusMessage
156 get { return _statusMessage; }
159 _statusMessage = value;
160 NotifyOfPropertyChange(() => StatusMessage);
164 private ObservableConcurrentCollection<AccountInfo> _accounts = new ObservableConcurrentCollection<AccountInfo>();
165 public ObservableConcurrentCollection<AccountInfo> Accounts
167 get { return _accounts; }
171 private string _pauseSyncCaption="Pause Syncing";
172 public string PauseSyncCaption
174 get { return _pauseSyncCaption; }
177 _pauseSyncCaption = value;
178 NotifyOfPropertyChange(() => PauseSyncCaption);
182 private readonly ObservableConcurrentCollection<FileEntry> _recentFiles = new ObservableConcurrentCollection<FileEntry>();
183 public ObservableConcurrentCollection<FileEntry> RecentFiles
185 get { return _recentFiles; }
189 private string _statusIcon="Images/Tray.ico";
190 public string StatusIcon
192 get { return _statusIcon; }
196 NotifyOfPropertyChange(() => StatusIcon);
204 public void ShowPreferences()
207 var preferences = new PreferencesViewModel(_windowManager,_events, this,Settings);
208 _windowManager.ShowDialog(preferences);
213 public PithosCommand OpenPithosFolderCommand { get; private set; }
215 public void OpenPithosFolder()
217 var account = Settings.Accounts.FirstOrDefault(acc => acc.IsActive);
220 Process.Start(account.RootPath);
223 public void OpenPithosFolder(AccountInfo account)
225 Process.Start(account.AccountPath);
229 public void GoToSite(AccountInfo account)
231 var site = String.Format("{0}/ui/?token={1}&user={2}",
232 Properties.Settings.Default.PithosSite,account.Token,
237 public void ShowFileProperties()
239 var account = Settings.Accounts.First(acc => acc.IsActive);
240 var dir = new DirectoryInfo(account.RootPath + @"\pithos");
241 var files=dir.GetFiles();
243 var idx=r.Next(0, files.Length);
244 ShowFileProperties(files[idx].FullName);
247 public void ShowFileProperties(string filePath)
249 if (String.IsNullOrWhiteSpace(filePath))
250 throw new ArgumentNullException("filePath");
251 if (!File.Exists(filePath))
252 throw new ArgumentException(String.Format("Non existent file {0}",filePath),"filePath");
253 Contract.EndContractBlock();
255 var pair=(from monitor in Monitors
256 where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase)
257 select monitor).FirstOrDefault();
258 var account = pair.Key;
259 var accountMonitor = pair.Value;
261 ObjectInfo info = accountMonitor.GetObjectInfo(filePath);
265 var fileProperties = new FilePropertiesViewModel(this, info,filePath);
266 _windowManager.ShowWindow(fileProperties);
269 public ObjectInfo RefreshObjectInfo(ObjectInfo currentInfo)
271 if (currentInfo==null)
272 throw new ArgumentNullException("currentInfo");
273 Contract.EndContractBlock();
275 var monitor = Monitors[currentInfo.Account];
276 var newInfo=monitor.CloudClient.GetObjectInfo(currentInfo.Account, currentInfo.Container, currentInfo.Name);
280 public void ToggleSynching()
283 foreach (var pair in Monitors)
285 var monitor = pair.Value;
286 monitor.Pause = !monitor.Pause;
287 isPaused = monitor.Pause;
290 PauseSyncCaption = isPaused ? "Resume syncing" : "Pause syncing";
291 var iconKey = isPaused? "TraySyncPaused" : "TrayInSynch";
292 StatusIcon = String.Format(@"Images/{0}.ico", iconKey);
295 public void ExitPithos()
297 foreach (var pair in Monitors)
299 var monitor = pair.Value;
303 ((Window)GetView()).Close();
308 private Dictionary<PithosStatus, StatusInfo> iconNames = new List<StatusInfo>
310 new StatusInfo(PithosStatus.InSynch, "All files up to date", "TrayInSynch"),
311 new StatusInfo(PithosStatus.Syncing, "Syncing Files", "TraySynching"),
312 new StatusInfo(PithosStatus.SyncPaused, "Sync Paused", "TraySyncPaused")
313 }.ToDictionary(s => s.Status);
315 readonly IWindowManager _windowManager;
318 public void UpdateStatus()
320 var pithosStatus = _statusChecker.GetPithosStatus();
322 if (iconNames.ContainsKey(pithosStatus))
324 var info = iconNames[pithosStatus];
325 StatusIcon = String.Format(@"Images/{0}.ico", info.IconName);
326 StatusMessage = String.Format("Pithos 1.0\r\n{0}", info.StatusText);
329 _events.Publish(new Notification { Title = "Start", Message = "Start Monitoring", Level = TraceLevel.Info});
334 private Task StartMonitor(PithosMonitor monitor,int retries=0)
336 return Task.Factory.StartNew(() =>
338 using (log4net.ThreadContext.Stacks["Monitor"].Push("Start"))
342 Log.InfoFormat("Start Monitoring {0}", monitor.UserName);
346 catch (WebException exc)
348 if (AbandonRetry(monitor, retries))
351 if (IsUnauthorized(exc))
353 var message = String.Format("API Key Expired for {0}. Starting Renewal",monitor.UserName);
354 Log.Error(message,exc);
355 TryAuthorize(monitor,retries);
359 TryLater(monitor, exc,retries);
362 catch (Exception exc)
364 if (AbandonRetry(monitor, retries))
367 TryLater(monitor,exc,retries);
373 private bool AbandonRetry(PithosMonitor monitor, int retries)
377 var message = String.Format("Monitoring of account {0} has failed too many times. Will not retry",
379 _events.Publish(new Notification
380 {Title = "Account monitoring failed", Message = message, Level = TraceLevel.Error});
387 private Task TryAuthorize(PithosMonitor monitor,int retries)
389 _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 });
391 var authorize= PithosAccount.RetrieveCredentialsAsync(Settings.PithosSite);
393 return authorize.ContinueWith(t =>
397 string message = String.Format("API Key retrieval for {0} failed", monitor.UserName);
398 Log.Error(message,t.Exception.InnerException);
399 _events.Publish(new Notification { Title = "Authorization failed", Message = message, Level = TraceLevel.Error });
402 var credentials = t.Result;
403 var account =Settings.Accounts.FirstOrDefault(act => act.AccountName == credentials.UserName);
404 account.ApiKey = credentials.Password;
405 monitor.ApiKey = credentials.Password;
407 Task.Factory.StartNewDelayed(10000, () => StartMonitor(monitor,retries+1));
411 private static bool IsUnauthorized(WebException exc)
414 throw new ArgumentNullException("exc");
415 Contract.EndContractBlock();
417 var response = exc.Response as HttpWebResponse;
418 if (response == null)
420 return (response.StatusCode == HttpStatusCode.Unauthorized);
423 private void TryLater(PithosMonitor monitor, Exception exc,int retries)
425 var message = String.Format("An exception occured. Can't start monitoring\nWill retry in 10 seconds");
426 Task.Factory.StartNewDelayed(10000, () => StartMonitor(monitor,retries+1));
427 _events.Publish(new Notification
428 {Title = "Error", Message = message, Level = TraceLevel.Error});
429 Log.Error(message, exc);
433 public void NotifyChange(string status, TraceLevel level=TraceLevel.Info)
435 this.StatusMessage = status;
437 _events.Publish(new Notification { Title = "Pithos", Message = status, Level = level });
440 public void NotifyChangedFile(string filePath)
442 var entry = new FileEntry {FullPath=filePath};
443 IProducerConsumerCollection<FileEntry> files=this.RecentFiles;
445 while (files.Count > 5)
446 files.TryTake(out popped);
450 public void NotifyAccount(AccountInfo account)
455 account.SiteUri= String.Format("{0}/ui/?token={1}&user={2}",
456 Properties.Settings.Default.PithosSite, account.Token,
459 IProducerConsumerCollection<AccountInfo> accounts = Accounts;
460 for (var i = 0; i < _accounts.Count; i++)
463 if (accounts.TryTake(out item))
465 if (item.UserName!=account.UserName)
467 accounts.TryAdd(item);
472 accounts.TryAdd(account);
476 public void RemoveMonitor(string accountName)
478 if (String.IsNullOrWhiteSpace(accountName))
481 PithosMonitor monitor;
482 if (Monitors.TryGetValue(accountName, out monitor))
484 Monitors.Remove(accountName);
489 public void RefreshOverlays()
491 foreach (var pair in Monitors)
493 var monitor = pair.Value;
495 var path = monitor.RootPath;
497 if (String.IsNullOrWhiteSpace(path))
500 if (!Directory.Exists(path) && !File.Exists(path))
503 IntPtr pathPointer = Marshal.StringToCoTaskMemAuto(path);
507 NativeMethods.SHChangeNotify(HChangeNotifyEventID.SHCNE_UPDATEITEM,
508 HChangeNotifyFlags.SHCNF_PATHW | HChangeNotifyFlags.SHCNF_FLUSHNOWAIT,
509 pathPointer, IntPtr.Zero);
513 Marshal.FreeHGlobal(pathPointer);
518 private void StartStatusService()
520 // Create a ServiceHost for the CalculatorService type and provide the base address.
521 var baseAddress = new Uri("net.pipe://localhost/pithos");
522 _statusService = new ServiceHost(typeof(StatusService), baseAddress);
524 var binding = new NetNamedPipeBinding(NetNamedPipeSecurityMode.None);
526 _statusService.AddServiceEndpoint(typeof(IStatusService), binding, "net.pipe://localhost/pithos/statuscache");
527 _statusService.AddServiceEndpoint(typeof(ISettingsService), binding, "net.pipe://localhost/pithos/settings");
530 //// Add a mex endpoint
531 var smb = new ServiceMetadataBehavior
533 HttpGetEnabled = true,
534 HttpGetUrl = new Uri("http://localhost:30000/pithos/mex")
536 _statusService.Description.Behaviors.Add(smb);
539 _statusService.Open();
542 private void StopStatusService()
544 if (_statusService == null)
547 if (_statusService.State == CommunicationState.Faulted)
548 _statusService.Abort();
549 else if (_statusService.State != CommunicationState.Closed)
550 _statusService.Close();
551 _statusService = null;
554 #region Event Handlers
556 public void Handle(SelectiveSynchChanges message)
558 var accountName = message.Account.AccountName;
559 PithosMonitor monitor;
560 if (_monitors.TryGetValue(accountName, out monitor))
562 monitor.AddSelectivePaths(message.Added);
563 monitor.RemoveSelectivePaths(message.Removed);
570 public void Handle(Notification notification)
572 if (!Settings.ShowDesktopNotifications)
574 BalloonIcon icon = BalloonIcon.None;
575 switch (notification.Level)
577 case TraceLevel.Error:
578 icon = BalloonIcon.Error;
580 case TraceLevel.Info:
581 case TraceLevel.Verbose:
582 icon = BalloonIcon.Info;
584 case TraceLevel.Warning:
585 icon = BalloonIcon.Warning;
588 icon = BalloonIcon.None;
592 if (Settings.ShowDesktopNotifications)
594 var tv = (ShellView) this.GetView();
595 tv.TaskbarView.ShowBalloonTip(notification.Title, notification.Message, icon);