Statistics
| Branch: | Revision:

root / trunk / Pithos.Client.WPF / ShellViewModel.cs @ 437abfca

History | View | Annotate | Download (22 kB)

1
using System.Collections.Concurrent;
2
using System.ComponentModel;
3
using System.ComponentModel.Composition;
4
using System.Diagnostics;
5
using System.Diagnostics.Contracts;
6
using System.IO;
7
using System.Net;
8
using System.Runtime.InteropServices;
9
using System.ServiceModel;
10
using System.ServiceModel.Description;
11
using System.Threading.Tasks;
12
using System.Windows;
13
using Caliburn.Micro;
14
using Hardcodet.Wpf.TaskbarNotification;
15
using Pithos.Client.WPF.Configuration;
16
using Pithos.Client.WPF.FileProperties;
17
using Pithos.Client.WPF.Properties;
18
using Pithos.Client.WPF.SelectiveSynch;
19
using Pithos.Client.WPF.Services;
20
using Pithos.Core;
21
using Pithos.Interfaces;
22
using System;
23
using System.Collections.Generic;
24
using System.Linq;
25
using System.Text;
26
using Pithos.Network;
27
using StatusService = Pithos.Client.WPF.Services.StatusService;
28

    
29
namespace Pithos.Client.WPF {
30
    using System.ComponentModel.Composition;
31

    
32
    [Export(typeof(IShell))]
33
    public class ShellViewModel : Screen, IStatusNotification, IShell,
34
        IHandle<Notification>, IHandle<SelectiveSynchChanges>, IHandle<ShowFilePropertiesEvent>
35
    {
36
       
37
        private IStatusChecker _statusChecker;
38
        private IEventAggregator _events;
39

    
40
        public PithosSettings Settings { get; private set; }
41

    
42
        public IScreen Parent { get; set; }
43

    
44

    
45
        private Dictionary<string, PithosMonitor> _monitors = new Dictionary<string, PithosMonitor>();
46
        public Dictionary<string, PithosMonitor> Monitors
47
        {
48
            get { return _monitors; }
49
        }
50

    
51
        private ServiceHost _statusService { get; set; }
52

    
53
        private static readonly log4net.ILog Log = log4net.LogManager.GetLogger("Pithos");
54

    
55
        [ImportingConstructor]
56
        public ShellViewModel(IWindowManager windowManager, IEventAggregator events, IStatusChecker statusChecker, PithosSettings settings)
57
        {
58
            try
59
            {
60

    
61
                _windowManager = windowManager;
62
                OpenPithosFolderCommand = new PithosCommand(OpenPithosFolder);
63
                _statusChecker = statusChecker;
64
                _events = events;
65
                _events.Subscribe(this);
66

    
67
                Settings = settings;
68

    
69
                StatusMessage = "In Synch";
70

    
71
            }
72
            catch (Exception exc)
73
            {
74
                Log.Error("Error while starting the ShellViewModel",exc);
75
                throw;
76
            }
77
        }
78

    
79
        protected override void OnActivate()
80
        {
81
            base.OnActivate();
82

    
83
            
84
            var tasks=from account in Settings.Accounts
85
                           select MonitorAccount(account);
86

    
87
            try
88
            {
89

    
90
                Task.Factory.Iterate(tasks).Wait();
91
                _statusService=StatusService.Start();
92

    
93
            }
94
            catch (AggregateException exc)
95
            {
96
                exc.Handle(e =>{
97
                    Log.Error("Error while starting monitoring", e);
98
                    return true;
99
                });
100
                throw;
101
            }
102
            
103
        }
104

    
105
        protected override void OnDeactivate(bool close)
106
        {
107
            base.OnDeactivate(close);
108
            if (close)
109
            {
110
                StatusService.Stop(_statusService);
111
                _statusService = null;
112
            }
113
        }
114

    
115
        public Task MonitorAccount(AccountSettings account)
116
        {
117
            return Task.Factory.StartNew(() =>
118
            {
119
                PithosMonitor monitor = null;
120
                var accountName = account.AccountName;
121

    
122
                if (_monitors.TryGetValue(accountName, out monitor))
123
                {
124
                    //If the account is active
125
                    if (account.IsActive)
126
                        //Start the monitor. It's OK to start an already started monitor,
127
                        //it will just ignore the call
128
                        monitor.Start();
129
                    else
130
                    {
131
                        //If the account is inactive
132
                        //Stop and remove the monitor
133
                        RemoveMonitor(accountName);
134
                    }
135
                    return;
136
                }
137

    
138
                //Create a new monitor/ Can't use MEF here, it would return a single instance for all monitors
139
                monitor = new PithosMonitor
140
                              {
141
                                  UserName = accountName,
142
                                  ApiKey = account.ApiKey,
143
                                  UsePithos = account.UsePithos,
144
                                  StatusNotification = this,
145
                                  RootPath = account.RootPath
146
                              };
147
                //PithosMonitor uses MEF so we need to resolve it
148
                IoC.BuildUp(monitor);
149

    
150
                var appSettings = Properties.Settings.Default;
151
                monitor.AuthenticationUrl = account.UsePithos
152
                                                ? appSettings.PithosAuthenticationUrl
153
                                                : appSettings.CloudfilesAuthenticationUrl;
154

    
155
                _monitors[accountName] = monitor;
156

    
157
                if (account.IsActive)
158
                {
159
                    //Don't start a monitor if it doesn't have an account and ApiKey
160
                    if (String.IsNullOrWhiteSpace(monitor.UserName) ||
161
                        String.IsNullOrWhiteSpace(monitor.ApiKey))
162
                        return;
163
                    StartMonitor(monitor);
164
                }
165
            });
166
        }
167

    
168

    
169
        protected override void OnViewLoaded(object view)
170
        {
171
            var window = (Window)view;
172
            window.Hide();
173
            UpdateStatus();
174
            base.OnViewLoaded(view);
175
        }
176

    
177

    
178
        #region Status Properties
179

    
180
        private string _statusMessage;
181
        public string StatusMessage
182
        {
183
            get { return _statusMessage; }
184
            set
185
            {
186
                _statusMessage = value;
187
                NotifyOfPropertyChange(() => StatusMessage);
188
            }
189
        }
190

    
191
        private ObservableConcurrentCollection<AccountInfo> _accounts = new ObservableConcurrentCollection<AccountInfo>();
192
        public ObservableConcurrentCollection<AccountInfo> Accounts
193
        {
194
            get { return _accounts; }
195
        }
196

    
197

    
198
        private string _pauseSyncCaption="Pause Syncing";
199
        public string PauseSyncCaption
200
        {
201
            get { return _pauseSyncCaption; }
202
            set
203
            {
204
                _pauseSyncCaption = value;
205
                NotifyOfPropertyChange(() => PauseSyncCaption);
206
            }
207
        }
208

    
209
        private readonly ObservableConcurrentCollection<FileEntry> _recentFiles = new ObservableConcurrentCollection<FileEntry>();
210
        public ObservableConcurrentCollection<FileEntry> RecentFiles
211
        {
212
            get { return _recentFiles; }
213
        }
214

    
215

    
216
        private string _statusIcon="Images/Tray.ico";
217
        public string StatusIcon
218
        {
219
            get { return _statusIcon; }
220
            set
221
            {
222
                _statusIcon = value;
223
                NotifyOfPropertyChange(() => StatusIcon);
224
            }
225
        }
226

    
227
        #endregion
228

    
229
        #region Commands
230

    
231
        public void ShowPreferences()
232
        {
233
            Settings.Reload();
234
            var preferences = new PreferencesViewModel(_windowManager,_events, this,Settings);            
235
            _windowManager.ShowDialog(preferences);
236
            
237
        }
238

    
239

    
240
        public PithosCommand OpenPithosFolderCommand { get; private set; }
241

    
242
        public void OpenPithosFolder()
243
        {
244
            var account = Settings.Accounts.FirstOrDefault(acc => acc.IsActive);
245
            if (account == null)
246
                return;
247
            Process.Start(account.RootPath);
248
        }
249

    
250
        public void OpenPithosFolder(AccountInfo account)
251
        {
252
            Process.Start(account.AccountPath);
253
        }
254

    
255
        
256
        public void GoToSite(AccountInfo account)
257
        {
258
            var site = String.Format("{0}/ui/?token={1}&user={2}",
259
                Properties.Settings.Default.PithosSite,account.Token,
260
                account.UserName);
261
            Process.Start(site);
262
        }
263

    
264
        public void ShowFileProperties()
265
        {
266
            var account = Settings.Accounts.First(acc => acc.IsActive);            
267
            var dir = new DirectoryInfo(account.RootPath + @"\pithos");
268
            var files=dir.GetFiles();
269
            var r=new Random();
270
            var idx=r.Next(0, files.Length);
271
            ShowFileProperties(files[idx].FullName);            
272
        }
273

    
274
        public void ShowFileProperties(string filePath)
275
        {
276
            if (String.IsNullOrWhiteSpace(filePath))
277
                throw new ArgumentNullException("filePath");
278
            if (!File.Exists(filePath))
279
                throw new ArgumentException(String.Format("Non existent file {0}",filePath),"filePath");
280
            Contract.EndContractBlock();
281

    
282
            var pair=(from monitor in  Monitors
283
                               where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase)
284
                                   select monitor).FirstOrDefault();
285
            var account = pair.Key;
286
            var accountMonitor = pair.Value;
287

    
288
            ObjectInfo info = accountMonitor.GetObjectInfo(filePath);
289

    
290
            
291

    
292
            var fileProperties = new FilePropertiesViewModel(this, info,filePath);
293
            _windowManager.ShowWindow(fileProperties);
294
        } 
295
        
296
        public void ShowContainerProperties()
297
        {
298
            var account = Settings.Accounts.First(acc => acc.IsActive);            
299
            var dir = new DirectoryInfo(account.RootPath);
300
            var fullName = (from folder in dir.EnumerateDirectories()
301
                            where (folder.Attributes & FileAttributes.Hidden) == 0
302
                            select folder.FullName).First();
303
            ShowContainerProperties(fullName);            
304
        }
305

    
306
        public void ShowContainerProperties(string filePath)
307
        {
308
            if (String.IsNullOrWhiteSpace(filePath))
309
                throw new ArgumentNullException("filePath");
310
            if (!Directory.Exists(filePath))
311
                throw new ArgumentException(String.Format("Non existent file {0}",filePath),"filePath");
312
            Contract.EndContractBlock();
313

    
314
            var pair=(from monitor in  Monitors
315
                               where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase)
316
                                   select monitor).FirstOrDefault();
317
            var account = pair.Key;
318
            var accountMonitor = pair.Value;            
319
            ContainerInfo info = accountMonitor.GetContainerInfo(filePath);
320

    
321
            
322

    
323
            var containerProperties = new ContainerPropertiesViewModel(this, info,filePath);
324
            _windowManager.ShowWindow(containerProperties);
325
        }
