2 using System.Collections.Generic;
3 using System.ComponentModel.Composition;
4 using System.Data.SQLite;
5 using System.Diagnostics;
6 using System.Diagnostics.Contracts;
10 using System.Threading;
11 using System.Threading.Tasks;
12 using Castle.ActiveRecord;
13 using Castle.ActiveRecord.Framework.Config;
14 using Pithos.Interfaces;
18 namespace Pithos.Core.Agents
20 [Export(typeof(IStatusChecker)),Export(typeof(IStatusKeeper))]
21 public class StatusAgent:IStatusChecker,IStatusKeeper
23 [System.ComponentModel.Composition.Import]
24 public IPithosSettings Settings { get; set; }
26 private Agent<Action> _persistenceAgent;
29 private static readonly ILog Log = LogManager.GetLogger("StatusAgent");
33 var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
34 _pithosDataPath = Path.Combine(appDataPath , "Pithos");
36 if (!Directory.Exists(_pithosDataPath))
37 Directory.CreateDirectory(_pithosDataPath);
38 var source = GetConfiguration(_pithosDataPath);
39 ActiveRecordStarter.Initialize(source,typeof(FileState),typeof(FileTag));
40 ActiveRecordStarter.UpdateSchema();
42 if (!File.Exists(Path.Combine(_pithosDataPath ,"pithos.db")))
43 ActiveRecordStarter.CreateSchema();
49 private void CreateTrigger()
51 using (var connection = GetConnection())
52 using (var triggerCommand = connection.CreateCommand())
54 var cmdText = new StringBuilder()
55 .AppendLine("CREATE TRIGGER IF NOT EXISTS update_last_modified UPDATE ON FileState FOR EACH ROW")
57 .AppendLine("UPDATE FileState SET Modified=datetime('now') WHERE Id=old.Id;")
59 .AppendLine("CREATE TRIGGER IF NOT EXISTS insert_last_modified INSERT ON FileState FOR EACH ROW")
61 .AppendLine("UPDATE FileState SET Modified=datetime('now') WHERE Id=new.Id;")
64 triggerCommand.CommandText = cmdText;
65 triggerCommand.ExecuteNonQuery();
70 private static InPlaceConfigurationSource GetConfiguration(string pithosDbPath)
72 if (String.IsNullOrWhiteSpace(pithosDbPath))
73 throw new ArgumentNullException("pithosDbPath");
74 if (!Path.IsPathRooted(pithosDbPath))
75 throw new ArgumentException("path must be a rooted path", "pithosDbPath");
76 Contract.EndContractBlock();
78 var properties = new Dictionary<string, string>
80 {"connection.driver_class", "NHibernate.Driver.SQLite20Driver"},
81 {"dialect", "NHibernate.Dialect.SQLiteDialect"},
82 {"connection.provider", "NHibernate.Connection.DriverConnectionProvider"},
84 "proxyfactory.factory_class",
85 "NHibernate.ByteCode.Castle.ProxyFactoryFactory, NHibernate.ByteCode.Castle"
89 var connectionString = String.Format(@"Data Source={0}\pithos.db;Version=3;Enlist=N", pithosDbPath);
90 properties.Add("connection.connection_string", connectionString);
92 var source = new InPlaceConfigurationSource();
93 source.Add(typeof (ActiveRecordBase), properties);
94 source.SetDebugFlag(false);
98 public void StartProcessing(CancellationToken token)
100 _persistenceAgent = Agent<Action>.Start(queue =>
105 var job = queue.Receive();
106 job.ContinueWith(t =>
108 var action = job.Result;
113 catch (SQLiteException ex)
115 Log.ErrorFormat("[ERROR] SQL \n{0}", ex);
119 Log.ErrorFormat("[ERROR] STATE \n{0}", ex);
121 // ReSharper disable AccessToModifiedClosure
123 // ReSharper restore AccessToModifiedClosure
135 _persistenceAgent.Stop();
139 public void ProcessExistingFiles(IEnumerable<FileInfo> existingFiles)
141 if(existingFiles ==null)
142 throw new ArgumentNullException("existingFiles");
143 Contract.EndContractBlock();
145 //Find new or matching files with a left join to the stored states
146 var fileStates = FileState.Queryable;
147 var currentFiles=from file in existingFiles
148 join state in fileStates on file.FullName.ToLower() equals state.FilePath.ToLower() into gs
149 from substate in gs.DefaultIfEmpty()
150 select new {File = file, State = substate};
152 //To get the deleted files we must get the states that have no corresponding
154 //We can't use the File.Exists method inside a query, so we get all file paths from the states
155 var statePaths = (from state in fileStates
156 select new {state.Id, state.FilePath}).ToList();
158 var missingStates= (from path in statePaths
159 where !File.Exists(path.FilePath) && !Directory.Exists(path.FilePath)
160 select path.Id).ToList();
161 //Finally, retrieve the states that correspond to the deleted files
162 var deletedFiles = from state in fileStates
163 where missingStates.Contains(state.Id)
164 select new { File = default(FileInfo), State = state };
166 var pairs = currentFiles.Union(deletedFiles);
168 foreach(var pair in pairs)
170 var fileState = pair.State;
171 var file = pair.File;
172 if (fileState == null)
175 var fullPath = pair.File.FullName;
176 var createState = FileState.CreateForAsync(fullPath, BlockSize, BlockHash);
177 createState.ContinueWith(state => _persistenceAgent.Post(state.Result.Create));
179 else if (file == null)
181 //This file was deleted while we were down. We should mark it as deleted
182 //We have to go through UpdateStatus here because the state object we are using
183 //was created by a different ORM session.
184 _persistenceAgent.Post(()=> UpdateStatusDirect(fileState.Id, FileStatus.Deleted));
188 //This file has a matching state. Need to check for possible changes
189 var hashString = file.CalculateHash(BlockSize,BlockHash);
190 //If the hashes don't match the file was changed
191 if (fileState.Checksum != hashString)
193 _persistenceAgent.Post(() => UpdateStatusDirect(fileState.Id, FileStatus.Modified));
200 private int UpdateStatusDirect(Guid id, FileStatus status)
202 using (log4net.ThreadContext.Stacks["StatusAgent"].Push("UpdateStatusDirect"))
208 using (var connection = GetConnection())
210 var command = new SQLiteCommand("update FileState set FileStatus= :fileStatus where Id = :id ",
213 command.Parameters.AddWithValue("fileStatus", status);
215 command.Parameters.AddWithValue("id", id);
217 var affected = command.ExecuteNonQuery();
223 catch (Exception exc)
225 Log.Error(exc.ToString());
231 private int UpdateStatusDirect(string path, FileStatus status)
233 using (log4net.ThreadContext.Stacks["StatusAgent"].Push("UpdateStatusDirect"))
240 using (var connection = GetConnection())
243 new SQLiteCommand("update FileState set FileStatus= :fileStatus where FilePath = :path COLLATE NOCASE",
248 command.Parameters.AddWithValue("fileStatus", status);
250 command.Parameters.AddWithValue("path", path);
252 var affected = command.ExecuteNonQuery();
256 catch (Exception exc)
258 Log.Error(exc.ToString());
264 private int UpdateStatusDirect(string absolutePath, FileStatus fileStatus, FileOverlayStatus overlayStatus)
266 using (log4net.ThreadContext.Stacks["StatusAgent"].Push("UpdateStatusDirect"))
273 using (var connection = GetConnection())
277 "update FileState set OverlayStatus= :overlayStatus, FileStatus= :fileStatus where FilePath = :path COLLATE NOCASE ",
281 command.Parameters.AddWithValue("path", absolutePath);
282 command.Parameters.AddWithValue("fileStatus", fileStatus);
283 command.Parameters.AddWithValue("overlayStatus", overlayStatus);
285 var affected = command.ExecuteNonQuery();
289 catch (Exception exc)
291 Log.Error(exc.ToString());
299 public string BlockHash { get; set; }
301 public int BlockSize { get; set; }
302 public void ChangeRoots(string oldPath, string newPath)
304 if (String.IsNullOrWhiteSpace(oldPath))
305 throw new ArgumentNullException("oldPath");
306 if (!Path.IsPathRooted(oldPath))
307 throw new ArgumentException("oldPath must be an absolute path", "oldPath");
308 if (string.IsNullOrWhiteSpace(newPath))
309 throw new ArgumentNullException("newPath");
310 if (!Path.IsPathRooted(newPath))
311 throw new ArgumentException("newPath must be an absolute path", "newPath");
312 Contract.EndContractBlock();
314 FileState.ChangeRootPath(oldPath,newPath);
318 private PithosStatus _pithosStatus=PithosStatus.InSynch;
320 public void SetPithosStatus(PithosStatus status)
322 _pithosStatus = status;
325 public PithosStatus GetPithosStatus()
327 return _pithosStatus;
331 private readonly string _pithosDataPath;
334 public FileState GetStateByFilePath(string path)
336 if (String.IsNullOrWhiteSpace(path))
337 throw new ArgumentNullException("path");
338 if (!Path.IsPathRooted(path))
339 throw new ArgumentException("The path must be rooted", "path");
340 Contract.EndContractBlock();
345 using (var connection = GetConnection())
346 using (var command = new SQLiteCommand("select Id, FilePath, OverlayStatus,FileStatus ,Checksum ,Version ,VersionTimeStamp,IsShared ,SharedBy ,ShareWrite from FileState where FilePath=:path COLLATE NOCASE", connection))
349 command.Parameters.AddWithValue("path", path);
351 using (var reader = command.ExecuteReader())
355 //var values = new object[reader.FieldCount];
356 //reader.GetValues(values);
357 var state = new FileState
359 Id = reader.GetGuid(0),
360 FilePath = reader.IsDBNull(1)?"":reader.GetString(1),
361 OverlayStatus =reader.IsDBNull(2)?FileOverlayStatus.Unversioned: (FileOverlayStatus) reader.GetInt64(2),
362 FileStatus = reader.IsDBNull(3)?FileStatus.Missing:(FileStatus) reader.GetInt64(3),
363 Checksum = reader.IsDBNull(4)?"":reader.GetString(4),
364 Version = reader.IsDBNull(5)?default(long):reader.GetInt64(5),
365 VersionTimeStamp = reader.IsDBNull(6)?default(DateTime):reader.GetDateTime(6),
366 IsShared = !reader.IsDBNull(7) && reader.GetBoolean(7),
367 SharedBy = reader.IsDBNull(8)?"":reader.GetString(8),
368 ShareWrite = !reader.IsDBNull(9) && reader.GetBoolean(9)
371 var state = new FileState
373 Id = (Guid) values[0],
374 FilePath = (string) values[1],
375 OverlayStatus = (FileOverlayStatus) (long)values[2],
376 FileStatus = (FileStatus) (long)values[3],
377 Checksum = (string) values[4],
378 Version = (long?) values[5],
379 VersionTimeStamp = (DateTime?) values[6],
380 IsShared = (long)values[7] == 1,
381 SharedBy = (string) values[8],
382 ShareWrite = (long)values[9] == 1
395 catch (Exception exc)
397 Log.ErrorFormat(exc.ToString());
402 public FileOverlayStatus GetFileOverlayStatus(string path)
404 if (String.IsNullOrWhiteSpace(path))
405 throw new ArgumentNullException("path");
406 if (!Path.IsPathRooted(path))
407 throw new ArgumentException("The path must be rooted", "path");
408 Contract.EndContractBlock();
413 using (var connection = GetConnection())
414 using (var command = new SQLiteCommand("select OverlayStatus from FileState where FilePath=:path COLLATE NOCASE", connection))
417 command.Parameters.AddWithValue("path", path);
419 var s = command.ExecuteScalar();
420 return (FileOverlayStatus) Convert.ToInt32(s);
423 catch (Exception exc)
425 Log.ErrorFormat(exc.ToString());
426 return FileOverlayStatus.Unversioned;
430 private string GetConnectionString()
432 var connectionString = String.Format(@"Data Source={0}\pithos.db;Version=3;Enlist=N;Pooling=True", _pithosDataPath);
433 return connectionString;
436 private SQLiteConnection GetConnection()
438 var connectionString = GetConnectionString();
439 var connection = new SQLiteConnection(connectionString);
441 using(var cmd =connection.CreateCommand())
443 cmd.CommandText = "PRAGMA journal_mode=WAL";
444 cmd.ExecuteNonQuery();
449 public void SetFileOverlayStatus(string path, FileOverlayStatus overlayStatus)
451 if (String.IsNullOrWhiteSpace(path))
452 throw new ArgumentNullException("path");
453 if (!Path.IsPathRooted(path))
454 throw new ArgumentException("The path must be rooted","path");
455 Contract.EndContractBlock();
457 _persistenceAgent.Post(() => FileState.StoreOverlayStatus(path,overlayStatus));
460 /* public void RenameFileOverlayStatus(string oldPath, string newPath)
462 if (String.IsNullOrWhiteSpace(oldPath))
463 throw new ArgumentNullException("oldPath");
464 if (!Path.IsPathRooted(oldPath))
465 throw new ArgumentException("The oldPath must be rooted", "oldPath");
466 if (String.IsNullOrWhiteSpace(newPath))
467 throw new ArgumentNullException("newPath");
468 if (!Path.IsPathRooted(newPath))
469 throw new ArgumentException("The newPath must be rooted", "newPath");
470 Contract.EndContractBlock();
472 _persistenceAgent.Post(() =>FileState.RenameState(oldPath, newPath));
475 public void SetFileState(string path, FileStatus fileStatus, FileOverlayStatus overlayStatus)
477 if (String.IsNullOrWhiteSpace(path))
478 throw new ArgumentNullException("path");
479 if (!Path.IsPathRooted(path))
480 throw new ArgumentException("The path must be rooted", "path");
481 Contract.EndContractBlock();
483 Debug.Assert(!path.Contains(FolderConstants.CacheFolder));
484 Debug.Assert(!path.EndsWith(".ignore"));
486 _persistenceAgent.Post(() => UpdateStatusDirect(path, fileStatus, overlayStatus));
490 public void StoreInfo(string path,ObjectInfo objectInfo)
492 if (String.IsNullOrWhiteSpace(path))
493 throw new ArgumentNullException("path");
494 if (!Path.IsPathRooted(path))
495 throw new ArgumentException("The path must be rooted", "path");
496 if (objectInfo == null)
497 throw new ArgumentNullException("objectInfo", "objectInfo can't be empty");
498 Contract.EndContractBlock();
500 _persistenceAgent.Post(() =>
502 var filePath = path.ToLower();
503 //Load the existing files state and set its properties in one session
504 using (new SessionScope())
506 //Forgetting to use a sessionscope results in two sessions being created, one by
507 //FirstOrDefault and one by Save()
508 var state =FileState.FindByFilePath(filePath);
510 //Create a new empty state object if this is a new file
511 state = state ?? new FileState();
513 state.FilePath = filePath;
514 state.Checksum = objectInfo.Hash;
515 state.Version = objectInfo.Version;
516 state.VersionTimeStamp = objectInfo.VersionTimestamp;
518 state.FileStatus = FileStatus.Unchanged;
519 state.OverlayStatus = FileOverlayStatus.Normal;
530 public void StoreInfo(string path, ObjectInfo objectInfo)
532 if (String.IsNullOrWhiteSpace(path))
533 throw new ArgumentNullException("path");
534 if (!Path.IsPathRooted(path))
535 throw new ArgumentException("The path must be rooted", "path");
536 if (objectInfo == null)
537 throw new ArgumentNullException("objectInfo", "objectInfo can't be empty");
538 Contract.EndContractBlock();
540 _persistenceAgent.Post(() => StoreInfoDirect(path, objectInfo));
544 private void StoreInfoDirect(string path, ObjectInfo objectInfo)
549 using (var connection = GetConnection())
550 using (var command = new SQLiteCommand(connection))
552 if (StateExists(path, connection))
553 command.CommandText =
554 "update FileState set FileStatus= :fileStatus where FilePath = :path COLLATE NOCASE ";
557 command.CommandText =
558 "INSERT INTO FileState (Id,FilePath,Checksum,Version,VersionTimeStamp,FileStatus,OverlayStatus) VALUES (:id,:path,:checksum,:version,:versionTimeStamp,:fileStatus,:overlayStatus)";
559 command.Parameters.AddWithValue("id", Guid.NewGuid());
562 command.Parameters.AddWithValue("path", path);
563 command.Parameters.AddWithValue("checksum", objectInfo.Hash);
564 command.Parameters.AddWithValue("version", objectInfo.Version);
565 command.Parameters.AddWithValue("versionTimeStamp",
566 objectInfo.VersionTimestamp);
567 command.Parameters.AddWithValue("fileStatus", FileStatus.Unchanged);
568 command.Parameters.AddWithValue("overlayStatus",
569 FileOverlayStatus.Normal);
571 var affected = command.ExecuteNonQuery();
575 catch (Exception exc)
577 Log.Error(exc.ToString());
582 private bool StateExists(string filePath,SQLiteConnection connection)
584 using (var command = new SQLiteCommand("Select count(*) from FileState where FilePath=:path COLLATE NOCASE", connection))
586 command.Parameters.AddWithValue("path", filePath);
587 var result = command.ExecuteScalar();
588 return ((long)result >= 1);
593 public void SetFileStatus(string path, FileStatus status)
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();
601 _persistenceAgent.Post(() => UpdateStatusDirect(path, status));
604 public FileStatus GetFileStatus(string path)
606 if (String.IsNullOrWhiteSpace(path))
607 throw new ArgumentNullException("path");
608 if (!Path.IsPathRooted(path))
609 throw new ArgumentException("The path must be rooted", "path");
610 Contract.EndContractBlock();
613 using (var connection = GetConnection())
615 var command = new SQLiteCommand("select FileStatus from FileState where FilePath=:path COLLATE NOCASE", connection);
616 command.Parameters.AddWithValue("path", path);
618 var statusValue = command.ExecuteScalar();
619 if (statusValue==null)
620 return FileStatus.Missing;
621 return (FileStatus)Convert.ToInt32(statusValue);
626 /// Deletes the status of the specified file
628 /// <param name="path"></param>
629 public void ClearFileStatus(string path)
631 if (String.IsNullOrWhiteSpace(path))
632 throw new ArgumentNullException("path");
633 if (!Path.IsPathRooted(path))
634 throw new ArgumentException("The path must be rooted", "path");
635 Contract.EndContractBlock();
637 _persistenceAgent.Post(() => DeleteDirect(path));
641 /// Deletes the status of the specified folder and all its contents
643 /// <param name="path"></param>
644 public void ClearFolderStatus(string path)
646 if (String.IsNullOrWhiteSpace(path))
647 throw new ArgumentNullException("path");
648 if (!Path.IsPathRooted(path))
649 throw new ArgumentException("The path must be rooted", "path");
650 Contract.EndContractBlock();
652 _persistenceAgent.Post(() => DeleteFolderDirect(path));
655 public IEnumerable<FileState> GetChildren(FileState fileState)
657 if (fileState == null)
658 throw new ArgumentNullException("fileState");
659 Contract.EndContractBlock();
661 var children = from state in FileState.Queryable
662 where state.FilePath.StartsWith(fileState.FilePath + "\\")
667 private int DeleteDirect(string filePath)
669 using (log4net.ThreadContext.Stacks["StatusAgent"].Push("DeleteDirect"))
676 using (var connection = GetConnection())
678 var command = new SQLiteCommand("delete from FileState where FilePath = :path COLLATE NOCASE",
681 command.Parameters.AddWithValue("path", filePath);
683 var affected = command.ExecuteNonQuery();
687 catch (Exception exc)
689 Log.Error(exc.ToString());
695 private int DeleteFolderDirect(string filePath)
697 using (log4net.ThreadContext.Stacks["StatusAgent"].Push("DeleteDirect"))
704 using (var connection = GetConnection())
706 var command = new SQLiteCommand("delete from FileState where FilePath = :path or FilePath like :path + '/%' COLLATE NOCASE",
709 command.Parameters.AddWithValue("path", filePath);
711 var affected = command.ExecuteNonQuery();
715 catch (Exception exc)
717 Log.Error(exc.ToString());
723 public void UpdateFileChecksum(string path, string checksum)
725 if (String.IsNullOrWhiteSpace(path))
726 throw new ArgumentNullException("path");
727 if (!Path.IsPathRooted(path))
728 throw new ArgumentException("The path must be rooted", "path");
729 Contract.EndContractBlock();
731 _persistenceAgent.Post(() => FileState.UpdateChecksum(path, checksum));