#region /* ----------------------------------------------------------------------- * * * Copyright 2011-2012 GRNET S.A. All rights reserved. * * Redistribution and use in source and binary forms, with or * without modification, are permitted provided that the following * conditions are met: * * 1. Redistributions of source code must retain the above * copyright notice, this list of conditions and the following * disclaimer. * * 2. Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following * disclaimer in the documentation and/or other materials * provided with the distribution. * * * THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. * * The views and conclusions contained in the software and * documentation are those of the authors and should not be * interpreted as representing official policies, either expressed * or implied, of GRNET S.A. * * ----------------------------------------------------------------------- */ #endregion using System.Collections.Concurrent; using System.Diagnostics; using System.Diagnostics.Contracts; using System.IO; using System.Net; using System.Reflection; using System.Runtime.InteropServices; using System.ServiceModel; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls.Primitives; using AppLimit.NetSparkle; using Caliburn.Micro; using Hardcodet.Wpf.TaskbarNotification; using Pithos.Client.WPF.Configuration; using Pithos.Client.WPF.FileProperties; using Pithos.Client.WPF.Preferences; using Pithos.Client.WPF.SelectiveSynch; using Pithos.Client.WPF.Services; using Pithos.Client.WPF.Shell; using Pithos.Core; using Pithos.Core.Agents; using Pithos.Interfaces; using System; using System.Collections.Generic; using System.Linq; using Pithos.Network; using StatusService = Pithos.Client.WPF.Services.StatusService; namespace Pithos.Client.WPF { using System.ComponentModel.Composition; /// /// The "shell" of the Pithos application displays the taskbar icon, menu and notifications. /// The shell also hosts the status service called by shell extensions to retrieve file info /// /// /// It is a strange "shell" as its main visible element is an icon instead of a window /// The shell subscribes to the following events: /// * Notification: Raised by components that want to notify the user. Usually displayed in a balloon /// * 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 /// * ShowFilePropertiesEvent: Raised when a shell command requests the display of the file/container properties dialog /// //TODO: CODE SMELL Why does the shell handle the SelectiveSynchChanges? [Export(typeof(IShell))] public class ShellViewModel : Screen, IStatusNotification, IShell, IHandle, IHandle, IHandle { //The Status Checker provides the current synch state //TODO: Could we remove the status checker and use events in its place? private readonly IStatusChecker _statusChecker; private readonly IEventAggregator _events; public PithosSettings Settings { get; private set; } private readonly ConcurrentDictionary _monitors = new ConcurrentDictionary(); /// /// Dictionary of account monitors, keyed by account /// /// /// One monitor class is created for each account. The Shell needs access to the monitors to execute start/stop/pause commands, /// retrieve account and boject info /// // TODO: Does the Shell REALLY need access to the monitors? Could we achieve the same results with a better design? // TODO: The monitors should be internal to Pithos.Core, even though exposing them makes coding of the Object and Container windows easier public ConcurrentDictionary Monitors { get { return _monitors; } } /// /// The status service is used by Shell extensions to retrieve file status information /// //TODO: CODE SMELL! This is the shell! While hosting in the shell makes executing start/stop commands easier, it is still a smell private ServiceHost _statusService; //Logging in the Pithos client is provided by log4net private static readonly log4net.ILog Log = log4net.LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); //Lazily initialized File Version info. This is done once and lazily to avoid blocking the UI private readonly Lazy _fileVersion; private readonly PollAgent _pollAgent; /// /// The Shell depends on MEF to provide implementations for windowManager, events, the status checker service and the settings /// /// /// The PithosSettings class encapsulates the app's settings to abstract their storage mechanism (App settings, a database or registry) /// [ImportingConstructor] public ShellViewModel(IWindowManager windowManager, IEventAggregator events, IStatusChecker statusChecker, PithosSettings settings,PollAgent pollAgent) { try { _windowManager = windowManager; //CHECK: Caliburn doesn't need explicit command construction //OpenPithosFolderCommand = new PithosCommand(OpenPithosFolder); _statusChecker = statusChecker; //The event subst _events = events; _events.Subscribe(this); _pollAgent = pollAgent; Settings = settings; Proxy.SetFromSettings(settings); StatusMessage = "In Synch"; _fileVersion= new Lazy(() => { Assembly assembly = Assembly.GetExecutingAssembly(); var fileVersion = FileVersionInfo.GetVersionInfo(assembly.Location); return fileVersion; }); _accounts.CollectionChanged += (sender, e) => { NotifyOfPropertyChange(() => OpenFolderCaption); NotifyOfPropertyChange(() => HasAccounts); }; } catch (Exception exc) { Log.Error("Error while starting the ShellViewModel",exc); throw; } } protected override void OnActivate() { base.OnActivate(); _sparkle = new Sparkle(Settings.UpdateUrl); _sparkle.updateDetected += OnUpgradeDetected; _sparkle.ShowDiagnosticWindow = Settings.UpdateDiagnostics; //Must delay opening the upgrade window //to avoid Windows Messages sent by the TaskbarIcon TaskEx.Delay(5000).ContinueWith(_=> Execute.OnUIThread(()=> _sparkle.StartLoop(true,true,Settings.UpdateCheckInterval))); StartMonitoring(); } private void OnUpgradeDetected(object sender, UpdateDetectedEventArgs e) { Log.InfoFormat("Update detected {0}",e.LatestVersion); } public void CheckForUpgrade() { _sparkle.StopLoop(); _sparkle.Dispose(); _sparkle=new Sparkle(Settings.UpdateUrl); _sparkle.StartLoop(true,true,Settings.UpdateCheckInterval); } private async void StartMonitoring() { try { var accounts = Settings.Accounts.Select(MonitorAccount); await TaskEx.WhenAll(accounts); _statusService = StatusService.Start(); /* foreach (var account in Settings.Accounts) { await MonitorAccount(account); } */ } catch (AggregateException exc) { exc.Handle(e => { Log.Error("Error while starting monitoring", e); return true; }); throw; } } protected override void OnDeactivate(bool close) { base.OnDeactivate(close); if (close) { StatusService.Stop(_statusService); _statusService = null; } } public Task MonitorAccount(AccountSettings account) { return Task.Factory.StartNew(() => { PithosMonitor monitor; var accountName = account.AccountName; if (_monitors.TryGetValue(accountName, out monitor)) { //If the account is active if (account.IsActive) { //The Api Key may have changed throuth the Preferences dialog monitor.ApiKey = account.ApiKey; Debug.Assert(monitor.StatusNotification == this,"An existing monitor should already have a StatusNotification service object"); monitor.RootPath = account.RootPath; //Start the monitor. It's OK to start an already started monitor, //it will just ignore the call StartMonitor(monitor).Wait(); } else { //If the account is inactive //Stop and remove the monitor RemoveMonitor(accountName); } return; } //Create a new monitor/ Can't use MEF here, it would return a single instance for all monitors monitor = new PithosMonitor { UserName = accountName, ApiKey = account.ApiKey, StatusNotification = this, RootPath = account.RootPath }; //PithosMonitor uses MEF so we need to resolve it IoC.BuildUp(monitor); monitor.AuthenticationUrl = account.ServerUrl; _monitors[accountName] = monitor; if (account.IsActive) { //Don't start a monitor if it doesn't have an account and ApiKey if (String.IsNullOrWhiteSpace(monitor.UserName) || String.IsNullOrWhiteSpace(monitor.ApiKey)) return; StartMonitor(monitor); } }); } protected override void OnViewLoaded(object view) { UpdateStatus(); var window = (Window)view; TaskEx.Delay(1000).ContinueWith(t => Execute.OnUIThread(window.Hide)); base.OnViewLoaded(view); } #region Status Properties private string _statusMessage; public string StatusMessage { get { return _statusMessage; } set { _statusMessage = value; NotifyOfPropertyChange(() => StatusMessage); } } private readonly ObservableConcurrentCollection _accounts = new ObservableConcurrentCollection(); public ObservableConcurrentCollection Accounts { get { return _accounts; } } public bool HasAccounts { get { return _accounts.Count > 0; } } public string OpenFolderCaption { get { return (_accounts.Count == 0) ? "No Accounts Defined" : "Open Pithos Folder"; } } private string _pauseSyncCaption="Pause Synching"; public string PauseSyncCaption { get { return _pauseSyncCaption; } set { _pauseSyncCaption = value; NotifyOfPropertyChange(() => PauseSyncCaption); } } private readonly ObservableConcurrentCollection _recentFiles = new ObservableConcurrentCollection(); public ObservableConcurrentCollection RecentFiles { get { return _recentFiles; } } private string _statusIcon="../Images/Pithos.ico"; public string StatusIcon { get { return _statusIcon; } set { //TODO: Ensure all status icons use the Pithos logo _statusIcon = value; NotifyOfPropertyChange(() => StatusIcon); } } #endregion #region Commands public void ShowPreferences() { ShowPreferences(null); } public void ShowPreferences(string currentTab) { //Settings.Reload(); var preferences = new PreferencesViewModel(_windowManager, _events, this, Settings,currentTab); _windowManager.ShowDialog(preferences); } public void AboutPithos() { var about = new AboutViewModel(); _windowManager.ShowWindow(about); } public void SendFeedback() { var feedBack = IoC.Get(); _windowManager.ShowWindow(feedBack); } //public PithosCommand OpenPithosFolderCommand { get; private set; } public void OpenPithosFolder() { var account = Settings.Accounts.FirstOrDefault(acc => acc.IsActive); if (account == null) return; Process.Start(account.RootPath); } public void OpenPithosFolder(AccountInfo account) { Process.Start(account.AccountPath); } /* public void GoToSite() { var site = Properties.Settings.Default.PithosSite; Process.Start(site); } */ public void GoToSite(AccountInfo account) { var uri = account.SiteUri.Replace("http://","https://"); Process.Start(uri); } /// /// Open an explorer window to the target path's directory /// and select the file /// /// public void GoToFile(FileEntry entry) { var fullPath = entry.FullPath; if (!File.Exists(fullPath) && !Directory.Exists(fullPath)) return; Process.Start("explorer.exe","/select, " + fullPath); } public void OpenLogPath() { var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); var pithosDataPath = Path.Combine(appDataPath, "GRNET"); Process.Start(pithosDataPath); } public void ShowFileProperties() { var account = Settings.Accounts.First(acc => acc.IsActive); var dir = new DirectoryInfo(account.RootPath + @"\pithos"); var files=dir.GetFiles(); var r=new Random(); var idx=r.Next(0, files.Length); ShowFileProperties(files[idx].FullName); } public void ShowFileProperties(string filePath) { if (String.IsNullOrWhiteSpace(filePath)) throw new ArgumentNullException("filePath"); if (!File.Exists(filePath) && !Directory.Exists(filePath)) throw new ArgumentException(String.Format("Non existent file {0}",filePath),"filePath"); Contract.EndContractBlock(); var pair=(from monitor in Monitors where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase) select monitor).FirstOrDefault(); var accountMonitor = pair.Value; if (accountMonitor == null) return; var infoTask=Task.Factory.StartNew(()=>accountMonitor.GetObjectInfo(filePath)); var fileProperties = new FilePropertiesViewModel(this, infoTask,filePath); _windowManager.ShowWindow(fileProperties); } public void ShowContainerProperties() { var account = Settings.Accounts.First(acc => acc.IsActive); var dir = new DirectoryInfo(account.RootPath); var fullName = (from folder in dir.EnumerateDirectories() where (folder.Attributes & FileAttributes.Hidden) == 0 select folder.FullName).First(); ShowContainerProperties(fullName); } public void ShowContainerProperties(string filePath) { if (String.IsNullOrWhiteSpace(filePath)) throw new ArgumentNullException("filePath"); if (!Directory.Exists(filePath)) throw new ArgumentException(String.Format("Non existent file {0}",filePath),"filePath"); Contract.EndContractBlock(); var pair=(from monitor in Monitors where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase) select monitor).FirstOrDefault(); var accountMonitor = pair.Value; var info = accountMonitor.GetContainerInfo(filePath); var containerProperties = new ContainerPropertiesViewModel(this, info,filePath); _windowManager.ShowWindow(containerProperties); } public void SynchNow() { _pollAgent.SynchNow(); } public ObjectInfo RefreshObjectInfo(ObjectInfo currentInfo) { if (currentInfo==null) throw new ArgumentNullException("currentInfo"); Contract.EndContractBlock(); var monitor = Monitors[currentInfo.Account]; var newInfo=monitor.CloudClient.GetObjectInfo(currentInfo.Account, currentInfo.Container, currentInfo.Name); return newInfo; } public ContainerInfo RefreshContainerInfo(ContainerInfo container) { if (container == null) throw new ArgumentNullException("container"); Contract.EndContractBlock(); var monitor = Monitors[container.Account]; var newInfo = monitor.CloudClient.GetContainerInfo(container.Account, container.Name); return newInfo; } public void ToggleSynching() { bool isPaused=false; foreach (var pair in Monitors) { var monitor = pair.Value; monitor.Pause = !monitor.Pause; isPaused = monitor.Pause; } PauseSyncCaption = isPaused ? "Resume syncing" : "Pause syncing"; var iconKey = isPaused? "TraySyncPaused" : "TrayInSynch"; StatusIcon = String.Format(@"../Images/{0}.ico", iconKey); } public void ExitPithos() { foreach (var pair in Monitors) { var monitor = pair.Value; monitor.Stop(); } ((Window)GetView()).Close(); } #endregion private readonly Dictionary _iconNames = new List { new StatusInfo(PithosStatus.InSynch, "All files up to date", "TrayInSynch"), new StatusInfo(PithosStatus.PollSyncing, "Polling Files", "TraySynching"), new StatusInfo(PithosStatus.LocalSyncing, "Syncing Files", "TraySynching"), new StatusInfo(PithosStatus.SyncPaused, "Sync Paused", "TraySyncPaused") }.ToDictionary(s => s.Status); readonly IWindowManager _windowManager; //private int _syncCount=0; private PithosStatus _pithosStatus = PithosStatus.Disconnected; public void SetPithosStatus(PithosStatus status) { if (_pithosStatus == PithosStatus.LocalSyncing && status == PithosStatus.PollComplete) return; if (_pithosStatus == PithosStatus.PollSyncing && status == PithosStatus.LocalComplete) return; if (status == PithosStatus.LocalComplete || status == PithosStatus.PollComplete) _pithosStatus = PithosStatus.InSynch; else _pithosStatus = status; UpdateStatus(); } public void SetPithosStatus(PithosStatus status,string message) { StatusMessage = message; SetPithosStatus(status); } /// /// Updates the visual status indicators of the application depending on status changes, e.g. icon, stat /// public void UpdateStatus() { if (_iconNames.ContainsKey(_pithosStatus)) { var info = _iconNames[_pithosStatus]; StatusIcon = String.Format(@"../Images/{0}.ico", info.IconName); } if (_pithosStatus == PithosStatus.InSynch) StatusMessage = "All files up to date"; } private Task StartMonitor(PithosMonitor monitor,int retries=0) { return Task.Factory.StartNew(() => { using (log4net.ThreadContext.Stacks["Monitor"].Push("Start")) { try { Log.InfoFormat("Start Monitoring {0}", monitor.UserName); monitor.Start(); } catch (WebException exc) { if (AbandonRetry(monitor, retries)) return; HttpStatusCode statusCode =HttpStatusCode.OK; var response = exc.Response as HttpWebResponse; if(response!=null) statusCode = response.StatusCode; switch (statusCode) { case HttpStatusCode.Unauthorized: var message = String.Format("API Key Expired for {0}. Starting Renewal", monitor.UserName); Log.Error(message, exc); var account = Settings.Accounts.Find(acc => acc.AccountName == monitor.UserName); account.IsExpired = true; Notify(new ExpirationNotification(account)); //TryAuthorize(monitor.UserName, retries).Wait(); break; case HttpStatusCode.ProxyAuthenticationRequired: TryAuthenticateProxy(monitor,retries); break; default: TryLater(monitor, exc, retries); break; } } catch (Exception exc) { if (AbandonRetry(monitor, retries)) return; TryLater(monitor,exc,retries); } } }); } private void TryAuthenticateProxy(PithosMonitor monitor,int retries) { Execute.OnUIThread(() => { var proxyAccount = IoC.Get(); proxyAccount.Settings = Settings; if (true != _windowManager.ShowDialog(proxyAccount)) return; StartMonitor(monitor, retries); NotifyOfPropertyChange(() => Accounts); }); } private bool AbandonRetry(PithosMonitor monitor, int retries) { if (retries > 1) { var message = String.Format("Monitoring of account {0} has failed too many times. Will not retry", monitor.UserName); _events.Publish(new Notification {Title = "Account monitoring failed", Message = message, Level = TraceLevel.Error}); return true; } return false; } private void TryLater(PithosMonitor monitor, Exception exc,int retries) { var message = String.Format("An exception occured. Can't start monitoring\nWill retry in 10 seconds"); Task.Factory.StartNewDelayed(10000, () => StartMonitor(monitor,retries+1)); _events.Publish(new Notification {Title = "Error", Message = message, Level = TraceLevel.Error}); Log.Error(message, exc); } public void NotifyChange(string status, TraceLevel level=TraceLevel.Info) { StatusMessage = status; _events.Publish(new Notification { Title = "Pithos", Message = status, Level = level }); } public void NotifyChangedFile(string filePath) { if (RecentFiles.Any(e => e.FullPath == filePath)) return; IProducerConsumerCollection files=RecentFiles; FileEntry popped; while (files.Count > 5) files.TryTake(out popped); var entry = new FileEntry { FullPath = filePath }; files.TryAdd(entry); } public void NotifyAccount(AccountInfo account) { if (account== null) return; //TODO: What happens to an existing account whose Token has changed? account.SiteUri= String.Format("{0}/ui/?token={1}&user={2}", account.SiteUri, Uri.EscapeDataString(account.Token), Uri.EscapeDataString(account.UserName)); if (Accounts.All(item => item.UserName != account.UserName)) Accounts.TryAdd(account); } public void NotifyConflicts(IEnumerable conflictFiles, string message) { if (conflictFiles == null) return; //Convert to list to avoid multiple iterations var files = conflictFiles.ToList(); if (files.Count==0) return; UpdateStatus(); //TODO: Create a more specific message. For now, just show a warning NotifyForFiles(files,message,TraceLevel.Warning); } public void NotifyForFiles(IEnumerable files, string message,TraceLevel level=TraceLevel.Info) { if (files == null) return; if (!files.Any()) return; StatusMessage = message; _events.Publish(new Notification { Title = "Pithos", Message = message, Level = level}); } public void Notify(Notification notification) { _events.Publish(notification); } public void RemoveMonitor(string accountName) { if (String.IsNullOrWhiteSpace(accountName)) return; var accountInfo=_accounts.FirstOrDefault(account => account.UserName == accountName); if (accountInfo != null) { _accounts.TryRemove(accountInfo); _pollAgent.RemoveAccount(accountInfo); } PithosMonitor monitor; if (Monitors.TryRemove(accountName, out monitor)) { monitor.Stop(); //TODO: Also remove any pending actions for this account //from the network queue } } public void RefreshOverlays() { foreach (var pair in Monitors) { var monitor = pair.Value; var path = monitor.RootPath; if (String.IsNullOrWhiteSpace(path)) continue; if (!Directory.Exists(path) && !File.Exists(path)) continue; IntPtr pathPointer = Marshal.StringToCoTaskMemAuto(path); try { NativeMethods.SHChangeNotify(HChangeNotifyEventID.SHCNE_UPDATEITEM, HChangeNotifyFlags.SHCNF_PATHW | HChangeNotifyFlags.SHCNF_FLUSHNOWAIT, pathPointer, IntPtr.Zero); } finally { Marshal.FreeHGlobal(pathPointer); } } } #region Event Handlers public void Handle(SelectiveSynchChanges message) { var accountName = message.Account.AccountName; PithosMonitor monitor; if (_monitors.TryGetValue(accountName, out monitor)) { monitor.SetSelectivePaths(message.Uris,message.Added,message.Removed); } } private bool _pollStarted; private Sparkle _sparkle; //SMELL: Doing so much work for notifications in the shell is wrong //The notifications should be moved to their own view/viewmodel pair //and different templates should be used for different message types //This will also allow the addition of extra functionality, eg. actions // public void Handle(Notification notification) { UpdateStatus(); if (!Settings.ShowDesktopNotifications) return; if (notification is PollNotification) { _pollStarted = true; return; } if (notification is CloudNotification) { if (!_pollStarted) return; _pollStarted= false; notification.Title = "Pithos"; notification.Message = "Start Synchronisation"; } var progress = notification as ProgressNotification; if (progress != null) { StatusMessage = String.Format("Pithos {0}\r\n{1} {2:p2} of {3} - {4}", _fileVersion.Value.FileVersion, progress.Action, progress.Block/(double)progress.TotalBlocks, progress.FileSize.ToByteSize(), progress.FileName); return; } var info = notification as StatusNotification; if (info != null) { StatusMessage = String.Format("Pithos {0}\r\n{1}", _fileVersion.Value.FileVersion, info.Title); return; } if (String.IsNullOrWhiteSpace(notification.Message) && String.IsNullOrWhiteSpace(notification.Title)) return; ShowBalloonFor(notification); } private void ShowBalloonFor(Notification notification) { Contract.Requires(notification!=null); if (!Settings.ShowDesktopNotifications) return; BalloonIcon icon; switch (notification.Level) { case TraceLevel.Info: case TraceLevel.Verbose: return; case TraceLevel.Error: icon = BalloonIcon.Error; break; case TraceLevel.Warning: icon = BalloonIcon.Warning; break; default: return; } var tv = (ShellView) GetView(); System.Action clickAction = null; if (notification is ExpirationNotification) { clickAction = () => ShowPreferences("AccountTab"); } var balloon = new PithosBalloon { Title = notification.Title, Message = notification.Message, Icon = icon, ClickAction = clickAction }; tv.TaskbarView.ShowCustomBalloon(balloon, PopupAnimation.Fade, 4000); } #endregion public void Handle(ShowFilePropertiesEvent message) { if (message == null) throw new ArgumentNullException("message"); if (String.IsNullOrWhiteSpace(message.FileName) ) throw new ArgumentException("message"); Contract.EndContractBlock(); var fileName = message.FileName; //TODO: Display file properties for non-container folders if (File.Exists(fileName)) //Retrieve the full name with exact casing. Pithos names are case sensitive ShowFileProperties(FileInfoExtensions.GetProperFilePathCapitalization(fileName)); else if (Directory.Exists(fileName)) //Retrieve the full name with exact casing. Pithos names are case sensitive { var path = FileInfoExtensions.GetProperDirectoryCapitalization(fileName); if (IsContainer(path)) ShowContainerProperties(path); else ShowFileProperties(path); } } private bool IsContainer(string path) { var matchingFolders = from account in _accounts from rootFolder in Directory.GetDirectories(account.AccountPath) where rootFolder.Equals(path, StringComparison.InvariantCultureIgnoreCase) select rootFolder; return matchingFolders.Any(); } public FileStatus GetFileStatus(string localFileName) { if (String.IsNullOrWhiteSpace(localFileName)) throw new ArgumentNullException("localFileName"); Contract.EndContractBlock(); var statusKeeper = IoC.Get(); var status=statusKeeper.GetFileStatus(localFileName); return status; } } }