326

    
327
        public ObjectInfo RefreshObjectInfo(ObjectInfo currentInfo)
328
        {
329
            if (currentInfo==null)
330
                throw new ArgumentNullException("currentInfo");
331
            Contract.EndContractBlock();
332

    
333
            var monitor = Monitors[currentInfo.Account];
334
            var newInfo=monitor.CloudClient.GetObjectInfo(currentInfo.Account, currentInfo.Container, currentInfo.Name);
335
            return newInfo;
336
        }
337

    
338
        public ContainerInfo RefreshContainerInfo(ContainerInfo container)
339
        {
340
            if (container == null)
341
                throw new ArgumentNullException("container");
342
            Contract.EndContractBlock();
343

    
344
            var monitor = Monitors[container.Account];
345
            var newInfo = monitor.CloudClient.GetContainerInfo(container.Account, container.Name);
346
            return newInfo;
347
        }
348

    
349

    
350
        public void ToggleSynching()
351
        {
352
            bool isPaused=false;
353
            foreach (var pair in Monitors)
354
            {
355
                var monitor = pair.Value;
356
                monitor.Pause = !monitor.Pause;
357
                isPaused = monitor.Pause;
358
            }
359

    
360
            PauseSyncCaption = isPaused ? "Resume syncing" : "Pause syncing";
361
            var iconKey = isPaused? "TraySyncPaused" : "TrayInSynch";
362
            StatusIcon = String.Format(@"Images/{0}.ico", iconKey);
363
        }
