Modified StoreInfoDirect to use only NHibernate
[pithos-ms-client] / trunk / Pithos.Core / Agents / StatusAgent.cs
1 #region
2 /* -----------------------------------------------------------------------
3  * <copyright file="StatusAgent.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.Data;
46 using System.Data.SQLite;
47 using System.Diagnostics;
48 using System.Diagnostics.Contracts;
49 using System.IO;
50 using System.Linq;
51 using System.Reflection;
52 using System.Security.Cryptography;
53 using System.Text;
54 using System.Threading;
55 using System.Threading.Tasks;
56 using Castle.ActiveRecord;
57 using Castle.ActiveRecord.Framework;
58 using Castle.ActiveRecord.Framework.Config;
59 using Castle.ActiveRecord.Queries;
60 using NHibernate;
61 using NHibernate.ByteCode.Castle;
62 using NHibernate.Cfg;
63 using NHibernate.Cfg.Loquacious;
64 using NHibernate.Dialect;
65 using NHibernate.Exceptions;
66 using Pithos.Interfaces;
67 using Pithos.Network;
68 using log4net;
69 using Environment = System.Environment;
70
71 namespace Pithos.Core.Agents
72 {
73     [Export(typeof(IStatusChecker)),Export(typeof(IStatusKeeper))]
74     public class StatusAgent:IStatusChecker,IStatusKeeper
75     {
76         private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
77
78         [System.ComponentModel.Composition.Import]
79         public IPithosSettings Settings { get; set; }
80
81         [System.ComponentModel.Composition.Import]
82         public IStatusNotification StatusNotification { get; set; }
83
84         private Agent<Action> _persistenceAgent;
85
86
87
88         public StatusAgent()
89         {            
90             var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
91
92             _pithosDataPath = Path.Combine(appDataPath , "GRNET\\PITHOS");
93             if (!Directory.Exists(_pithosDataPath))
94                 Directory.CreateDirectory(_pithosDataPath);
95
96             var dbPath = Path.Combine(_pithosDataPath, "pithos.db");
97
98             MigrateOldDb(dbPath, appDataPath);
99
100
101             var source = GetConfiguration(_pithosDataPath);
102             ActiveRecordStarter.Initialize(source,typeof(FileState),typeof(FileTag));
103             
104             ActiveRecordStarter.UpdateSchema();
105
106
107             if (!File.Exists(dbPath))
108                 ActiveRecordStarter.CreateSchema();
109
110             CreateTrigger();
111             
112         }
113
114
115         private static void MigrateOldDb(string dbPath, string appDataPath)
116         {
117             if(String.IsNullOrWhiteSpace(dbPath))
118                 throw new ArgumentNullException("dbPath");
119             if(String.IsNullOrWhiteSpace(appDataPath))
120                 throw new ArgumentNullException("appDataPath");
121             Contract.EndContractBlock();
122
123             var oldDbPath = Path.Combine(appDataPath, "Pithos", "pithos.db");
124             var oldDbInfo = new FileInfo(oldDbPath);
125             if (oldDbInfo.Exists && !File.Exists(dbPath))
126             {
127                 Log.InfoFormat("Moving database from {0} to {1}",oldDbInfo.FullName,dbPath);
128                 var oldDirectory = oldDbInfo.Directory;
129                 oldDbInfo.MoveTo(dbPath);
130                 
131                 if (Log.IsDebugEnabled)
132                     Log.DebugFormat("Deleting {0}",oldDirectory.FullName);
133                 
134                 oldDirectory.Delete(true);
135             }
136         }
137
138         private void CreateTrigger()
139         {
140             using (var connection = GetConnection())
141             using (var triggerCommand = connection.CreateCommand())
142             {
143                 var cmdText = new StringBuilder()
144                     .AppendLine("CREATE TRIGGER IF NOT EXISTS update_last_modified UPDATE ON FileState FOR EACH ROW")
145                     .AppendLine("BEGIN")
146                     .AppendLine("UPDATE FileState SET Modified=datetime('now')  WHERE Id=old.Id;")
147                     .AppendLine("END;")
148                     .AppendLine("CREATE TRIGGER IF NOT EXISTS insert_last_modified INSERT ON FileState FOR EACH ROW")
149                     .AppendLine("BEGIN")
150                     .AppendLine("UPDATE FileState SET Modified=datetime('now')  WHERE Id=new.Id;")
151                     .AppendLine("END;")
152                     .ToString();
153                 triggerCommand.CommandText = cmdText;                
154                 triggerCommand.ExecuteNonQuery();
155             }
156         }
157
158
159         private static InPlaceConfigurationSource GetConfiguration(string pithosDbPath)
160         {
161             if (String.IsNullOrWhiteSpace(pithosDbPath))
162                 throw new ArgumentNullException("pithosDbPath");
163             if (!Path.IsPathRooted(pithosDbPath))
164                 throw new ArgumentException("path must be a rooted path", "pithosDbPath");
165             Contract.EndContractBlock();
166
167             var properties = new Dictionary<string, string>
168                                  {
169                                      {"connection.driver_class", "NHibernate.Driver.SQLite20Driver"},
170                                      {"dialect", "NHibernate.Dialect.SQLiteDialect"},
171                                      {"connection.provider", "NHibernate.Connection.DriverConnectionProvider"},
172                                      {
173                                          "proxyfactory.factory_class",
174                                          "NHibernate.ByteCode.Castle.ProxyFactoryFactory, NHibernate.ByteCode.Castle"
175                                          },
176                                  };
177
178             var connectionString = String.Format(@"Data Source={0}\pithos.db;Version=3;Enlist=N", pithosDbPath);
179             properties.Add("connection.connection_string", connectionString);
180
181             var source = new InPlaceConfigurationSource();                        
182             source.Add(typeof (ActiveRecordBase), properties);
183             source.SetDebugFlag(false);            
184             return source;
185         }
186
187         public void StartProcessing(CancellationToken token)
188         {
189             _persistenceAgent = Agent<Action>.Start(queue =>
190             {
191                 Action loop = null;
192                 loop = () =>
193                 {
194                     var job = queue.Receive();
195                     job.ContinueWith(t =>
196                     {
197                         var action = job.Result;
198                         try
199                         {
200                             action();
201                         }
202                         catch (SQLiteException ex)
203                         {
204                             Log.ErrorFormat("[ERROR] SQL \n{0}", ex);
205                         }
206                         catch (Exception ex)
207                         {
208                             Log.ErrorFormat("[ERROR] STATE \n{0}", ex);
209                         }
210                         queue.NotifyComplete(action);
211 // ReSharper disable AccessToModifiedClosure
212                         queue.DoAsync(loop);
213 // ReSharper restore AccessToModifiedClosure
214                     });
215                 };
216                 loop();
217             });
218             
219         }
220
221        
222
223         public void Stop()
224         {
225             _persistenceAgent.Stop();            
226         }
227
228
229         public void ProcessExistingFiles(IEnumerable<FileInfo> existingFiles)
230         {
231             if (existingFiles == null)
232                 throw new ArgumentNullException("existingFiles");
233             Contract.EndContractBlock();
234
235             //Find new or matching files with a left join to the stored states
236             var fileStates = FileState.Queryable.ToList();
237             var currentFiles = from file in existingFiles
238                                join state in fileStates on file.FullName.ToLower() equals state.FilePath.ToLower() into
239                                    gs
240                                from substate in gs.DefaultIfEmpty()
241                                select Tuple.Create(file, substate);
242
243             //To get the deleted files we must get the states that have no corresponding
244             //files. 
245             //We can't use the File.Exists method inside a query, so we get all file paths from the states
246             var statePaths = (from state in fileStates
247                               select new {state.Id, state.FilePath}).ToList();
248             //and check each one
249             var missingStates = (from path in statePaths
250                                  where !File.Exists(path.FilePath) && !Directory.Exists(path.FilePath)
251                                  select path.Id).ToList();
252             //Finally, retrieve the states that correspond to the deleted files            
253             var deletedFiles = from state in fileStates
254                                where missingStates.Contains(state.Id)
255                                select Tuple.Create(default(FileInfo), state);
256
257             var pairs = currentFiles.Union(deletedFiles).ToList();
258
259             i = 1;
260             var total = pairs.Count;
261             foreach (var pair in pairs)
262             {
263                 ProcessFile(total, pair);
264             }
265         }
266
267         int i = 1;
268
269         private void ProcessFile(int total, Tuple<FileInfo,FileState> pair)
270         {
271             var idx = Interlocked.Increment(ref i);
272             using (StatusNotification.GetNotifier("Indexing file {0} of {1}", "Indexed file {0} of {1} ", idx, total))
273             {
274                 var fileState = pair.Item2;
275                 var file = pair.Item1;
276                 if (fileState == null)
277                 {
278                     //This is a new file                        
279                     var createState = FileState.CreateFor(file,StatusNotification);
280                     _persistenceAgent.Post(createState.Create);
281                 }
282                 else if (file == null)
283                 {
284                     //This file was deleted while we were down. We should mark it as deleted
285                     //We have to go through UpdateStatus here because the state object we are using
286                     //was created by a different ORM session.
287                     _persistenceAgent.Post(() => UpdateStatusDirect((Guid) fileState.Id, FileStatus.Deleted));
288                 }
289                 else
290                 {
291                     //This file has a matching state. Need to check for possible changes
292                     //To check for changes, we use the cheap (in CPU terms) MD5 algorithm
293                     //on the entire file.
294
295                     var hashString = file.ComputeShortHash(StatusNotification);
296                     Debug.Assert(hashString.Length==32);
297
298
299                     //TODO: Need a way to attach the hashes to the filestate so we don't
300                     //recalculate them each time a call to calculate has is made
301                     //We can either store them to the filestate or add them to a 
302                     //dictionary
303
304                     //If the hashes don't match the file was changed
305                     if (fileState.ETag != hashString)
306                     {
307                         _persistenceAgent.Post(() => UpdateStatusDirect((Guid) fileState.Id, FileStatus.Modified));
308                     }
309                 }
310             }
311         }
312
313
314         private int UpdateStatusDirect(Guid id, FileStatus status)
315         {
316             using (log4net.ThreadContext.Stacks["StatusAgent"].Push("UpdateStatusDirect"))
317             {
318
319                 try
320                 {
321                     using (var session = ActiveRecordMediator.GetSessionFactoryHolder().CreateSession(typeof(FileState)))
322                     {
323                         var walquery = session.CreateSQLQuery("PRAGMA journal_mode=WAL");
324                         walquery.List();
325
326                           var updatecmd = session.CreateSQLQuery(
327                             "update FileState set FileStatus= :fileStatus where Id = :id  ")
328                             .SetGuid("id", id)
329                             .SetEnum("fileStatus", status);
330                         var affected = updatecmd.ExecuteUpdate();
331                         
332                         return affected;
333                     }
334
335                 }
336                 catch (Exception exc)
337                 {
338                     Log.Error(exc.ToString());
339                     throw;
340                 }
341             }
342         }
343         
344         private int UpdateStatusDirect(string path, FileStatus status)
345         {
346             using (log4net.ThreadContext.Stacks["StatusAgent"].Push("UpdateStatusDirect"))
347             {
348
349                 try
350                 {                    
351                     using (var session = ActiveRecordMediator.GetSessionFactoryHolder().CreateSession(typeof(FileState)))
352                     using (session.BeginTransaction(IsolationLevel.ReadCommitted))
353                     {
354
355                         var walquery = session.CreateSQLQuery("PRAGMA journal_mode=WAL");
356                         walquery.List();
357
358                         var updatecmd = session.CreateSQLQuery(
359                             "update FileState set FileStatus= :fileStatus where FilePath = :path COLLATE NOCASE")
360                             .SetString("path", path)
361                             .SetEnum("fileStatus", status);
362                         var affected = updatecmd.ExecuteUpdate();
363
364                         if (affected == 0)
365                         {                            
366                             var createdState = FileState.CreateFor(FileInfoExtensions.FromPath(path), StatusNotification);
367                             createdState.FileStatus = status;
368                             session.Save(createdState);
369                         }
370                         return affected;
371                     }
372                 }
373                 catch (Exception exc)
374                 {
375                     Log.Error(exc.ToString());
376                     throw;
377                 }
378             }
379         }
380
381         private int UpdateStatusDirect(string absolutePath, FileStatus fileStatus, FileOverlayStatus overlayStatus, string conflictReason)
382         {
383             using (log4net.ThreadContext.Stacks["StatusAgent"].Push("UpdateStatusDirect"))
384             {
385
386                 try
387                 {
388
389                     using (var session = ActiveRecordMediator.GetSessionFactoryHolder().CreateSession(typeof(FileState)))
390                     using (session.BeginTransaction(IsolationLevel.ReadCommitted))
391                     {
392
393                         var walquery = session.CreateSQLQuery("PRAGMA journal_mode=WAL");
394                         walquery.List();
395
396
397                         var updatecmd = session.CreateSQLQuery("update FileState set OverlayStatus= :overlayStatus, FileStatus= :fileStatus,ConflictReason= :conflictReason where FilePath = :path COLLATE NOCASE")
398                                                 .SetString("path", absolutePath)
399                                                 .SetEnum("fileStatus", fileStatus)
400                                                 .SetEnum("overlayStatus", overlayStatus)
401                                                 .SetString("conflictReason", conflictReason);
402                         var affected = updatecmd.ExecuteUpdate();
403
404                         if (affected == 0)
405                         {
406                             var createdState = FileState.CreateFor(FileInfoExtensions.FromPath(absolutePath), StatusNotification);
407                             createdState.FileStatus = fileStatus;
408                             createdState.OverlayStatus = overlayStatus;
409                             createdState.ConflictReason = conflictReason;
410                             createdState.LastMD5 = String.Empty;
411                             session.Save(createdState);
412                             //createdState.Create();
413                         }
414                         return affected;
415                     }
416                 }
417                 catch (Exception exc)
418                 {
419                     Log.Error(exc.ToString());
420                     throw;
421                 }
422             }
423         }
424         
425
426
427         public string BlockHash { get; set; }
428
429         public int BlockSize { get; set; }
430         public void ChangeRoots(string oldPath, string newPath)
431         {
432             if (String.IsNullOrWhiteSpace(oldPath))
433                 throw new ArgumentNullException("oldPath");
434             if (!Path.IsPathRooted(oldPath))
435                 throw new ArgumentException("oldPath must be an absolute path", "oldPath");
436             if (string.IsNullOrWhiteSpace(newPath))
437                 throw new ArgumentNullException("newPath");
438             if (!Path.IsPathRooted(newPath))
439                 throw new ArgumentException("newPath must be an absolute path", "newPath");
440             Contract.EndContractBlock();
441
442             FileState.ChangeRootPath(oldPath,newPath);
443
444         }
445
446
447
448         private readonly string _pithosDataPath;
449
450
451         public FileState GetStateByFilePath(string path)
452         {
453             if (String.IsNullOrWhiteSpace(path))
454                 throw new ArgumentNullException("path");
455             if (!Path.IsPathRooted(path))
456                 throw new ArgumentException("The path must be rooted", "path");
457             Contract.EndContractBlock();
458
459             try
460             {
461                 
462                 using (var connection = GetConnection())
463                 using (var command = new SQLiteCommand("select Id, FilePath, OverlayStatus,FileStatus ,Checksum ,ETag,Version    ,VersionTimeStamp,IsShared   ,SharedBy   ,ShareWrite, LastMD5,LastLength,LastWriteDate  from FileState where FilePath=:path COLLATE NOCASE", connection))
464                 {
465                     
466                     command.Parameters.AddWithValue("path", path);
467                     
468                     using (var reader = command.ExecuteReader())
469                     {
470                         if (reader.Read())
471                         {
472                             //var values = new object[reader.FieldCount];
473                             //reader.GetValues(values);
474                             var state = new FileState
475                                             {
476                                                 Id = reader.GetGuid(0),
477                                                 FilePath = reader.IsDBNull(1)?"":reader.GetString(1),
478                                                 OverlayStatus =reader.IsDBNull(2)?FileOverlayStatus.Unversioned: (FileOverlayStatus) reader.GetInt64(2),
479                                                 FileStatus = reader.IsDBNull(3)?FileStatus.Missing:(FileStatus) reader.GetInt64(3),
480                                                 Checksum = reader.IsDBNull(4)?"":reader.GetString(4),
481                                                 ETag= reader.IsDBNull(5)?"":reader.GetString(5),
482                                                 Version = reader.IsDBNull(6)?default(long):reader.GetInt64(6),
483                                                 VersionTimeStamp = reader.IsDBNull(7)?default(DateTime):reader.GetDateTime(7),
484                                                 IsShared = !reader.IsDBNull(8) && reader.GetBoolean(8),
485                                                 SharedBy = reader.IsDBNull(9)?"":reader.GetString(9),
486                                                 ShareWrite = !reader.IsDBNull(10) && reader.GetBoolean(10),
487                                                 LastMD5=reader.GetString(11),
488                                                 LastLength=reader.IsDBNull(12)? default(long):reader.GetInt64(12),
489                                                 LastWriteDate=reader.IsDBNull(13)?default(DateTime):reader.GetDateTime(13)
490                                             };
491
492                             return state;
493                         }
494                         else
495                         {
496                             return null;
497                         }
498
499                     }                    
500                 }
501             }
502             catch (Exception exc)
503             {
504                 Log.ErrorFormat(exc.ToString());
505                 throw;
506             }            
507         }
508
509         public FileOverlayStatus GetFileOverlayStatus(string path)
510         {
511             if (String.IsNullOrWhiteSpace(path))
512                 throw new ArgumentNullException("path");
513             if (!Path.IsPathRooted(path))
514                 throw new ArgumentException("The path must be rooted", "path");
515             Contract.EndContractBlock();
516
517             try
518             {
519                 
520                 using (var connection = GetConnection())
521                 using (var command = new SQLiteCommand("select OverlayStatus from FileState where FilePath=:path  COLLATE NOCASE", connection))
522                 {
523                     
524                     command.Parameters.AddWithValue("path", path);
525                     
526                     var s = command.ExecuteScalar();
527                     return (FileOverlayStatus) Convert.ToInt32(s);
528                 }
529             }
530             catch (Exception exc)
531             {
532                 Log.ErrorFormat(exc.ToString());
533                 return FileOverlayStatus.Unversioned;
534             }
535         }
536
537         private string GetConnectionString()
538         {
539             var connectionString = String.Format(@"Data Source={0}\pithos.db;Version=3;Enlist=N;Pooling=True", _pithosDataPath);
540             return connectionString;
541         }
542
543         private SQLiteConnection GetConnection()
544         {
545             var connectionString = GetConnectionString();
546             var connection = new SQLiteConnection(connectionString);
547             connection.Open();
548             using(var cmd =connection.CreateCommand())
549             {
550                 cmd.CommandText = "PRAGMA journal_mode=WAL";
551                 cmd.ExecuteNonQuery();
552             }
553             return connection;
554         }
555
556        /* public void SetFileOverlayStatus(string path, FileOverlayStatus overlayStatus)
557         {
558             if (String.IsNullOrWhiteSpace(path))
559                 throw new ArgumentNullException("path");
560             if (!Path.IsPathRooted(path))
561                 throw new ArgumentException("The path must be rooted","path");
562             Contract.EndContractBlock();
563
564             _persistenceAgent.Post(() => FileState.StoreOverlayStatus(path,overlayStatus));
565         }*/
566
567         public Task SetFileOverlayStatus(string path, FileOverlayStatus overlayStatus, string etag = null)
568         {
569             if (String.IsNullOrWhiteSpace(path))
570                 throw new ArgumentNullException("path");
571             if (!Path.IsPathRooted(path))
572                 throw new ArgumentException("The path must be rooted","path");
573             Contract.EndContractBlock();
574
575             return _persistenceAgent.PostAndAwait(() => FileState.StoreOverlayStatus(path,overlayStatus,etag));
576         }
577
578        /* public void RenameFileOverlayStatus(string oldPath, string newPath)
579         {
580             if (String.IsNullOrWhiteSpace(oldPath))
581                 throw new ArgumentNullException("oldPath");
582             if (!Path.IsPathRooted(oldPath))
583                 throw new ArgumentException("The oldPath must be rooted", "oldPath");
584             if (String.IsNullOrWhiteSpace(newPath))
585                 throw new ArgumentNullException("newPath");
586             if (!Path.IsPathRooted(newPath))
587                 throw new ArgumentException("The newPath must be rooted", "newPath");
588             Contract.EndContractBlock();
589
590             _persistenceAgent.Post(() =>FileState.RenameState(oldPath, newPath));
591         }*/
592
593         public void SetFileState(string path, FileStatus fileStatus, FileOverlayStatus overlayStatus, string conflictReason)
594         {
595             if (String.IsNullOrWhiteSpace(path))
596                 throw new ArgumentNullException("path");
597             if (!Path.IsPathRooted(path))
598                 throw new ArgumentException("The path must be rooted", "path");
599             Contract.EndContractBlock();
600
601             Debug.Assert(!path.Contains(FolderConstants.CacheFolder));
602             Debug.Assert(!path.EndsWith(".ignore"));
603
604             _persistenceAgent.Post(() => UpdateStatusDirect(path, fileStatus, overlayStatus, conflictReason));
605         }
606
607         
608         public void StoreInfo(string path, ObjectInfo objectInfo)
609         {
610             if (String.IsNullOrWhiteSpace(path))
611                 throw new ArgumentNullException("path");
612             if (!Path.IsPathRooted(path))
613                 throw new ArgumentException("The path must be rooted", "path");
614             if (objectInfo == null)
615                 throw new ArgumentNullException("objectInfo", "objectInfo can't be empty");
616             Contract.EndContractBlock();
617
618             _persistenceAgent.Post(() => StoreInfoDirect(path, objectInfo));
619
620         }
621
622         private void StoreInfoDirect(string path, ObjectInfo objectInfo)
623         {
624             try
625             {
626                     using (var session = ActiveRecordMediator.GetSessionFactoryHolder().CreateSession(typeof(FileState)))
627                     using (session.BeginTransaction(IsolationLevel.ReadCommitted))
628                     {
629
630                         var walquery = session.CreateSQLQuery("PRAGMA journal_mode=WAL");
631                         walquery.List();
632
633                         Func<IQuery, IQuery> setCriteria = q => q
634                                                     .SetString("path", path)
635                                                     .SetString("checksum",objectInfo.X_Object_Hash)
636                                                     .SetString("etag", objectInfo.ETag)
637                                                     .SetInt64("version", objectInfo.Version.Value)
638                                                     .SetDateTime("versionTimeStamp",objectInfo.VersionTimestamp.Value)
639                                                     .SetEnum("fileStatus", FileStatus.Unchanged)
640                                                     .SetEnum("overlayStatus",FileOverlayStatus.Normal)
641                                                     .SetString("objectID", objectInfo.UUID);
642                         IQuery updatecmd = session.CreateSQLQuery(
643                             "update FileState set FilePath=:path,FileStatus= :fileStatus,OverlayStatus= :overlayStatus, Checksum=:checksum, ETag=:etag,LastMD5=:etag,Version=:version,VersionTimeStamp=:versionTimeStamp where ObjectID = :objectID  ");
644                         updatecmd = setCriteria(updatecmd);                           
645                         var affected = updatecmd.ExecuteUpdate();                        
646
647                     //If the ID exists, update the status
648                     if (affected == 0)
649                     {
650                         //If the ID doesn't exist, try to update using the path, and store the ID as well.
651                         updatecmd = session.CreateSQLQuery(
652                             "update FileState set FileStatus= :fileStatus,OverlayStatus= :overlayStatus, ObjectID=:objectID, Checksum=:checksum, ETag=:etag,LastMD5=:etag,Version=:version,VersionTimeStamp=:versionTimeStamp where FilePath = :path  COLLATE NOCASE ");
653                         updatecmd=setCriteria(updatecmd);
654                         affected = updatecmd.ExecuteUpdate();
655                     }
656                     if (affected==0)
657                     {
658                         IQuery insertCmd=session.CreateSQLQuery(
659                             "INSERT INTO FileState (Id,FilePath,Checksum,Version,VersionTimeStamp,ETag,LastMD5,FileStatus,OverlayStatus,ObjectID) VALUES (:id,:path,:checksum,:version,:versionTimeStamp,:etag,:etag,:fileStatus,:overlayStatus,:objectID)");
660                         insertCmd=setCriteria(insertCmd).SetGuid("id", Guid.NewGuid());
661                         affected = insertCmd.ExecuteUpdate();
662                     }
663
664                 }
665             }
666             catch (Exception exc)
667             {
668                 Log.ErrorFormat("Failed to update [{0}]:[{1}]\r\n{2}",path,objectInfo.UUID, exc);
669                 throw;
670             }
671         }
672
673         private bool StateExists(string filePath,SQLiteConnection connection)
674         {
675             using (var command = new SQLiteCommand("Select count(*) from FileState where FilePath=:path  COLLATE NOCASE", connection))
676             {
677                 command.Parameters.AddWithValue("path", filePath);
678                 var result = command.ExecuteScalar();
679                 return ((long)result >= 1);
680             }
681
682         }
683
684         private bool StateExistsByID(string objectId,SQLiteConnection connection)
685         {
686             using (var command = new SQLiteCommand("Select count(*) from FileState where ObjectId=:id", connection))
687             {
688                 command.Parameters.AddWithValue("id", objectId);
689                 var result = command.ExecuteScalar();
690                 return ((long)result >= 1);
691             }
692
693         }
694
695         public void SetFileStatus(string path, FileStatus status)
696         {
697             if (String.IsNullOrWhiteSpace(path))
698                 throw new ArgumentNullException("path");
699             if (!Path.IsPathRooted(path))
700                 throw new ArgumentException("The path must be rooted", "path");
701             Contract.EndContractBlock();
702             
703             _persistenceAgent.Post(() => UpdateStatusDirect(path, status));
704         }
705
706         public FileStatus GetFileStatus(string path)
707         {
708             if (String.IsNullOrWhiteSpace(path))
709                 throw new ArgumentNullException("path");
710             if (!Path.IsPathRooted(path))
711                 throw new ArgumentException("The path must be rooted", "path");
712             Contract.EndContractBlock();
713
714             
715             using (var connection = GetConnection())
716             {
717                 var command = new SQLiteCommand("select FileStatus from FileState where FilePath=:path  COLLATE NOCASE", connection);
718                 command.Parameters.AddWithValue("path", path);
719                 
720                 var statusValue = command.ExecuteScalar();
721                 if (statusValue==null)
722                     return FileStatus.Missing;
723                 return (FileStatus)Convert.ToInt32(statusValue);
724             }
725         }
726
727         /// <summary>
728         /// Deletes the status of the specified file
729         /// </summary>
730         /// <param name="path"></param>
731         public void ClearFileStatus(string path)
732         {
733             if (String.IsNullOrWhiteSpace(path))
734                 throw new ArgumentNullException("path");
735             if (!Path.IsPathRooted(path))
736                 throw new ArgumentException("The path must be rooted", "path");
737             Contract.EndContractBlock();
738
739             _persistenceAgent.Post(() => DeleteDirect(path));   
740         }
741
742         /// <summary>
743         /// Deletes the status of the specified folder and all its contents
744         /// </summary>
745         /// <param name="path"></param>
746         public void ClearFolderStatus(string path)
747         {
748             if (String.IsNullOrWhiteSpace(path))
749                 throw new ArgumentNullException("path");
750             if (!Path.IsPathRooted(path))
751                 throw new ArgumentException("The path must be rooted", "path");
752             Contract.EndContractBlock();
753             //TODO: May throw if the agent is cleared for some reason. Should never happen
754             _persistenceAgent.Post(() => DeleteFolderDirect(path));   
755         }
756
757         public IEnumerable<FileState> GetChildren(FileState fileState)
758         {
759             if (fileState == null)
760                 throw new ArgumentNullException("fileState");
761             Contract.EndContractBlock();
762
763             var children = from state in FileState.Queryable
764                            where state.FilePath.StartsWith(fileState.FilePath + "\\")
765                            select state;
766             return children;
767         }
768
769         public void EnsureFileState(string path)
770         {
771             var existingState = GetStateByFilePath(path);
772             if (existingState != null)
773                 return;
774             var fileInfo = FileInfoExtensions.FromPath(path);
775             using (new SessionScope())
776             {
777                 var newState = FileState.CreateFor(fileInfo,StatusNotification);
778                 newState.FileStatus=FileStatus.Missing;
779                 _persistenceAgent.PostAndAwait(newState.CreateAndFlush).Wait();
780             }
781
782         }
783
784         private int DeleteDirect(string filePath)
785         {
786             using (log4net.ThreadContext.Stacks["StatusAgent"].Push("DeleteDirect"))
787             {
788
789                 try
790                 {
791
792                     
793                     using (var connection = GetConnection())
794                     {
795                         var command = new SQLiteCommand("delete from FileState where FilePath = :path  COLLATE NOCASE",
796                                                         connection);
797
798                         command.Parameters.AddWithValue("path", filePath);
799                         
800                         var affected = command.ExecuteNonQuery();
801                         return affected;
802                     }
803                 }
804                 catch (Exception exc)
805                 {
806                     Log.Error(exc.ToString());
807                     throw;
808                 }
809             }
810         }
811
812         private int DeleteFolderDirect(string filePath)
813         {
814             using (log4net.ThreadContext.Stacks["StatusAgent"].Push("DeleteDirect"))
815             {
816
817                 try
818                 {
819
820                     
821                     using (var connection = GetConnection())
822                     {
823                         var command = new SQLiteCommand(@"delete from FileState where FilePath = :path or FilePath like :path || '\%'  COLLATE NOCASE",
824                                                         connection);
825
826                         command.Parameters.AddWithValue("path", filePath);
827                         
828                         var affected = command.ExecuteNonQuery();
829                         return affected;
830                     }
831                 }
832                 catch (Exception exc)
833                 {
834                     Log.Error(exc.ToString());
835                     throw;
836                 }
837             }
838         }
839
840         public void UpdateFileChecksum(string path, string etag, string checksum)
841         {
842             if (String.IsNullOrWhiteSpace(path))
843                 throw new ArgumentNullException("path");
844             if (!Path.IsPathRooted(path))
845                 throw new ArgumentException("The path must be rooted", "path");            
846             Contract.EndContractBlock();
847
848             _persistenceAgent.Post(() => FileState.UpdateChecksum(path, etag,checksum));
849         }
850
851         public void UpdateLastMD5(FileInfo file, string etag)
852         {
853             if (file==null)
854                 throw new ArgumentNullException("file");
855             if (String.IsNullOrWhiteSpace(etag))
856                 throw new ArgumentNullException("etag");
857             Contract.EndContractBlock();
858
859             _persistenceAgent.Post(() => FileState.UpdateLastMD5(file, etag));
860         }
861
862
863         public void CleanupOrphanStates()
864         {
865             //Orphan states are those that do not correspond to an account, ie. their paths
866             //do not start with the root path of any registered account
867
868             var roots=(from account in Settings.Accounts
869                       select account.RootPath).ToList();
870             
871             var allStates = from state in FileState.Queryable
872                 select state.FilePath;
873
874             foreach (var statePath in allStates)
875             {
876                 if (!roots.Any(root=>statePath.StartsWith(root,StringComparison.InvariantCultureIgnoreCase)))
877                     this.DeleteDirect(statePath);
878             }
879         }
880
881         public void CleanupStaleStates(AccountInfo accountInfo, List<ObjectInfo> objectInfos)
882         {
883             if (accountInfo == null)
884                 throw new ArgumentNullException("accountInfo");
885             if (objectInfos == null)
886                 throw new ArgumentNullException("objectInfos");
887             Contract.EndContractBlock();
888             
889
890
891             //Stale states are those that have no corresponding local or server file
892             
893
894             var agent=FileAgent.GetFileAgent(accountInfo);
895
896             var localFiles=agent.EnumerateFiles();
897             var localSet = new HashSet<string>(localFiles);
898
899             //RelativeUrlToFilePath will fail for
900             //infos of accounts, containers which have no Name
901
902             var serverFiles = from info in objectInfos
903                               where info.Name != null
904                               select Path.Combine(accountInfo.AccountPath,info.RelativeUrlToFilePath(accountInfo.UserName));
905             var serverSet = new HashSet<string>(serverFiles);
906
907             var allStates = from state in FileState.Queryable
908                             where state.FilePath.StartsWith(agent.RootPath)
909                             select state.FilePath;
910             var stateSet = new HashSet<string>(allStates);
911             stateSet.ExceptWith(serverSet);
912             stateSet.ExceptWith(localSet);
913
914             foreach (var remainder in stateSet)
915             {
916                 DeleteDirect(remainder);
917             }
918
919             
920         }
921     }
922
923    
924 }