Fix for NH/LINQ conflict with IsAtOrBelow
[pithos-ms-client] / trunk / Pithos.Core / PithosMonitor.cs
1 #region
2 /* -----------------------------------------------------------------------
3  * <copyright file="PithosMonitor.cs" company="GRNet">
4  * 
5  * Copyright 2011-2012 GRNET S.A. All rights reserved.
6  *
7  * Redistribution and use in source and binary forms, with or
8  * without modification, are permitted provided that the following
9  * conditions are met:
10  *
11  *   1. Redistributions of source code must retain the above
12  *      copyright notice, this list of conditions and the following
13  *      disclaimer.
14  *
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.
19  *
20  *
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.
33  *
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.
38  * </copyright>
39  * -----------------------------------------------------------------------
40  */
41 #endregion
42 using System;
43 using System.Collections.Generic;
44 using System.ComponentModel.Composition;
45 using System.Diagnostics.Contracts;
46 using System.IO;
47 using System.Linq;
48 using System.Reflection;
49 using System.Threading;
50 using System.Threading.Tasks;
51 using Pithos.Core.Agents;
52 using Pithos.Interfaces;
53 using Pithos.Network;
54 using log4net;
55
56 namespace Pithos.Core
57 {
58     [Export(typeof(PithosMonitor))]
59     public class PithosMonitor:IDisposable
60     {
61         private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
62
63         private int _blockSize;
64         private string _blockHash;
65
66         [Import]
67         public IPithosSettings Settings{get;set;}
68
69         private IStatusKeeper _statusKeeper;
70
71         [Import]
72         public IStatusKeeper StatusKeeper
73         {
74             get { return _statusKeeper; }
75             set
76             {
77                 _statusKeeper = value;
78                 FileAgent.StatusKeeper = value;
79             }
80         }
81
82
83
84
85         private IPithosWorkflow _workflow;
86
87         [Import]
88         public IPithosWorkflow Workflow
89         {
90             get { return _workflow; }
91             set
92             {
93                 _workflow = value;
94                 FileAgent.Workflow = value;
95             }
96         }
97
98         public ICloudClient CloudClient { get; set; }
99
100         public IStatusNotification StatusNotification { get; set; }
101
102         //[Import]
103         public FileAgent FileAgent { get; private set; }
104
105 /*
106         private WorkflowAgent _workflowAgent;
107
108         [Import]
109         public WorkflowAgent WorkflowAgent
110         {
111             get { return _workflowAgent; }
112             set
113             {
114                 _workflowAgent = value;
115                 //FileAgent.WorkflowAgent = value;
116             }
117         }
118 */
119         
120         [Import]
121         public NetworkAgent NetworkAgent { get; set; }
122
123         private PollAgent _pollAgent;
124
125         [Import]
126         public PollAgent PollAgent
127         {
128             get { return _pollAgent; }
129             set
130             {
131                 _pollAgent = value;
132                 FileAgent.PollAgent = value;
133             }
134         }
135
136         private Selectives _selectives;
137
138         [Import]
139         public Selectives Selectives
140         {
141             get { return _selectives; }
142             set
143             {
144                 _selectives = value;
145                 FileAgent.Selectives = value;
146             }
147         }
148
149         public string UserName { get; set; }
150         private string _apiKey;
151         public string ApiKey
152         {
153             get { return _apiKey; }
154             set
155             {
156                 _apiKey = value;
157                 if (_accountInfo != null)
158                     _accountInfo.Token = value;
159             }
160         }
161
162         private AccountInfo _accountInfo;
163
164         public AccountInfo Account
165         {
166             get { return _accountInfo; }
167         }
168
169
170
171
172
173         public bool Pause { get; set; }       
174         /*public bool Pause
175         {
176             get { return FileAgent.Pause; }
177             set
178             {
179                 FileAgent.Pause = value;
180             }
181         }*/
182
183         private string _rootPath;
184         public string RootPath
185         {
186             get { return _rootPath; }
187             set 
188             {
189                 _rootPath = String.IsNullOrWhiteSpace(value) 
190                     ? String.Empty 
191                     : value.ToLower();
192             }
193         }
194
195
196         CancellationTokenSource _cancellationSource;
197
198         public PithosMonitor()
199         {
200             FileAgent = new FileAgent();            
201         }
202         private bool _started;
203
204         public void Start()
205         {            
206             if (String.IsNullOrWhiteSpace(ApiKey))
207                 throw new InvalidOperationException("The ApiKey is empty");
208             if (String.IsNullOrWhiteSpace(UserName))
209                 throw new InvalidOperationException("The UserName is empty");
210             if (String.IsNullOrWhiteSpace(AuthenticationUrl))
211                 throw new InvalidOperationException("The Authentication url is empty");
212             Contract.EndContractBlock();
213
214             //If the account doesn't have a valid path, don't start monitoring but don't throw either
215             if (String.IsNullOrWhiteSpace(RootPath))
216                 //TODO; Warn user?
217                 return;
218
219             //WorkflowAgent.StatusNotification = StatusNotification;
220
221             StatusNotification.NotifyChange("Starting");
222             if (_started)
223             {
224                 if (!_cancellationSource.IsCancellationRequested)
225                     return;
226             }
227             _cancellationSource = new CancellationTokenSource();
228
229             lock (this)
230             {
231                 CloudClient = new CloudFilesClient(UserName, ApiKey)
232                                   {UsePithos = true, AuthenticationUrl = AuthenticationUrl};
233                 _accountInfo = CloudClient.Authenticate();
234             }
235             _accountInfo.SiteUri = AuthenticationUrl;
236             _accountInfo.AccountPath = RootPath;
237
238
239             var pithosFolder = Path.Combine(RootPath, FolderConstants.PithosContainer);
240             if (!Directory.Exists(pithosFolder))
241                 Directory.CreateDirectory(pithosFolder);
242             //Create the cache folder and ensure it is hidden
243             CreateHiddenFolder(RootPath, FolderConstants.CacheFolder);
244
245             var policy=CloudClient.GetAccountPolicies(_accountInfo);
246
247             StatusNotification.NotifyAccount(policy);
248             EnsurePithosContainers();
249             
250             StatusKeeper.BlockHash = _blockHash;
251             StatusKeeper.BlockSize = _blockSize;
252             
253             StatusKeeper.StartProcessing(_cancellationSource.Token);
254             CleanupUnselectedStates();
255             IndexLocalFiles();
256             //Extract the URIs from the string collection
257             var settings = Settings.Accounts.First(s => s.AccountKey == _accountInfo.AccountKey );
258                             
259             var selectiveUrls=settings.SelectiveFolders.Cast<string>().Select(url => new Uri(url,UriKind.RelativeOrAbsolute))
260                 .Where(uri=>uri.IsAbsoluteUri).ToArray();
261
262             SetSelectivePaths(selectiveUrls,null,null);
263             
264             StartWatcherAgent();
265
266             StartNetworkAgent();
267             
268             //WorkflowAgent.RestartInterruptedFiles(_accountInfo);
269             _started = true;
270         }
271
272         private void EnsurePithosContainers()
273         {
274
275             //Create the two default containers if they are missing
276             var pithosContainers = new List<string>{ FolderConstants.TrashContainer,FolderConstants.PithosContainer};
277             foreach (var container in pithosContainers)
278             {                
279                 var info=CloudClient.GetContainerInfo(UserName, container);
280                 if (info == ContainerInfo.Empty)
281                 {
282                     CloudClient.CreateContainer(UserName, container);
283                     info = CloudClient.GetContainerInfo(UserName, container);
284                 }
285                 _blockSize = info.BlockSize;
286                 _blockHash = info.BlockHash;
287                 _accountInfo.BlockSize = _blockSize;
288                 _accountInfo.BlockHash = _blockHash;
289             }
290         }
291
292         public string AuthenticationUrl { get; set; }
293
294         private void IndexLocalFiles()
295         {
296             using (ThreadContext.Stacks["Operation"].Push("Indexing local files"))
297             {
298                 
299                 try
300                 {
301                     //StatusNotification.NotifyChange("Indexing Local Files");
302                     Log.Info("Start local indexing");
303                     StatusNotification.SetPithosStatus(PithosStatus.LocalSyncing,"Indexing Local Files");                    
304
305                     var cachePath = Path.Combine(RootPath, FolderConstants.CacheFolder);
306                     var directory = new DirectoryInfo(RootPath);
307                     var files =
308                         from file in directory.EnumerateFiles("*", SearchOption.AllDirectories)
309                         where !file.FullName.StartsWith(cachePath, StringComparison.InvariantCultureIgnoreCase) &&
310                               !file.Extension.Equals("ignore", StringComparison.InvariantCultureIgnoreCase)
311                         select file;
312                     StatusKeeper.ProcessExistingFiles(files);
313
314                 }
315                 catch (Exception exc)
316                 {
317                     Log.Error("[ERROR]", exc);
318                 }
319                 finally
320                 {
321                     Log.Info("[END]");
322                 }
323                 StatusNotification.SetPithosStatus(PithosStatus.LocalComplete,"Indexing Completed");
324             }
325         }
326
327         
328   
329
330
331        /* private void StartWorkflowAgent()
332         {
333             WorkflowAgent.StatusNotification = StatusNotification;
334
335 /*            //On Vista and up we can check for a network connection
336             bool connected=Environment.OSVersion.Version.Major < 6 || NetworkListManager.IsConnectedToInternet;
337             //If we are not connected retry later
338             if (!connected)
339             {
340                 Task.Factory.StartNewDelayed(10000, StartWorkflowAgent);
341                 return;
342             }#1#
343
344             try
345             {
346                 WorkflowAgent.Start();                
347             }
348             catch (Exception)
349             {
350                 //Faild to authenticate due to network or account error
351                 //Retry after a while
352                 Task.Factory.StartNewDelayed(10000, StartWorkflowAgent);
353             }
354         }*/
355
356
357         private void StartNetworkAgent()
358         {
359             NetworkAgent.StatusNotification = StatusNotification;
360
361             //TODO: The Network and Poll agents are not account specific
362             //They should be moved outside PithosMonitor
363 /*
364             NetworkAgent.Start();
365 */
366
367             PollAgent.AddAccount(_accountInfo);
368
369             PollAgent.StatusNotification = StatusNotification;
370
371             PollAgent.PollRemoteFiles();
372         }
373
374         //Make sure a hidden cache folder exists to store partial downloads
375         private static void CreateHiddenFolder(string rootPath, string folderName)
376         {
377             if (String.IsNullOrWhiteSpace(rootPath))
378                 throw new ArgumentNullException("rootPath");
379             if (!Path.IsPathRooted(rootPath))
380                 throw new ArgumentException("rootPath");
381             if (String.IsNullOrWhiteSpace(folderName))
382                 throw new ArgumentNullException("folderName");
383             Contract.EndContractBlock();
384
385             var folder = Path.Combine(rootPath, folderName);
386             if (!Directory.Exists(folder))
387             {
388                 var info = Directory.CreateDirectory(folder);
389                 info.Attributes |= FileAttributes.Hidden;
390
391                 Log.InfoFormat("Created cache Folder: {0}", folder);
392             }
393             else
394             {
395                 var info = new DirectoryInfo(folder);
396                 if ((info.Attributes & FileAttributes.Hidden) == 0)
397                 {
398                     info.Attributes |= FileAttributes.Hidden;
399                     Log.InfoFormat("Reset cache folder to hidden: {0}", folder);
400                 }                                
401             }
402         }
403
404        
405
406
407         private void StartWatcherAgent()
408         {
409             if (Log.IsDebugEnabled)
410                 Log.DebugFormat("Start Folder Monitoring [{0}]",RootPath);
411
412             AgentLocator<FileAgent>.Register(FileAgent,RootPath);
413             
414             FileAgent.IdleTimeout = Settings.FileIdleTimeout;
415             FileAgent.StatusKeeper = StatusKeeper;
416             FileAgent.StatusNotification = StatusNotification;
417             FileAgent.Workflow = Workflow;
418             FileAgent.CachePath = Path.Combine(RootPath, FolderConstants.CacheFolder);
419             FileAgent.Start(_accountInfo, RootPath);
420         }
421
422         public void Stop()
423         {
424 /*
425             AgentLocator<FileAgent>.Remove(RootPath);
426
427             if (FileAgent!=null)
428                 FileAgent.Stop();
429             FileAgent = null;
430 */
431         }
432
433
434         ~PithosMonitor()
435         {
436             Dispose(false);
437         }
438
439         public void Dispose()
440         {
441             Dispose(true);
442             GC.SuppressFinalize(this);
443         }
444
445         protected virtual void Dispose(bool disposing)
446         {
447             if (disposing)
448             {
449                 Stop();
450             }
451         }
452
453
454         public void MoveFileStates(string oldPath, string newPath)
455         {
456             if (String.IsNullOrWhiteSpace(oldPath))
457                 throw new ArgumentNullException("oldPath");
458             if (!Path.IsPathRooted(oldPath))
459                 throw new ArgumentException("oldPath must be an absolute path","oldPath");
460             if (string.IsNullOrWhiteSpace(newPath))
461                 throw new ArgumentNullException("newPath");
462             if (!Path.IsPathRooted(newPath))
463                 throw new ArgumentException("newPath must be an absolute path","newPath");
464             Contract.EndContractBlock();
465
466             StatusKeeper.ChangeRoots(oldPath, newPath);
467         }
468
469         private void CleanupUnselectedStates()
470         {
471             //var settings = Settings.Accounts.First(s => s.AccountKey == _accountInfo.AccountKey);
472             if (!Selectives.IsSelectiveEnabled(_accountInfo.AccountKey)) return;
473
474             List<string> selectivePaths;
475             if (Selectives.SelectivePaths.TryGetValue(_accountInfo.AccountKey, out selectivePaths))
476             {\r
477                 var statePaths= FileState.Queryable.Select(state => state.FilePath).ToList();\r
478                 var removedPaths = statePaths.Where(sp => !selectivePaths.Any(sp.IsAtOrBelow));
479
480                 UnversionSelectivePaths(removedPaths.ToList());
481             }
482             else
483             {
484                 StatusKeeper.ClearFolderStatus(Account.AccountPath);
485             }
486         }
487
488         public void SetSelectivePaths(Uri[] uris,Uri[] added, Uri[] removed)
489         {
490             //Convert the uris to paths
491             //var selectivePaths = UrisToFilePaths(uris);
492             
493             var selectiveUri = uris.ToList();
494             this.Selectives.SetSelectedUris(_accountInfo,selectiveUri);
495
496             var removedPaths = UrisToFilePaths(removed);
497             UnversionSelectivePaths(removedPaths);
498
499         }
500
501         /// <summary>
502         /// Mark all unselected paths as Unversioned
503         /// </summary>
504         /// <param name="removed"></param>
505         private void UnversionSelectivePaths(List<string> removed)
506         {
507             if (removed == null)
508                 return;
509
510             //Ensure we remove any file state below the deleted folders
511             FileState.UnversionPaths(removed);
512         }
513
514
515         /// <summary>
516         /// Return a list of absolute filepaths from a list of Uris
517         /// </summary>
518         /// <param name="uris"></param>
519         /// <returns></returns>
520         public List<string> UrisToFilePaths(IEnumerable<Uri> uris)
521         {
522             if (uris == null)
523                 return new List<string>();
524
525             var own = (from uri in uris
526                        where uri.ToString().StartsWith(_accountInfo.StorageUri.ToString())
527                                    let relativePath = _accountInfo.StorageUri.MakeRelativeUri(uri).RelativeUriToFilePath()
528                                    //Trim the account name
529                                    select Path.Combine(RootPath, relativePath.After(_accountInfo.UserName + '\\'))).ToList();
530             var others= (from uri in uris
531                          where !uri.ToString().StartsWith(_accountInfo.StorageUri.ToString())
532                                    let relativePath = _accountInfo.StorageUri.MakeRelativeUri(uri).RelativeUriToFilePath()
533                                    //Trim the account name
534                                    select Path.Combine(RootPath,"others-shared", relativePath)).ToList();
535             return own.Union(others).ToList();            
536         }
537
538
539         public ObjectInfo GetObjectInfo(string filePath)
540         {
541             if (String.IsNullOrWhiteSpace(filePath))
542                 throw new ArgumentNullException("filePath");
543             Contract.EndContractBlock();
544
545             var file=new FileInfo(filePath);
546             string relativeUrl;//=file.AsRelativeUrlTo(this.RootPath);
547             var relativePath = file.AsRelativeTo(RootPath);
548             
549             string accountName,container;
550             
551             var parts=relativePath.Split('\\');
552
553             var accountInfo = _accountInfo;
554             if (relativePath.StartsWith(FolderConstants.OthersFolder))
555             {                
556                 accountName = parts[1];
557                 container = parts[2];
558                 relativeUrl = String.Join("/", parts.Splice(3));
559                 //Create the root URL for the target account
560                 var oldName = UserName;
561                 var absoluteUri =  _accountInfo.StorageUri.AbsoluteUri;
562                 var nameIndex=absoluteUri.IndexOf(oldName, StringComparison.Ordinal);
563                 var root=absoluteUri.Substring(0, nameIndex);
564
565                 accountInfo = new AccountInfo
566                 {
567                     UserName = accountName,
568                     AccountPath = Path.Combine(accountInfo.AccountPath, parts[0], parts[1]),
569                     StorageUri = new Uri(root + accountName),
570                     BlockHash=accountInfo.BlockHash,
571                     BlockSize=accountInfo.BlockSize,
572                     Token=accountInfo.Token
573                 };
574             }
575             else
576             {
577                 accountName = UserName;
578                 container = parts[0];
579                 relativeUrl = String.Join("/", parts.Splice(1));
580             }
581             
582             var client = new CloudFilesClient(accountInfo);
583             var objectInfo=client.GetObjectInfo(accountName, container, relativeUrl);
584             return objectInfo;
585         }
586         
587         public Task<ContainerInfo> GetContainerInfo(string filePath)
588         {
589             if (String.IsNullOrWhiteSpace(filePath))
590                 throw new ArgumentNullException("filePath");
591             Contract.EndContractBlock();
592
593             var file=new FileInfo(filePath);
594             var relativePath = file.AsRelativeTo(RootPath);
595             
596             string accountName,container;
597             
598             var parts=relativePath.Split('\\');
599
600             var accountInfo = _accountInfo;
601             if (relativePath.StartsWith(FolderConstants.OthersFolder))
602             {                
603                 accountName = parts[1];
604                 container = parts[2];                
605                 //Create the root URL for the target account
606                 var oldName = UserName;
607                 var absoluteUri =  _accountInfo.StorageUri.AbsoluteUri;
608                 var nameIndex=absoluteUri.IndexOf(oldName, StringComparison.Ordinal);
609                 var root=absoluteUri.Substring(0, nameIndex);
610
611                 accountInfo = new AccountInfo
612                 {
613                     UserName = accountName,
614                     AccountPath = Path.Combine(accountInfo.AccountPath, parts[0], parts[1]),
615                     StorageUri = new Uri(root + accountName),
616                     BlockHash=accountInfo.BlockHash,
617                     BlockSize=accountInfo.BlockSize,
618                     Token=accountInfo.Token
619                 };
620             }
621             else
622             {
623                 accountName = UserName;
624                 container = parts[0];                
625             }
626
627             return Task.Factory.StartNew(() =>
628             {
629                 var client = new CloudFilesClient(accountInfo);
630                 var containerInfo = client.GetContainerInfo(accountName, container);
631                 return containerInfo;
632             });
633         }
634     }
635 }