Statistics
| Branch: | Revision:

root / trunk / Pithos.Client.WPF / ShellViewModel.cs @ ff26eb71

History | View | Annotate | Download (22.2 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
            StartMonitoring();                    
84
        }
85

    
86

    
87
        private async Task StartMonitoring()
88
        {
89
            try
90
            {                
91
                foreach (var account in Settings.Accounts)
92
                {
93
                    await MonitorAccount(account);
94
                }
95
                _statusService = StatusService.Start();
96
            }
97
            catch (AggregateException exc)
98
            {
99
                exc.Handle(e =>
100
                {
101
                    Log.Error("Error while starting monitoring", e);
102
                    return true;
103
                });
104
                throw;
105
            }
106
        }
107

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

    
118
        public Task MonitorAccount(AccountSettings account)
119
        {
120
            return Task.Factory.StartNew(() =>
121
            {                                
122
                throw new Exception("Smoo");
123
                PithosMonitor monitor = null;
124
                var accountName = account.AccountName;
125

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

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

    
154
                var appSettings = Properties.Settings.Default;
155
                monitor.AuthenticationUrl = account.UsePithos
156
                                                ? appSettings.PithosAuthenticationUrl
157
                                                : appSettings.CloudfilesAuthenticationUrl;
158

    
159
                _monitors[accountName] = monitor;
160

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

    
172

    
173
        protected override void OnViewLoaded(object view)
174
        {
175
            var window = (Window)view;
176
            window.Hide();
177
            UpdateStatus();
178
            base.OnViewLoaded(view);
179
        }
180

    
181

    
182
        #region Status Properties
183

    
184
        private string _statusMessage;
185
        public string StatusMessage
186
        {
187
            get { return _statusMessage; }
188
            set
189
            {
190
                _statusMessage = value;
191
                NotifyOfPropertyChange(() => StatusMessage);
192
            }
193
        }
194

    
195
        private ObservableConcurrentCollection<AccountInfo> _accounts = new ObservableConcurrentCollection<AccountInfo>();
196
        public ObservableConcurrentCollection<AccountInfo> Accounts
197
        {
198
            get { return _accounts; }
199
        }
200

    
201

    
202
        private string _pauseSyncCaption="Pause Syncing";
203
        public string PauseSyncCaption
204
        {
205
            get { return _pauseSyncCaption; }
206
            set
207
            {
208
                _pauseSyncCaption = value;
209
                NotifyOfPropertyChange(() => PauseSyncCaption);
210
            }
211
        }
212

    
213
        private readonly ObservableConcurrentCollection<FileEntry> _recentFiles = new ObservableConcurrentCollection<FileEntry>();
214
        public ObservableConcurrentCollection<FileEntry> RecentFiles
215
        {
216
            get { return _recentFiles; }
217
        }
218

    
219

    
220
        private string _statusIcon="Images/Tray.ico";
221
        public string StatusIcon
222
        {
223
            get { return _statusIcon; }
224
            set
225
            {
226
                _statusIcon = value;
227
                NotifyOfPropertyChange(() => StatusIcon);
228
            }
229
        }
230

    
231
        #endregion
232

    
233
        #region Commands
234

    
235
        public void ShowPreferences()
236
        {
237
            Settings.Reload();
238
            var preferences = new PreferencesViewModel(_windowManager,_events, this,Settings);            
239
            _windowManager.ShowDialog(preferences);
240
            
241
        }
242

    
243

    
244
        public PithosCommand OpenPithosFolderCommand { get; private set; }
245

    
246
        public void OpenPithosFolder()
247
        {
248
            var account = Settings.Accounts.FirstOrDefault(acc => acc.IsActive);
249
            if (account == null)
250
                return;
251
            Process.Start(account.RootPath);
252
        }
253

    
254
        public void OpenPithosFolder(AccountInfo account)
255
        {
256
            Process.Start(account.AccountPath);
257
        }
258

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

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

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

    
286
            var pair=(from monitor in  Monitors
287
                               where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase)
288
                                   select monitor).FirstOrDefault();
289
            var account = pair.Key;
290
            var accountMonitor = pair.Value;
291

    
292
            ObjectInfo info = accountMonitor.GetObjectInfo(filePath);
293

    
294
            
295

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

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

    
318
            var pair=(from monitor in  Monitors
319
                               where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase)
320
                                   select monitor).FirstOrDefault();
321
            var account = pair.Key;
322
            var accountMonitor = pair.Value;            
323
            ContainerInfo info = accountMonitor.GetContainerInfo(filePath);
324

    
325
            
326

    
327
            var containerProperties = new ContainerPropertiesViewModel(this, info,filePath);
328
            _windowManager.ShowWindow(containerProperties);
329
        }
330

    
331
        public ObjectInfo RefreshObjectInfo(ObjectInfo currentInfo)
332
        {
333
            if (currentInfo==null)
334
                throw new ArgumentNullException("currentInfo");
335
            Contract.EndContractBlock();
336

    
337
            var monitor = Monitors[currentInfo.Account];
338
            var newInfo=monitor.CloudClient.GetObjectInfo(currentInfo.Account, currentInfo.Container, currentInfo.Name);
339
            return newInfo;
340
        }
341

    
342
        public ContainerInfo RefreshContainerInfo(ContainerInfo container)
