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