Selective filtering modifications to allow uploading of new root folders
[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             {
477
478                 var removedPaths = from state in FileState.Queryable
479                                    from path in selectivePaths
480                                    where !state.FilePath.IsAtOrBelow(path)
481                                    select state.FilePath;
482
483                 UnversionSelectivePaths(removedPaths.ToList());
484             }
485             else
486             {
487                 StatusKeeper.ClearFolderStatus(Account.AccountPath);
488             }
489         }
490
491         public void SetSelectivePaths(Uri[] uris,Uri[] added, Uri[] removed)
492         {
493             //Convert the uris to paths
494             //var selectivePaths = UrisToFilePaths(uris);
495             
496             var selectiveUri = uris.ToList();
497             this.Selectives.SetSelectedUris(_accountInfo,selectiveUri);
498
499             var removedPaths = UrisToFilePaths(removed);
500             UnversionSelectivePaths(removedPaths);
501
502         }
503
504         /// <summary>
505         /// Mark all unselected paths as Unversioned
506         /// </summary>
507         /// <param name="removed"></param>
508         private void UnversionSelectivePaths(List<string> removed)
509         {
510             if (removed == null)
511                 return;
512
513             //Ensure we remove any file state below the deleted folders
514             FileState.UnversionPaths(removed);
515         }
516
517
518         /// <summary>
519         /// Return a list of absolute filepaths from a list of Uris
520         /// </summary>
521         /// <param name="uris"></param>
522         /// <returns></returns>
523         public List<string> UrisToFilePaths(IEnumerable<Uri> uris)
524         {
525             if (uris == null)
526                 return new List<string>();
527
528             var own = (from uri in uris
529                        where uri.ToString().StartsWith(_accountInfo.StorageUri.ToString())
530                                    let relativePath = _accountInfo.StorageUri.MakeRelativeUri(uri).RelativeUriToFilePath()
531                                    //Trim the account name
532                                    select Path.Combine(RootPath, relativePath.After(_accountInfo.UserName + '\\'))).ToList();
533             var others= (from uri in uris
534                          where !uri.ToString().StartsWith(_accountInfo.StorageUri.ToString())
535                                    let relativePath = _accountInfo.StorageUri.MakeRelativeUri(uri).RelativeUriToFilePath()
536                                    //Trim the account name
537                                    select Path.Combine(RootPath,"others-shared", relativePath)).ToList();
538             return own.Union(others).ToList();            
539         }
540
541
542         public ObjectInfo GetObjectInfo(string filePath)
543         {
544             if (String.IsNullOrWhiteSpace(filePath))
545                 throw new ArgumentNullException("filePath");
546             Contract.EndContractBlock();
547
548             var file=new FileInfo(filePath);
549             string relativeUrl;//=file.AsRelativeUrlTo(this.RootPath);
550             var relativePath = file.AsRelativeTo(RootPath);
551             
552             string accountName,container;
553             
554             var parts=relativePath.Split('\\');
555
556             var accountInfo = _accountInfo;
557             if (relativePath.StartsWith(FolderConstants.OthersFolder))
558             {                
559                 accountName = parts[1];
560                 container = parts[2];
561                 relativeUrl = String.Join("/", parts.Splice(3));
562                 //Create the root URL for the target account
563                 var oldName = UserName;
564                 var absoluteUri =  _accountInfo.StorageUri.AbsoluteUri;
565                 var nameIndex=absoluteUri.IndexOf(oldName, StringComparison.Ordinal);
566                 var root=absoluteUri.Substring(0, nameIndex);
567
568                 accountInfo = new AccountInfo
569                 {
570                     UserName = accountName,
571                     AccountPath = Path.Combine(accountInfo.AccountPath, parts[0], parts[1]),
572                     StorageUri = new Uri(root + accountName),
573                     BlockHash=accountInfo.BlockHash,
574                     BlockSize=accountInfo.BlockSize,
575                     Token=accountInfo.Token
576                 };
577             }
578             else
579             {
580                 accountName = UserName;
581                 container = parts[0];
582                 relativeUrl = String.Join("/", parts.Splice(1));
583             }
584             
585             var client = new CloudFilesClient(accountInfo);
586             var objectInfo=client.GetObjectInfo(accountName, container, relativeUrl);
587             return objectInfo;
588         }
589         
590         public Task<ContainerInfo> GetContainerInfo(string filePath)
591         {
592             if (String.IsNullOrWhiteSpace(filePath))
593                 throw new ArgumentNullException("filePath");
594             Contract.EndContractBlock();
595
596             var file=new FileInfo(filePath);
597             var relativePath = file.AsRelativeTo(RootPath);
598             
599             string accountName,container;
600             
601             var parts=relativePath.Split('\\');
602
603             var accountInfo = _accountInfo;
604             if (relativePath.StartsWith(FolderConstants.OthersFolder))
605             {                
606                 accountName = parts[1];
607                 container = parts[2];                
608                 //Create the root URL for the target account
609                 var oldName = UserName;
610                 var absoluteUri =  _accountInfo.StorageUri.AbsoluteUri;
611                 var nameIndex=absoluteUri.IndexOf(oldName, StringComparison.Ordinal);
612                 var root=absoluteUri.Substring(0, nameIndex);
613
614                 accountInfo = new AccountInfo
615                 {
616                     UserName = accountName,
617                     AccountPath = Path.Combine(accountInfo.AccountPath, parts[0], parts[1]),
618                     StorageUri = new Uri(root + accountName),
619                     BlockHash=accountInfo.BlockHash,
620                     BlockSize=accountInfo.BlockSize,
621                     Token=accountInfo.Token
622                 };
623             }
624             else
625             {
626                 accountName = UserName;
627                 container = parts[0];                
628             }
629
630             return Task.Factory.StartNew(() =>
631             {
632                 var client = new CloudFilesClient(accountInfo);
633                 var containerInfo = client.GetContainerInfo(accountName, container);
634                 return containerInfo;
635             });
636         }
637     }
638 }