343
        {
344
            if (container == null)
345
                throw new ArgumentNullException("container");
346
            Contract.EndContractBlock();
347

    
348
            var monitor = Monitors[container.Account];
349
            var newInfo = monitor.CloudClient.GetContainerInfo(container.Account, container.Name);
350
            return newInfo;
351
        }
352

    
353

    
354
        public void ToggleSynching()
355
        {
356
            bool isPaused=false;
357
            foreach (var pair in Monitors)
358
            {
359
                var monitor = pair.Value;
360
                monitor.Pause = !monitor.Pause;
361
                isPaused = monitor.Pause;
362
            }
363

    
364
            PauseSyncCaption = isPaused ? "Resume syncing" : "Pause syncing";
365
            var iconKey = isPaused? "TraySyncPaused" : "TrayInSynch";
366
            StatusIcon = String.Format(@"Images/{0}.ico", iconKey);
367
        }
368

    
369
        public void ExitPithos()
370
        {
371
            foreach (var pair in Monitors)
372
            {
373
                var monitor = pair.Value;
374
                monitor.Stop();
375
            }
376

    
377
            ((Window)GetView()).Close();
378
        }
379
        #endregion
380

    
381

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

    
389
        readonly IWindowManager _windowManager;
390

    
391

    
392
        public void UpdateStatus()
393
        {
394
            var pithosStatus = _statusChecker.GetPithosStatus();
395

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

    
406

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

    
418
                        monitor.Start();
419
                    }
420
                    catch (WebException exc)
421
                    {
422
                        if (AbandonRetry(monitor, retries))
423
                            return;
424

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

    
441
                        TryLater(monitor,exc,retries);
442
                    }
443
                }
444
            });
445
        }
446

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

    
460

    
461
        private Task TryAuthorize(PithosMonitor monitor,int retries)
462
        {
463
            _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 });
464

    
465
            var authorize= PithosAccount.RetrieveCredentialsAsync(Settings.PithosSite);
466

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

    
485
        private static bool IsUnauthorized(WebException exc)
486
        {
487
            if (exc==null)
488
                throw new ArgumentNullException("exc");
489
            Contract.EndContractBlock();
490

    
491
            var response = exc.Response as HttpWebResponse;
492
            if (response == null)
493
                return false;
494
            return (response.StatusCode == HttpStatusCode.Unauthorized);
495
        }
496

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

    
506

    
507
        public void NotifyChange(string status, TraceLevel level=TraceLevel.Info)
508
        {
509
            this.StatusMessage = status;
510
            
511
            _events.Publish(new Notification { Title = "Pithos", Message = status, Level = level });
512
        }
513

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

    
524
        public void NotifyAccount(AccountInfo account)
525
        {
526
            if (account== null)
527
                return;
528

    
529
            account.SiteUri= String.Format("{0}/ui/?token={1}&user={2}",
530
                Properties.Settings.Default.PithosSite, account.Token,
531
                account.UserName);
532

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

    
546
            accounts.TryAdd(account);
547
        }
548

    
549

    
550
        public void RemoveMonitor(string accountName)
551
        {
552
            if (String.IsNullOrWhiteSpace(accountName))
553
                return;
554

    
555
            PithosMonitor monitor;
556
            if (Monitors.TryGetValue(accountName, out monitor))
557
            {
558
                Monitors.Remove(accountName);
559
                monitor.Stop();
560
            }
561
        }
562

    
563
        public void RefreshOverlays()
564
        {
565
            foreach (var pair in Monitors)
566
            {
567
                var monitor = pair.Value;
568

    
569
                var path = monitor.RootPath;
570

    
571
                if (String.IsNullOrWhiteSpace(path))
572
                    continue;
573

    
574
                if (!Directory.Exists(path) && !File.Exists(path))
575
                    continue;
576

    
577
                IntPtr pathPointer = Marshal.StringToCoTaskMemAuto(path);
578

    
579
                try
580
                {
581
                    NativeMethods.SHChangeNotify(HChangeNotifyEventID.SHCNE_UPDATEITEM,
582
                                                 HChangeNotifyFlags.SHCNF_PATHW | HChangeNotifyFlags.SHCNF_FLUSHNOWAIT,
583
                                                 pathPointer, IntPtr.Zero);
584
                }
585
                finally
586
                {
587
                    Marshal.FreeHGlobal(pathPointer);
588
                }
589
            }
590
        }
591

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

    
603
            }
604
            
605
        }
606

    
607

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

    
630
            if (Settings.ShowDesktopNotifications)
631
            {
632
                var tv = (ShellView) this.GetView();
633
                tv.TaskbarView.ShowBalloonTip(notification.Title, notification.Message, icon);
634
            }
635
        }
636
        #endregion
637

    
638
        public void Handle(ShowFilePropertiesEvent message)
639
        {
640
            if (message == null)
641
                throw new ArgumentNullException("message");
642
            if (String.IsNullOrWhiteSpace(message.FileName) )
643
                throw new ArgumentException("message");
644
            Contract.EndContractBlock();
645

    
646
            var fileName = message.FileName;
647

    
648
            if (File.Exists(fileName))
649
                this.ShowFileProperties(fileName);
650
            else if (Directory.Exists(fileName))
651
                this.ShowContainerProperties(fileName);
652
        }
653
    }
654
}