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