All containers and first level folders are automatically added to Selective Sync...
[pithos-ms-client] / trunk / Pithos.Client.WPF / Preferences / PreferencesViewModel.cs
1 #region
2 /* -----------------------------------------------------------------------
3  * <copyright file="PreferencesViewModel.cs" company="GRNet">
4  * 
5  * Copyright 2011-2012 GRNET S.A. All rights reserved.
6  *
7  * Redistribution and use in source and binary forms, with or
8  * without modification, are permitted provided that the following
9  * conditions are met:
10  *
11  *   1. Redistributions of source code must retain the above
12  *      copyright notice, this list of conditions and the following
13  *      disclaimer.
14  *
15  *   2. Redistributions in binary form must reproduce the above
16  *      copyright notice, this list of conditions and the following
17  *      disclaimer in the documentation and/or other materials
18  *      provided with the distribution.
19  *
20  *
21  * THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
22  * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
23  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
24  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
25  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
26  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
27  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
28  * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
29  * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
30  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
31  * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
32  * POSSIBILITY OF SUCH DAMAGE.
33  *
34  * The views and conclusions contained in the software and
35  * documentation are those of the authors and should not be
36  * interpreted as representing official policies, either expressed
37  * or implied, of GRNET S.A.
38  * </copyright>
39  * -----------------------------------------------------------------------
40  */
41 #endregion
42
43
44 using System.Collections.Concurrent;
45 using System.Collections.Generic;
46 using System.Collections.Specialized;
47 using System.ComponentModel.Composition;
48 using System.Diagnostics;
49 using System.IO;
50 using System.Net;
51 using System.Reflection;
52 using System.Threading.Tasks;
53 using System.Windows;
54 using System.Windows.Forms;
55 using Caliburn.Micro;
56 using Pithos.Client.WPF.Configuration;
57 using Pithos.Client.WPF.Properties;
58 using Pithos.Client.WPF.SelectiveSynch;
59 using Pithos.Client.WPF.Utils;
60 using Pithos.Core;
61 using Pithos.Interfaces;
62 using System;
63 using System.Linq;
64 using Pithos.Network;
65 using MessageBox = System.Windows.MessageBox;
66 using Screen = Caliburn.Micro.Screen;
67
68 namespace Pithos.Client.WPF.Preferences
69 {
70     /// <summary>
71     /// The preferences screen displays user and account settings and updates the PithosMonitor
72     /// classes when account settings change.
73     /// </summary>
74     /// <remarks>
75     /// The class is a single ViewModel for all Preferences tabs. It can be broken in separate
76     /// ViewModels, one for each tab.
77     /// </remarks>
78     [Export, PartCreationPolicy(CreationPolicy.Shared)]
79     public class PreferencesViewModel : Screen
80     {
81         private readonly IEventAggregator _events;
82
83         //Logging in the Pithos client is provided by log4net
84         private static readonly log4net.ILog Log = log4net.LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
85
86         private PithosSettings _settings;
87         public PithosSettings Settings
88         {
89             get { return _settings; }
90             set
91             {
92                 _settings = value;
93                 NotifyOfPropertyChange(()=>Settings);
94             }
95         }
96
97         private ObservableConcurrentCollection<AccountViewModel> _accounts;
98         public ObservableConcurrentCollection<AccountViewModel> Accounts
99         {
100             get { return _accounts; }
101             set 
102             { 
103                 _accounts = value;
104                 NotifyOfPropertyChange(()=>Accounts);
105             }
106         }
107         
108         public bool StartOnSystemStartup { get; set; }
109
110         public ShellViewModel Shell { get;  set; }
111         //ShellExtensionController _extensionController=new ShellExtensionController();
112
113         public PreferencesViewModel(IWindowManager windowManager, IEventAggregator events, ShellViewModel shell, PithosSettings settings, string currentTab)
114         {
115             // ReSharper disable DoNotCallOverridableMethodsInConstructor
116             //Caliburn.Micro uses DisplayName for the view's title
117             DisplayName = "Pithos+ Preferences";
118             // ReSharper restore DoNotCallOverridableMethodsInConstructor
119
120             _windowManager = windowManager;
121             _events = events;
122
123             Shell = shell;
124             
125             Settings=settings;
126             Accounts = new ObservableConcurrentCollection<AccountViewModel>();
127             if (settings.Accounts == null)
128             {
129                 settings.Accounts=new AccountsCollection();
130                 settings.Save();
131             }
132             var accountVMs = from account in settings.Accounts
133                              select new AccountViewModel(account);
134
135             Accounts.AddFromEnumerable(accountVMs);
136             
137             var startupPath = Environment.GetFolderPath(Environment.SpecialFolder.Startup);
138             _shortcutPath = Path.Combine(startupPath, "Pithos.lnk");
139
140
141             StartOnSystemStartup = File.Exists(_shortcutPath);
142
143             SelectedTab = currentTab;
144         }
145
146         private string _selectedTab="General";
147         public string SelectedTab
148         {
149             get { return _selectedTab; }
150             set
151             {
152                 _selectedTab = value??"General";
153                 NotifyOfPropertyChange(()=>SelectedTab);
154                 NotifyOfPropertyChange(() => AccountTabSelected);
155             }
156         }
157
158
159         public bool AccountTabSelected
160         {
161             get { return _selectedTab == "AccountTab"; }
162         }
163         #region Preferences Properties
164
165         private bool _noProxy;
166         public bool NoProxy
167         {
168             get { return _noProxy; }
169             set
170             {
171                 _noProxy = value;
172                 NotifyOfPropertyChange(()=>NoProxy);
173             }
174         }
175
176
177         private bool _defaultProxy;
178
179         public bool DefaultProxy
180         {
181             get { return _defaultProxy; }
182             set
183             {
184                 _defaultProxy = value;
185                 NotifyOfPropertyChange(() => DefaultProxy);
186             }
187         }
188
189
190         private bool _manualProxy;
191
192         public bool ManualProxy
193         {
194             get { return _manualProxy; }
195             set
196             {
197                 _manualProxy = value;
198                 NotifyOfPropertyChange(() => ManualProxy);
199             }
200         }
201         #endregion
202
203
204         public int StartupDelay
205         {
206             get { return (int) Settings.StartupDelay.TotalMinutes; }
207             set
208             {
209                 if (value<0)
210                     throw new ArgumentOutOfRangeException("value",Resources.PreferencesViewModel_StartupDelay_Greater_or_equal_to_0);
211                 Settings.StartupDelay = TimeSpan.FromMinutes(value);
212                 NotifyOfPropertyChange(()=>StartupDelay);
213             }
214         }
215        
216         #region Commands
217         
218         public bool CanSelectiveSyncFolders
219         {
220             get { return CurrentAccount != null; }
221         }
222
223         public void SelectiveSyncFolders()
224         {
225             //var monitor = Shell.Monitors[CurrentAccount.AccountKey];
226             
227
228             var model = new SelectiveSynchViewModel(/*monitor,*/_events,CurrentAccount.Account,CurrentAccount.ApiKey);
229             if (_windowManager.ShowDialog(model) == true)
230             {
231                 
232             }
233         }
234
235        /* private bool _networkTracing;
236         public bool NetworkTracing
237         {
238             get { return _networkTracing; }
239             set
240             {
241                 _networkTracing = value;
242                 
243             }
244         }*/
245
246         public void RefreshApiKey()
247         {
248             //_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 });
249             if (CurrentAccount == null)
250                 return;
251             try
252             {
253
254                 var name = CurrentAccount.AccountName;
255
256                 var loginUri = new Uri(new Uri(CurrentAccount.ServerUrl), "login");
257                 var credentials = PithosAccount.RetrieveCredentials(loginUri.ToString(),name);
258                 if (credentials==null)
259                     return;
260                 //The server will return credentials for a different account, not just the current account
261                 //We need to find the correct account first
262                 var account = Accounts.First(act => act.AccountName == credentials.UserName && act.ServerUrl == CurrentAccount.ServerUrl);
263                 account.ApiKey = credentials.Password;                
264                 account.IsExpired = false;
265                 Settings.Save();
266                 TaskEx.Delay(10000).ContinueWith(_ =>Shell.MonitorAccount(account.Account));
267                 NotifyOfPropertyChange(() => Accounts);
268             }
269             catch (AggregateException exc)
270             {
271                 string message = String.Format("API Key retrieval failed");
272                 Log.Error(message, exc.InnerException);
273                 _events.Publish(new Notification { Title = "Authorization failed", Message = message, Level = TraceLevel.Error });
274             }
275             catch (Exception exc)
276             {
277                 string message = String.Format("API Key retrieval failed");
278                 Log.Error(message, exc);
279                 _events.Publish(new Notification { Title = "Authorization failed", Message = message, Level = TraceLevel.Error });
280             }
281
282         }
283
284     
285         public void OpenLogPath()
286         {
287             Shell.OpenLogPath();
288         }
289
290         public void OpenLogConsole()
291         {
292             var logView=IoC.Get<LogConsole.LogConsoleViewModel>();            
293             _windowManager.ShowWindow(logView);
294         }
295
296         public void SaveChanges()
297         {
298             DoSave();
299             TryClose(true);
300         }
301
302         public void RejectChanges()
303         {
304             Settings.Reload();
305             TryClose(false);
306         }
307
308         public void ApplyChanges()
309         {
310             DoSave();
311         }
312
313         private void DoSave()
314         {
315             //SetStartupMode();            
316
317             //Ensure we save the settings changes first
318             foreach (var account in _accountsToRemove)
319             {
320                 Settings.Accounts.Remove(account);
321             }
322
323             foreach (var account in _accountsToAdd)
324             {
325                 Settings.Accounts.Add(account);    
326             }
327
328             Settings.Save();
329
330
331             try
332             {
333                 foreach (var account in _accountsToRemove)
334                 {
335                     Shell.RemoveMonitor(account.ServerUrl, account.AccountName);
336                     Shell.RemoveAccountFromDatabase(account);
337                 }
338
339                 foreach (var account in Settings.Accounts)
340                 {
341                     Shell.MonitorAccount(account);
342                 }
343             }                
344             finally
345             {
346                 _accountsToRemove.Clear();
347                 _accountsToAdd.Clear();
348             }
349
350             NotifyOfPropertyChange(()=>Settings);
351
352             if (IgnoreCertificateErrors)
353                 ServicePointManager.ServerCertificateValidationCallback= (sender,certificate,chain,errors)=> true;
354             else
355             {
356                 ServicePointManager.ServerCertificateValidationCallback = null;
357             }
358         }
359
360      /*   public void ChangePithosFolder()
361         {
362             var browser = new FolderBrowserDialog();
363             browser.SelectedPath = Settings.PithosPath;
364             var result = browser.ShowDialog((IWin32Window)GetView());
365             if (result == DialogResult.OK)
366             {
367                 var newPath = browser.SelectedPath;
368                 var accountName = CurrentAccount.AccountName;
369                 var monitor = Shell.Monitors[accountName];
370                 monitor.Stop();
371                 
372                 Shell.Monitors.Remove(accountName);
373
374                 Directory.Move(Settings.PithosPath, newPath);
375                 Settings.PithosPath = newPath;
376                 Settings.Save();
377
378                 Shell.MonitorAccount(CurrentAccount);
379
380                 NotifyOfPropertyChange(() => Settings);                
381             }
382         }
383 */
384
385
386         readonly List<AccountSettings> _accountsToAdd=new List<AccountSettings>();
387        public void AddAccount()
388        {
389            var wizard = new AddAccountViewModel();
390            if (_windowManager.ShowDialog(wizard) == true)
391            {
392                string selectedPath = wizard.AccountPath;
393                var initialRootPath = wizard.ShouldCreateOkeanosFolder?
394                    Path.Combine(selectedPath, "Okeanos")
395                    :selectedPath;
396                var actualRootPath= initialRootPath;
397                if (wizard.ShouldCreateOkeanosFolder)
398                {
399                    int attempt = 1;
400                    while (Directory.Exists(actualRootPath) || File.Exists(actualRootPath))
401                    {
402                        actualRootPath = String.Format("{0} {1}", initialRootPath, attempt++);
403                    }
404                }
405
406
407
408                var account = Accounts.FirstOrDefault(act => act.AccountName == wizard.AccountName && act.ServerUrl == wizard.CurrentServer);
409                if (account != null)
410                {
411                    if (MessageBox.Show("The account you specified already exists. Do you want to update it?", "The account exists") == MessageBoxResult.Yes)
412                    {
413                        account.ApiKey = wizard.Token;
414                        account.IsExpired = false;
415                        CurrentAccount = account;
416                    }
417                }
418                else
419                {
420                    var newAccount = new AccountSettings
421                                         {
422                                             AccountName = wizard.AccountName,
423                                             ServerUrl = wizard.CurrentServer,
424                                             ApiKey = wizard.Token,
425                                             RootPath = actualRootPath,
426                                             IsActive = wizard.IsAccountActive,
427                                         };
428
429
430                    var client = new CloudFilesClient(newAccount.AccountName, newAccount.ApiKey)
431                                     {
432                                         AuthenticationUrl = newAccount.ServerUrl, UsePithos = true
433                                     };
434                    client.Authenticate();
435
436
437                    var dirs = (from container in client.ListContainers(newAccount.AccountName)
438                                from dir in client.ListObjects(newAccount.AccountName, container.Name)
439                                where container.Name != "trash"
440                                select dir)
441                                .ToTree()
442                                .Select(d=>d.Uri.ToString())
443                                .ToArray();
444                               
445                    newAccount.SelectiveFolders.AddRange(dirs);
446
447                    //TODO:Add the "pithos" container as a default selection                   
448
449                    _accountsToAdd.Add(newAccount);
450                    var accountVm = new AccountViewModel(newAccount);
451                    (Accounts as IProducerConsumerCollection<AccountViewModel>).TryAdd(accountVm);
452                    CurrentAccount = accountVm;
453                }
454                NotifyOfPropertyChange(() => Accounts);
455                NotifyOfPropertyChange(() => Settings);   
456            }
457
458
459             
460        }
461
462 /*
463         public void AddPithosAccount()
464        {
465             var credentials=PithosAccount.RetrieveCredentials(null);
466             if (credentials == null)
467                 return;
468             var account = Settings.Accounts.FirstOrDefault(act => act.AccountName == credentials.UserName);
469             var accountVM = new AccountViewModel(account);
470             if (account == null)
471             {
472                 account=new AccountSettings{
473                     AccountName=credentials.UserName,
474                     ApiKey=credentials.Password
475                 };
476                 Settings.Accounts.Add(account);
477                 accountVM = new AccountViewModel(account);
478                 (Accounts as IProducerConsumerCollection<AccountViewModel>).TryAdd(accountVM);
479             }
480             else
481             {
482                 account.ApiKey=credentials.Password;
483             }
484             //SelectedAccountIndex= Settings.Accounts.IndexOf(account);
485             CurrentAccount = accountVM;
486             NotifyOfPropertyChange(() => Accounts);
487             NotifyOfPropertyChange(()=>Settings);                       
488        }
489 */
490
491
492         readonly List<AccountSettings> _accountsToRemove = new List<AccountSettings>();
493         public void RemoveAccount()
494         {
495             Accounts.TryRemove(CurrentAccount);
496             _accountsToRemove.Add(CurrentAccount.Account);
497
498             CurrentAccount = null;
499             NotifyOfPropertyChange(() => Accounts);
500
501             
502             //NotifyOfPropertyChange("Settings.Accounts");
503         }
504
505         public bool CanRemoveAccount
506         {
507             get { return (CurrentAccount != null); }
508         }
509
510         public bool CanClearAccountCache
511         {
512             get { return (CurrentAccount != null); }
513         }
514
515         public void ClearAccountCache()
516         {
517             if (MessageBoxResult.Yes == MessageBox.Show("You are about to delete all partially downloaded files from the account's cache.\n" +
518                             " You will have to download all partially downloaded data again\n" +
519                             "This change can not be undone\n\n" +
520             "Do you wish to delete all partially downloaded data?", "Warning! Clearing account cache",
521                             MessageBoxButton.YesNo,MessageBoxImage.Question,MessageBoxResult.No))
522             {
523
524                 var cachePath = Path.Combine(CurrentAccount.RootPath, FolderConstants.CacheFolder);
525                 var dir = new DirectoryInfo(cachePath);
526                 //The file may not exist if we just created the account
527                 if (!dir.Exists)
528                     return;
529                 dir.EnumerateFiles().Apply(file=>file.Delete());
530                 dir.EnumerateDirectories().Apply(folder => folder.Delete(true));
531             }
532         }
533
534
535         public bool ExtensionsActivated
536         {
537             get { return Settings.ExtensionsActivated; }
538             set
539             {
540                 if (Settings.ExtensionsActivated == value)
541                     return;
542
543                 Settings.ExtensionsActivated = value;
544
545 /*
546                 if (value)
547                     _extensionController.RegisterExtensions();
548                 else
549                 {
550                     _extensionController.UnregisterExtensions();
551                 }
552 */
553                 NotifyOfPropertyChange(() => ExtensionsActivated);
554             }
555         }
556
557         public bool DebugLoggingEnabled
558         {
559             get { return Settings.DebugLoggingEnabled; }
560             set { 
561                 Settings.DebugLoggingEnabled = value;
562                 NotifyOfPropertyChange(()=>DebugLoggingEnabled);
563             }
564         }
565
566         public bool IgnoreCertificateErrors
567         {
568             get { return Settings.IgnoreCertificateErrors; }
569             set {
570                 Settings.IgnoreCertificateErrors = value;
571                 NotifyOfPropertyChange(() => IgnoreCertificateErrors);
572             }
573         }
574        
575         #endregion
576
577        /* private int _selectedAccountIndex;
578         public int SelectedAccountIndex
579         {
580             get { return _selectedAccountIndex; }
581             set
582             {
583                 //var accountCount=Settings.Accounts.Count;
584                 //if (accountCount == 0)
585                 //    return;
586                 //if (0 <= value && value < accountCount)
587                 //    _selectedAccountIndex = value;
588                 //else
589                 //    _selectedAccountIndex = 0;
590                 _selectedAccountIndex = value;
591                 NotifyOfPropertyChange(() => CurrentAccount);
592                 NotifyOfPropertyChange(() => CanRemoveAccount);
593                 NotifyOfPropertyChange(()=>SelectedAccountIndex);
594             }
595         }*/
596
597         private AccountViewModel _currentAccount;
598         private readonly IWindowManager _windowManager;
599         private readonly string _shortcutPath;
600
601
602         
603         public AccountViewModel CurrentAccount
604         {
605             get { return _currentAccount; }
606             set
607             {
608                 _currentAccount = value;
609                 NotifyOfPropertyChange(()=>CurrentAccount);
610                 NotifyOfPropertyChange(() => CanRemoveAccount);
611                 NotifyOfPropertyChange(() => CanSelectiveSyncFolders);
612                 NotifyOfPropertyChange(() => CanMoveAccountFolder);
613                 NotifyOfPropertyChange(() => CanClearAccountCache);
614             }
615         }
616
617 /*
618         public AccountSettings CurrentAccount
619         {
620             get {
621                 if (0 <= SelectedAccountIndex && SelectedAccountIndex < Settings.Accounts.Count)                    
622                     return Settings.Accounts[SelectedAccountIndex];
623                 return null;
624             }
625
626         }
627 */
628
629
630         public bool CanMoveAccountFolder
631         {
632             get { return CurrentAccount != null; }
633         }
634
635     public void MoveAccountFolder()
636     {
637
638         using (var dlg = new FolderBrowserDialog())
639         {
640             var currentFolder = CurrentAccount.RootPath;
641             dlg.SelectedPath = currentFolder;
642             //Ask the user to select a folder
643             //Note: We need a parent window here, which we retrieve with GetView            
644             var view = (Window)GetView();            
645             if (DialogResult.OK != dlg.ShowDialog(new Wpf32Window(view)))
646                 return;            
647
648             var newPath= dlg.SelectedPath;                
649             //Find the account's monitor and stop it
650             PithosMonitor monitor;
651             if (Shell.Monitors.TryGetValue(CurrentAccount.AccountKey, out monitor))
652             {
653                 monitor.Stop();
654
655
656                 var oldPath = monitor.RootPath;
657                 //The old directory may not exist eg. if we create an account for the first time
658                 if (Directory.Exists(oldPath))
659                 {
660                     //If it does, do the move
661
662                     //Now Create all of the directories
663                     foreach (string dirPath in Directory.EnumerateDirectories(oldPath, "*",
664                                                            SearchOption.AllDirectories))
665                         Directory.CreateDirectory(dirPath.Replace(oldPath, newPath));
666
667                     //Copy all the files
668                     foreach (string newFilePath in Directory.EnumerateFiles(oldPath, "*.*",
669                                                                             SearchOption.AllDirectories))
670                         File.Copy(newFilePath, newFilePath.Replace(oldPath, newPath));
671
672                     Log.InfoFormat("Deleting account folder {0}",oldPath);
673                     Directory.Delete(oldPath, true);
674
675                     //We also need to change the path of the existing file states
676                     monitor.MoveFileStates(oldPath, newPath);
677                 }
678             }
679             //Replace the old rootpath with the new
680             CurrentAccount.RootPath = newPath;
681             //TODO: This will save all settings changes. Too coarse grained, need to fix at a later date
682             Settings.Save();            
683             //And start the monitor on the new RootPath            
684             if (monitor != null)
685             {
686                 monitor.RootPath = newPath;
687                 if (CurrentAccount.IsActive)
688                     monitor.Start();
689             }
690             else
691                 Shell.MonitorAccount(CurrentAccount.Account);
692             //Finally, notify that the Settings, CurrentAccount have changed
693             NotifyOfPropertyChange(() => CurrentAccount);
694             NotifyOfPropertyChange(() => Settings);
695
696         }
697     }
698     }
699 }