364

    
365
        public void ExitPithos()
366
        {
367
            foreach (var pair in Monitors)
368
            {
369
                var monitor = pair.Value;
370
                monitor.Stop();
371
            }
372

    
373
            ((Window)GetView()).Close();
374
        }
375
        #endregion
376

    
377

    
378
        private Dictionary<PithosStatus, StatusInfo> iconNames = new List<StatusInfo>
379
            {
380
                new StatusInfo(PithosStatus.InSynch, "All files up to date", "TrayInSynch"),
381
                new StatusInfo(PithosStatus.Syncing, "Syncing Files", "TraySynching"),
382
                new StatusInfo(PithosStatus.SyncPaused, "Sync Paused", "TraySyncPaused")
383
            }.ToDictionary(s => s.Status);
384

    
385
        readonly IWindowManager _windowManager;
386

    
387

    
388
        public void UpdateStatus()
389
        {
390
            var pithosStatus = _statusChecker.GetPithosStatus();
391

    
392
            if (iconNames.ContainsKey(pithosStatus))
393
            {
394
                var info = iconNames[pithosStatus];
395
                StatusIcon = String.Format(@"Images/{0}.ico", info.IconName);
396
                StatusMessage = String.Format("Pithos 1.0\r\n{0}", info.StatusText);
397
            }
398
            
399
            _events.Publish(new Notification { Title = "Start", Message = "Start Monitoring", Level = TraceLevel.Info});
400
        }
