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>
90 //The Status Checker provides the current synch state
91 //TODO: Could we remove the status checker and use events in its place?
92 private readonly IStatusChecker _statusChecker;
93 private readonly IEventAggregator _events;
95 public PithosSettings Settings { get; private set; }
98 private readonly ConcurrentDictionary<string, PithosMonitor> _monitors = new ConcurrentDictionary<string, PithosMonitor>();
100 /// Dictionary of account monitors, keyed by account
103 /// One monitor class is created for each account. The Shell needs access to the monitors to execute start/stop/pause commands,
104 /// retrieve account and boject info
106 // TODO: Does the Shell REALLY need access to the monitors? Could we achieve the same results with a better design?
107 // TODO: The monitors should be internal to Pithos.Core, even though exposing them makes coding of the Object and Container windows easier
108 public ConcurrentDictionary<string, PithosMonitor> Monitors
110 get { return _monitors; }
115 /// The status service is used by Shell extensions to retrieve file status information
117 //TODO: CODE SMELL! This is the shell! While hosting in the shell makes executing start/stop commands easier, it is still a smell
118 private ServiceHost _statusService;
120 //Logging in the Pithos client is provided by log4net
121 private static readonly log4net.ILog Log = log4net.LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
123 //Lazily initialized File Version info. This is done once and lazily to avoid blocking the UI
124 private readonly Lazy<FileVersionInfo> _fileVersion;
126 private readonly PollAgent _pollAgent;
129 /// The Shell depends on MEF to provide implementations for windowManager, events, the status checker service and the settings
132 /// The PithosSettings class encapsulates the app's settings to abstract their storage mechanism (App settings, a database or registry)
134 [ImportingConstructor]
135 public ShellViewModel(IWindowManager windowManager, IEventAggregator events, IStatusChecker statusChecker, PithosSettings settings,PollAgent pollAgent)
140 _windowManager = windowManager;
141 //CHECK: Caliburn doesn't need explicit command construction
142 //OpenPithosFolderCommand = new PithosCommand(OpenPithosFolder);
143 _statusChecker = statusChecker;
146 _events.Subscribe(this);
148 _pollAgent = pollAgent;
151 Proxy.SetFromSettings(settings);
153 StatusMessage = "In Synch";
155 _fileVersion= new Lazy<FileVersionInfo>(() =>
157 Assembly assembly = Assembly.GetExecutingAssembly();
158 var fileVersion = FileVersionInfo.GetVersionInfo(assembly.Location);
161 _accounts.CollectionChanged += (sender, e) =>
163 NotifyOfPropertyChange(() => OpenFolderCaption);
164 NotifyOfPropertyChange(() => HasAccounts);
168 catch (Exception exc)
170 Log.Error("Error while starting the ShellViewModel",exc);
176 protected override void OnActivate()
187 private async void StartMonitoring()
191 var accounts = Settings.Accounts.Select(MonitorAccount);
192 await TaskEx.WhenAll(accounts);
193 _statusService = StatusService.Start();
196 foreach (var account in Settings.Accounts)
198 await MonitorAccount(account);
203 catch (AggregateException exc)
207 Log.Error("Error while starting monitoring", e);
214 protected override void OnDeactivate(bool close)
216 base.OnDeactivate(close);
219 StatusService.Stop(_statusService);
220 _statusService = null;
224 public Task MonitorAccount(AccountSettings account)
226 return Task.Factory.StartNew(() =>
228 PithosMonitor monitor;
229 var accountName = account.AccountName;
231 if (_monitors.TryGetValue(accountName, out monitor))
233 //If the account is active
234 if (account.IsActive)
236 //The Api Key may have changed throuth the Preferences dialog
237 monitor.ApiKey = account.ApiKey;
238 Debug.Assert(monitor.StatusNotification == this,"An existing monitor should already have a StatusNotification service object");
239 monitor.RootPath = account.RootPath;
240 //Start the monitor. It's OK to start an already started monitor,
241 //it will just ignore the call
242 StartMonitor(monitor).Wait();
246 //If the account is inactive
247 //Stop and remove the monitor
248 RemoveMonitor(accountName);
254 //Create a new monitor/ Can't use MEF here, it would return a single instance for all monitors
255 monitor = new PithosMonitor
257 UserName = accountName,
258 ApiKey = account.ApiKey,
259 StatusNotification = this,
260 RootPath = account.RootPath
262 //PithosMonitor uses MEF so we need to resolve it
263 IoC.BuildUp(monitor);
265 monitor.AuthenticationUrl = account.ServerUrl;
267 _monitors[accountName] = monitor;
269 if (account.IsActive)
271 //Don't start a monitor if it doesn't have an account and ApiKey
272 if (String.IsNullOrWhiteSpace(monitor.UserName) ||
273 String.IsNullOrWhiteSpace(monitor.ApiKey))
275 StartMonitor(monitor);
281 protected override void OnViewLoaded(object view)
284 var window = (Window)view;
285 TaskEx.Delay(1000).ContinueWith(t => Execute.OnUIThread(window.Hide));
286 base.OnViewLoaded(view);
290 #region Status Properties
292 private string _statusMessage;
293 public string StatusMessage
295 get { return _statusMessage; }
298 _statusMessage = value;
299 NotifyOfPropertyChange(() => StatusMessage);
303 private readonly ObservableConcurrentCollection<AccountInfo> _accounts = new ObservableConcurrentCollection<AccountInfo>();
304 public ObservableConcurrentCollection<AccountInfo> Accounts
306 get { return _accounts; }
309 public bool HasAccounts
311 get { return _accounts.Count > 0; }
315 public string OpenFolderCaption
319 return (_accounts.Count == 0)
320 ? "No Accounts Defined"
321 : "Open Pithos Folder";
325 private string _pauseSyncCaption="Pause Synching";
326 public string PauseSyncCaption
328 get { return _pauseSyncCaption; }
331 _pauseSyncCaption = value;
332 NotifyOfPropertyChange(() => PauseSyncCaption);
336 private readonly ObservableConcurrentCollection<FileEntry> _recentFiles = new ObservableConcurrentCollection<FileEntry>();
337 public ObservableConcurrentCollection<FileEntry> RecentFiles
339 get { return _recentFiles; }
343 private string _statusIcon="../Images/Pithos.ico";
344 public string StatusIcon
346 get { return _statusIcon; }
349 //TODO: Ensure all status icons use the Pithos logo
351 NotifyOfPropertyChange(() => StatusIcon);
359 public void ShowPreferences()
361 ShowPreferences(null);
364 public void ShowPreferences(string currentTab)
367 var preferences = new PreferencesViewModel(_windowManager, _events, this, Settings,currentTab);
368 _windowManager.ShowDialog(preferences);
372 public void AboutPithos()
374 var about = new AboutViewModel();
375 _windowManager.ShowWindow(about);
378 public void SendFeedback()
380 var feedBack = IoC.Get<FeedbackViewModel>();
381 _windowManager.ShowWindow(feedBack);
384 //public PithosCommand OpenPithosFolderCommand { get; private set; }
386 public void OpenPithosFolder()
388 var account = Settings.Accounts.FirstOrDefault(acc => acc.IsActive);
391 Process.Start(account.RootPath);
394 public void OpenPithosFolder(AccountInfo account)
396 Process.Start(account.AccountPath);
401 public void GoToSite()
403 var site = Properties.Settings.Default.PithosSite;
408 public void GoToSite(AccountInfo account)
410 Process.Start(account.SiteUri);
414 /// Open an explorer window to the target path's directory
415 /// and select the file
417 /// <param name="entry"></param>
418 public void GoToFile(FileEntry entry)
420 var fullPath = entry.FullPath;
421 if (!File.Exists(fullPath) && !Directory.Exists(fullPath))
423 Process.Start("explorer.exe","/select, " + fullPath);
426 public void OpenLogPath()
428 var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
429 var pithosDataPath = Path.Combine(appDataPath, "GRNET");
431 Process.Start(pithosDataPath);
434 public void ShowFileProperties()
436 var account = Settings.Accounts.First(acc => acc.IsActive);
437 var dir = new DirectoryInfo(account.RootPath + @"\pithos");
438 var files=dir.GetFiles();
440 var idx=r.Next(0, files.Length);
441 ShowFileProperties(files[idx].FullName);
444 public void ShowFileProperties(string filePath)
446 if (String.IsNullOrWhiteSpace(filePath))
447 throw new ArgumentNullException("filePath");
448 if (!File.Exists(filePath) && !Directory.Exists(filePath))
449 throw new ArgumentException(String.Format("Non existent file {0}",filePath),"filePath");
450 Contract.EndContractBlock();
452 var pair=(from monitor in Monitors
453 where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase)
454 select monitor).FirstOrDefault();
455 var accountMonitor = pair.Value;
457 if (accountMonitor == null)
460 var infoTask=Task.Factory.StartNew(()=>accountMonitor.GetObjectInfo(filePath));
464 var fileProperties = new FilePropertiesViewModel(this, infoTask,filePath);
465 _windowManager.ShowWindow(fileProperties);
468 public void ShowContainerProperties()
470 var account = Settings.Accounts.First(acc => acc.IsActive);
471 var dir = new DirectoryInfo(account.RootPath);
472 var fullName = (from folder in dir.EnumerateDirectories()
473 where (folder.Attributes & FileAttributes.Hidden) == 0
474 select folder.FullName).First();
475 ShowContainerProperties(fullName);
478 public void ShowContainerProperties(string filePath)
480 if (String.IsNullOrWhiteSpace(filePath))
481 throw new ArgumentNullException("filePath");
482 if (!Directory.Exists(filePath))
483 throw new ArgumentException(String.Format("Non existent file {0}",filePath),"filePath");
484 Contract.EndContractBlock();
486 var pair=(from monitor in Monitors
487 where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase)
488 select monitor).FirstOrDefault();
489 var accountMonitor = pair.Value;
490 var info = accountMonitor.GetContainerInfo(filePath);
494 var containerProperties = new ContainerPropertiesViewModel(this, info,filePath);
495 _windowManager.ShowWindow(containerProperties);
498 public void SynchNow()
500 _pollAgent.SynchNow();
503 public ObjectInfo RefreshObjectInfo(ObjectInfo currentInfo)
505 if (currentInfo==null)
506 throw new ArgumentNullException("currentInfo");
507 Contract.EndContractBlock();
509 var monitor = Monitors[currentInfo.Account];
510 var newInfo=monitor.CloudClient.GetObjectInfo(currentInfo.Account, currentInfo.Container, currentInfo.Name);
514 public ContainerInfo RefreshContainerInfo(ContainerInfo container)
516 if (container == null)
517 throw new ArgumentNullException("container");
518 Contract.EndContractBlock();
520 var monitor = Monitors[container.Account];
521 var newInfo = monitor.CloudClient.GetContainerInfo(container.Account, container.Name);
526 public void ToggleSynching()
529 foreach (var pair in Monitors)
531 var monitor = pair.Value;
532 monitor.Pause = !monitor.Pause;
533 isPaused = monitor.Pause;
536 PauseSyncCaption = isPaused ? "Resume syncing" : "Pause syncing";
537 var iconKey = isPaused? "TraySyncPaused" : "TrayInSynch";
538 StatusIcon = String.Format(@"../Images/{0}.ico", iconKey);
541 public void ExitPithos()
543 foreach (var pair in Monitors)
545 var monitor = pair.Value;
549 ((Window)GetView()).Close();
554 private readonly Dictionary<PithosStatus, StatusInfo> _iconNames = new List<StatusInfo>
556 new StatusInfo(PithosStatus.InSynch, "All files up to date", "TrayInSynch"),
557 new StatusInfo(PithosStatus.Syncing, "Syncing Files", "TraySynching"),
558 new StatusInfo(PithosStatus.SyncPaused, "Sync Paused", "TraySyncPaused")
559 }.ToDictionary(s => s.Status);
561 readonly IWindowManager _windowManager;
565 /// Updates the visual status indicators of the application depending on status changes, e.g. icon, stat
567 public void UpdateStatus()
569 var pithosStatus = _statusChecker.GetPithosStatus();
571 if (_iconNames.ContainsKey(pithosStatus))
573 var info = _iconNames[pithosStatus];
574 StatusIcon = String.Format(@"../Images/{0}.ico", info.IconName);
578 StatusMessage = String.Format("Pithos {0}\r\n{1}", _fileVersion.Value.FileVersion,info.StatusText);
581 //_events.Publish(new Notification { Title = "Start", Message = "Start Monitoring", Level = TraceLevel.Info});
586 private Task StartMonitor(PithosMonitor monitor,int retries=0)
588 return Task.Factory.StartNew(() =>
590 using (log4net.ThreadContext.Stacks["Monitor"].Push("Start"))
594 Log.InfoFormat("Start Monitoring {0}", monitor.UserName);
598 catch (WebException exc)
600 if (AbandonRetry(monitor, retries))
603 HttpStatusCode statusCode =HttpStatusCode.OK;
604 var response = exc.Response as HttpWebResponse;
606 statusCode = response.StatusCode;
610 case HttpStatusCode.Unauthorized:
611 var message = String.Format("API Key Expired for {0}. Starting Renewal",
613 Log.Error(message, exc);
614 var account = Settings.Accounts.Find(acc => acc.AccountName == monitor.UserName);
615 account.IsExpired = true;
616 Notify(new ExpirationNotification(account));
617 //TryAuthorize(monitor.UserName, retries).Wait();
619 case HttpStatusCode.ProxyAuthenticationRequired:
620 TryAuthenticateProxy(monitor,retries);
623 TryLater(monitor, exc, retries);
627 catch (Exception exc)
629 if (AbandonRetry(monitor, retries))
632 TryLater(monitor,exc,retries);
638 private void TryAuthenticateProxy(PithosMonitor monitor,int retries)
640 Execute.OnUIThread(() =>
642 var proxyAccount = IoC.Get<ProxyAccountViewModel>();
643 proxyAccount.Settings = Settings;
644 if (true != _windowManager.ShowDialog(proxyAccount))
646 StartMonitor(monitor, retries);
647 NotifyOfPropertyChange(() => Accounts);
651 private bool AbandonRetry(PithosMonitor monitor, int retries)
655 var message = String.Format("Monitoring of account {0} has failed too many times. Will not retry",
657 _events.Publish(new Notification
658 {Title = "Account monitoring failed", Message = message, Level = TraceLevel.Error});
665 private void TryLater(PithosMonitor monitor, Exception exc,int retries)
667 var message = String.Format("An exception occured. Can't start monitoring\nWill retry in 10 seconds");
668 Task.Factory.StartNewDelayed(10000, () => StartMonitor(monitor,retries+1));
669 _events.Publish(new Notification
670 {Title = "Error", Message = message, Level = TraceLevel.Error});
671 Log.Error(message, exc);
675 public void NotifyChange(string status, TraceLevel level=TraceLevel.Info)
677 StatusMessage = status;
679 _events.Publish(new Notification { Title = "Pithos", Message = status, Level = level });
682 public void NotifyChangedFile(string filePath)
684 if (RecentFiles.Any(e => e.FullPath == filePath))
687 IProducerConsumerCollection<FileEntry> files=RecentFiles;
689 while (files.Count > 5)
690 files.TryTake(out popped);
691 var entry = new FileEntry { FullPath = filePath };
695 public void NotifyAccount(AccountInfo account)
699 //TODO: What happens to an existing account whose Token has changed?
700 account.SiteUri= String.Format("{0}/ui/?token={1}&user={2}",
701 account.SiteUri, Uri.EscapeDataString(account.Token),
702 Uri.EscapeDataString(account.UserName));
704 if (Accounts.All(item => item.UserName != account.UserName))
705 Accounts.TryAdd(account);
709 public void NotifyConflicts(IEnumerable<FileSystemInfo> conflictFiles, string message)
711 if (conflictFiles == null)
713 //Convert to list to avoid multiple iterations
714 var files = conflictFiles.ToList();
719 //TODO: Create a more specific message. For now, just show a warning
720 NotifyForFiles(files,message,TraceLevel.Warning);
724 public void NotifyForFiles(IEnumerable<FileSystemInfo> files, string message,TraceLevel level=TraceLevel.Info)
731 StatusMessage = message;
733 _events.Publish(new Notification { Title = "Pithos", Message = message, Level = level});
736 public void Notify(Notification notification)
738 _events.Publish(notification);
742 public void RemoveMonitor(string accountName)
744 if (String.IsNullOrWhiteSpace(accountName))
747 var accountInfo=_accounts.FirstOrDefault(account => account.UserName == accountName);
748 if (accountInfo != null)
750 _accounts.TryRemove(accountInfo);
751 _pollAgent.RemoveAccount(accountInfo);
754 PithosMonitor monitor;
755 if (Monitors.TryRemove(accountName, out monitor))
758 //TODO: Also remove any pending actions for this account
759 //from the network queue
763 public void RefreshOverlays()
765 foreach (var pair in Monitors)
767 var monitor = pair.Value;
769 var path = monitor.RootPath;
771 if (String.IsNullOrWhiteSpace(path))
774 if (!Directory.Exists(path) && !File.Exists(path))
777 IntPtr pathPointer = Marshal.StringToCoTaskMemAuto(path);
781 NativeMethods.SHChangeNotify(HChangeNotifyEventID.SHCNE_UPDATEITEM,
782 HChangeNotifyFlags.SHCNF_PATHW | HChangeNotifyFlags.SHCNF_FLUSHNOWAIT,
783 pathPointer, IntPtr.Zero);
787 Marshal.FreeHGlobal(pathPointer);
792 #region Event Handlers
794 public void Handle(SelectiveSynchChanges message)
796 var accountName = message.Account.AccountName;
797 PithosMonitor monitor;
798 if (_monitors.TryGetValue(accountName, out monitor))
800 monitor.SetSelectivePaths(message.Uris,message.Added,message.Removed);
807 private bool _pollStarted;
809 //SMELL: Doing so much work for notifications in the shell is wrong
810 //The notifications should be moved to their own view/viewmodel pair
811 //and different templates should be used for different message types
812 //This will also allow the addition of extra functionality, eg. actions
814 public void Handle(Notification notification)
818 if (!Settings.ShowDesktopNotifications)
821 if (notification is PollNotification)
826 if (notification is CloudNotification)
831 notification.Title = "Pithos";
832 notification.Message = "Start Synchronisation";
835 if (String.IsNullOrWhiteSpace(notification.Message) && String.IsNullOrWhiteSpace(notification.Title))
839 switch (notification.Level)
841 case TraceLevel.Error:
842 icon = BalloonIcon.Error;
844 case TraceLevel.Info:
845 case TraceLevel.Verbose:
846 icon = BalloonIcon.Info;
848 case TraceLevel.Warning:
849 icon = BalloonIcon.Warning;
852 icon = BalloonIcon.None;
856 if (Settings.ShowDesktopNotifications)
858 var tv = (ShellView) GetView();
859 System.Action clickAction = null;
860 if (notification is ExpirationNotification)
862 clickAction = ()=>ShowPreferences("AccountTab");
864 var balloon=new PithosBalloon{Title=notification.Title,Message=notification.Message,Icon=icon,ClickAction=clickAction};
865 tv.TaskbarView.ShowCustomBalloon(balloon,PopupAnimation.Fade,4000);
866 // tv.TaskbarView.ShowBalloonTip(notification.Title, notification.Message, icon);
871 public void Handle(ShowFilePropertiesEvent message)
874 throw new ArgumentNullException("message");
875 if (String.IsNullOrWhiteSpace(message.FileName) )
876 throw new ArgumentException("message");
877 Contract.EndContractBlock();
879 var fileName = message.FileName;
880 //TODO: Display file properties for non-container folders
881 if (File.Exists(fileName))
882 //Retrieve the full name with exact casing. Pithos names are case sensitive
883 ShowFileProperties(FileInfoExtensions.GetProperFilePathCapitalization(fileName));
884 else if (Directory.Exists(fileName))
885 //Retrieve the full name with exact casing. Pithos names are case sensitive
887 var path = FileInfoExtensions.GetProperDirectoryCapitalization(fileName);
888 if (IsContainer(path))
889 ShowContainerProperties(path);
891 ShowFileProperties(path);
895 private bool IsContainer(string path)
897 var matchingFolders = from account in _accounts
898 from rootFolder in Directory.GetDirectories(account.AccountPath)
899 where rootFolder.Equals(path, StringComparison.InvariantCultureIgnoreCase)
901 return matchingFolders.Any();
904 public FileStatus GetFileStatus(string localFileName)
906 if (String.IsNullOrWhiteSpace(localFileName))
907 throw new ArgumentNullException("localFileName");
908 Contract.EndContractBlock();
910 var statusKeeper = IoC.Get<IStatusKeeper>();
911 var status=statusKeeper.GetFileStatus(localFileName);