2 /* -----------------------------------------------------------------------
\r
3 * <copyright file="PreferencesViewModel.cs" company="GRNet">
\r
5 * Copyright 2011-2012 GRNET S.A. All rights reserved.
\r
7 * Redistribution and use in source and binary forms, with or
\r
8 * without modification, are permitted provided that the following
\r
9 * conditions are met:
\r
11 * 1. Redistributions of source code must retain the above
\r
12 * copyright notice, this list of conditions and the following
\r
15 * 2. Redistributions in binary form must reproduce the above
\r
16 * copyright notice, this list of conditions and the following
\r
17 * disclaimer in the documentation and/or other materials
\r
18 * provided with the distribution.
\r
21 * THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
\r
22 * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
\r
23 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
\r
24 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
\r
25 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
\r
26 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
\r
27 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
\r
28 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
\r
29 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
\r
30 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
\r
31 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
\r
32 * POSSIBILITY OF SUCH DAMAGE.
\r
34 * The views and conclusions contained in the software and
\r
35 * documentation are those of the authors and should not be
\r
36 * interpreted as representing official policies, either expressed
\r
37 * or implied, of GRNET S.A.
\r
39 * -----------------------------------------------------------------------
\r
44 using System.Collections.Concurrent;
\r
45 using System.Collections.Generic;
\r
46 using System.Collections.Specialized;
\r
47 using System.ComponentModel.Composition;
\r
48 using System.Diagnostics;
\r
51 using System.Reflection;
\r
52 using System.Threading.Tasks;
\r
53 using System.Windows;
\r
54 using System.Windows.Forms;
\r
55 using Caliburn.Micro;
\r
56 using Pithos.Client.WPF.Configuration;
\r
57 using Pithos.Client.WPF.Properties;
\r
58 using Pithos.Client.WPF.SelectiveSynch;
\r
59 using Pithos.Client.WPF.Utils;
\r
61 using Pithos.Core.Agents;
\r
62 using Pithos.Interfaces;
\r
65 using Pithos.Network;
\r
66 using MessageBox = System.Windows.MessageBox;
\r
67 using Screen = Caliburn.Micro.Screen;
\r
69 namespace Pithos.Client.WPF.Preferences
\r
72 /// The preferences screen displays user and account settings and updates the PithosMonitor
\r
73 /// classes when account settings change.
\r
76 /// The class is a single ViewModel for all Preferences tabs. It can be broken in separate
\r
77 /// ViewModels, one for each tab.
\r
79 [Export, PartCreationPolicy(CreationPolicy.Shared)]
\r
80 public class PreferencesViewModel : Screen
\r
82 private readonly IEventAggregator _events;
\r
84 //Logging in the Pithos client is provided by log4net
\r
85 private static readonly log4net.ILog Log =
\r
86 log4net.LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
\r
88 private PithosSettings _settings;
\r
90 public PithosSettings Settings
\r
92 get { return _settings; }
\r
96 NotifyOfPropertyChange(() => Settings);
\r
100 private ObservableConcurrentCollection<AccountViewModel> _accounts;
\r
102 public ObservableConcurrentCollection<AccountViewModel> Accounts
\r
104 get { return _accounts; }
\r
108 NotifyOfPropertyChange(() => Accounts);
\r
112 public bool StartOnSystemStartup { get; set; }
\r
114 public ShellViewModel Shell { get; set; }
\r
115 //ShellExtensionController _extensionController=new ShellExtensionController();
\r
117 [ImportingConstructor]
\r
118 public PreferencesViewModel(IWindowManager windowManager, IEventAggregator events, ShellViewModel shell,
\r
119 PithosSettings settings)
\r
121 this.DisplayName = "Pithos+ Preferences";
\r
123 // ReSharper disable DoNotCallOverridableMethodsInConstructor
\r
124 //Caliburn.Micro uses DisplayName for the view's title
\r
125 DisplayName = "Pithos+ Preferences";
\r
126 // ReSharper restore DoNotCallOverridableMethodsInConstructor
\r
128 _windowManager = windowManager;
\r
133 Settings = settings;
\r
134 Accounts = new ObservableConcurrentCollection<AccountViewModel>();
\r
137 var startupPath = Environment.GetFolderPath(Environment.SpecialFolder.Startup);
\r
138 _shortcutPath = Path.Combine(startupPath, "Pithos.lnk");
\r
141 StartOnSystemStartup = File.Exists(_shortcutPath);
\r
143 //SelectedTab = currentTab;
\r
146 protected override void OnViewLoaded(object view)
\r
148 base.OnViewLoaded(view);
\r
154 if (Settings.Accounts == null)
\r
156 Settings.Accounts = new AccountsCollection();
\r
159 var accountVMs = from account in Settings.Accounts
\r
160 select new AccountViewModel(account);
\r
162 Accounts.AddFromEnumerable(accountVMs);
\r
166 private string _selectedTab = "General";
\r
168 public string SelectedTab
\r
170 get { return _selectedTab; }
\r
173 _selectedTab = value ?? "GeneralTab";
\r
174 NotifyOfPropertyChange(() => SelectedTab);
\r
175 NotifyOfPropertyChange(() => AccountTabSelected);
\r
180 public bool AccountTabSelected
\r
182 get { return _selectedTab == "AccountTab"; }
\r
185 #region Preferences Properties
\r
187 private bool _noProxy;
\r
189 public bool NoProxy
\r
191 get { return _noProxy; }
\r
195 NotifyOfPropertyChange(() => NoProxy);
\r
200 private bool _defaultProxy;
\r
202 public bool DefaultProxy
\r
204 get { return _defaultProxy; }
\r
207 _defaultProxy = value;
\r
208 NotifyOfPropertyChange(() => DefaultProxy);
\r
213 private bool _manualProxy;
\r
215 public bool ManualProxy
\r
217 get { return _manualProxy; }
\r
220 _manualProxy = value;
\r
221 NotifyOfPropertyChange(() => ManualProxy);
\r
228 public int StartupDelay
\r
230 get { return (int) Settings.StartupDelay.TotalMinutes; }
\r
234 throw new ArgumentOutOfRangeException("value",
\r
236 PreferencesViewModel_StartupDelay_Greater_or_equal_to_0);
\r
237 Settings.StartupDelay = TimeSpan.FromMinutes(value);
\r
238 NotifyOfPropertyChange(() => StartupDelay);
\r
244 public bool CanSelectiveSyncFolders
\r
246 get { return CurrentAccount != null && CurrentAccount.SelectiveSyncEnabled; }
\r
249 public void SelectiveSyncFolders()
\r
251 //var monitor = Shell.Monitors[CurrentAccount.AccountKey];
\r
254 var model = new SelectiveSynchViewModel(_events, CurrentAccount.Account, CurrentAccount.ApiKey, false);
\r
255 if (_windowManager.ShowDialog(model) == true)
\r
261 /* private bool _networkTracing;
\r
262 public bool NetworkTracing
\r
264 get { return _networkTracing; }
\r
267 _networkTracing = value;
\r
272 public void RefreshApiKey()
\r
274 //_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 });
\r
275 if (CurrentAccount == null)
\r
280 var name = CurrentAccount.AccountName;
\r
282 var loginUri = new Uri(CurrentAccount.ServerUrl).Combine("login");
\r
283 var credentials = PithosAccount.RetrieveCredentials(loginUri.ToString(), name);
\r
284 if (credentials == null)
\r
286 //The server will return credentials for a different account, not just the current account
\r
287 //We need to find the correct account first
\r
290 act => act.AccountName == credentials.UserName && act.ServerUrl == CurrentAccount.ServerUrl);
\r
291 account.ApiKey = credentials.Password;
\r
292 account.IsExpired = false;
\r
294 TaskEx.Delay(10000).ContinueWith(_ => Shell.MonitorAccount(account.Account));
\r
295 NotifyOfPropertyChange(() => Accounts);
\r
297 catch (AggregateException exc)
\r
299 string message = String.Format("API Key retrieval failed");
\r
300 Log.Error(message, exc.InnerException);
\r
301 _events.Publish(new Notification
\r
302 {Title = "Authorization failed", Message = message, Level = TraceLevel.Error});
\r
304 catch (Exception exc)
\r
306 string message = String.Format("API Key retrieval failed");
\r
307 Log.Error(message, exc);
\r
308 _events.Publish(new Notification
\r
309 {Title = "Authorization failed", Message = message, Level = TraceLevel.Error});
\r
314 public bool CanWipeAccount
\r
316 get { return CurrentAccount != null
\r
317 && Shell.Monitors.ContainsKey(CurrentAccount.AccountKey)
\r
318 && Shell.Monitors[CurrentAccount.AccountKey].CloudClient!=null;
\r
322 public async void WipeAccount()
\r
324 PithosMonitor aMonitor;
\r
325 if (Shell.Monitors.TryGetValue(CurrentAccount.AccountKey, out aMonitor))
\r
327 if (aMonitor.CloudClient == null)
\r
329 Log.ErrorFormat("Tried to wipe account [{0}] before authenticating",CurrentAccount.AccountKey);
\r
332 var message = String.Format("You are about to wipe all data in the {0} account at {1}. Are you sure?",
\r
333 CurrentAccount.AccountName, CurrentAccount.ServerUrl);
\r
334 if (MessageBoxResult.Yes !=
\r
335 MessageBox.Show(message, "Warning! Account data will be wiped", MessageBoxButton.YesNo,
\r
336 MessageBoxImage.Hand, MessageBoxResult.No))
\r
339 await aMonitor.CloudClient.WipeContainer("", new Uri("pithos", UriKind.Relative));
\r
342 MessageBox.Show("Account wiped");
\r
347 public void OpenLogPath()
\r
349 Shell.OpenLogPath();
\r
352 public void OpenLogConsole()
\r
354 var logView = IoC.Get<LogConsole.LogConsoleViewModel>();
\r
355 _windowManager.ShowWindow(logView);
\r
358 public void SaveChanges()
\r
361 TryClose( /*true*/);
\r
364 public void RejectChanges()
\r
367 TryClose( /*false*/);
\r
370 public void ApplyChanges()
\r
375 private void DoSave()
\r
377 //SetStartupMode();
\r
379 //Ensure we save the settings changes first
\r
380 foreach (var account in _accountsToRemove)
\r
382 Settings.Accounts.Remove(account);
\r
385 foreach (var account in _accountsToAdd)
\r
387 Settings.Accounts.Add(account);
\r
395 foreach (var account in _accountsToRemove)
\r
397 Shell.RemoveMonitor(account.ServerUrl, account.AccountName);
\r
398 Shell.RemoveAccountFromDatabase(account);
\r
401 foreach (var account in Settings.Accounts)
\r
403 Shell.MonitorAccount(account);
\r
406 var poller = IoC.Get<PollAgent>();
\r
411 _accountsToRemove.Clear();
\r
412 _accountsToAdd.Clear();
\r
415 NotifyOfPropertyChange(() => Settings);
\r
417 if (IgnoreCertificateErrors)
\r
418 ServicePointManager.ServerCertificateValidationCallback = (sender, certificate, chain, errors) => true;
\r
421 ServicePointManager.ServerCertificateValidationCallback = null;
\r
425 /* public void ChangePithosFolder()
\r
427 var browser = new FolderBrowserDialog();
\r
428 browser.SelectedPath = Settings.PithosPath;
\r
429 var result = browser.ShowDialog((IWin32Window)GetView());
\r
430 if (result == DialogResult.OK)
\r
432 var newPath = browser.SelectedPath;
\r
433 var accountName = CurrentAccount.AccountName;
\r
434 var monitor = Shell.Monitors[accountName];
\r
437 Shell.Monitors.Remove(accountName);
\r
439 Directory.Move(Settings.PithosPath, newPath);
\r
440 Settings.PithosPath = newPath;
\r
443 Shell.MonitorAccount(CurrentAccount);
\r
445 NotifyOfPropertyChange(() => Settings);
\r
451 private readonly List<AccountSettings> _accountsToAdd = new List<AccountSettings>();
\r
453 public async void AddAccount()
\r
455 var wizard = new AddAccountViewModel();
\r
456 if (_windowManager.ShowDialog(wizard) == true)
\r
460 string selectedPath = wizard.AccountPath;
\r
461 var initialRootPath = wizard.ShouldCreateOkeanosFolder
\r
462 ? Path.Combine(selectedPath, "Okeanos")
\r
464 var actualRootPath = initialRootPath;
\r
465 if (wizard.ShouldCreateOkeanosFolder)
\r
468 while (Directory.Exists(actualRootPath) || File.Exists(actualRootPath))
\r
470 actualRootPath = String.Format("{0} {1}", initialRootPath, attempt++);
\r
477 Accounts.FirstOrDefault(
\r
478 act => act.AccountName == wizard.AccountName && act.ServerUrl == wizard.CurrentServer);
\r
479 if (account != null)
\r
482 MessageBox.Show("The account you specified already exists. Do you want to update it?",
\r
483 "The account exists") == MessageBoxResult.Yes)
\r
485 account.ApiKey = wizard.Token;
\r
486 account.IsExpired = false;
\r
487 CurrentAccount = account;
\r
492 var newAccount = new AccountSettings
\r
494 AccountName = wizard.AccountName,
\r
495 ServerUrl = wizard.CurrentServer,
\r
496 ApiKey = wizard.Token,
\r
497 RootPath = actualRootPath,
\r
498 IsActive = wizard.IsAccountActive,
\r
502 var client = new CloudFilesClient(newAccount.AccountName, newAccount.ApiKey)
\r
504 AuthenticationUrl = newAccount.ServerUrl,
\r
508 await InitializeSelectiveFolders(newAccount, client);
\r
511 //TODO:Add the "pithos" container as a default selection
\r
513 _accountsToAdd.Add(newAccount);
\r
514 var accountVm = new AccountViewModel(newAccount);
\r
515 (Accounts as IProducerConsumerCollection<AccountViewModel>).TryAdd(accountVm);
\r
516 CurrentAccount = accountVm;
\r
518 NotifyOfPropertyChange(() => Accounts);
\r
519 NotifyOfPropertyChange(() => Settings);
\r
521 catch (WebException exc)
\r
523 Log.ErrorFormat("[Add Account] Connectivity Error: {0}", exc);
\r
524 MessageBox.Show("Unable to connect to Pithos. Please try again later", "Connectivity Error",
\r
525 MessageBoxButton.OK, MessageBoxImage.Exclamation);
\r
533 private async Task InitializeSelectiveFolders(AccountSettings newAccount, CloudFilesClient client,
\r
540 await client.Authenticate().ConfigureAwait(false);
\r
542 var containers = await client.ListContainers(newAccount.AccountName).ConfigureAwait(false);
\r
543 var containerUris = from container in containers
\r
544 select String.Format(@"{0}/v1/{1}/{2}",
\r
545 newAccount.ServerUrl, newAccount.AccountName,
\r
548 newAccount.SelectiveFolders.AddRange(containerUris.ToArray());
\r
550 var objectInfos = (from container in containers
\r
551 from dir in client.ListObjects(newAccount.AccountName, container.Name)
\r
552 where container.Name.ToString() != "trash"
\r
553 select dir).ToList();
\r
554 var tree = objectInfos.ToTree();
\r
556 var selected = (from root in tree
\r
558 select child.Uri.ToString()).ToArray();
\r
559 newAccount.SelectiveFolders.AddRange(selected);
\r
562 catch (WebException)
\r
573 public void AddPithosAccount()
\r
575 var credentials=PithosAccount.RetrieveCredentials(null);
\r
576 if (credentials == null)
\r
578 var account = Settings.Accounts.FirstOrDefault(act => act.AccountName == credentials.UserName);
\r
579 var accountVM = new AccountViewModel(account);
\r
580 if (account == null)
\r
582 account=new AccountSettings{
\r
583 AccountName=credentials.UserName,
\r
584 ApiKey=credentials.Password
\r
586 Settings.Accounts.Add(account);
\r
587 accountVM = new AccountViewModel(account);
\r
588 (Accounts as IProducerConsumerCollection<AccountViewModel>).TryAdd(accountVM);
\r
592 account.ApiKey=credentials.Password;
\r
594 //SelectedAccountIndex= Settings.Accounts.IndexOf(account);
\r
595 CurrentAccount = accountVM;
\r
596 NotifyOfPropertyChange(() => Accounts);
\r
597 NotifyOfPropertyChange(()=>Settings);
\r
602 private readonly List<AccountSettings> _accountsToRemove = new List<AccountSettings>();
\r
604 public void RemoveAccount()
\r
606 Accounts.TryRemove(CurrentAccount);
\r
607 _accountsToRemove.Add(CurrentAccount.Account);
\r
609 CurrentAccount = null;
\r
610 NotifyOfPropertyChange(() => Accounts);
\r
613 //NotifyOfPropertyChange("Settings.Accounts");
\r
616 public bool CanRemoveAccount
\r
618 get { return (CurrentAccount != null); }
\r
621 public bool CanClearAccountCache
\r
623 get { return (CurrentAccount != null); }
\r
626 public void ClearAccountCache()
\r
628 if (MessageBoxResult.Yes ==
\r
629 MessageBox.Show("You are about to delete all partially downloaded files from the account's cache.\n" +
\r
630 " You will have to download all partially downloaded data again\n" +
\r
631 "This change can not be undone\n\n" +
\r
632 "Do you wish to delete all partially downloaded data?",
\r
633 "Warning! Clearing account cache",
\r
634 MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No))
\r
637 var cachePath = Path.Combine(CurrentAccount.RootPath, FolderConstants.CacheFolder);
\r
638 var dir = new DirectoryInfo(cachePath);
\r
639 //The file may not exist if we just created the account
\r
642 dir.EnumerateFiles().Apply(file => file.Delete());
\r
643 dir.EnumerateDirectories().Apply(folder => folder.Delete(true));
\r
648 public bool ExtensionsActivated
\r
650 get { return Settings.ExtensionsActivated; }
\r
653 if (Settings.ExtensionsActivated == value)
\r
656 Settings.ExtensionsActivated = value;
\r
660 _extensionController.RegisterExtensions();
\r
663 _extensionController.UnregisterExtensions();
\r
666 NotifyOfPropertyChange(() => ExtensionsActivated);
\r
670 public bool DebugLoggingEnabled
\r
672 get { return Settings.DebugLoggingEnabled; }
\r
675 Settings.DebugLoggingEnabled = value;
\r
676 NotifyOfPropertyChange(() => DebugLoggingEnabled);
\r
680 public bool IgnoreCertificateErrors
\r
682 get { return Settings.IgnoreCertificateErrors; }
\r
685 Settings.IgnoreCertificateErrors = value;
\r
686 NotifyOfPropertyChange(() => IgnoreCertificateErrors);
\r
692 /* private int _selectedAccountIndex;
\r
693 public int SelectedAccountIndex
\r
695 get { return _selectedAccountIndex; }
\r
698 //var accountCount=Settings.Accounts.Count;
\r
699 //if (accountCount == 0)
\r
701 //if (0 <= value && value < accountCount)
\r
702 // _selectedAccountIndex = value;
\r
704 // _selectedAccountIndex = 0;
\r
705 _selectedAccountIndex = value;
\r
706 NotifyOfPropertyChange(() => CurrentAccount);
\r
707 NotifyOfPropertyChange(() => CanRemoveAccount);
\r
708 NotifyOfPropertyChange(()=>SelectedAccountIndex);
\r
712 private AccountViewModel _currentAccount;
\r
713 private readonly IWindowManager _windowManager;
\r
714 private readonly string _shortcutPath;
\r
718 public AccountViewModel CurrentAccount
\r
720 get { return _currentAccount; }
\r
723 _currentAccount = value;
\r
725 if (_currentAccount != null)
\r
726 _currentAccount.PropertyChanged += (o, e) => NotifyOfPropertyChange(() => CanSelectiveSyncFolders);
\r
728 NotifyOfPropertyChange(() => CurrentAccount);
\r
729 NotifyOfPropertyChange(() => CanRemoveAccount);
\r
730 NotifyOfPropertyChange(() => CanSelectiveSyncFolders);
\r
731 NotifyOfPropertyChange(() => CanMoveAccountFolder);
\r
732 NotifyOfPropertyChange(() => CanClearAccountCache);
\r
733 NotifyOfPropertyChange(() => CanWipeAccount);
\r
738 public AccountSettings CurrentAccount
\r
741 if (0 <= SelectedAccountIndex && SelectedAccountIndex < Settings.Accounts.Count)
\r
742 return Settings.Accounts[SelectedAccountIndex];
\r
750 public bool CanMoveAccountFolder
\r
752 get { return CurrentAccount != null; }
\r
755 public void MoveAccountFolder()
\r
758 using (var dlg = new FolderBrowserDialog())
\r
760 var currentFolder = CurrentAccount.RootPath;
\r
761 dlg.SelectedPath = currentFolder;
\r
762 //Ask the user to select a folder
\r
763 //Note: We need a parent window here, which we retrieve with GetView
\r
764 var view = (Window) GetView();
\r
765 if (DialogResult.OK != dlg.ShowDialog(new Wpf32Window(view)))
\r
768 var newPath = dlg.SelectedPath;
\r
769 //Find the account's monitor and stop it
\r
770 PithosMonitor monitor;
\r
771 if (Shell.Monitors.TryGetValue(CurrentAccount.AccountKey, out monitor))
\r
773 //Problem: Doesn't stop the poll agent
\r
776 monitor.MoveRootFolder(newPath);
\r
780 //Replace the old rootpath with the new
\r
781 CurrentAccount.RootPath = newPath;
\r
782 //TODO: This will save all settings changes. Too coarse grained, need to fix at a later date
\r
784 //And start the monitor on the new RootPath
\r
785 if (monitor != null)
\r
787 monitor.RootPath = newPath;
\r
788 if (CurrentAccount.IsActive)
\r
789 Shell.StartMonitor(monitor);
\r
793 Shell.MonitorAccount(CurrentAccount.Account);
\r
794 //Finally, notify that the Settings, CurrentAccount have changed
\r
795 NotifyOfPropertyChange(() => CurrentAccount);
\r
796 NotifyOfPropertyChange(() => Settings);
\r