2 /* -----------------------------------------------------------------------
3 * <copyright file="ShellViewModel.cs" company="GRNet">
5 * Copyright 2011-2012 GRNET S.A. All rights reserved.
7 * Redistribution and use in source and binary forms, with or
8 * without modification, are permitted provided that the following
11 * 1. Redistributions of source code must retain the above
12 * copyright notice, this list of conditions and the following
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.
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.
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.
39 * -----------------------------------------------------------------------
42 using System.Collections.Concurrent;
43 using System.Diagnostics;
44 using System.Diagnostics.Contracts;
47 using System.Reflection;
48 using System.Runtime.InteropServices;
49 using System.ServiceModel;
50 using System.Threading.Tasks;
52 using System.Windows.Controls.Primitives;
54 using Hardcodet.Wpf.TaskbarNotification;
55 using Pithos.Client.WPF.Configuration;
56 using Pithos.Client.WPF.FileProperties;
57 using Pithos.Client.WPF.Preferences;
58 using Pithos.Client.WPF.Properties;
59 using Pithos.Client.WPF.SelectiveSynch;
60 using Pithos.Client.WPF.Services;
61 using Pithos.Client.WPF.Shell;
63 using Pithos.Core.Agents;
64 using Pithos.Interfaces;
66 using System.Collections.Generic;
69 using StatusService = Pithos.Client.WPF.Services.StatusService;
71 namespace Pithos.Client.WPF {
72 using System.ComponentModel.Composition;
76 /// The "shell" of the Pithos application displays the taskbar icon, menu and notifications.
77 /// The shell also hosts the status service called by shell extensions to retrieve file info
80 /// It is a strange "shell" as its main visible element is an icon instead of a window
81 /// The shell subscribes to the following events:
82 /// * Notification: Raised by components that want to notify the user. Usually displayed in a balloon
83 /// * SelectiveSynchChanges: Notifies that the user made changes to the selective synch folders for an account. Raised by the Selective Synch dialog. Located here because the monitors are here
84 /// * ShowFilePropertiesEvent: Raised when a shell command requests the display of the file/container properties dialog
86 //TODO: CODE SMELL Why does the shell handle the SelectiveSynchChanges?
87 [Export(typeof(IShell))]
88 public class ShellViewModel : Screen, IStatusNotification, IShell,
89 IHandle<Notification>, IHandle<SelectiveSynchChanges>, IHandle<ShowFilePropertiesEvent>
91 //The Status Checker provides the current synch state
92 //TODO: Could we remove the status checker and use events in its place?
93 private readonly IStatusChecker _statusChecker;
94 private readonly IEventAggregator _events;
96 public PithosSettings Settings { get; private set; }
99 private readonly ConcurrentDictionary<string, PithosMonitor> _monitors = new ConcurrentDictionary<string, PithosMonitor>();
101 /// Dictionary of account monitors, keyed by account
104 /// One monitor class is created for each account. The Shell needs access to the monitors to execute start/stop/pause commands,
105 /// retrieve account and boject info
107 // TODO: Does the Shell REALLY need access to the monitors? Could we achieve the same results with a better design?
108 // TODO: The monitors should be internal to Pithos.Core, even though exposing them makes coding of the Object and Container windows easier
109 public ConcurrentDictionary<string, PithosMonitor> Monitors
111 get { return _monitors; }
116 /// The status service is used by Shell extensions to retrieve file status information
118 //TODO: CODE SMELL! This is the shell! While hosting in the shell makes executing start/stop commands easier, it is still a smell
119 private ServiceHost _statusService;
121 //Logging in the Pithos client is provided by log4net
122 private static readonly log4net.ILog Log = log4net.LogManager.GetLogger("Pithos");
124 //Lazily initialized File Version info. This is done once and lazily to avoid blocking the UI
125 private Lazy<FileVersionInfo> _fileVersion;
128 /// The Shell depends on MEF to provide implementations for windowManager, events, the status checker service and the settings
131 /// The PithosSettings class encapsulates the app's settings to abstract their storage mechanism (App settings, a database or registry)
133 [ImportingConstructor]
134 public ShellViewModel(IWindowManager windowManager, IEventAggregator events, IStatusChecker statusChecker, PithosSettings settings)
139 _windowManager = windowManager;
140 //CHECK: Caliburn doesn't need explicit command construction
141 //OpenPithosFolderCommand = new PithosCommand(OpenPithosFolder);
142 _statusChecker = statusChecker;
145 _events.Subscribe(this);
149 Proxy.SetFromSettings(settings);
151 StatusMessage = "In Synch";
153 _fileVersion= new Lazy<FileVersionInfo>(() =>
155 Assembly assembly = Assembly.GetExecutingAssembly();
156 var fileVersion = FileVersionInfo.GetVersionInfo(assembly.Location);
159 _accounts.CollectionChanged += (sender, e) =>
161 NotifyOfPropertyChange(() => OpenFolderCaption);
162 NotifyOfPropertyChange(() => HasAccounts);
166 catch (Exception exc)
168 Log.Error("Error while starting the ShellViewModel",exc);
174 protected override void OnActivate()
185 private async void StartMonitoring()
189 var accounts = Settings.Accounts.Select(MonitorAccount);
190 await TaskEx.WhenAll(accounts);
191 _statusService = StatusService.Start();
194 foreach (var account in Settings.Accounts)
196 await MonitorAccount(account);
201 catch (AggregateException exc)
205 Log.Error("Error while starting monitoring", e);
212 protected override void OnDeactivate(bool close)
214 base.OnDeactivate(close);
217 StatusService.Stop(_statusService);
218 _statusService = null;
222 public Task MonitorAccount(AccountSettings account)
224 return Task.Factory.StartNew(() =>
226 PithosMonitor monitor;
227 var accountName = account.AccountName;
229 if (_monitors.TryGetValue(accountName, out monitor))
231 //If the account is active
232 if (account.IsActive)
233 //Start the monitor. It's OK to start an already started monitor,
234 //it will just ignore the call
235 StartMonitor(monitor).Wait();
238 //If the account is inactive
239 //Stop and remove the monitor
240 RemoveMonitor(accountName);
246 //Create a new monitor/ Can't use MEF here, it would return a single instance for all monitors
247 monitor = new PithosMonitor
249 UserName = accountName,
250 ApiKey = account.ApiKey,
251 StatusNotification = this,
252 RootPath = account.RootPath
254 //PithosMonitor uses MEF so we need to resolve it
255 IoC.BuildUp(monitor);
257 monitor.AuthenticationUrl = account.ServerUrl;
259 _monitors[accountName] = monitor;
261 if (account.IsActive)
263 //Don't start a monitor if it doesn't have an account and ApiKey
264 if (String.IsNullOrWhiteSpace(monitor.UserName) ||
265 String.IsNullOrWhiteSpace(monitor.ApiKey))
267 StartMonitor(monitor);
273 protected override void OnViewLoaded(object view)
276 var window = (Window)view;
277 TaskEx.Delay(1000).ContinueWith(t => Execute.OnUIThread(window.Hide));
278 base.OnViewLoaded(view);
282 #region Status Properties
284 private string _statusMessage;
285 public string StatusMessage
287 get { return _statusMessage; }
290 _statusMessage = value;
291 NotifyOfPropertyChange(() => StatusMessage);
295 private readonly ObservableConcurrentCollection<AccountInfo> _accounts = new ObservableConcurrentCollection<AccountInfo>();
296 public ObservableConcurrentCollection<AccountInfo> Accounts
298 get { return _accounts; }
301 public bool HasAccounts
303 get { return _accounts.Count > 0; }
307 public string OpenFolderCaption
311 return (_accounts.Count == 0)
312 ? "No Accounts Defined"
313 : "Open Pithos Folder";
317 private string _pauseSyncCaption="Pause Synching";
318 public string PauseSyncCaption
320 get { return _pauseSyncCaption; }
323 _pauseSyncCaption = value;
324 NotifyOfPropertyChange(() => PauseSyncCaption);
328 private readonly ObservableConcurrentCollection<FileEntry> _recentFiles = new ObservableConcurrentCollection<FileEntry>();
329 public ObservableConcurrentCollection<FileEntry> RecentFiles
331 get { return _recentFiles; }
335 private string _statusIcon="../Images/Pithos.ico";
336 public string StatusIcon
338 get { return _statusIcon; }
341 //TODO: Ensure all status icons use the Pithos logo
343 NotifyOfPropertyChange(() => StatusIcon);
351 public void ShowPreferences()
353 ShowPreferences(null);
356 public void ShowPreferences(string currentTab)
359 var preferences = new PreferencesViewModel(_windowManager, _events, this, Settings,currentTab);
360 _windowManager.ShowDialog(preferences);
364 public void AboutPithos()
366 var about = new AboutViewModel();
367 _windowManager.ShowWindow(about);
370 public void SendFeedback()
372 var feedBack = IoC.Get<FeedbackViewModel>();
373 _windowManager.ShowWindow(feedBack);
376 //public PithosCommand OpenPithosFolderCommand { get; private set; }
378 public void OpenPithosFolder()
380 var account = Settings.Accounts.FirstOrDefault(acc => acc.IsActive);
383 Process.Start(account.RootPath);
386 public void OpenPithosFolder(AccountInfo account)
388 Process.Start(account.AccountPath);
393 public void GoToSite()
395 var site = Properties.Settings.Default.PithosSite;
400 public void GoToSite(AccountInfo account)
402 /*var site = String.Format("{0}/ui/?token={1}&user={2}",
403 account.SiteUri,account.Token,
405 Process.Start(account.SiteUri);
408 public void ShowFileProperties()
410 var account = Settings.Accounts.First(acc => acc.IsActive);
411 var dir = new DirectoryInfo(account.RootPath + @"\pithos");
412 var files=dir.GetFiles();
414 var idx=r.Next(0, files.Length);
415 ShowFileProperties(files[idx].FullName);
418 public void ShowFileProperties(string filePath)
420 if (String.IsNullOrWhiteSpace(filePath))
421 throw new ArgumentNullException("filePath");
422 if (!File.Exists(filePath) && !Directory.Exists(filePath))
423 throw new ArgumentException(String.Format("Non existent file {0}",filePath),"filePath");
424 Contract.EndContractBlock();
426 var pair=(from monitor in Monitors
427 where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase)
428 select monitor).FirstOrDefault();
429 var accountMonitor = pair.Value;
431 if (accountMonitor == null)
434 var infoTask=Task.Factory.StartNew(()=>accountMonitor.GetObjectInfo(filePath));
438 var fileProperties = new FilePropertiesViewModel(this, infoTask,filePath);
439 _windowManager.ShowWindow(fileProperties);
442 public void ShowContainerProperties()
444 var account = Settings.Accounts.First(acc => acc.IsActive);
445 var dir = new DirectoryInfo(account.RootPath);
446 var fullName = (from folder in dir.EnumerateDirectories()
447 where (folder.Attributes & FileAttributes.Hidden) == 0
448 select folder.FullName).First();
449 ShowContainerProperties(fullName);
452 public void ShowContainerProperties(string filePath)
454 if (String.IsNullOrWhiteSpace(filePath))
455 throw new ArgumentNullException("filePath");
456 if (!Directory.Exists(filePath))
457 throw new ArgumentException(String.Format("Non existent file {0}",filePath),"filePath");
458 Contract.EndContractBlock();
460 var pair=(from monitor in Monitors
461 where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase)
462 select monitor).FirstOrDefault();
463 var accountMonitor = pair.Value;
464 var info = accountMonitor.GetContainerInfo(filePath);
468 var containerProperties = new ContainerPropertiesViewModel(this, info,filePath);
469 _windowManager.ShowWindow(containerProperties);
472 public void SynchNow()
474 var agent = IoC.Get<PollAgent>();
478 public ObjectInfo RefreshObjectInfo(ObjectInfo currentInfo)
480 if (currentInfo==null)
481 throw new ArgumentNullException("currentInfo");
482 Contract.EndContractBlock();
484 var monitor = Monitors[currentInfo.Account];
485 var newInfo=monitor.CloudClient.GetObjectInfo(currentInfo.Account, currentInfo.Container, currentInfo.Name);
489 public ContainerInfo RefreshContainerInfo(ContainerInfo container)
491 if (container == null)
492 throw new ArgumentNullException("container");
493 Contract.EndContractBlock();
495 var monitor = Monitors[container.Account];
496 var newInfo = monitor.CloudClient.GetContainerInfo(container.Account, container.Name);
501 public void ToggleSynching()
504 foreach (var pair in Monitors)
506 var monitor = pair.Value;
507 monitor.Pause = !monitor.Pause;
508 isPaused = monitor.Pause;
511 PauseSyncCaption = isPaused ? "Resume syncing" : "Pause syncing";
512 var iconKey = isPaused? "TraySyncPaused" : "TrayInSynch";
513 StatusIcon = String.Format(@"../Images/{0}.ico", iconKey);
516 public void ExitPithos()
518 foreach (var pair in Monitors)
520 var monitor = pair.Value;
524 ((Window)GetView()).Close();
529 private readonly Dictionary<PithosStatus, StatusInfo> _iconNames = new List<StatusInfo>
531 new StatusInfo(PithosStatus.InSynch, "All files up to date", "TrayInSynch"),
532 new StatusInfo(PithosStatus.Syncing, "Syncing Files", "TraySynching"),
533 new StatusInfo(PithosStatus.SyncPaused, "Sync Paused", "TraySyncPaused")
534 }.ToDictionary(s => s.Status);
536 readonly IWindowManager _windowManager;
540 /// Updates the visual status indicators of the application depending on status changes, e.g. icon, stat
542 public void UpdateStatus()
544 var pithosStatus = _statusChecker.GetPithosStatus();
546 if (_iconNames.ContainsKey(pithosStatus))
548 var info = _iconNames[pithosStatus];
549 StatusIcon = String.Format(@"../Images/{0}.ico", info.IconName);
553 StatusMessage = String.Format("Pithos {0}\r\n{1}", _fileVersion.Value.FileVersion,info.StatusText);
556 //_events.Publish(new Notification { Title = "Start", Message = "Start Monitoring", Level = TraceLevel.Info});
561 private Task StartMonitor(PithosMonitor monitor,int retries=0)
563 return Task.Factory.StartNew(() =>
565 using (log4net.ThreadContext.Stacks["Monitor"].Push("Start"))
569 Log.InfoFormat("Start Monitoring {0}", monitor.UserName);
573 catch (WebException exc)
575 if (AbandonRetry(monitor, retries))
578 HttpStatusCode statusCode =HttpStatusCode.OK;
579 var response = exc.Response as HttpWebResponse;
581 statusCode = response.StatusCode;
585 case HttpStatusCode.Unauthorized:
586 var message = String.Format("API Key Expired for {0}. Starting Renewal",
588 Log.Error(message, exc);
589 var account = Settings.Accounts.Find(acc => acc.AccountName == monitor.UserName);
590 account.IsExpired = true;
591 Notify(new ExpirationNotification(account));
592 //TryAuthorize(monitor.UserName, retries).Wait();
594 case HttpStatusCode.ProxyAuthenticationRequired:
595 TryAuthenticateProxy(monitor,retries);
598 TryLater(monitor, exc, retries);
602 catch (Exception exc)
604 if (AbandonRetry(monitor, retries))
607 TryLater(monitor,exc,retries);
613 private void TryAuthenticateProxy(PithosMonitor monitor,int retries)
615 Execute.OnUIThread(() =>
617 var proxyAccount = IoC.Get<ProxyAccountViewModel>();
618 proxyAccount.Settings = this.Settings;
619 if (true != _windowManager.ShowDialog(proxyAccount))
621 StartMonitor(monitor, retries);
622 NotifyOfPropertyChange(() => Accounts);
626 private bool AbandonRetry(PithosMonitor monitor, int retries)
630 var message = String.Format("Monitoring of account {0} has failed too many times. Will not retry",
632 _events.Publish(new Notification
633 {Title = "Account monitoring failed", Message = message, Level = TraceLevel.Error});
640 public async Task TryAuthorize(string userName, int retries)
642 _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 });
647 var credentials = await PithosAccount.RetrieveCredentials(Settings.PithosLoginUrl);
649 var account = Settings.Accounts.First(act => act.AccountName == credentials.UserName);
650 //The server may return credentials for a different account
651 var monitor = _monitors[account.AccountName];
652 account.ApiKey = credentials.Password;
653 monitor.ApiKey = credentials.Password;
654 account.IsExpired = false;
656 TaskEx.Delay(10000).ContinueWith(_=>
657 StartMonitor(monitor, retries + 1));
658 NotifyOfPropertyChange(()=>Accounts);
660 catch (AggregateException exc)
662 string message = String.Format("API Key retrieval for {0} failed", userName);
663 Log.Error(message, exc.InnerException);
664 _events.Publish(new Notification { Title = "Authorization failed", Message = message, Level = TraceLevel.Error });
666 catch (Exception exc)
668 string message = String.Format("API Key retrieval for {0} failed", userName);
669 Log.Error(message, exc);
670 _events.Publish(new Notification { Title = "Authorization failed", Message = message, Level = TraceLevel.Error });
675 private static bool IsUnauthorized(WebException exc)
678 throw new ArgumentNullException("exc");
679 Contract.EndContractBlock();
681 var response = exc.Response as HttpWebResponse;
682 if (response == null)
684 return (response.StatusCode == HttpStatusCode.Unauthorized);
687 private void TryLater(PithosMonitor monitor, Exception exc,int retries)
689 var message = String.Format("An exception occured. Can't start monitoring\nWill retry in 10 seconds");
690 Task.Factory.StartNewDelayed(10000, () => StartMonitor(monitor,retries+1));
691 _events.Publish(new Notification
692 {Title = "Error", Message = message, Level = TraceLevel.Error});
693 Log.Error(message, exc);
697 public void NotifyChange(string status, TraceLevel level=TraceLevel.Info)
699 StatusMessage = status;
701 _events.Publish(new Notification { Title = "Pithos", Message = status, Level = level });
704 public void NotifyChangedFile(string filePath)
706 var entry = new FileEntry {FullPath=filePath};
707 IProducerConsumerCollection<FileEntry> files=RecentFiles;
709 while (files.Count > 5)
710 files.TryTake(out popped);
714 public void NotifyAccount(AccountInfo account)
718 //TODO: What happens to an existing account whose Token has changed?
719 account.SiteUri= String.Format("{0}/ui/?token={1}&user={2}",
720 account.SiteUri, Uri.EscapeDataString(account.Token),
721 Uri.EscapeDataString(account.UserName));
723 if (Accounts.All(item => item.UserName != account.UserName))
724 Accounts.TryAdd(account);
728 public void NotifyConflicts(IEnumerable<FileSystemInfo> conflictFiles, string message)
730 if (conflictFiles == null)
732 if (!conflictFiles.Any())
736 //TODO: Create a more specific message. For now, just show a warning
737 NotifyForFiles(conflictFiles,message,TraceLevel.Warning);
741 public void NotifyForFiles(IEnumerable<FileSystemInfo> files, string message,TraceLevel level=TraceLevel.Info)
748 StatusMessage = message;
750 _events.Publish(new Notification { Title = "Pithos", Message = message, Level = level});
753 public void Notify(Notification notification)
755 _events.Publish(notification);
759 public void RemoveMonitor(string accountName)
761 if (String.IsNullOrWhiteSpace(accountName))
764 var accountInfo=_accounts.FirstOrDefault(account => account.UserName == accountName);
765 _accounts.TryRemove(accountInfo);
767 PithosMonitor monitor;
768 if (Monitors.TryRemove(accountName, out monitor))
774 public void RefreshOverlays()
776 foreach (var pair in Monitors)
778 var monitor = pair.Value;
780 var path = monitor.RootPath;
782 if (String.IsNullOrWhiteSpace(path))
785 if (!Directory.Exists(path) && !File.Exists(path))
788 IntPtr pathPointer = Marshal.StringToCoTaskMemAuto(path);
792 NativeMethods.SHChangeNotify(HChangeNotifyEventID.SHCNE_UPDATEITEM,
793 HChangeNotifyFlags.SHCNF_PATHW | HChangeNotifyFlags.SHCNF_FLUSHNOWAIT,
794 pathPointer, IntPtr.Zero);
798 Marshal.FreeHGlobal(pathPointer);
803 #region Event Handlers
805 public void Handle(SelectiveSynchChanges message)
807 var accountName = message.Account.AccountName;
808 PithosMonitor monitor;
809 if (_monitors.TryGetValue(accountName, out monitor))
811 monitor.SetSelectivePaths(message.Uris,message.Added,message.Removed);
818 private bool _pollStarted = false;
820 //SMELL: Doing so much work for notifications in the shell is wrong
821 //The notifications should be moved to their own view/viewmodel pair
822 //and different templates should be used for different message types
823 //This will also allow the addition of extra functionality, eg. actions
825 public void Handle(Notification notification)
829 if (!Settings.ShowDesktopNotifications)
832 if (notification is PollNotification)
837 if (notification is CloudNotification)
842 notification.Title = "Pithos";
843 notification.Message = "Start Synchronisation";
846 if (String.IsNullOrWhiteSpace(notification.Message) && String.IsNullOrWhiteSpace(notification.Title))
850 switch (notification.Level)
852 case TraceLevel.Error:
853 icon = BalloonIcon.Error;
855 case TraceLevel.Info:
856 case TraceLevel.Verbose:
857 icon = BalloonIcon.Info;
859 case TraceLevel.Warning:
860 icon = BalloonIcon.Warning;
863 icon = BalloonIcon.None;
867 if (Settings.ShowDesktopNotifications)
869 var tv = (ShellView) GetView();
870 System.Action clickAction = null;
871 if (notification is ExpirationNotification)
873 clickAction = ()=>ShowPreferences("AccountTab");
875 var balloon=new PithosBalloon{Title=notification.Title,Message=notification.Message,Icon=icon,ClickAction=clickAction};
876 tv.TaskbarView.ShowCustomBalloon(balloon,PopupAnimation.Fade,4000);
877 // tv.TaskbarView.ShowBalloonTip(notification.Title, notification.Message, icon);
882 public void Handle(ShowFilePropertiesEvent message)
885 throw new ArgumentNullException("message");
886 if (String.IsNullOrWhiteSpace(message.FileName) )
887 throw new ArgumentException("message");
888 Contract.EndContractBlock();
890 var fileName = message.FileName;
891 //TODO: Display file properties for non-container folders
892 if (File.Exists(fileName))
893 //Retrieve the full name with exact casing. Pithos names are case sensitive
894 ShowFileProperties(FileInfoExtensions.GetProperFilePathCapitalization(fileName));
895 else if (Directory.Exists(fileName))
896 //Retrieve the full name with exact casing. Pithos names are case sensitive
898 var path = FileInfoExtensions.GetProperDirectoryCapitalization(fileName);
899 if (IsContainer(path))
900 ShowContainerProperties(path);
902 ShowFileProperties(path);
906 private bool IsContainer(string path)
908 var matchingFolders = from account in _accounts
909 from rootFolder in Directory.GetDirectories(account.AccountPath)
910 where rootFolder.Equals(path, StringComparison.InvariantCultureIgnoreCase)
912 return matchingFolders.Any();
915 public FileStatus GetFileStatus(string localFileName)
917 if (String.IsNullOrWhiteSpace(localFileName))
918 throw new ArgumentNullException("localFileName");
919 Contract.EndContractBlock();
921 var statusKeeper = IoC.Get<IStatusKeeper>();
922 var status=statusKeeper.GetFileStatus(localFileName);