Fixes for stale states, case insensitive moves
[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             IndexLocalFiles();
255             //Extract the URIs from the string collection
256             var settings = Settings.Accounts.First(s => s.AccountKey == _accountInfo.AccountKey );
257                             
258             var selectiveUrls=settings.SelectiveFolders.Cast<string>().Select(url => new Uri(url,UriKind.RelativeOrAbsolute))
259                 .Where(uri=>uri.IsAbsoluteUri).ToArray();
260
261             SetSelectivePaths(selectiveUrls,null,null);
262             
263             StartWatcherAgent();
264
265             StartNetworkAgent();
266             
267             //WorkflowAgent.RestartInterruptedFiles(_accountInfo);
268             _started = true;
269         }
270
271         private void EnsurePithosContainers()
272         {
273
274             //Create the two default containers if they are missing
275             var pithosContainers = new List<string>{ FolderConstants.TrashContainer,FolderConstants.PithosContainer};
276             foreach (var container in pithosContainers)
277             {                
278                 var info=CloudClient.GetContainerInfo(UserName, container);
279                 if (info == ContainerInfo.Empty)
280                 {
281                     CloudClient.CreateContainer(UserName, container);
282                     info = CloudClient.GetContainerInfo(UserName, container);
283                 }
284                 _blockSize = info.BlockSize;
285                 _blockHash = info.BlockHash;
286                 _accountInfo.BlockSize = _blockSize;
287                 _accountInfo.BlockHash = _blockHash;
288             }
289         }
290
291         public string AuthenticationUrl { get; set; }
292
293         private void IndexLocalFiles()
294         {
295             using (ThreadContext.Stacks["Operation"].Push("Indexing local files"))
296             {
297                 
298                 try
299                 {
300                     //StatusNotification.NotifyChange("Indexing Local Files");
301                     Log.Info("Start local indexing");
302                     StatusNotification.SetPithosStatus(PithosStatus.LocalSyncing,"Indexing Local Files");                    
303
304                     var cachePath = Path.Combine(RootPath, FolderConstants.CacheFolder);
305                     var directory = new DirectoryInfo(RootPath);
306                     var files =
307                         from file in directory.EnumerateFiles("*", SearchOption.AllDirectories)
308                         where !file.FullName.StartsWith(cachePath, StringComparison.InvariantCultureIgnoreCase) &&
309                               !file.Extension.Equals("ignore", StringComparison.InvariantCultureIgnoreCase)
310                         select file;
311                     StatusKeeper.ProcessExistingFiles(files);
312
313                 }
314                 catch (Exception exc)
315                 {
316                     Log.Error("[ERROR]", exc);
317                 }
318                 finally
319                 {
320                     Log.Info("[END]");
321                 }
322                 StatusNotification.SetPithosStatus(PithosStatus.LocalComplete,"Indexing Completed");
323             }
324         }
325
326         
327   
328
329
330        /* private void StartWorkflowAgent()
331         {
332             WorkflowAgent.StatusNotification = StatusNotification;
333
334 /*            //On Vista and up we can check for a network connection
335             bool connected=Environment.OSVersion.Version.Major < 6 || NetworkListManager.IsConnectedToInternet;
336             //If we are not connected retry later
337             if (!connected)
338             {
339                 Task.Factory.StartNewDelayed(10000, StartWorkflowAgent);
340                 return;
341             }#1#
342
343             try
344             {
345                 WorkflowAgent.Start();                
346             }
347             catch (Exception)
348             {
349                 //Faild to authenticate due to network or account error
350                 //Retry after a while
351                 Task.Factory.StartNewDelayed(10000, StartWorkflowAgent);
352             }
353         }*/
354
355
356         private void StartNetworkAgent()
357         {
358             NetworkAgent.StatusNotification = StatusNotification;
359
360             //TODO: The Network and Poll agents are not account specific
361             //They should be moved outside PithosMonitor
362 /*
363             NetworkAgent.Start();
364 */
365
366             PollAgent.AddAccount(_accountInfo);
367
368             PollAgent.StatusNotification = StatusNotification;
369
370             PollAgent.PollRemoteFiles();
371         }
372
373         //Make sure a hidden cache folder exists to store partial downloads
374         private static void CreateHiddenFolder(string rootPath, string folderName)
375         {
376             if (String.IsNullOrWhiteSpace(rootPath))
377                 throw new ArgumentNullException("rootPath");
378             if (!Path.IsPathRooted(rootPath))
379                 throw new ArgumentException("rootPath");
380             if (String.IsNullOrWhiteSpace(folderName))
381                 throw new ArgumentNullException("folderName");
382             Contract.EndContractBlock();
383
384             var folder = Path.Combine(rootPath, folderName);
385             if (!Directory.Exists(folder))
386             {
387                 var info = Directory.CreateDirectory(folder);
388                 info.Attributes |= FileAttributes.Hidden;
389
390                 Log.InfoFormat("Created cache Folder: {0}", folder);
391             }
392             else
393             {
394                 var info = new DirectoryInfo(folder);
395                 if ((info.Attributes & FileAttributes.Hidden) == 0)
396                 {
397                     info.Attributes |= FileAttributes.Hidden;
398                     Log.InfoFormat("Reset cache folder to hidden: {0}", folder);
399                 }                                
400             }
401         }
402
403        
404
405
406         private void StartWatcherAgent()
407         {
408             if (Log.IsDebugEnabled)
409                 Log.DebugFormat("Start Folder Monitoring [{0}]",RootPath);
410
411             AgentLocator<FileAgent>.Register(FileAgent,RootPath);
412             
413             FileAgent.IdleTimeout = Settings.FileIdleTimeout;
414             FileAgent.StatusKeeper = StatusKeeper;
415             FileAgent.StatusNotification = StatusNotification;
416             FileAgent.Workflow = Workflow;
417             FileAgent.CachePath = Path.Combine(RootPath, FolderConstants.CacheFolder);
418             FileAgent.Start(_accountInfo, RootPath);
419         }
420
421         public void Stop()
422         {
423 /*
424             AgentLocator<FileAgent>.Remove(RootPath);
425
426             if (FileAgent!=null)
427                 FileAgent.Stop();
428             FileAgent = null;
429 */
430         }
431
432
433         ~PithosMonitor()
434         {
435             Dispose(false);
436         }
437
438         public void Dispose()
439         {
440             Dispose(true);
441             GC.SuppressFinalize(this);
442         }
443
444         protected virtual void Dispose(bool disposing)
445         {
446             if (disposing)
447             {
448                 Stop();
449             }
450         }
451
452
453         public void MoveFileStates(string oldPath, string newPath)
454         {
455             if (String.IsNullOrWhiteSpace(oldPath))
456                 throw new ArgumentNullException("oldPath");
457             if (!Path.IsPathRooted(oldPath))
458                 throw new ArgumentException("oldPath must be an absolute path","oldPath");
459             if (string.IsNullOrWhiteSpace(newPath))
460                 throw new ArgumentNullException("newPath");
461             if (!Path.IsPathRooted(newPath))
462                 throw new ArgumentException("newPath must be an absolute path","newPath");
463             Contract.EndContractBlock();
464
465             StatusKeeper.ChangeRoots(oldPath, newPath);
466         }
467
468         public void SetSelectivePaths(Uri[] uris,Uri[] added, Uri[] removed)
469         {
470             //Convert the uris to paths
471             var selectivePaths = UrisToFilePaths(uris);
472             
473             var selectiveUri = uris.ToList();
474             this.Selectives.SetSelectedUris(_accountInfo,selectiveUri);
475
476             var removedPaths = UrisToFilePaths(removed);
477             UnversionSelectivePaths(removedPaths);
478
479         }
480
481         /// <summary>
482         /// Mark all unselected paths as Unversioned
483         /// </summary>
484         /// <param name="removed"></param>
485         private void UnversionSelectivePaths(List<string> removed)
486         {
487             if (removed == null)
488                 return;
489
490             //Ensure we remove any file state below the deleted folders
491             FileState.UnversionPaths(removed);
492         }
493
494
495         /// <summary>
496         /// Return a list of absolute filepaths from a list of Uris
497         /// </summary>
498         /// <param name="uris"></param>
499         /// <returns></returns>
500         private List<string> UrisToFilePaths(IEnumerable<Uri> uris)
501         {
502             if (uris == null)
503                 return new List<string>();
504
505             var own = (from uri in uris
506                        where uri.ToString().StartsWith(_accountInfo.StorageUri.ToString())
507                                    let relativePath = _accountInfo.StorageUri.MakeRelativeUri(uri).RelativeUriToFilePath()
508                                    //Trim the account name
509                                    select Path.Combine(RootPath, relativePath.After(_accountInfo.UserName + '\\'))).ToList();
510             var others= (from uri in uris
511                          where !uri.ToString().StartsWith(_accountInfo.StorageUri.ToString())
512                                    let relativePath = _accountInfo.StorageUri.MakeRelativeUri(uri).RelativeUriToFilePath()
513                                    //Trim the account name
514                                    select Path.Combine(RootPath,"others-shared", relativePath)).ToList();
515             return own.Union(others).ToList();            
516         }
517
518
519         public ObjectInfo GetObjectInfo(string filePath)
520         {
521             if (String.IsNullOrWhiteSpace(filePath))
522                 throw new ArgumentNullException("filePath");
523             Contract.EndContractBlock();
524
525             var file=new FileInfo(filePath);
526             string relativeUrl;//=file.AsRelativeUrlTo(this.RootPath);
527             var relativePath = file.AsRelativeTo(RootPath);
528             
529             string accountName,container;
530             
531             var parts=relativePath.Split('\\');
532
533             var accountInfo = _accountInfo;
534             if (relativePath.StartsWith(FolderConstants.OthersFolder))
535             {                
536                 accountName = parts[1];
537                 container = parts[2];
538                 relativeUrl = String.Join("/", parts.Splice(3));
539                 //Create the root URL for the target account
540                 var oldName = UserName;
541                 var absoluteUri =  _accountInfo.StorageUri.AbsoluteUri;
542                 var nameIndex=absoluteUri.IndexOf(oldName, StringComparison.Ordinal);
543                 var root=absoluteUri.Substring(0, nameIndex);
544
545                 accountInfo = new AccountInfo
546                 {
547                     UserName = accountName,
548                     AccountPath = Path.Combine(accountInfo.AccountPath, parts[0], parts[1]),
549                     StorageUri = new Uri(root + accountName),
550                     BlockHash=accountInfo.BlockHash,
551                     BlockSize=accountInfo.BlockSize,
552                     Token=accountInfo.Token
553                 };
554             }
555             else
556             {
557                 accountName = UserName;
558                 container = parts[0];
559                 relativeUrl = String.Join("/", parts.Splice(1));
560             }
561             
562             var client = new CloudFilesClient(accountInfo);
563             var objectInfo=client.GetObjectInfo(accountName, container, relativeUrl);
564             return objectInfo;
565         }
566         
567         public Task<ContainerInfo> GetContainerInfo(string filePath)
568         {
569             if (String.IsNullOrWhiteSpace(filePath))
570                 throw new ArgumentNullException("filePath");
571             Contract.EndContractBlock();
572
573             var file=new FileInfo(filePath);
574             var relativePath = file.AsRelativeTo(RootPath);
575             
576             string accountName,container;
577             
578             var parts=relativePath.Split('\\');
579
580             var accountInfo = _accountInfo;
581             if (relativePath.StartsWith(FolderConstants.OthersFolder))
582             {                
583                 accountName = parts[1];
584                 container = parts[2];                
585                 //Create the root URL for the target account
586                 var oldName = UserName;
587                 var absoluteUri =  _accountInfo.StorageUri.AbsoluteUri;
588                 var nameIndex=absoluteUri.IndexOf(oldName, StringComparison.Ordinal);
589                 var root=absoluteUri.Substring(0, nameIndex);
590
591                 accountInfo = new AccountInfo
592                 {
593                     UserName = accountName,
594                     AccountPath = Path.Combine(accountInfo.AccountPath, parts[0], parts[1]),
595                     StorageUri = new Uri(root + accountName),
596                     BlockHash=accountInfo.BlockHash,
597                     BlockSize=accountInfo.BlockSize,
598                     Token=accountInfo.Token
599                 };
600             }
601             else
602             {
603                 accountName = UserName;
604                 container = parts[0];                
605             }
606
607             return Task.Factory.StartNew(() =>
608             {
609                 var client = new CloudFilesClient(accountInfo);
610                 var containerInfo = client.GetContainerInfo(accountName, container);
611                 return containerInfo;
612             });
613         }
614     }
615 }