401

    
402

    
403
       
404
        private Task StartMonitor(PithosMonitor monitor,int retries=0)
405
        {
406
            return Task.Factory.StartNew(() =>
407
            {
408
                using (log4net.ThreadContext.Stacks["Monitor"].Push("Start"))
409
                {
410
                    try
411
                    {
412
                        Log.InfoFormat("Start Monitoring {0}", monitor.UserName);
413

    
414
                        monitor.Start();
415
                    }
416
                    catch (WebException exc)
417
                    {
418
                        if (AbandonRetry(monitor, retries))
419
                            return;
420

    
421
                        if (IsUnauthorized(exc))
422
                        {
423
                            var message = String.Format("API Key Expired for {0}. Starting Renewal",monitor.UserName);                            
424
                            Log.Error(message,exc);
425
                            TryAuthorize(monitor,retries).Wait();
426
                        }
427
                        else
428
                        {
429
                            TryLater(monitor, exc,retries);
430
                        }
431
                    }
432
                    catch (Exception exc)
433
                    {
434
                        if (AbandonRetry(monitor, retries)) 
435
                            return;
436

    
437
                        TryLater(monitor,exc,retries);
438
                    }
439
                }
440
            });
441
        }
442

    
443
        private bool AbandonRetry(PithosMonitor monitor, int retries)
444
        {
445
            if (retries > 1)
446
            {
447
                var message = String.Format("Monitoring of account {0} has failed too many times. Will not retry",
448
                                            monitor.UserName);
449
                _events.Publish(new Notification
450
                                    {Title = "Account monitoring failed", Message = message, Level = TraceLevel.Error});
451
                return true;
452
            }
453
            return false;
454
        }
455

    
456

    
457
        private Task TryAuthorize(PithosMonitor monitor,int retries)
458
        {
459
            _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 });
460

    
461
            var authorize= PithosAccount.RetrieveCredentialsAsync(Settings.PithosSite);
462

    
463
            return authorize.ContinueWith(t =>
464
            {
465
                if (t.IsFaulted)
466
                {                    
467
                    string message = String.Format("API Key retrieval for {0} failed", monitor.UserName);
468
                    Log.Error(message,t.Exception.InnerException);
469
                    _events.Publish(new Notification { Title = "Authorization failed", Message = message, Level = TraceLevel.Error });
470
                    return;
471
                }
472
                var credentials = t.Result;                
473
                var account =Settings.Accounts.FirstOrDefault(act => act.AccountName == credentials.UserName);
474
                account.ApiKey = credentials.Password;
475
                monitor.ApiKey = credentials.Password;
476
                Settings.Save();
477
                Task.Factory.StartNewDelayed(10000, () => StartMonitor(monitor,retries+1));
478
            });
479
        }
480

    
481
        private static bool IsUnauthorized(WebException exc)
482
        {
483
            if (exc==null)
484
                throw new ArgumentNullException("exc");
485
            Contract.EndContractBlock();
486

    
487
            var response = exc.Response as HttpWebResponse;
488
            if (response == null)
489
                return false;
490
            return (response.StatusCode == HttpStatusCode.Unauthorized);
491
        }
492

    
493
        private void TryLater(PithosMonitor monitor, Exception exc,int retries)
494
        {
495
            var message = String.Format("An exception occured. Can't start monitoring\nWill retry in 10 seconds");
496
            Task.Factory.StartNewDelayed(10000, () => StartMonitor(monitor,retries+1));
497
            _events.Publish(new Notification
498
                                {Title = "Error", Message = message, Level = TraceLevel.Error});
499
            Log.Error(message, exc);
500
        }
501

    
502

    
503
        public void NotifyChange(string status, TraceLevel level=TraceLevel.Info)
504
        {
505
            this.StatusMessage = status;
506
            
507
            _events.Publish(new Notification { Title = "Pithos", Message = status, Level = level });
508
        }
509

    
510
        public void NotifyChangedFile(string filePath)
