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.SelectiveSynch;
59 using Pithos.Client.WPF.Services;
60 using Pithos.Client.WPF.Shell;
62 using Pithos.Core.Agents;
63 using Pithos.Interfaces;
65 using System.Collections.Generic;
68 using StatusService = Pithos.Client.WPF.Services.StatusService;
70 namespace Pithos.Client.WPF {
71 using System.ComponentModel.Composition;
75 /// The "shell" of the Pithos application displays the taskbar icon, menu and notifications.
76 /// The shell also hosts the status service called by shell extensions to retrieve file info
79 /// It is a strange "shell" as its main visible element is an icon instead of a window
80 /// The shell subscribes to the following events:
81 /// * Notification: Raised by components that want to notify the user. Usually displayed in a balloon
82 /// * 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
83 /// * ShowFilePropertiesEvent: Raised when a shell command requests the display of the file/container properties dialog
85 //TODO: CODE SMELL Why does the shell handle the SelectiveSynchChanges?
86 [Export(typeof(IShell))]
87 public class ShellViewModel : Screen, IStatusNotification, IShell,
88 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(MethodBase.GetCurrentMethod().DeclaringType);
124 //Lazily initialized File Version info. This is done once and lazily to avoid blocking the UI
125 private readonly Lazy<FileVersionInfo> _fileVersion;
127 private readonly PollAgent _pollAgent;
130 /// The Shell depends on MEF to provide implementations for windowManager, events, the status checker service and the settings
133 /// The PithosSettings class encapsulates the app's settings to abstract their storage mechanism (App settings, a database or registry)
135 [ImportingConstructor]
136 public ShellViewModel(IWindowManager windowManager, IEventAggregator events, IStatusChecker statusChecker, PithosSettings settings,PollAgent pollAgent)
141 _windowManager = windowManager;
142 //CHECK: Caliburn doesn't need explicit command construction
143 //OpenPithosFolderCommand = new PithosCommand(OpenPithosFolder);
144 _statusChecker = statusChecker;
147 _events.Subscribe(this);
149 _pollAgent = pollAgent;
152 Proxy.SetFromSettings(settings);
154 StatusMessage = "In Synch";
156 _fileVersion= new Lazy<FileVersionInfo>(() =>
158 Assembly assembly = Assembly.GetExecutingAssembly();
159 var fileVersion = FileVersionInfo.GetVersionInfo(assembly.Location);
162 _accounts.CollectionChanged += (sender, e) =>
164 NotifyOfPropertyChange(() => OpenFolderCaption);
165 NotifyOfPropertyChange(() => HasAccounts);
169 catch (Exception exc)
171 Log.Error("Error while starting the ShellViewModel",exc);
178 protected override void OnActivate()
189 private async void StartMonitoring()
193 var accounts = Settings.Accounts.Select(MonitorAccount);
194 await TaskEx.WhenAll(accounts);
195 _statusService = StatusService.Start();
198 foreach (var account in Settings.Accounts)
200 await MonitorAccount(account);
205 catch (AggregateException exc)
209 Log.Error("Error while starting monitoring", e);
216 protected override void OnDeactivate(bool close)
218 base.OnDeactivate(close);
221 StatusService.Stop(_statusService);
222 _statusService = null;
226 public Task MonitorAccount(AccountSettings account)
228 return Task.Factory.StartNew(() =>
230 PithosMonitor monitor;
231 var accountName = account.AccountName;
233 if (_monitors.TryGetValue(accountName, out monitor))
235 //If the account is active
236 if (account.IsActive)
238 //The Api Key may have changed throuth the Preferences dialog
239 monitor.ApiKey = account.ApiKey;
240 Debug.Assert(monitor.StatusNotification == this,"An existing monitor should already have a StatusNotification service object");
241 monitor.RootPath = account.RootPath;
242 //Start the monitor. It's OK to start an already started monitor,
243 //it will just ignore the call
244 StartMonitor(monitor).Wait();
248 //If the account is inactive
249 //Stop and remove the monitor
250 RemoveMonitor(accountName);
256 //Create a new monitor/ Can't use MEF here, it would return a single instance for all monitors
257 monitor = new PithosMonitor
259 UserName = accountName,
260 ApiKey = account.ApiKey,
261 StatusNotification = this,
262 RootPath = account.RootPath
264 //PithosMonitor uses MEF so we need to resolve it
265 IoC.BuildUp(monitor);
267 monitor.AuthenticationUrl = account.ServerUrl;
269 _monitors[accountName] = monitor;
271 if (account.IsActive)
273 //Don't start a monitor if it doesn't have an account and ApiKey
274 if (String.IsNullOrWhiteSpace(monitor.UserName) ||
275 String.IsNullOrWhiteSpace(monitor.ApiKey))
277 StartMonitor(monitor);
283 protected override void OnViewLoaded(object view)
286 var window = (Window)view;
287 TaskEx.Delay(1000).ContinueWith(t => Execute.OnUIThread(window.Hide));
288 base.OnViewLoaded(view);
292 #region Status Properties
294 private string _statusMessage;
295 public string StatusMessage
297 get { return _statusMessage; }
300 _statusMessage = value;
301 NotifyOfPropertyChange(() => StatusMessage);
305 private readonly ObservableConcurrentCollection<AccountInfo> _accounts = new ObservableConcurrentCollection<AccountInfo>();
306 public ObservableConcurrentCollection<AccountInfo> Accounts
308 get { return _accounts; }
311 public bool HasAccounts
313 get { return _accounts.Count > 0; }
317 public string OpenFolderCaption
321 return (_accounts.Count == 0)
322 ? "No Accounts Defined"
323 : "Open Pithos Folder";
327 private string _pauseSyncCaption="Pause Synching";
328 public string PauseSyncCaption
330 get { return _pauseSyncCaption; }
333 _pauseSyncCaption = value;
334 NotifyOfPropertyChange(() => PauseSyncCaption);
338 private readonly ObservableConcurrentCollection<FileEntry> _recentFiles = new ObservableConcurrentCollection<FileEntry>();
339 public ObservableConcurrentCollection<FileEntry> RecentFiles
341 get { return _recentFiles; }
345 private string _statusIcon="../Images/Pithos.ico";
346 public string StatusIcon
348 get { return _statusIcon; }
351 //TODO: Ensure all status icons use the Pithos logo
353 NotifyOfPropertyChange(() => StatusIcon);
361 public void ShowPreferences()
363 ShowPreferences(null);
366 public void ShowPreferences(string currentTab)
369 var preferences = new PreferencesViewModel(_windowManager, _events, this, Settings,currentTab);
370 _windowManager.ShowDialog(preferences);
374 public void AboutPithos()
376 var about = new AboutViewModel();
377 _windowManager.ShowWindow(about);
380 public void SendFeedback()
382 var feedBack = IoC.Get<FeedbackViewModel>();
383 _windowManager.ShowWindow(feedBack);
386 //public PithosCommand OpenPithosFolderCommand { get; private set; }
388 public void OpenPithosFolder()
390 var account = Settings.Accounts.FirstOrDefault(acc => acc.IsActive);
393 Process.Start(account.RootPath);
396 public void OpenPithosFolder(AccountInfo account)
398 Process.Start(account.AccountPath);
403 public void GoToSite()
405 var site = Properties.Settings.Default.PithosSite;
410 public void GoToSite(AccountInfo account)
412 var uri = account.SiteUri.Replace("http://","https://");
417 /// Open an explorer window to the target path's directory
418 /// and select the file
420 /// <param name="entry"></param>
421 public void GoToFile(FileEntry entry)
423 var fullPath = entry.FullPath;
424 if (!File.Exists(fullPath) && !Directory.Exists(fullPath))
426 Process.Start("explorer.exe","/select, " + fullPath);
429 public void OpenLogPath()
431 var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
432 var pithosDataPath = Path.Combine(appDataPath, "GRNET");
434 Process.Start(pithosDataPath);
437 public void ShowFileProperties()
439 var account = Settings.Accounts.First(acc => acc.IsActive);
440 var dir = new DirectoryInfo(account.RootPath + @"\pithos");
441 var files=dir.GetFiles();
443 var idx=r.Next(0, files.Length);
444 ShowFileProperties(files[idx].FullName);
447 public void ShowFileProperties(string filePath)
449 if (String.IsNullOrWhiteSpace(filePath))
450 throw new ArgumentNullException("filePath");
451 if (!File.Exists(filePath) && !Directory.Exists(filePath))
452 throw new ArgumentException(String.Format("Non existent file {0}",filePath),"filePath");
453 Contract.EndContractBlock();
455 var pair=(from monitor in Monitors
456 where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase)
457 select monitor).FirstOrDefault();
458 var accountMonitor = pair.Value;
460 if (accountMonitor == null)
463 var infoTask=Task.Factory.StartNew(()=>accountMonitor.GetObjectInfo(filePath));
467 var fileProperties = new FilePropertiesViewModel(this, infoTask,filePath);
468 _windowManager.ShowWindow(fileProperties);
471 public void ShowContainerProperties()
473 var account = Settings.Accounts.First(acc => acc.IsActive);
474 var dir = new DirectoryInfo(account.RootPath);
475 var fullName = (from folder in dir.EnumerateDirectories()
476 where (folder.Attributes & FileAttributes.Hidden) == 0
477 select folder.FullName).First();
478 ShowContainerProperties(fullName);
481 public void ShowContainerProperties(string filePath)
483 if (String.IsNullOrWhiteSpace(filePath))
484 throw new ArgumentNullException("filePath");
485 if (!Directory.Exists(filePath))
486 throw new ArgumentException(String.Format("Non existent file {0}",filePath),"filePath");
487 Contract.EndContractBlock();
489 var pair=(from monitor in Monitors
490 where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase)
491 select monitor).FirstOrDefault();
492 var accountMonitor = pair.Value;
493 var info = accountMonitor.GetContainerInfo(filePath);
497 var containerProperties = new ContainerPropertiesViewModel(this, info,filePath);
498 _windowManager.ShowWindow(containerProperties);
501 public void SynchNow()
503 _pollAgent.SynchNow();
506 public ObjectInfo RefreshObjectInfo(ObjectInfo currentInfo)
508 if (currentInfo==null)
509 throw new ArgumentNullException("currentInfo");
510 Contract.EndContractBlock();
512 var monitor = Monitors[currentInfo.Account];
513 var newInfo=monitor.CloudClient.GetObjectInfo(currentInfo.Account, currentInfo.Container, currentInfo.Name);
517 public ContainerInfo RefreshContainerInfo(ContainerInfo container)
519 if (container == null)
520 throw new ArgumentNullException("container");
521 Contract.EndContractBlock();
523 var monitor = Monitors[container.Account];
524 var newInfo = monitor.CloudClient.GetContainerInfo(container.Account, container.Name);
529 public void ToggleSynching()
532 foreach (var pair in Monitors)
534 var monitor = pair.Value;
535 monitor.Pause = !monitor.Pause;
536 isPaused = monitor.Pause;
540 PauseSyncCaption = isPaused ? "Resume syncing" : "Pause syncing";
541 var iconKey = isPaused? "TraySyncPaused" : "TrayInSynch";
542 StatusIcon = String.Format(@"../Images/{0}.ico", iconKey);
545 public void ExitPithos()
547 foreach (var pair in Monitors)
549 var monitor = pair.Value;
553 ((Window)GetView()).Close();
558 private readonly Dictionary<PithosStatus, StatusInfo> _iconNames = new List<StatusInfo>
560 new StatusInfo(PithosStatus.InSynch, "All files up to date", "TrayInSynch"),
561 new StatusInfo(PithosStatus.PollSyncing, "Polling Files", "TraySynching"),
562 new StatusInfo(PithosStatus.LocalSyncing, "Syncing Files", "TraySynching"),
563 new StatusInfo(PithosStatus.SyncPaused, "Sync Paused", "TraySyncPaused")
564 }.ToDictionary(s => s.Status);
566 readonly IWindowManager _windowManager;
568 //private int _syncCount=0;
571 private PithosStatus _pithosStatus = PithosStatus.Disconnected;
573 public void SetPithosStatus(PithosStatus status)
575 if (_pithosStatus == PithosStatus.LocalSyncing && status == PithosStatus.PollComplete)
577 if (_pithosStatus == PithosStatus.PollSyncing && status == PithosStatus.LocalComplete)
579 if (status == PithosStatus.LocalComplete || status == PithosStatus.PollComplete)
580 _pithosStatus = PithosStatus.InSynch;
582 _pithosStatus = status;
586 public void SetPithosStatus(PithosStatus status,string message)
588 StatusMessage = message;
589 SetPithosStatus(status);
595 /// Updates the visual status indicators of the application depending on status changes, e.g. icon, stat
597 public void UpdateStatus()
600 if (_iconNames.ContainsKey(_pithosStatus))
602 var info = _iconNames[_pithosStatus];
603 StatusIcon = String.Format(@"../Images/{0}.ico", info.IconName);
606 if (_pithosStatus == PithosStatus.InSynch)
607 StatusMessage = "All files up to date";
612 private Task StartMonitor(PithosMonitor monitor,int retries=0)
614 return Task.Factory.StartNew(() =>
616 using (log4net.ThreadContext.Stacks["Monitor"].Push("Start"))
620 Log.InfoFormat("Start Monitoring {0}", monitor.UserName);
624 catch (WebException exc)
626 if (AbandonRetry(monitor, retries))
629 HttpStatusCode statusCode =HttpStatusCode.OK;
630 var response = exc.Response as HttpWebResponse;
632 statusCode = response.StatusCode;
636 case HttpStatusCode.Unauthorized:
637 var message = String.Format("API Key Expired for {0}. Starting Renewal",
639 Log.Error(message, exc);
640 var account = Settings.Accounts.Find(acc => acc.AccountName == monitor.UserName);
641 account.IsExpired = true;
642 Notify(new ExpirationNotification(account));
643 //TryAuthorize(monitor.UserName, retries).Wait();
645 case HttpStatusCode.ProxyAuthenticationRequired:
646 TryAuthenticateProxy(monitor,retries);
649 TryLater(monitor, exc, retries);
653 catch (Exception exc)
655 if (AbandonRetry(monitor, retries))
658 TryLater(monitor,exc,retries);
664 private void TryAuthenticateProxy(PithosMonitor monitor,int retries)
666 Execute.OnUIThread(() =>
668 var proxyAccount = IoC.Get<ProxyAccountViewModel>();
669 proxyAccount.Settings = Settings;
670 if (true != _windowManager.ShowDialog(proxyAccount))
672 StartMonitor(monitor, retries);
673 NotifyOfPropertyChange(() => Accounts);
677 private bool AbandonRetry(PithosMonitor monitor, int retries)
681 var message = String.Format("Monitoring of account {0} has failed too many times. Will not retry",
683 _events.Publish(new Notification
684 {Title = "Account monitoring failed", Message = message, Level = TraceLevel.Error});
691 private void TryLater(PithosMonitor monitor, Exception exc,int retries)
693 var message = String.Format("An exception occured. Can't start monitoring\nWill retry in 10 seconds");
694 Task.Factory.StartNewDelayed(10000, () => StartMonitor(monitor,retries+1));
695 _events.Publish(new Notification
696 {Title = "Error", Message = message, Level = TraceLevel.Error});
697 Log.Error(message, exc);
701 public void NotifyChange(string status, TraceLevel level=TraceLevel.Info)
703 StatusMessage = status;
705 _events.Publish(new Notification { Title = "Pithos", Message = status, Level = level });
708 public void NotifyChangedFile(string filePath)
710 if (RecentFiles.Any(e => e.FullPath == filePath))
713 IProducerConsumerCollection<FileEntry> files=RecentFiles;
715 while (files.Count > 5)
716 files.TryTake(out popped);
717 var entry = new FileEntry { FullPath = filePath };
721 public void NotifyAccount(AccountInfo account)
725 //TODO: What happens to an existing account whose Token has changed?
726 account.SiteUri= String.Format("{0}/ui/?token={1}&user={2}",
727 account.SiteUri, Uri.EscapeDataString(account.Token),
728 Uri.EscapeDataString(account.UserName));
730 if (Accounts.All(item => item.UserName != account.UserName))
731 Accounts.TryAdd(account);
735 public void NotifyConflicts(IEnumerable<FileSystemInfo> conflictFiles, string message)
737 if (conflictFiles == null)
739 //Convert to list to avoid multiple iterations
740 var files = conflictFiles.ToList();
745 //TODO: Create a more specific message. For now, just show a warning
746 NotifyForFiles(files,message,TraceLevel.Warning);
750 public void NotifyForFiles(IEnumerable<FileSystemInfo> files, string message,TraceLevel level=TraceLevel.Info)
757 StatusMessage = message;
759 _events.Publish(new Notification { Title = "Pithos", Message = message, Level = level});
762 public void Notify(Notification notification)
764 _events.Publish(notification);
768 public void RemoveMonitor(string accountName)
770 if (String.IsNullOrWhiteSpace(accountName))
773 var accountInfo=_accounts.FirstOrDefault(account => account.UserName == accountName);
774 if (accountInfo != null)
776 _accounts.TryRemove(accountInfo);
777 _pollAgent.RemoveAccount(accountInfo);
780 PithosMonitor monitor;
781 if (Monitors.TryRemove(accountName, out monitor))
784 //TODO: Also remove any pending actions for this account
785 //from the network queue
789 public void RefreshOverlays()
791 foreach (var pair in Monitors)
793 var monitor = pair.Value;
795 var path = monitor.RootPath;
797 if (String.IsNullOrWhiteSpace(path))
800 if (!Directory.Exists(path) && !File.Exists(path))
803 IntPtr pathPointer = Marshal.StringToCoTaskMemAuto(path);
807 NativeMethods.SHChangeNotify(HChangeNotifyEventID.SHCNE_UPDATEITEM,
808 HChangeNotifyFlags.SHCNF_PATHW | HChangeNotifyFlags.SHCNF_FLUSHNOWAIT,
809 pathPointer, IntPtr.Zero);
813 Marshal.FreeHGlobal(pathPointer);
818 #region Event Handlers
820 public void Handle(SelectiveSynchChanges message)
822 var accountName = message.Account.AccountName;
823 PithosMonitor monitor;
824 if (_monitors.TryGetValue(accountName, out monitor))
826 monitor.SetSelectivePaths(message.Uris,message.Added,message.Removed);
833 private bool _pollStarted;
835 //SMELL: Doing so much work for notifications in the shell is wrong
836 //The notifications should be moved to their own view/viewmodel pair
837 //and different templates should be used for different message types
838 //This will also allow the addition of extra functionality, eg. actions
840 public void Handle(Notification notification)
844 if (!Settings.ShowDesktopNotifications)
847 if (notification is PollNotification)
852 if (notification is CloudNotification)
857 notification.Title = "Pithos";
858 notification.Message = "Start Synchronisation";
861 var progress = notification as ProgressNotification;
862 if (progress != null)
864 StatusMessage = String.Format("Pithos {0}\r\n{1} {2:p2} of {3} - {4}",
865 _fileVersion.Value.FileVersion,
867 progress.Block/(double)progress.TotalBlocks,
868 progress.FileSize.ToByteSize(),
873 var info = notification as StatusNotification;
876 StatusMessage = String.Format("Pithos {0}\r\n{1}",
877 _fileVersion.Value.FileVersion,
881 if (String.IsNullOrWhiteSpace(notification.Message) && String.IsNullOrWhiteSpace(notification.Title))
884 ShowBalloonFor(notification);
887 private void ShowBalloonFor(Notification notification)
889 Contract.Requires(notification!=null);
891 if (!Settings.ShowDesktopNotifications)
895 switch (notification.Level)
897 case TraceLevel.Info:
898 case TraceLevel.Verbose:
900 case TraceLevel.Error:
901 icon = BalloonIcon.Error;
903 case TraceLevel.Warning:
904 icon = BalloonIcon.Warning;
910 var tv = (ShellView) GetView();
911 System.Action clickAction = null;
912 if (notification is ExpirationNotification)
914 clickAction = () => ShowPreferences("AccountTab");
916 var balloon = new PithosBalloon
918 Title = notification.Title,
919 Message = notification.Message,
921 ClickAction = clickAction
923 tv.TaskbarView.ShowCustomBalloon(balloon, PopupAnimation.Fade, 4000);
928 public void Handle(ShowFilePropertiesEvent message)
931 throw new ArgumentNullException("message");
932 if (String.IsNullOrWhiteSpace(message.FileName) )
933 throw new ArgumentException("message");
934 Contract.EndContractBlock();
936 var fileName = message.FileName;
937 //TODO: Display file properties for non-container folders
938 if (File.Exists(fileName))
939 //Retrieve the full name with exact casing. Pithos names are case sensitive
940 ShowFileProperties(FileInfoExtensions.GetProperFilePathCapitalization(fileName));
941 else if (Directory.Exists(fileName))
942 //Retrieve the full name with exact casing. Pithos names are case sensitive
944 var path = FileInfoExtensions.GetProperDirectoryCapitalization(fileName);
945 if (IsContainer(path))
946 ShowContainerProperties(path);
948 ShowFileProperties(path);
952 private bool IsContainer(string path)
954 var matchingFolders = from account in _accounts
955 from rootFolder in Directory.GetDirectories(account.AccountPath)
956 where rootFolder.Equals(path, StringComparison.InvariantCultureIgnoreCase)
958 return matchingFolders.Any();
961 public FileStatus GetFileStatus(string localFileName)
963 if (String.IsNullOrWhiteSpace(localFileName))
964 throw new ArgumentNullException("localFileName");
965 Contract.EndContractBlock();
967 var statusKeeper = IoC.Get<IStatusKeeper>();
968 var status=statusKeeper.GetFileStatus(localFileName);