First version of File Properties window. Works with random file
[pithos-ms-client] / trunk / Pithos.Client.WPF / ShellViewModel.cs
1 using System.Collections.Concurrent;
2 using System.ComponentModel.Composition;
3 using System.Diagnostics;
4 using System.Diagnostics.Contracts;
5 using System.IO;
6 using System.Net;
7 using System.Runtime.InteropServices;
8 using System.ServiceModel;
9 using System.ServiceModel.Description;
10 using System.Threading.Tasks;
11 using System.Windows;
12 using Caliburn.Micro;
13 using Hardcodet.Wpf.TaskbarNotification;
14 using Pithos.Client.WPF.Configuration;
15 using Pithos.Client.WPF.Properties;
16 using Pithos.Client.WPF.SelectiveSynch;
17 using Pithos.Core;
18 using Pithos.Interfaces;
19 using System;
20 using System.Collections.Generic;
21 using System.Linq;
22 using System.Text;
23 using Pithos.Network;
24 using StatusService = Pithos.Client.WPF.Services.StatusService;
25
26 namespace Pithos.Client.WPF {
27     using System.ComponentModel.Composition;
28
29     [Export(typeof(IShell))]
30     public class ShellViewModel : Screen, IStatusNotification, IShell, 
31         IHandle<Notification>, IHandle<SelectiveSynchChanges>
32     {
33        
34         private IStatusChecker _statusChecker;
35         private IEventAggregator _events;
36
37         public PithosSettings Settings { get; private set; }
38
39         public IScreen Parent { get; set; }
40
41
42         private Dictionary<string, PithosMonitor> _monitors = new Dictionary<string, PithosMonitor>();
43         public Dictionary<string, PithosMonitor> Monitors
44         {
45             get { return _monitors; }
46         }
47
48         private ServiceHost _statusService { get; set; }
49
50         private static readonly log4net.ILog Log = log4net.LogManager.GetLogger("Pithos");
51
52         [ImportingConstructor]
53         public ShellViewModel(IWindowManager windowManager, IEventAggregator events, IStatusChecker statusChecker, PithosSettings settings)
54         {
55             try
56             {
57
58                 _windowManager = windowManager;
59                 OpenPithosFolderCommand = new PithosCommand(OpenPithosFolder);
60                 _statusChecker = statusChecker;
61                 _events = events;
62                 _events.Subscribe(this);
63
64                 Settings = settings;
65
66                 StatusMessage = "In Synch";
67
68             }
69             catch (Exception exc)
70             {
71                 Log.Error("Error while starting the ShellViewModel",exc);
72                 throw;
73             }
74         }
75
76         protected override void OnActivate()
77         {
78             base.OnActivate();
79             foreach (var account in Settings.Accounts)
80             {
81
82                 MonitorAccount(account);
83             }
84
85             StartStatusService();
86         }
87
88         public void MonitorAccount(AccountSettings account)
89         {
90             Task.Factory.StartNew(() =>
91             {
92                 PithosMonitor monitor = null;
93                 var accountName = account.AccountName;
94
95                 if (_monitors.TryGetValue(accountName, out monitor))
96                 {
97                     //If the account is active
98                     if (account.IsActive)
99                         //Start the monitor. It's OK to start an already started monitor,
100                         //it will just ignore the call
101                         monitor.Start();
102                     else
103                     {
104                         //If the account is inactive
105                         //Stop and remove the monitor
106                         RemoveMonitor(accountName);
107                     }
108                     return;
109                 }
110
111                 //Create a new monitor/ Can't use MEF here, it would return a single instance for all monitors
112                 monitor = new PithosMonitor
113                               {
114                                   UserName = accountName,
115                                   ApiKey = account.ApiKey,
116                                   UsePithos = account.UsePithos,
117                                   StatusNotification = this,
118                                   RootPath = account.RootPath
119                               };
120                 //PithosMonitor uses MEF so we need to resolve it
121                 IoC.BuildUp(monitor);
122
123                 var appSettings = Properties.Settings.Default;
124                 monitor.AuthenticationUrl = account.UsePithos
125                                                 ? appSettings.PithosAuthenticationUrl
126                                                 : appSettings.CloudfilesAuthenticationUrl;
127
128                 _monitors[accountName] = monitor;
129
130                 if (account.IsActive)
131                 {
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))
135                         return;
136                     StartMonitor(monitor);
137                 }
138             });
139         }
140
141
142         protected override void OnViewLoaded(object view)
143         {
144             var window = (Window)view;
145             window.Hide();
146             UpdateStatus();
147             base.OnViewLoaded(view);
148         }
149
150
151         #region Status Properties
152
153         private string _statusMessage;
154         public string StatusMessage
155         {
156             get { return _statusMessage; }
157             set
158             {
159                 _statusMessage = value;
160                 NotifyOfPropertyChange(() => StatusMessage);
161             }
162         }
163
164         private ObservableConcurrentCollection<AccountInfo> _accounts = new ObservableConcurrentCollection<AccountInfo>();
165         public ObservableConcurrentCollection<AccountInfo> Accounts
166         {
167             get { return _accounts; }
168         }
169
170
171         private string _pauseSyncCaption="Pause Syncing";
172         public string PauseSyncCaption
173         {
174             get { return _pauseSyncCaption; }
175             set
176             {
177                 _pauseSyncCaption = value;
178                 NotifyOfPropertyChange(() => PauseSyncCaption);
179             }
180         }
181
182         private readonly ObservableConcurrentCollection<FileEntry> _recentFiles = new ObservableConcurrentCollection<FileEntry>();
183         public ObservableConcurrentCollection<FileEntry> RecentFiles
184         {
185             get { return _recentFiles; }
186         }
187
188
189         private string _statusIcon="Images/Tray.ico";
190         public string StatusIcon
191         {
192             get { return _statusIcon; }
193             set
194             {
195                 _statusIcon = value;
196                 NotifyOfPropertyChange(() => StatusIcon);
197             }
198         }
199
200         #endregion
201
202         #region Commands
203
204         public void ShowPreferences()
205         {
206             Settings.Reload();
207             var preferences = new PreferencesViewModel(_windowManager,_events, this,Settings);            
208             _windowManager.ShowDialog(preferences);
209             
210         }
211
212
213         public PithosCommand OpenPithosFolderCommand { get; private set; }
214
215         public void OpenPithosFolder()
216         {
217             var account = Settings.Accounts.FirstOrDefault(acc => acc.IsActive);
218             if (account == null)
219                 return;
220             Process.Start(account.RootPath);
221         }
222
223         public void OpenPithosFolder(AccountInfo account)
224         {
225             Process.Start(account.AccountPath);
226         }
227
228         
229         public void GoToSite(AccountInfo account)
230         {
231             var site = String.Format("{0}/ui/?token={1}&user={2}",
232                 Properties.Settings.Default.PithosSite,account.Token,
233                 account.UserName);
234             Process.Start(site);
235         }
236
237         public void ShowFileProperties()
238         {
239             var account = Settings.Accounts.First(acc => acc.IsActive);            
240             var dir = new DirectoryInfo(account.RootPath + @"\pithos");
241             var files=dir.GetFiles();
242             var r=new Random();
243             var idx=r.Next(0, files.Length);
244             ShowFileProperties(files[idx].FullName);            
245         }
246
247         public void ShowFileProperties(string filePath)
248         {
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();
254
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;
260
261             ObjectInfo info = accountMonitor.GetObjectInfo(filePath);
262
263             
264
265             var fileProperties = new FilePropertiesViewModel(this, info,filePath);
266             _windowManager.ShowWindow(fileProperties);
267         }
268
269         public ObjectInfo RefreshObjectInfo(ObjectInfo currentInfo)
270         {
271             if (currentInfo==null)
272                 throw new ArgumentNullException("currentInfo");
273             Contract.EndContractBlock();
274
275             var monitor = Monitors[currentInfo.Account];
276             var newInfo=monitor.CloudClient.GetObjectInfo(currentInfo.Account, currentInfo.Container, currentInfo.Name);
277             return newInfo;
278         }
279
280         public void ToggleSynching()
281         {
282             bool isPaused=false;
283             foreach (var pair in Monitors)
284             {
285                 var monitor = pair.Value;
286                 monitor.Pause = !monitor.Pause;
287                 isPaused = monitor.Pause;
288             }
289
290             PauseSyncCaption = isPaused ? "Resume syncing" : "Pause syncing";
291             var iconKey = isPaused? "TraySyncPaused" : "TrayInSynch";
292             StatusIcon = String.Format(@"Images/{0}.ico", iconKey);
293         }
294
295         public void ExitPithos()
296         {
297             foreach (var pair in Monitors)
298             {
299                 var monitor = pair.Value;
300                 monitor.Stop();
301             }
302
303             ((Window)GetView()).Close();
304         }
305         #endregion
306
307
308         private Dictionary<PithosStatus, StatusInfo> iconNames = new List<StatusInfo>
309             {
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);
314
315         readonly IWindowManager _windowManager;
316
317
318         public void UpdateStatus()
319         {
320             var pithosStatus = _statusChecker.GetPithosStatus();
321
322             if (iconNames.ContainsKey(pithosStatus))
323             {
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);
327             }
328             
329             _events.Publish(new Notification { Title = "Start", Message = "Start Monitoring", Level = TraceLevel.Info});
330         }
331
332
333        
334         private Task StartMonitor(PithosMonitor monitor,int retries=0)
335         {
336             return Task.Factory.StartNew(() =>
337             {
338                 using (log4net.ThreadContext.Stacks["Monitor"].Push("Start"))
339                 {
340                     try
341                     {
342                         Log.InfoFormat("Start Monitoring {0}", monitor.UserName);
343
344                         monitor.Start();
345                     }
346                     catch (WebException exc)
347                     {
348                         if (AbandonRetry(monitor, retries))
349                             return;
350
351                         if (IsUnauthorized(exc))
352                         {
353                             var message = String.Format("API Key Expired for {0}. Starting Renewal",monitor.UserName);                            
354                             Log.Error(message,exc);
355                             TryAuthorize(monitor,retries);
356                         }
357                         else
358                         {
359                             TryLater(monitor, exc,retries);
360                         }
361                     }
362                     catch (Exception exc)
363                     {
364                         if (AbandonRetry(monitor, retries)) 
365                             return;
366
367                         TryLater(monitor,exc,retries);
368                     }
369                 }
370             });
371         }
372
373         private bool AbandonRetry(PithosMonitor monitor, int retries)
374         {
375             if (retries > 1)
376             {
377                 var message = String.Format("Monitoring of account {0} has failed too many times. Will not retry",
378                                             monitor.UserName);
379                 _events.Publish(new Notification
380                                     {Title = "Account monitoring failed", Message = message, Level = TraceLevel.Error});
381                 return true;
382             }
383             return false;
384         }
385
386
387         private Task TryAuthorize(PithosMonitor monitor,int retries)
388         {
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 });
390
391             var authorize= PithosAccount.RetrieveCredentialsAsync(Settings.PithosSite);
392
393             return authorize.ContinueWith(t =>
394             {
395                 if (t.IsFaulted)
396                 {                    
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 });
400                     return;
401                 }
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;
406                 Settings.Save();
407                 Task.Factory.StartNewDelayed(10000, () => StartMonitor(monitor,retries+1));
408             });
409         }
410
411         private static bool IsUnauthorized(WebException exc)
412         {
413             if (exc==null)
414                 throw new ArgumentNullException("exc");
415             Contract.EndContractBlock();
416
417             var response = exc.Response as HttpWebResponse;
418             if (response == null)
419                 return false;
420             return (response.StatusCode == HttpStatusCode.Unauthorized);
421         }
422
423         private void TryLater(PithosMonitor monitor, Exception exc,int retries)
424         {
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);
430         }
431
432
433         public void NotifyChange(string status, TraceLevel level=TraceLevel.Info)
434         {
435             this.StatusMessage = status;
436             
437             _events.Publish(new Notification { Title = "Pithos", Message = status, Level = level });
438         }
439
440         public void NotifyChangedFile(string filePath)
441         {
442             var entry = new FileEntry {FullPath=filePath};
443             IProducerConsumerCollection<FileEntry> files=this.RecentFiles;
444             FileEntry popped;
445             while (files.Count > 5)
446                 files.TryTake(out popped);
447             files.TryAdd(entry);
448         }
449
450         public void NotifyAccount(AccountInfo account)
451         {
452             if (account== null)
453                 return;
454
455             account.SiteUri= String.Format("{0}/ui/?token={1}&user={2}",
456                 Properties.Settings.Default.PithosSite, account.Token,
457                 account.UserName);
458
459             IProducerConsumerCollection<AccountInfo> accounts = Accounts;
460             for (var i = 0; i < _accounts.Count; i++)
461             {
462                 AccountInfo item;
463                 if (accounts.TryTake(out item))
464                 {
465                     if (item.UserName!=account.UserName)
466                     {
467                         accounts.TryAdd(item);
468                     }
469                 }
470             }
471
472             accounts.TryAdd(account);
473         }
474
475
476         public void RemoveMonitor(string accountName)
477         {
478             if (String.IsNullOrWhiteSpace(accountName))
479                 return;
480
481             PithosMonitor monitor;
482             if (Monitors.TryGetValue(accountName, out monitor))
483             {
484                 Monitors.Remove(accountName);
485                 monitor.Stop();
486             }
487         }
488
489         public void RefreshOverlays()
490         {
491             foreach (var pair in Monitors)
492             {
493                 var monitor = pair.Value;
494
495                 var path = monitor.RootPath;
496
497                 if (String.IsNullOrWhiteSpace(path))
498                     continue;
499
500                 if (!Directory.Exists(path) && !File.Exists(path))
501                     continue;
502
503                 IntPtr pathPointer = Marshal.StringToCoTaskMemAuto(path);
504
505                 try
506                 {
507                     NativeMethods.SHChangeNotify(HChangeNotifyEventID.SHCNE_UPDATEITEM,
508                                                  HChangeNotifyFlags.SHCNF_PATHW | HChangeNotifyFlags.SHCNF_FLUSHNOWAIT,
509                                                  pathPointer, IntPtr.Zero);
510                 }
511                 finally
512                 {
513                     Marshal.FreeHGlobal(pathPointer);
514                 }
515             }
516         }
517
518         private void StartStatusService()
519         {
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);
523
524             var binding = new NetNamedPipeBinding(NetNamedPipeSecurityMode.None);
525
526             _statusService.AddServiceEndpoint(typeof(IStatusService), binding, "net.pipe://localhost/pithos/statuscache");
527             _statusService.AddServiceEndpoint(typeof(ISettingsService), binding, "net.pipe://localhost/pithos/settings");
528
529
530             //// Add a mex endpoint
531             var smb = new ServiceMetadataBehavior
532             { 
533                 HttpGetEnabled = true,
534                 HttpGetUrl = new Uri("http://localhost:30000/pithos/mex")
535             };
536             _statusService.Description.Behaviors.Add(smb);
537
538
539             _statusService.Open();
540         }
541
542         private void StopStatusService()
543         {
544             if (_statusService == null)
545                 return;
546
547             if (_statusService.State == CommunicationState.Faulted)
548                 _statusService.Abort();
549             else if (_statusService.State != CommunicationState.Closed)
550                 _statusService.Close();
551             _statusService = null;
552
553         }
554         #region Event Handlers
555         
556         public void Handle(SelectiveSynchChanges message)
557         {
558             var accountName = message.Account.AccountName;
559             PithosMonitor monitor;
560             if (_monitors.TryGetValue(accountName, out monitor))
561             {
562                 monitor.AddSelectivePaths(message.Added);
563                 monitor.RemoveSelectivePaths(message.Removed);
564
565             }
566             
567         }
568
569
570         public void Handle(Notification notification)
571         {
572             if (!Settings.ShowDesktopNotifications)
573                 return;
574             BalloonIcon icon = BalloonIcon.None;
575             switch (notification.Level)
576             {
577                 case TraceLevel.Error:
578                     icon = BalloonIcon.Error;
579                     break;
580                 case TraceLevel.Info:
581                 case TraceLevel.Verbose:
582                     icon = BalloonIcon.Info;
583                     break;
584                 case TraceLevel.Warning:
585                     icon = BalloonIcon.Warning;
586                     break;
587                 default:
588                     icon = BalloonIcon.None;
589                     break;
590             }
591
592             if (Settings.ShowDesktopNotifications)
593             {
594                 var tv = (ShellView) this.GetView();
595                 tv.TaskbarView.ShowBalloonTip(notification.Title, notification.Message, icon);
596             }
597         }
598         #endregion
599     }
600 }