511
        {
512
            var entry = new FileEntry {FullPath=filePath};
513
            IProducerConsumerCollection<FileEntry> files=this.RecentFiles;
514
            FileEntry popped;
515
            while (files.Count > 5)
516
                files.TryTake(out popped);
517
            files.TryAdd(entry);
518
        }
519

    
520
        public void NotifyAccount(AccountInfo account)
521
        {
522
            if (account== null)
523
                return;
524

    
525
            account.SiteUri= String.Format("{0}/ui/?token={1}&user={2}",
526
                Properties.Settings.Default.PithosSite, account.Token,
527
                account.UserName);
528

    
529
            IProducerConsumerCollection<AccountInfo> accounts = Accounts;
530
            for (var i = 0; i < _accounts.Count; i++)
531
            {
532
                AccountInfo item;
533
                if (accounts.TryTake(out item))
534
                {
535
                    if (item.UserName!=account.UserName)
536
                    {
537
                        accounts.TryAdd(item);
538
                    }
539
                }
540
            }
541

    
542
            accounts.TryAdd(account);
543
        }
544

    
545

    
546
        public void RemoveMonitor(string accountName)
547
        {
548
            if (String.IsNullOrWhiteSpace(accountName))
549
                return;
550

    
551
            PithosMonitor monitor;
552
            if (Monitors.TryGetValue(accountName, out monitor))
553
            {
554
                Monitors.Remove(accountName);
555
                monitor.Stop();
556
            }
557
        }
558

    
559
        public void RefreshOverlays()
560
        {
561
            foreach (var pair in Monitors)
562
            {
563
                var monitor = pair.Value;
564

    
565
                var path = monitor.RootPath;
566

    
567
                if (String.IsNullOrWhiteSpace(path))
568
                    continue;
569

    
570
                if (!Directory.Exists(path) && !File.Exists(path))
571
                    continue;
572

    
573
                IntPtr pathPointer = Marshal.StringToCoTaskMemAuto(path);
574

    
575
                try
576
                {
577
                    NativeMethods.SHChangeNotify(HChangeNotifyEventID.SHCNE_UPDATEITEM,
578
                                                 HChangeNotifyFlags.SHCNF_PATHW | HChangeNotifyFlags.SHCNF_FLUSHNOWAIT,
579
                                                 pathPointer, IntPtr.Zero);
580
                }
581
                finally
582
                {
583
                    Marshal.FreeHGlobal(pathPointer);
584
                }
585
            }
586
        }
587

    
588
        #region Event Handlers
589
        
590
        public void Handle(SelectiveSynchChanges message)
591
        {
592
            var accountName = message.Account.AccountName;
593
            PithosMonitor monitor;
594
            if (_monitors.TryGetValue(accountName, out monitor))
595
            {
596
                monitor.AddSelectivePaths(message.Added);
597
                monitor.RemoveSelectivePaths(message.Removed);
598

    
599
            }
600
            
601
        }
602

    
603

    
604
        public void Handle(Notification notification)
605
        {
606
            if (!Settings.ShowDesktopNotifications)
607
                return;
608
            BalloonIcon icon = BalloonIcon.None;
609
            switch (notification.Level)
610
            {
611
                case TraceLevel.Error:
612
                    icon = BalloonIcon.Error;
613
                    break;
614
                case TraceLevel.Info:
615
                case TraceLevel.Verbose:
616
                    icon = BalloonIcon.Info;
617
                    break;
618
                case TraceLevel.Warning:
619
                    icon = BalloonIcon.Warning;
620
                    break;
621
                default:
622
                    icon = BalloonIcon.None;
623
                    break;
624
            }
625

    
626
            if (Settings.ShowDesktopNotifications)
627
            {
628
                var tv = (ShellView) this.GetView();
629
                tv.TaskbarView.ShowBalloonTip(notification.Title, notification.Message, icon);
630
            }
631
        }
632
        #endregion
633

    
634
        public void Handle(ShowFilePropertiesEvent message)
635
        {
636
            if (message == null)
637
                throw new ArgumentNullException("message");
638
            if (String.IsNullOrWhiteSpace(message.FileName) )
639
                throw new ArgumentException("message");
640
            Contract.EndContractBlock();
641

    
642
            var fileName = message.FileName;
643

    
644
            if (File.Exists(fileName))
645
                this.ShowFileProperties(fileName);
646
            else if (Directory.Exists(fileName))
647
                this.ShowContainerProperties(fileName);
648
        }
649
    }
650
}