Statistics
| Branch: | Revision:

root / trunk / Pithos.Client.WPF / ShellViewModel.cs @ 7b0a5fec

History | View | Annotate | Download (20.1 kB)

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
}