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