Some timeout issues
[pithos-ms-client] / trunk / Pithos.Core / Agents / StatusAgent.cs
1 #region\r
2 /* -----------------------------------------------------------------------\r
3  * <copyright file="StatusAgent.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.Data;\r
46 using System.Data.SqlServerCe;\r
47 using System.Diagnostics;\r
48 using System.Diagnostics.Contracts;\r
49 using System.IO;\r
50 using System.Linq;\r
51 using System.Reflection;\r
52 using System.Threading;\r
53 using NHibernate;\r
54 using NHibernate.Cfg;\r
55 using NHibernate.Cfg.MappingSchema;\r
56 using NHibernate.Criterion;\r
57 using NHibernate.Dialect;\r
58 using NHibernate.Linq;\r
59 using NHibernate.Mapping.ByCode;\r
60 using NHibernate.Tool.hbm2ddl;\r
61 using Pithos.Interfaces;\r
62 using Pithos.Network;\r
63 using log4net;\r
64 using Environment = System.Environment;\r
65 \r
66 namespace Pithos.Core.Agents\r
67 {\r
68     [Export(typeof(IStatusChecker)),Export(typeof(IStatusKeeper))]\r
69     public class StatusAgent:IStatusChecker,IStatusKeeper\r
70     {\r
71         private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);\r
72 \r
73         [System.ComponentModel.Composition.Import]\r
74         public IPithosSettings Settings { get; set; }\r
75 \r
76         [System.ComponentModel.Composition.Import]\r
77         public IStatusNotification StatusNotification { get; set; }\r
78 \r
79         //private Agent<Action> _persistenceAgent;\r
80 \r
81 \r
82 \r
83         public StatusAgent()\r
84         {\r
85             var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);\r
86 \r
87             _pithosDataPath = Path.Combine(appDataPath, "GRNET\\PITHOS");\r
88             if (!Directory.Exists(_pithosDataPath))\r
89                 Directory.CreateDirectory(_pithosDataPath);\r
90 \r
91             var dbPath = Path.Combine(_pithosDataPath, "pithos.sdf");\r
92 \r
93             //MigrateOldDb(dbPath, appDataPath);\r
94 \r
95 \r
96             var cfg = Configure(dbPath);\r
97 \r
98             var connectionString = "Data Source=" + dbPath;\r
99             using (var sqlCeEngine = new SqlCeEngine(connectionString))\r
100             {\r
101                 if (!File.Exists(dbPath))\r
102                 {\r
103                     sqlCeEngine.CreateDatabase();\r
104                     new SchemaExport(cfg).Execute(true, true, false);\r
105                     _factory = cfg.BuildSessionFactory();\r
106                     using (var session = _factory.OpenStatelessSession())\r
107                     {\r
108                         session.Insert(new PithosVersion {Id = 1, Version = "0.0.0.0"});\r
109                     }\r
110                 }\r
111                 else\r
112                 {\r
113                     try\r
114                     {\r
115                         if (!sqlCeEngine.Verify(VerifyOption.Enhanced))\r
116                             sqlCeEngine.Repair(connectionString, RepairOption.RecoverAllOrFail);\r
117                     }\r
118                     catch(SqlCeException ex)\r
119                     {\r
120                         //Rethrow except for sharing errors while repairing\r
121                         if (ex.NativeError != 25035)\r
122                             throw;\r
123                     }\r
124                     var update = new SchemaUpdate(cfg);\r
125                     update.Execute(script=>Log.WarnFormat("[DBUPDATE] : {0}",script),true);\r
126                     _factory = cfg.BuildSessionFactory();                    \r
127                 }\r
128                 UpgradeDatabase();\r
129             }\r
130         }\r
131 \r
132         private void UpgradeDatabase()\r
133         {\r
134             using (var session = _factory.OpenSession())\r
135             {\r
136 \r
137                 var storedVersion = session.Get<PithosVersion>(1);\r
138                 var actualVersion = Assembly.GetEntryAssembly().GetName().Version;\r
139 \r
140                 if (actualVersion == new Version(storedVersion.Version)) \r
141                     return;\r
142                 \r
143                 storedVersion.Version = actualVersion.ToString();\r
144                 session.Update(storedVersion);\r
145                 session.Flush();\r
146             }\r
147         }\r
148 \r
149 \r
150         private static Configuration Configure(string pithosDbPath)\r
151         {\r
152             if (String.IsNullOrWhiteSpace(pithosDbPath))\r
153                 throw new ArgumentNullException("pithosDbPath");\r
154             if (!Path.IsPathRooted(pithosDbPath))\r
155                 throw new ArgumentException("path must be a rooted path", "pithosDbPath");\r
156             Contract.EndContractBlock();\r
157 \r
158 \r
159             var cfg = new Configuration();                \r
160                 cfg.DataBaseIntegration(db=>\r
161                                          {\r
162                                              db.Dialect<MsSqlCe40Dialect>();\r
163                                              db.ConnectionString = "Data Source=" + pithosDbPath;\r
164                                              db.AutoCommentSql = true;\r
165                                              db.KeywordsAutoImport = Hbm2DDLKeyWords.AutoQuote;\r
166                                              db.SchemaAction = SchemaAutoAction.Update;\r
167                                              db.LogSqlInConsole = true;                                             \r
168                                          })                                                     \r
169                 .SessionFactory()                      \r
170                     .GenerateStatistics()                    \r
171                     .Integrate.Schema\r
172                         .Updating();            \r
173             var mapping = GetMapping();            \r
174             cfg.AddMapping(mapping);\r
175 \r
176             return cfg;\r
177         }\r
178 \r
179         private static HbmMapping GetMapping()\r
180         {\r
181             var mapper = new ModelMapper();\r
182             mapper.Class<FileState>(fm =>\r
183                                         {\r
184                                             fm.Id(x => x.Id, m => m.Generator(Generators.GuidComb));\r
185                                             fm.Property(x => x.ObjectID, m =>\r
186                                             {\r
187                                                 m.Index("IX_FileState_ObjectID");\r
188                                             });\r
189                                             fm.Property(x => x.FilePath, m =>\r
190                                             {\r
191                                                 m.Unique(true);\r
192                                                 m.UniqueKey("U_FileState_FilePath");\r
193                                                 m.Index("IX_FileState_FilePath");                                                \r
194                                             });\r
195                                             fm.Property(x => x.OverlayStatus);\r
196                                             fm.Property(x => x.FileStatus);\r
197                                             fm.Property(x => x.ConflictReason);\r
198                                             fm.Property(x => x.Checksum, m => m.Length(64));\r
199                                             fm.Property(x => x.ETag, m => m.Length(64));\r
200                                             fm.Property(x => x.Hashes, m => m.Type(NHibernateUtil.StringClob));\r
201                                             fm.Property(x => x.LastWriteDate);\r
202                                             fm.Property(x => x.LastLength);\r
203                                             fm.Property(x => x.Version);\r
204                                             fm.Property(x => x.VersionTimeStamp);\r
205                                             fm.Property(x => x.IsShared);\r
206                                             fm.Property(x => x.SharedBy);\r
207                                             fm.Property(x => x.ShareWrite);\r
208                                             fm.Property(x => x.IsFolder);\r
209                                             fm.Property(x => x.Modified);                                            \r
210                                         });\r
211             mapper.Class<PithosVersion>(fm =>\r
212                                         {\r
213                                             fm.Id(x => x.Id, m => m.Generator(Generators.Assigned));\r
214                                             fm.Property(x => x.Version, m => m.Length(20));\r
215                                         });\r
216 \r
217 \r
218             var mapping = mapper.CompileMappingFor(new[] {typeof (FileState),typeof(PithosVersion)});\r
219             return mapping;\r
220         }\r
221 \r
222         public void StartProcessing(CancellationToken token)\r
223         {\r
224            \r
225             \r
226         }\r
227 \r
228        \r
229 \r
230         public void Stop()\r
231         {\r
232           \r
233         }\r
234 \r
235 \r
236         public void ProcessExistingFiles(IEnumerable<FileInfo> existingFiles)\r
237         {\r
238             if (existingFiles == null)\r
239                 throw new ArgumentNullException("existingFiles");\r
240             Contract.EndContractBlock();\r
241 \r
242             //Find new or matching files with a left join to the stored states\r
243             using (var session = _factory.OpenSession())\r
244             {\r
245 \r
246                 var fileStates = session.Query<FileState>().ToList();\r
247                 var currentFiles = from file in existingFiles\r
248                                    join state in fileStates on file.FullName.ToLower() equals state.FilePath.ToLower()\r
249                                        into\r
250                                        gs\r
251                                    from substate in gs.DefaultIfEmpty()\r
252                                    select Tuple.Create(file, substate);\r
253 \r
254                 //To get the deleted files we must get the states that have no corresponding\r
255                 //files. \r
256                 //We can't use the File.Exists method inside a query, so we get all file paths from the states\r
257                 var statePaths = (from state in fileStates\r
258                                   select new {state.Id, state.FilePath}).ToList();\r
259                 //and check each one\r
260                 var missingStates = (from path in statePaths\r
261                                      where !File.Exists(path.FilePath) && !Directory.Exists(path.FilePath)\r
262                                      select path.Id).ToList();\r
263                 //Finally, retrieve the states that correspond to the deleted files            \r
264                 var deletedFiles = from state in fileStates\r
265                                    where missingStates.Contains(state.Id)\r
266                                    select Tuple.Create(default(FileInfo), state);\r
267 \r
268                 var pairs = currentFiles.Union(deletedFiles).ToList();\r
269 \r
270                 i = 1;\r
271                 var total = pairs.Count;\r
272                 foreach (var pair in pairs)\r
273                 {\r
274                     ProcessFile(session,total, pair);\r
275                 }\r
276                 session.Flush();\r
277             }\r
278         }\r
279 \r
280         int i = 1;\r
281 \r
282         private void ProcessFile(ISession session,int total, System.Tuple<FileInfo, FileState> pair)\r
283         {\r
284             var idx = Interlocked.Increment(ref i);\r
285             using (StatusNotification.GetNotifier("Indexing file {0} of {1}", "Indexed file {0} of {1} ", true,idx, total))\r
286             {\r
287                 var fileState = pair.Item2;\r
288                 var file = pair.Item1;\r
289                 if (fileState == null)\r
290                 {\r
291                     //This is a new file                        \r
292                     var createState = FileState.CreateFor(file,StatusNotification);                    \r
293                     session.Save(createState);\r
294                     //_persistenceAgent.Post(createState.Create);\r
295                 }\r
296                 else if (file == null)\r
297                 {\r
298                     //This file was deleted while we were down. We should mark it as deleted\r
299                     //We have to go through UpdateStatus here because the state object we are using\r
300                     //was created by a different ORM session.\r
301                     UpdateStatusDirect(session,fileState.Id, FileStatus.Deleted);\r
302                     //_persistenceAgent.Post(() => UpdateStatusDirect((Guid) fileState.Id, FileStatus.Deleted));\r
303                 }\r
304                 //else\r
305                 //{\r
306                 //    //This file has a matching state. Need to check for possible changes\r
307                 //    //To check for changes, we use the cheap (in CPU terms) MD5 algorithm\r
308                 //    //on the entire file.\r
309 \r
310                 //    var hashString = file.ComputeShortHash(StatusNotification);\r
311                 //    Debug.Assert(hashString.Length==32);\r
312 \r
313 \r
314                 //    //TODO: Need a way to attach the hashes to the filestate so we don't\r
315                 //    //recalculate them each time a call to calculate has is made\r
316                 //    //We can either store them to the filestate or add them to a \r
317                 //    //dictionary\r
318 \r
319                 //    //If the hashes don't match the file was changed\r
320                 //    if (fileState.ETag != hashString)\r
321                 //    {\r
322                 //        _persistenceAgent.Post(() => UpdateStatusDirect((Guid) fileState.Id, FileStatus.Modified));\r
323                 //    }\r
324                 //}\r
325             }\r
326         }\r
327 \r
328 \r
329         private int UpdateStatusDirect(ISession session,Guid id, FileStatus status)\r
330         {\r
331             using (ThreadContext.Stacks["StatusAgent"].Push("UpdateStatusDirect"))\r
332             {\r
333 \r
334                 try\r
335                 {\r
336                         //var updatecmd = session.CreateSQLQuery(\r
337                     var updatecmd = session.CreateQuery(\r
338                         "update FileState set FileStatus= :fileStatus, Modified=:modified where Id = :id  ")\r
339                         .SetGuid("id", id)\r
340                         .SetEnum("fileStatus", status)\r
341                         .SetDateTime("modified",DateTime.Now);\r
342                     var affected = updatecmd.ExecuteUpdate();\r
343                         session.Flush();\r
344                     return affected;\r
345                 }\r
346                 catch (Exception exc)\r
347                 {\r
348                     Log.Error(exc.ToString());\r
349                     throw;\r
350                 }\r
351             }\r
352         }\r
353 \r
354 \r
355         public string BlockHash { get; set; }\r
356 \r
357         public int BlockSize { get; set; }\r
358 \r
359         public void ChangeRoots(string oldPath, string newPath)\r
360         {\r
361             if (String.IsNullOrWhiteSpace(oldPath))\r
362                 throw new ArgumentNullException("oldPath");\r
363             if (!Path.IsPathRooted(oldPath))\r
364                 throw new ArgumentException("oldPath must be an absolute path", "oldPath");\r
365             if (String.IsNullOrWhiteSpace(newPath))\r
366                 throw new ArgumentNullException("newPath");\r
367             if (!Path.IsPathRooted(newPath))\r
368                 throw new ArgumentException("newPath must be an absolute path", "newPath");\r
369             Contract.EndContractBlock();\r
370 \r
371             ChangeRootPath(oldPath,newPath);\r
372 \r
373         }\r
374 \r
375 \r
376 \r
377         private readonly string _pithosDataPath;\r
378         private readonly ISessionFactory _factory;\r
379 \r
380         public FileState GetStateByFilePath(string path)\r
381         {\r
382             if (String.IsNullOrWhiteSpace(path))\r
383                 throw new ArgumentNullException("path");\r
384             if (!Path.IsPathRooted(path))\r
385                 throw new ArgumentException("The path must be rooted", "path");\r
386             Contract.EndContractBlock();\r
387 \r
388             try\r
389             {\r
390                 \r
391                 using(var session=_factory.OpenStatelessSession())\r
392                 {\r
393                     var state=session.Query<FileState>().SingleOrDefault(s => s.FilePath == path);\r
394                     if (state==null)\r
395                         return null;\r
396                     state.FilePath=state.FilePath??String.Empty;\r
397                     state.OverlayStatus = state.OverlayStatus ??FileOverlayStatus.Unversioned;\r
398                     state.FileStatus = state.FileStatus ?? FileStatus.Missing;\r
399                     state.Checksum = state.Checksum ?? String.Empty;\r
400                     state.ETag = state.ETag ?? String.Empty;\r
401                     state.SharedBy = state.SharedBy ?? String.Empty;\r
402                     return state;\r
403                 }\r
404 \r
405             }\r
406             catch (Exception exc)\r
407             {\r
408                 Log.ErrorFormat(exc.ToString());\r
409                 throw;\r
410             }            \r
411         }\r
412 \r
413         public FileOverlayStatus GetFileOverlayStatus(string path)\r
414         {\r
415             if (String.IsNullOrWhiteSpace(path))\r
416                 throw new ArgumentNullException("path");\r
417             if (!Path.IsPathRooted(path))\r
418                 throw new ArgumentException("The path must be rooted", "path");\r
419             Contract.EndContractBlock();\r
420 \r
421             try\r
422             {\r
423                 \r
424                 using(var session=_factory.OpenStatelessSession())\r
425                 {\r
426                     return (from state in session.Query<FileState>()\r
427                             where state.FilePath == path\r
428                             select state.OverlayStatus)\r
429                             .Single()\r
430                             .GetValueOrDefault(FileOverlayStatus.Unversioned);\r
431                 }\r
432             }\r
433             catch (Exception exc)\r
434             {\r
435                 Log.ErrorFormat(exc.ToString());\r
436                 return FileOverlayStatus.Unversioned;\r
437             }\r
438         }\r
439 \r
440         public void SetFileOverlayStatus(string path, FileOverlayStatus overlayStatus)\r
441         {\r
442             if (String.IsNullOrWhiteSpace(path))\r
443                 throw new ArgumentNullException("path");\r
444             if (!Path.IsPathRooted(path))\r
445                 throw new ArgumentException("The path must be rooted","path");\r
446             Contract.EndContractBlock();\r
447 \r
448             StoreOverlayStatus(path,overlayStatus);\r
449         }\r
450 \r
451         public void SetFileState(string path, FileStatus fileStatus, FileOverlayStatus overlayStatus, string conflictReason)\r
452         {\r
453             if (String.IsNullOrWhiteSpace(path))\r
454                 throw new ArgumentNullException("path");\r
455             if (!Path.IsPathRooted(path))\r
456                 throw new ArgumentException("The path must be rooted", "path");\r
457             Contract.EndContractBlock();\r
458 \r
459             Debug.Assert(!path.Contains(FolderConstants.CacheFolder));\r
460             Debug.Assert(!path.EndsWith(".ignore"));            \r
461             using (ThreadContext.Stacks["StatusAgent"].Push("UpdateStatusDirect"))\r
462             {\r
463 \r
464                 try\r
465                 {\r
466 \r
467                     using (var session = _factory.OpenSession())\r
468                     using (var tx=session.BeginTransaction(IsolationLevel.ReadCommitted))\r
469                     {\r
470 \r
471                         //var updatecmd = session.CreateSQLQuery("update FileState set OverlayStatus= :overlayStatus, FileStatus= :fileStatus,ConflictReason= :conflictReason where FilePath = :path ")\r
472                         var updatecmd = session.CreateQuery("update FileState set OverlayStatus= :overlayStatus, FileStatus= :fileStatus,ConflictReason= :conflictReason, Modified=:modified where FilePath = :path")\r
473                             .SetString("path", path)\r
474                             .SetEnum("fileStatus", fileStatus)\r
475                             .SetEnum("overlayStatus", overlayStatus)\r
476                             .SetString("conflictReason", conflictReason)\r
477                             .SetDateTime("modified",DateTime.Now);\r
478                         var affected = updatecmd.ExecuteUpdate();\r
479 \r
480                         if (affected == 0)\r
481                         {\r
482                             //Can happen when downloading a new file\r
483                             var createdState = FileState.CreateFor(FileInfoExtensions.FromPath(path), StatusNotification);\r
484                             createdState.FileStatus = fileStatus;\r
485                             createdState.OverlayStatus = overlayStatus;                            \r
486                             createdState.ConflictReason = conflictReason;\r
487                             session.Save(createdState);\r
488                             //createdState.Create();\r
489                         }\r
490                         session.Flush();\r
491                         tx.Commit();                        \r
492                     }\r
493                 }\r
494                 catch (Exception exc)\r
495                 {\r
496                     Log.Error(exc.ToString());\r
497                     throw;\r
498                 }\r
499             }            \r
500         }\r
501 \r
502 \r
503         public void StoreInfo(string path, ObjectInfo objectInfo, TreeHash treeHash)\r
504         {\r
505             if (String.IsNullOrWhiteSpace(path))\r
506                 throw new ArgumentNullException("path");\r
507             if (treeHash==null)\r
508                 throw new ArgumentNullException("treeHash");\r
509             if (!Path.IsPathRooted(path))\r
510                 throw new ArgumentException("The path must be rooted", "path");\r
511             if (objectInfo == null)\r
512                 throw new ArgumentNullException("objectInfo", "objectInfo can't be empty");\r
513             Contract.EndContractBlock();\r
514 \r
515             StoreInfoDirect(path, objectInfo, treeHash);\r
516 \r
517         }\r
518 \r
519         public void StoreInfo(string path, ObjectInfo objectInfo)\r
520         {\r
521             if (String.IsNullOrWhiteSpace(path))\r
522                 throw new ArgumentNullException("path");\r
523             if (!Path.IsPathRooted(path))\r
524                 throw new ArgumentException("The path must be rooted", "path");\r
525             if (objectInfo == null)\r
526                 throw new ArgumentNullException("objectInfo", "objectInfo can't be empty");\r
527             Contract.EndContractBlock();\r
528 \r
529             StoreInfoDirect(path, objectInfo, null);\r
530 \r
531         }\r
532 \r
533         private void StoreInfoDirect(string path, ObjectInfo objectInfo,TreeHash treeHash)\r
534         {\r
535             try\r
536             {\r
537                     using (var session = _factory.OpenSession())\r
538                     using (var tx=session.BeginTransaction(IsolationLevel.ReadCommitted))\r
539                     {\r
540 \r
541                         //An entry for the new path may exist, \r
542                         IQuery deletecmd = session.CreateQuery(\r
543                             "delete from FileState where FilePath=:path and ObjectID is null")\r
544                             .SetString("path", path);\r
545                         deletecmd.ExecuteUpdate();\r
546 \r
547                         //string md5=treeHash.NullSafe(t=>t.MD5);                        \r
548                         string hashes = treeHash.NullSafe(t => t.ToJson());\r
549 \r
550                         var info = FileInfoExtensions.FromPath(path);\r
551                         var lastWriteTime = info.LastWriteTime;\r
552                         var isFolder = (info is DirectoryInfo);\r
553                         var lastLength = isFolder ? 0 : ((FileInfo) info).Length;\r
554 \r
555                         var state = session.Query<FileState>().SingleOrDefault(s => s.ObjectID == (objectInfo.UUID??"§"))   //Handle null UUIDs\r
556                                      ?? session.Query<FileState>().SingleOrDefault(s => s.FilePath == path)\r
557                                      ?? new FileState();\r
558                         state.FilePath = path;\r
559                         state.IsFolder = isFolder;\r
560                         state.LastWriteDate = lastWriteTime;\r
561                         state.LastLength = lastLength;\r
562                         state.Checksum = objectInfo.X_Object_Hash;\r
563                         state.Hashes = hashes;\r
564                         state.Version = objectInfo.Version.GetValueOrDefault();\r
565                         state.VersionTimeStamp = objectInfo.VersionTimestamp;\r
566                         state.ETag = objectInfo.ETag;\r
567                         state.FileStatus = FileStatus.Unchanged;\r
568                         state.OverlayStatus = FileOverlayStatus.Normal;\r
569                         state.ObjectID = objectInfo.UUID;\r
570                         state.Modified = DateTime.Now;\r
571                         session.SaveOrUpdate(state);\r
572 \r
573 \r
574 \r
575                         session.Flush();\r
576                         tx.Commit();\r
577                         Log.ErrorFormat("DebugDB [{0}]:[{1}]\r\n{2}", path, objectInfo.UUID, objectInfo.X_Object_Hash);\r
578                     }\r
579             }\r
580             catch (Exception exc)\r
581             {\r
582                 Log.ErrorFormat("Failed to update [{0}]:[{1}]\r\n{2}",path,objectInfo.UUID, exc);\r
583                 throw;\r
584             }\r
585         }\r
586 \r
587 \r
588 \r
589         public void SetFileStatus(string path, FileStatus status)\r
590         {\r
591             if (String.IsNullOrWhiteSpace(path))\r
592                 throw new ArgumentNullException("path");\r
593             if (!Path.IsPathRooted(path))\r
594                 throw new ArgumentException("The path must be rooted", "path");\r
595             Contract.EndContractBlock();\r
596 \r
597             using (ThreadContext.Stacks["StatusAgent"].Push("UpdateStatusDirect"))\r
598             {\r
599 \r
600                 try\r
601                 {                    \r
602                     using (var session = _factory.OpenSession())\r
603                     using (var tx=session.BeginTransaction(IsolationLevel.ReadCommitted))\r
604                     {\r
605 \r
606                         //var updatecmd = session.CreateSQLQuery(\r
607                         var updatecmd = session.CreateQuery(\r
608                             "update FileState set FileStatus= :fileStatus, Modified=:modified where FilePath = :path ")\r
609                             .SetString("path", path)\r
610                             .SetEnum("fileStatus", status)\r
611                             .SetDateTime("modified",DateTime.Now);\r
612                         var affected = updatecmd.ExecuteUpdate();\r
613 \r
614                         if (affected == 0)\r
615                         {                            \r
616                             var createdState = FileState.CreateFor(FileInfoExtensions.FromPath(path), StatusNotification);\r
617                             createdState.FileStatus = status;\r
618                             session.Save(createdState);\r
619                         }\r
620                         session.Flush();\r
621                         tx.Commit();\r
622                     }\r
623                 }\r
624                 catch (Exception exc)\r
625                 {\r
626                     Log.Error(exc.ToString());\r
627                     throw;\r
628                 }\r
629             }            \r
630         }\r
631 \r
632         public FileStatus GetFileStatus(string path)\r
633         {\r
634             if (String.IsNullOrWhiteSpace(path))\r
635                 throw new ArgumentNullException("path");\r
636             if (!Path.IsPathRooted(path))\r
637                 throw new ArgumentException("The path must be rooted", "path");\r
638             Contract.EndContractBlock();\r
639 \r
640             \r
641             using(var session=_factory.OpenStatelessSession())\r
642                 return (from state in session.Query<FileState>()\r
643                         select state.FileStatus).SingleOrDefault()??FileStatus.Missing;\r
644         }\r
645 \r
646         /// <summary>\r
647         /// Deletes the status of the specified file\r
648         /// </summary>\r
649         /// <param name="path"></param>\r
650         public void ClearFileStatus(string path)\r
651         {\r
652             if (String.IsNullOrWhiteSpace(path))\r
653                 throw new ArgumentNullException("path");\r
654             if (!Path.IsPathRooted(path))\r
655                 throw new ArgumentException("The path must be rooted", "path");\r
656             Contract.EndContractBlock();\r
657             using(var session=_factory.OpenSession())\r
658             {\r
659                 DeleteDirect(session,path);\r
660                 session.Flush();                \r
661             }\r
662         }\r
663 \r
664         /// <summary>\r
665         /// Deletes the status of the specified folder and all its contents\r
666         /// </summary>\r
667         /// <param name="path"></param>\r
668         public void ClearFolderStatus(string path)\r
669         {\r
670             if (String.IsNullOrWhiteSpace(path))\r
671                 throw new ArgumentNullException("path");\r
672             if (!Path.IsPathRooted(path))\r
673                 throw new ArgumentException("The path must be rooted", "path");\r
674             Contract.EndContractBlock();\r
675             using (ThreadContext.Stacks["StatusAgent"].Push("DeleteDirect"))\r
676             {\r
677 \r
678                 try\r
679                 {\r
680                     using (var session = _factory.OpenSession())\r
681                     {\r
682                         DeleteDirect(session,path);\r
683                         session.Flush();\r
684                     }\r
685                 }\r
686                 catch (Exception exc)\r
687                 {\r
688                     Log.Error(exc.ToString());\r
689                     throw;\r
690                 }\r
691             }            \r
692         }\r
693 \r
694         public IEnumerable<FileState> GetChildren(FileState fileState)\r
695         {\r
696             if (fileState == null)\r
697                 throw new ArgumentNullException("fileState");\r
698             Contract.EndContractBlock();\r
699 \r
700             var session = _factory.GetCurrentSession();\r
701             var children = from state in session.Query<FileState>()\r
702                            where state.FilePath.StartsWith(fileState.FilePath + "\\")\r
703                            select state;\r
704             return children;\r
705         }\r
706 \r
707         public void EnsureFileState(string path)\r
708         {\r
709             var existingState = GetStateByFilePath(path);\r
710             if (existingState != null)\r
711                 return;\r
712             var fileInfo = FileInfoExtensions.FromPath(path);\r
713             \r
714             using (var session=_factory.OpenSession())\r
715             {\r
716                 var newState = FileState.CreateFor(fileInfo,StatusNotification);\r
717                 newState.FileStatus=FileStatus.Missing;\r
718                 session.SaveOrUpdate(newState);\r
719                 session.Flush();\r
720                 //_persistenceAgent.PostAndAwait(newState.CreateAndFlush).Wait();\r
721             }\r
722 \r
723         }\r
724 \r
725         private void DeleteDirect(ISession session,string filePath)\r
726         {\r
727             using (ThreadContext.Stacks["StatusAgent"].Push("DeleteDirect"))\r
728             {\r
729 \r
730                 try\r
731                 {\r
732                     var deletes= session.CreateQuery("delete from FileState where FilePath = :path")\r
733                         .SetParameter("path", filePath)\r
734                         .ExecuteUpdate();                    \r
735                 }\r
736                 catch (Exception exc)\r
737                 {\r
738                     Log.Error(exc.ToString());\r
739                     throw;\r
740                 }\r
741             }\r
742         }\r
743 \r
744 \r
745 \r
746 \r
747         public void CleanupOrphanStates()\r
748         {\r
749             //Orphan states are those that do not correspond to an account, ie. their paths\r
750             //do not start with the root path of any registered account\r
751 \r
752             var roots=(from account in Settings.Accounts\r
753                       select account.RootPath).ToList();\r
754 \r
755             using (var session = _factory.OpenSession())\r
756             {\r
757                 var allStates = from state in session.Query<FileState>()\r
758                                 select state.FilePath;\r
759 \r
760                 foreach (var statePath in allStates)\r
761                 {\r
762                     if (!roots.Any(root => statePath.StartsWith(root, StringComparison.InvariantCultureIgnoreCase)))\r
763                         this.DeleteDirect(session,statePath);\r
764                 }\r
765                 session.Flush();\r
766             }\r
767         }\r
768 \r
769         public void SaveCopy<T>(T state) where T:class\r
770         {\r
771             using (var session = _factory.OpenSession())\r
772             {\r
773                 session.Merge(state);\r
774                 session.Flush();\r
775             }\r
776         }\r
777 \r
778         public void CleanupStaleStates(AccountInfo accountInfo, List<ObjectInfo> objectInfos)\r
779         {\r
780             if (accountInfo == null)\r
781                 throw new ArgumentNullException("accountInfo");\r
782             if (objectInfos == null)\r
783                 throw new ArgumentNullException("objectInfos");\r
784             Contract.EndContractBlock();\r
785             \r
786 \r
787 \r
788             //Stale states are those that have no corresponding local or server file\r
789             \r
790 \r
791             var agent=FileAgent.GetFileAgent(accountInfo);\r
792 \r
793             var localFiles=agent.EnumerateFiles();\r
794             var localSet = new HashSet<string>(localFiles);\r
795 \r
796             //RelativeUrlToFilePath will fail for\r
797             //infos of accounts, containers which have no Name\r
798 \r
799             var serverFiles = from info in objectInfos\r
800                               where info.Name != null\r
801                               select Path.Combine(accountInfo.AccountPath,info.RelativeUrlToFilePath(accountInfo.UserName));\r
802             var serverSet = new HashSet<string>(serverFiles);\r
803 \r
804             using (var session = _factory.OpenSession())\r
805             {\r
806 \r
807                 var allStates = from state in session.Query<FileState>()\r
808                                 where state.FilePath.StartsWith(agent.RootPath)\r
809                                 select state.FilePath;\r
810                 var stateSet = new HashSet<string>(allStates);\r
811                 stateSet.ExceptWith(serverSet);\r
812                 stateSet.ExceptWith(localSet);\r
813 \r
814                 foreach (var remainder in stateSet)\r
815                 {\r
816                     DeleteDirect(session,remainder);\r
817                 }\r
818                 session.Flush();\r
819             }\r
820         }\r
821 \r
822         public static TreeHash CalculateTreeHash(FileSystemInfo fileInfo, AccountInfo accountInfo, FileState fileState, byte hashingParallelism, CancellationToken cancellationToken, IProgress<HashProgress> progress)\r
823         {\r
824             fileInfo.Refresh();\r
825             //If the file doesn't exist, return the empty treehash\r
826             if (!fileInfo.Exists)\r
827                 return TreeHash.Empty;\r
828 \r
829             //FileState may be null if there is no stored state for this file\r
830             //if (fileState==null)\r
831                 return Signature.CalculateTreeHashAsync(fileInfo,\r
832                                                  accountInfo.BlockSize,\r
833                                                  accountInfo.BlockHash,\r
834                                                  hashingParallelism,\r
835                                                  cancellationToken, progress);\r
836             //Can we use the stored hashes?\r
837             //var localTreeHash = fileState.LastMD5 == Signature.CalculateMD5(fileInfo)\r
838             //                        ? TreeHash.Parse(fileState.Hashes)\r
839             //                        : Signature.CalculateTreeHashAsync(fileInfo,\r
840             //                                                           accountInfo.BlockSize,\r
841             //                                                           accountInfo.BlockHash,\r
842             //                                                           hashingParallelism,\r
843             //                                                           cancellationToken, progress);\r
844             //return localTreeHash;\r
845         }\r
846 \r
847 \r
848 \r
849         private object ExecuteWithRetry(Func<ISession, object, object> call, object state)\r
850         {\r
851             int retries = 3;\r
852             while (retries > 0)\r
853                 try\r
854                 {\r
855                     using (var session=_factory.OpenSession())\r
856                     {\r
857                         var result=call(session, state);\r
858                         session.Flush();\r
859                         return result;\r
860                     }\r
861                 }\r
862                 catch (Exception/* ActiveRecordException */)\r
863                 {\r
864                     retries--;\r
865                     if (retries <= 0)\r
866                         throw;\r
867                 }\r
868             return null;\r
869         }\r
870 \r
871         //TODO: Must separate between UpdateChecksum and UpdateFileHashes\r
872 \r
873         public  void ChangeRootPath(string oldPath, string newPath)\r
874         {\r
875             if (String.IsNullOrWhiteSpace(oldPath))\r
876                 throw new ArgumentNullException("oldPath");\r
877             if (!Path.IsPathRooted(oldPath))\r
878                 throw new ArgumentException("oldPath must be an absolute path", "oldPath");\r
879             if (string.IsNullOrWhiteSpace(newPath))\r
880                 throw new ArgumentNullException("newPath");\r
881             if (!Path.IsPathRooted(newPath))\r
882                 throw new ArgumentException("newPath must be an absolute path", "newPath");\r
883             Contract.EndContractBlock();\r
884 \r
885             //Ensure the paths end with the same character\r
886             if (!oldPath.EndsWith("\\"))\r
887                 oldPath = oldPath + "\\";\r
888             if (!newPath.EndsWith("\\"))\r
889                 newPath = newPath + "\\";\r
890 \r
891             ExecuteWithRetry((session, instance) =>\r
892             {\r
893                 const string hqlUpdate =\r
894                     "update FileState set FilePath = replace(FilePath,:oldPath,:newPath), Modified=:modified where FilePath like :oldPath || '%' ";\r
895                 var renames = session.CreateQuery(hqlUpdate)\r
896                     .SetString("oldPath", oldPath)\r
897                     .SetString("newPath", newPath)\r
898                     .SetDateTime("modified",DateTime.Now)\r
899                     .ExecuteUpdate();\r
900                 return renames;\r
901             }, null);\r
902         }\r
903 \r
904 \r
905         /// <summary>\r
906         /// Mark Unversioned all FileState rows from the database whose path\r
907         /// starts with one of the removed paths\r
908         /// </summary>\r
909         /// <param name="removed"></param>\r
910         public void UnversionPaths(List<string> removed)\r
911         {\r
912             if (removed == null)\r
913                 return;\r
914             if (removed.Count == 0)\r
915                 return;\r
916 \r
917             //Create a disjunction (list of OR statements\r
918             var disjunction = new Disjunction();\r
919             foreach (var path in removed)\r
920             {\r
921                 //with the restriction FileState.FilePath like '@path%'\r
922                 disjunction.Add(Restrictions.On<FileState>(s => s.FilePath)\r
923                     .IsLike(path, MatchMode.Start));\r
924             }\r
925 \r
926             //Generate a query from the disjunction\r
927             var query = QueryOver.Of<FileState>().Where(disjunction);\r
928 \r
929             ExecuteWithRetry((session, instance) =>\r
930             {\r
931                 using (var tx = session.BeginTransaction())\r
932                 {\r
933                     var states = query.GetExecutableQueryOver(session).List();\r
934                     foreach (var state in states)\r
935                     {\r
936                         state.FileStatus = FileStatus.Unversioned;\r
937                         state.OverlayStatus = FileOverlayStatus.Unversioned;\r
938                         session.Update(session);\r
939                     }\r
940                     tx.Commit();\r
941                 }\r
942                 return null;\r
943             }, null);\r
944         }\r
945 \r
946         public List<FileState> GetAllStates()\r
947         {\r
948             using(var session=_factory.OpenSession())\r
949             {\r
950                 return session.Query<FileState>().ToList();\r
951             }\r
952         }\r
953 \r
954         public List<string> GetAllStatePaths()\r
955         {\r
956             using (var session = _factory.OpenSession())\r
957             {\r
958                 return session.Query<FileState>().Select(state => state.FilePath).ToList();\r
959             }\r
960         }\r
961 \r
962         public List<FileState> GetConflictStates()\r
963         {\r
964             using (var session = _factory.OpenSession())\r
965             {\r
966                 var fileStates = from state in session.Query<FileState>()\r
967                                  where state.FileStatus == FileStatus.Conflict ||\r
968                                        state.OverlayStatus == FileOverlayStatus.Conflict\r
969                                  select state;\r
970                 return fileStates.ToList();\r
971             }\r
972         }\r
973 \r
974         public void UpdateFileChecksum(string path, string etag, TreeHash treeHash)\r
975         {\r
976             if (String.IsNullOrWhiteSpace(path))\r
977                 throw new ArgumentNullException("path");\r
978             if (!Path.IsPathRooted(path))\r
979                 throw new ArgumentException("The path must be rooted", "path");\r
980             Contract.EndContractBlock();\r
981 \r
982             if (string.IsNullOrWhiteSpace(path))\r
983                 throw new ArgumentNullException("absolutePath");\r
984             Contract.EndContractBlock();\r
985 \r
986             var hashes = treeHash.ToJson();\r
987             var topHash = treeHash.TopHash.ToHashString();\r
988 \r
989             ExecuteWithRetry((session, instance) =>\r
990             {\r
991                 const string hqlUpdate = "update FileState set Checksum= :checksum,Hashes=:hashes,ETag=:etag, Modified=:modified where FilePath = :path ";\r
992                 var updatedEntities = session.CreateQuery(hqlUpdate)\r
993                     .SetString("path", path)\r
994                     .SetString("checksum", topHash)\r
995                     .SetString("hashes", hashes)\r
996                     .SetString("etag", etag)\r
997                     .SetDateTime("modified", DateTime.Now)\r
998                     .ExecuteUpdate();\r
999                 return updatedEntities;\r
1000             }, null);\r
1001         }\r
1002 \r
1003         //Store only the hashes\r
1004         public  void UpdateFileHashes(string absolutePath, TreeHash treeHash)\r
1005         {\r
1006             if (string.IsNullOrWhiteSpace(absolutePath))\r
1007                 throw new ArgumentNullException("absolutePath");\r
1008             Contract.EndContractBlock();\r
1009 \r
1010             var hashes = treeHash.ToJson();\r
1011             var topHash = treeHash.TopHash.ToHashString();\r
1012 \r
1013             ExecuteWithRetry((session, instance) =>\r
1014             {\r
1015 \r
1016                 const string hqlUpdate = "update FileState set Hashes=:hashes,Modified=:modified where FilePath = :path ";\r
1017 /*\r
1018                 const string hqlUpdate = "update FileState set Checksum= :checksum,Hashes=:hashes where FilePath = :path ";\r
1019 */\r
1020                 var updatedEntities = session.CreateQuery(hqlUpdate)\r
1021                     .SetString("path", absolutePath)\r
1022                     //.SetString("checksum", topHash)\r
1023                     //                    .SetString("md5",treeHash.MD5)\r
1024                     .SetString("hashes", hashes)\r
1025                     .SetDateTime("modified", DateTime.Now)\r
1026                     .ExecuteUpdate();\r
1027                 return updatedEntities;\r
1028             }, null);\r
1029         }\r
1030 \r
1031 \r
1032         public void RenameState(string oldPath, string newPath)\r
1033         {\r
1034             if (string.IsNullOrWhiteSpace(oldPath))\r
1035                 throw new ArgumentNullException("oldPath");\r
1036             Contract.EndContractBlock();\r
1037 \r
1038             ExecuteWithRetry((session, instance) =>\r
1039             {\r
1040                 const string hqlUpdate =\r
1041                     "update FileState set FilePath= :newPath, Modified=:modified where FilePath = :oldPath ";\r
1042                 var updatedEntities = session.CreateQuery(hqlUpdate)\r
1043                     .SetString("oldPath", oldPath)\r
1044                     .SetString("newPath", newPath)\r
1045                     .SetDateTime("modified", DateTime.Now)\r
1046                     .ExecuteUpdate();\r
1047                 return updatedEntities;\r
1048             }, null);\r
1049 \r
1050         }\r
1051 \r
1052         public void StoreOverlayStatus(string absolutePath, FileOverlayStatus newStatus)\r
1053         {\r
1054             if (string.IsNullOrWhiteSpace(absolutePath))\r
1055                 throw new ArgumentNullException("absolutePath");\r
1056             Contract.EndContractBlock();\r
1057 \r
1058             using(var session=_factory.OpenSession())\r
1059             {\r
1060                 using (var tx = session.BeginTransaction())\r
1061                 {\r
1062                     var state = session.Query<FileState>().SingleOrDefault(s => s.FilePath == absolutePath) \r
1063                         ?? new FileState{\r
1064                             FilePath = absolutePath,                            \r
1065                             OverlayStatus = newStatus,\r
1066                             ETag = Signature.MERKLE_EMPTY,\r
1067                             //LastMD5=String.Empty,\r
1068                             IsFolder = Directory.Exists(absolutePath),\r
1069                             Modified=DateTime.Now\r
1070                         };\r
1071                     state.OverlayStatus = newStatus;\r
1072                     session.SaveOrUpdate(state);\r
1073                     session.Flush();\r
1074                     tx.Commit();                    \r
1075                 }\r
1076             };\r
1077         }\r
1078 \r
1079     }\r
1080 \r
1081    \r
1082 }\r