2 /* -----------------------------------------------------------------------
\r
3 * <copyright file="StatusAgent.cs" company="GRNet">
\r
5 * Copyright 2011-2012 GRNET S.A. All rights reserved.
\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
11 * 1. Redistributions of source code must retain the above
\r
12 * copyright notice, this list of conditions and the following
\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
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
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
39 * -----------------------------------------------------------------------
\r
43 using System.Collections.Generic;
\r
44 using System.ComponentModel.Composition;
\r
46 using System.Data.SQLite;
\r
47 using System.Diagnostics;
\r
48 using System.Diagnostics.Contracts;
\r
51 using System.Reflection;
\r
52 using System.Security.Cryptography;
\r
54 using System.Threading;
\r
55 using System.Threading.Tasks;
\r
56 using Castle.ActiveRecord;
\r
57 using Castle.ActiveRecord.Framework;
\r
58 using Castle.ActiveRecord.Framework.Config;
\r
59 using Castle.ActiveRecord.Queries;
\r
61 using NHibernate.ByteCode.Castle;
\r
62 using NHibernate.Cfg;
\r
63 using NHibernate.Cfg.Loquacious;
\r
64 using NHibernate.Dialect;
\r
65 using NHibernate.Exceptions;
\r
66 using Pithos.Interfaces;
\r
67 using Pithos.Network;
\r
69 using Environment = System.Environment;
\r
71 namespace Pithos.Core.Agents
\r
73 [Export(typeof(IStatusChecker)),Export(typeof(IStatusKeeper))]
\r
74 public class StatusAgent:IStatusChecker,IStatusKeeper
\r
76 private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
\r
78 [System.ComponentModel.Composition.Import]
\r
79 public IPithosSettings Settings { get; set; }
\r
81 [System.ComponentModel.Composition.Import]
\r
82 public IStatusNotification StatusNotification { get; set; }
\r
84 private Agent<Action> _persistenceAgent;
\r
88 public StatusAgent()
\r
90 var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
\r
92 _pithosDataPath = Path.Combine(appDataPath , "GRNET\\PITHOS");
\r
93 if (!Directory.Exists(_pithosDataPath))
\r
94 Directory.CreateDirectory(_pithosDataPath);
\r
96 var dbPath = Path.Combine(_pithosDataPath, "pithos.db");
\r
98 MigrateOldDb(dbPath, appDataPath);
\r
101 var source = GetConfiguration(_pithosDataPath);
\r
102 ActiveRecordStarter.Initialize(source,typeof(FileState),typeof(FileTag));
\r
108 if (!File.Exists(dbPath))
\r
109 ActiveRecordStarter.CreateSchema();
\r
116 private static void MigrateOldDb(string dbPath, string appDataPath)
\r
118 if(String.IsNullOrWhiteSpace(dbPath))
\r
119 throw new ArgumentNullException("dbPath");
\r
120 if(String.IsNullOrWhiteSpace(appDataPath))
\r
121 throw new ArgumentNullException("appDataPath");
\r
122 Contract.EndContractBlock();
\r
124 var oldDbPath = Path.Combine(appDataPath, "Pithos", "pithos.db");
\r
125 var oldDbInfo = new FileInfo(oldDbPath);
\r
126 if (oldDbInfo.Exists && !File.Exists(dbPath))
\r
128 Log.InfoFormat("Moving database from {0} to {1}",oldDbInfo.FullName,dbPath);
\r
129 var oldDirectory = oldDbInfo.Directory;
\r
130 oldDbInfo.MoveTo(dbPath);
\r
132 if (Log.IsDebugEnabled)
\r
133 Log.DebugFormat("Deleting {0}",oldDirectory.FullName);
\r
135 oldDirectory.Delete(true);
\r
139 private T? GetNull<T>(string commandText,SQLiteConnection connection) where T:struct
\r
141 using (var command= new SQLiteCommand(commandText, connection))
\r
143 var result = command.ExecuteScalar();
\r
144 if (result == null)
\r
150 private T Get<T>(string commandText,SQLiteConnection connection)
\r
152 using (var command= new SQLiteCommand(commandText, connection))
\r
154 var result = command.ExecuteScalar();
\r
155 if (result == null)
\r
161 private int Run(string commandText,SQLiteConnection connection)
\r
163 using (var command= new SQLiteCommand(commandText, connection))
\r
165 var result=command.ExecuteNonQuery();
\r
170 private void UpgradeDatabase()
\r
172 const string hasVersionText = "select 1 from sqlite_master where name='Version'";
\r
174 const string hasFilestateText = "select 1 from sqlite_master where name='FileState'";
\r
176 const string getVersionCmd = "select Version from version where Id=1";
\r
178 const string createVersionCmd = "create table Version(Id integer,Version TEXT);\n" +
\r
179 "INSERT INTO VERSION (Id,Version) VALUES(1,'0.0.0.0');";
\r
180 const string createFileStateCmd =
\r
181 "CREATE TABLE FileState (Id UNIQUEIDENTIFIER not null, ObjectID TEXT COLLATE NOCASE, FilePath TEXT unique COLLATE NOCASE, OverlayStatus INTEGER, FileStatus INTEGER, ConflictReason TEXT, Checksum TEXT COLLATE NOCASE, ETag TEXT not null COLLATE NOCASE, LastMD5 TEXT not null COLLATE NOCASE,LastWriteDate DATETIME, LastLength INTEGER, Version INTEGER, VersionTimeStamp DATETIME, IsShared INTEGER, SharedBy TEXT, ShareWrite INTEGER, IsFolder INTEGER, Modified DATETIME,Hashes TEXT null COLLATE NOCASE, primary key (Id),unique (FilePath))";
\r
182 const string upgradeText = "PRAGMA writable_schema = 1;\n" +
\r
183 "UPDATE SQLITE_MASTER SET SQL = 'CREATE TABLE FileState (Id UNIQUEIDENTIFIER not null, ObjectID TEXT COLLATE NOCASE, FilePath TEXT unique COLLATE NOCASE, OverlayStatus INTEGER, FileStatus INTEGER, ConflictReason TEXT, Checksum TEXT COLLATE NOCASE, ETag TEXT not null COLLATE NOCASE, LastMD5 TEXT not null COLLATE NOCASE,LastWriteDate DATETIME, LastLength INTEGER, Version INTEGER, VersionTimeStamp DATETIME, IsShared INTEGER, SharedBy TEXT, ShareWrite INTEGER, IsFolder INTEGER, Modified DATETIME, Hashes TEXT null COLLATE NOCASE, primary key (Id),unique (FilePath))' WHERE NAME = 'FileState';\n" +
\r
184 "PRAGMA writable_schema = 0;\n" +
\r
187 using (var connection = GetConnection())
\r
189 var hasVersion = false;
\r
190 hasVersion = GetNull<long>(hasVersionText, connection).HasValue;
\r
192 var storedVersion = new Version();
\r
196 var versionTxt = Get<string>(getVersionCmd, connection);
\r
197 storedVersion = new Version(versionTxt);
\r
200 Run(createVersionCmd, connection);
\r
202 var hasFileState = false;
\r
203 hasFileState = GetNull<long>(hasFilestateText, connection).HasValue;
\r
206 Run(createFileStateCmd, connection);
\r
209 var actualVersion = Assembly.GetEntryAssembly().GetName().Version;
\r
210 if (!hasVersion || actualVersion > storedVersion)
\r
211 Run(upgradeText, connection);
\r
213 if (actualVersion != storedVersion)
\r
214 using (var updateVersionCmd = new SQLiteCommand("UPDATE VERSION SET Version=:version where ID=1",
\r
217 updateVersionCmd.Parameters.AddWithValue(":version", actualVersion.ToString());
\r
218 var result = updateVersionCmd.ExecuteNonQuery();
\r
219 Debug.Assert(result > 0);
\r
224 private void CreateTrigger()
\r
226 using (var connection = GetConnection())
\r
227 using (var triggerCommand = connection.CreateCommand())
\r
229 var cmdText = new StringBuilder()
\r
230 .AppendLine("CREATE TRIGGER IF NOT EXISTS update_last_modified UPDATE ON FileState FOR EACH ROW")
\r
231 .AppendLine("BEGIN")
\r
232 .AppendLine("UPDATE FileState SET Modified=datetime('now') WHERE Id=old.Id;")
\r
233 .AppendLine("END;")
\r
234 .AppendLine("CREATE TRIGGER IF NOT EXISTS insert_last_modified INSERT ON FileState FOR EACH ROW")
\r
235 .AppendLine("BEGIN")
\r
236 .AppendLine("UPDATE FileState SET Modified=datetime('now') WHERE Id=new.Id;")
\r
237 .AppendLine("END;")
\r
239 triggerCommand.CommandText = cmdText;
\r
240 triggerCommand.ExecuteNonQuery();
\r
245 private static InPlaceConfigurationSource GetConfiguration(string pithosDbPath)
\r
247 if (String.IsNullOrWhiteSpace(pithosDbPath))
\r
248 throw new ArgumentNullException("pithosDbPath");
\r
249 if (!Path.IsPathRooted(pithosDbPath))
\r
250 throw new ArgumentException("path must be a rooted path", "pithosDbPath");
\r
251 Contract.EndContractBlock();
\r
253 var properties = new Dictionary<string, string>
\r
255 {"connection.driver_class", "NHibernate.Driver.SQLite20Driver"},
\r
256 {"dialect", "NHibernate.Dialect.SQLiteDialect"},
\r
257 {"connection.provider", "NHibernate.Connection.DriverConnectionProvider"},
\r
259 "proxyfactory.factory_class",
\r
260 "NHibernate.ByteCode.Castle.ProxyFactoryFactory, NHibernate.ByteCode.Castle"
\r
264 var connectionString = String.Format(@"Data Source={0}\pithos.db;Version=3;Enlist=N", pithosDbPath);
\r
265 properties.Add("connection.connection_string", connectionString);
\r
267 var source = new InPlaceConfigurationSource();
\r
268 source.Add(typeof (ActiveRecordBase), properties);
\r
269 source.SetDebugFlag(false);
\r
273 public void StartProcessing(CancellationToken token)
\r
275 _persistenceAgent = Agent<Action>.Start(queue =>
\r
277 Action loop = null;
\r
280 var job = queue.Receive();
\r
281 job.ContinueWith(t =>
\r
283 var action = job.Result;
\r
288 catch (SQLiteException ex)
\r
290 Log.ErrorFormat("[ERROR] SQL \n{0}", ex);
\r
292 catch (Exception ex)
\r
294 Log.ErrorFormat("[ERROR] STATE \n{0}", ex);
\r
296 queue.NotifyComplete(action);
\r
297 // ReSharper disable AccessToModifiedClosure
\r
298 queue.DoAsync(loop);
\r
299 // ReSharper restore AccessToModifiedClosure
\r
311 _persistenceAgent.Stop();
\r
315 public void ProcessExistingFiles(IEnumerable<FileInfo> existingFiles)
\r
317 if (existingFiles == null)
\r
318 throw new ArgumentNullException("existingFiles");
\r
319 Contract.EndContractBlock();
\r
321 //Find new or matching files with a left join to the stored states
\r
322 var fileStates = FileState.Queryable.ToList();
\r
323 var currentFiles = from file in existingFiles
\r
324 join state in fileStates on file.FullName.ToLower() equals state.FilePath.ToLower() into
\r
326 from substate in gs.DefaultIfEmpty()
\r
327 select Tuple.Create(file, substate);
\r
329 //To get the deleted files we must get the states that have no corresponding
\r
331 //We can't use the File.Exists method inside a query, so we get all file paths from the states
\r
332 var statePaths = (from state in fileStates
\r
333 select new {state.Id, state.FilePath}).ToList();
\r
334 //and check each one
\r
335 var missingStates = (from path in statePaths
\r
336 where !File.Exists(path.FilePath) && !Directory.Exists(path.FilePath)
\r
337 select path.Id).ToList();
\r
338 //Finally, retrieve the states that correspond to the deleted files
\r
339 var deletedFiles = from state in fileStates
\r
340 where missingStates.Contains(state.Id)
\r
341 select Tuple.Create(default(FileInfo), state);
\r
343 var pairs = currentFiles.Union(deletedFiles).ToList();
\r
346 var total = pairs.Count;
\r
347 foreach (var pair in pairs)
\r
349 ProcessFile(total, pair);
\r
355 private void ProcessFile(int total, Tuple<FileInfo,FileState> pair)
\r
357 var idx = Interlocked.Increment(ref i);
\r
358 using (StatusNotification.GetNotifier("Indexing file {0} of {1}", "Indexed file {0} of {1} ", idx, total))
\r
360 var fileState = pair.Item2;
\r
361 var file = pair.Item1;
\r
362 if (fileState == null)
\r
364 //This is a new file
\r
365 var createState = FileState.CreateFor(file,StatusNotification);
\r
366 _persistenceAgent.Post(createState.Create);
\r
368 else if (file == null)
\r
370 //This file was deleted while we were down. We should mark it as deleted
\r
371 //We have to go through UpdateStatus here because the state object we are using
\r
372 //was created by a different ORM session.
\r
373 _persistenceAgent.Post(() => UpdateStatusDirect((Guid) fileState.Id, FileStatus.Deleted));
\r
377 //This file has a matching state. Need to check for possible changes
\r
378 //To check for changes, we use the cheap (in CPU terms) MD5 algorithm
\r
379 //on the entire file.
\r
381 var hashString = file.ComputeShortHash(StatusNotification);
\r
382 Debug.Assert(hashString.Length==32);
\r
385 //TODO: Need a way to attach the hashes to the filestate so we don't
\r
386 //recalculate them each time a call to calculate has is made
\r
387 //We can either store them to the filestate or add them to a
\r
390 //If the hashes don't match the file was changed
\r
391 if (fileState.ETag != hashString)
\r
393 _persistenceAgent.Post(() => UpdateStatusDirect((Guid) fileState.Id, FileStatus.Modified));
\r
400 private int UpdateStatusDirect(Guid id, FileStatus status)
\r
402 using (log4net.ThreadContext.Stacks["StatusAgent"].Push("UpdateStatusDirect"))
\r
407 using (var session = ActiveRecordMediator.GetSessionFactoryHolder().CreateSession(typeof(FileState)))
\r
409 var walquery = session.CreateSQLQuery("PRAGMA journal_mode=WAL");
\r
412 //var updatecmd = session.CreateSQLQuery(
\r
413 var updatecmd = session.CreateQuery(
\r
414 "update FileState set FileStatus= :fileStatus where Id = :id ")
\r
416 .SetEnum("fileStatus", status);
\r
417 var affected = updatecmd.ExecuteUpdate();
\r
423 catch (Exception exc)
\r
425 Log.Error(exc.ToString());
\r
431 private int UpdateStatusDirect(string path, FileStatus status)
\r
433 using (log4net.ThreadContext.Stacks["StatusAgent"].Push("UpdateStatusDirect"))
\r
438 using (var session = ActiveRecordMediator.GetSessionFactoryHolder().CreateSession(typeof(FileState)))
\r
439 using (var tx=session.BeginTransaction(IsolationLevel.ReadCommitted))
\r
442 //var walquery = session.CreateSQLQuery("PRAGMA journal_mode=WAL");
\r
443 var walquery = session.CreateQuery("PRAGMA journal_mode=WAL");
\r
446 //var updatecmd = session.CreateSQLQuery(
\r
447 var updatecmd = session.CreateQuery(
\r
448 "update FileState set FileStatus= :fileStatus where FilePath = :path COLLATE NOCASE")
\r
449 .SetString("path", path)
\r
450 .SetEnum("fileStatus", status);
\r
451 var affected = updatecmd.ExecuteUpdate();
\r
455 var createdState = FileState.CreateFor(FileInfoExtensions.FromPath(path), StatusNotification);
\r
456 createdState.FileStatus = status;
\r
457 session.Save(createdState);
\r
463 catch (Exception exc)
\r
465 Log.Error(exc.ToString());
\r
471 private int UpdateStatusDirect(string absolutePath, FileStatus fileStatus, FileOverlayStatus overlayStatus, string conflictReason)
\r
473 using (log4net.ThreadContext.Stacks["StatusAgent"].Push("UpdateStatusDirect"))
\r
479 using (var session = ActiveRecordMediator.GetSessionFactoryHolder().CreateSession(typeof(FileState)))
\r
480 using (var tx=session.BeginTransaction(IsolationLevel.ReadCommitted))
\r
483 //var walquery = session.CreateSQLQuery("PRAGMA journal_mode=WAL");
\r
484 var walquery = session.CreateSQLQuery("PRAGMA journal_mode=WAL");
\r
488 //var updatecmd = session.CreateSQLQuery("update FileState set OverlayStatus= :overlayStatus, FileStatus= :fileStatus,ConflictReason= :conflictReason where FilePath = :path COLLATE NOCASE")
\r
489 var updatecmd = session.CreateQuery("update FileState set OverlayStatus= :overlayStatus, FileStatus= :fileStatus,ConflictReason= :conflictReason where FilePath = :path")
\r
490 .SetString("path", absolutePath)
\r
491 .SetEnum("fileStatus", fileStatus)
\r
492 .SetEnum("overlayStatus", overlayStatus)
\r
493 .SetString("conflictReason", conflictReason);
\r
494 var affected = updatecmd.ExecuteUpdate();
\r
498 var createdState = FileState.CreateFor(FileInfoExtensions.FromPath(absolutePath), StatusNotification);
\r
499 createdState.FileStatus = fileStatus;
\r
500 createdState.OverlayStatus = overlayStatus;
\r
501 createdState.ConflictReason = conflictReason;
\r
502 createdState.LastMD5 = String.Empty;
\r
503 session.Save(createdState);
\r
504 //createdState.Create();
\r
510 catch (Exception exc)
\r
512 Log.Error(exc.ToString());
\r
520 public string BlockHash { get; set; }
\r
522 public int BlockSize { get; set; }
\r
523 public void ChangeRoots(string oldPath, string newPath)
\r
525 if (String.IsNullOrWhiteSpace(oldPath))
\r
526 throw new ArgumentNullException("oldPath");
\r
527 if (!Path.IsPathRooted(oldPath))
\r
528 throw new ArgumentException("oldPath must be an absolute path", "oldPath");
\r
529 if (string.IsNullOrWhiteSpace(newPath))
\r
530 throw new ArgumentNullException("newPath");
\r
531 if (!Path.IsPathRooted(newPath))
\r
532 throw new ArgumentException("newPath must be an absolute path", "newPath");
\r
533 Contract.EndContractBlock();
\r
535 FileState.ChangeRootPath(oldPath,newPath);
\r
541 private readonly string _pithosDataPath;
\r
544 public FileState GetStateByFilePath(string path)
\r
546 if (String.IsNullOrWhiteSpace(path))
\r
547 throw new ArgumentNullException("path");
\r
548 if (!Path.IsPathRooted(path))
\r
549 throw new ArgumentException("The path must be rooted", "path");
\r
550 Contract.EndContractBlock();
\r
555 using (var connection = GetConnection())
\r
556 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))
\r
559 command.Parameters.AddWithValue("path", path);
\r
561 using (var reader = command.ExecuteReader())
\r
565 //var values = new object[reader.FieldCount];
\r
566 //reader.GetValues(values);
\r
567 var state = new FileState
\r
569 Id = reader.GetGuid(0),
\r
570 FilePath = reader.IsDBNull(1)?"":reader.GetString(1),
\r
571 OverlayStatus =reader.IsDBNull(2)?FileOverlayStatus.Unversioned: (FileOverlayStatus) reader.GetInt64(2),
\r
572 FileStatus = reader.IsDBNull(3)?FileStatus.Missing:(FileStatus) reader.GetInt64(3),
\r
573 Checksum = reader.IsDBNull(4)?"":reader.GetString(4),
\r
574 ETag= reader.IsDBNull(5)?"":reader.GetString(5),
\r
575 Version = reader.IsDBNull(6)?default(long):reader.GetInt64(6),
\r
576 VersionTimeStamp = reader.IsDBNull(7)?default(DateTime):reader.GetDateTime(7),
\r
577 IsShared = !reader.IsDBNull(8) && reader.GetBoolean(8),
\r
578 SharedBy = reader.IsDBNull(9)?"":reader.GetString(9),
\r
579 ShareWrite = !reader.IsDBNull(10) && reader.GetBoolean(10),
\r
580 LastMD5=reader.GetString(11),
\r
581 LastLength=reader.IsDBNull(12)? default(long):reader.GetInt64(12),
\r
582 LastWriteDate=reader.IsDBNull(13)?default(DateTime):reader.GetDateTime(13)
\r
595 catch (Exception exc)
\r
597 Log.ErrorFormat(exc.ToString());
\r
602 public FileOverlayStatus GetFileOverlayStatus(string path)
\r
604 if (String.IsNullOrWhiteSpace(path))
\r
605 throw new ArgumentNullException("path");
\r
606 if (!Path.IsPathRooted(path))
\r
607 throw new ArgumentException("The path must be rooted", "path");
\r
608 Contract.EndContractBlock();
\r
613 using (var connection = GetConnection())
\r
614 using (var command = new SQLiteCommand("select OverlayStatus from FileState where FilePath=:path COLLATE NOCASE", connection))
\r
617 command.Parameters.AddWithValue("path", path);
\r
619 var s = command.ExecuteScalar();
\r
620 return (FileOverlayStatus) Convert.ToInt32(s);
\r
623 catch (Exception exc)
\r
625 Log.ErrorFormat(exc.ToString());
\r
626 return FileOverlayStatus.Unversioned;
\r
630 private string GetConnectionString()
\r
632 var connectionString = String.Format(@"Data Source={0}\pithos.db;Version=3;Enlist=N;Pooling=True", _pithosDataPath);
\r
633 return connectionString;
\r
636 private SQLiteConnection GetConnection()
\r
638 var connectionString = GetConnectionString();
\r
639 var connection = new SQLiteConnection(connectionString);
\r
641 using(var cmd =connection.CreateCommand())
\r
643 cmd.CommandText = "PRAGMA journal_mode=WAL";
\r
644 cmd.ExecuteNonQuery();
\r
649 /* public void SetFileOverlayStatus(string path, FileOverlayStatus overlayStatus)
\r
651 if (String.IsNullOrWhiteSpace(path))
\r
652 throw new ArgumentNullException("path");
\r
653 if (!Path.IsPathRooted(path))
\r
654 throw new ArgumentException("The path must be rooted","path");
\r
655 Contract.EndContractBlock();
\r
657 _persistenceAgent.Post(() => FileState.StoreOverlayStatus(path,overlayStatus));
\r
660 public Task SetFileOverlayStatus(string path, FileOverlayStatus overlayStatus, string etag = null)
\r
662 if (String.IsNullOrWhiteSpace(path))
\r
663 throw new ArgumentNullException("path");
\r
664 if (!Path.IsPathRooted(path))
\r
665 throw new ArgumentException("The path must be rooted","path");
\r
666 Contract.EndContractBlock();
\r
668 return _persistenceAgent.PostAndAwait(() => FileState.StoreOverlayStatus(path,overlayStatus,etag));
\r
671 /* public void RenameFileOverlayStatus(string oldPath, string newPath)
\r
673 if (String.IsNullOrWhiteSpace(oldPath))
\r
674 throw new ArgumentNullException("oldPath");
\r
675 if (!Path.IsPathRooted(oldPath))
\r
676 throw new ArgumentException("The oldPath must be rooted", "oldPath");
\r
677 if (String.IsNullOrWhiteSpace(newPath))
\r
678 throw new ArgumentNullException("newPath");
\r
679 if (!Path.IsPathRooted(newPath))
\r
680 throw new ArgumentException("The newPath must be rooted", "newPath");
\r
681 Contract.EndContractBlock();
\r
683 _persistenceAgent.Post(() =>FileState.RenameState(oldPath, newPath));
\r
686 public void SetFileState(string path, FileStatus fileStatus, FileOverlayStatus overlayStatus, string conflictReason)
\r
688 if (String.IsNullOrWhiteSpace(path))
\r
689 throw new ArgumentNullException("path");
\r
690 if (!Path.IsPathRooted(path))
\r
691 throw new ArgumentException("The path must be rooted", "path");
\r
692 Contract.EndContractBlock();
\r
694 Debug.Assert(!path.Contains(FolderConstants.CacheFolder));
\r
695 Debug.Assert(!path.EndsWith(".ignore"));
\r
697 _persistenceAgent.Post(() => UpdateStatusDirect(path, fileStatus, overlayStatus, conflictReason));
\r
701 public void StoreInfo(string path, ObjectInfo objectInfo)
\r
703 if (String.IsNullOrWhiteSpace(path))
\r
704 throw new ArgumentNullException("path");
\r
705 if (!Path.IsPathRooted(path))
\r
706 throw new ArgumentException("The path must be rooted", "path");
\r
707 if (objectInfo == null)
\r
708 throw new ArgumentNullException("objectInfo", "objectInfo can't be empty");
\r
709 Contract.EndContractBlock();
\r
711 _persistenceAgent.Post(() => StoreInfoDirect(path, objectInfo));
\r
715 private void StoreInfoDirect(string path, ObjectInfo objectInfo)
\r
719 using (var session = ActiveRecordMediator.GetSessionFactoryHolder().CreateSession(typeof(FileState)))
\r
720 using (var tx=session.BeginTransaction(IsolationLevel.ReadCommitted))
\r
723 //var walquery = session.CreateSQLQuery("PRAGMA journal_mode=WAL");
\r
724 var walquery = session.CreateSQLQuery("PRAGMA journal_mode=WAL");
\r
727 //An entry for the new path may exist,
\r
728 IQuery deletecmd = session.CreateQuery(
\r
729 "delete from FileState where FilePath=:path and ObjectID is null")
\r
730 .SetString("path",path);
\r
731 deletecmd.ExecuteUpdate();
\r
734 Func<IQuery, IQuery> setCriteria = q => q
\r
735 .SetString("path", path)
\r
736 .SetString("checksum",objectInfo.X_Object_Hash)
\r
737 .SetString("etag", objectInfo.ETag)
\r
738 .SetInt64("version", objectInfo.Version.GetValueOrDefault())
\r
739 .SetDateTime("versionTimeStamp",objectInfo.VersionTimestamp.GetValueOrDefault())
\r
740 .SetEnum("fileStatus", FileStatus.Unchanged)
\r
741 .SetEnum("overlayStatus",FileOverlayStatus.Normal)
\r
742 .SetString("objectID", objectInfo.UUID);
\r
743 //IQuery updatecmd = session.CreateSQLQuery(
\r
744 IQuery updatecmd = session.CreateQuery(
\r
745 "update FileState set FilePath=:path,FileStatus= :fileStatus,OverlayStatus= :overlayStatus, Checksum=:checksum, ETag=:etag,LastMD5=:etag,Version=:version,VersionTimeStamp=:versionTimeStamp where ObjectID = :objectID ");
\r
746 updatecmd = setCriteria(updatecmd);
\r
747 var affected = updatecmd.ExecuteUpdate();
\r
749 //If the ID exists, update the status
\r
752 //If the ID doesn't exist, try to update using the path, and store the ID as well.
\r
753 //updatecmd = session.CreateSQLQuery(
\r
754 updatecmd = session.CreateQuery(
\r
755 // "update FileState set FileStatus= :fileStatus,OverlayStatus= :overlayStatus, ObjectID=:objectID, Checksum=:checksum, ETag=:etag,LastMD5=:etag,Version=:version,VersionTimeStamp=:versionTimeStamp where FilePath = :path COLLATE NOCASE ");
\r
756 "update FileState set FileStatus= :fileStatus,OverlayStatus= :overlayStatus, ObjectID=:objectID, Checksum=:checksum, ETag=:etag,LastMD5=:etag,Version=:version,VersionTimeStamp=:versionTimeStamp where FilePath = :path");
\r
757 updatecmd=setCriteria(updatecmd);
\r
758 affected = updatecmd.ExecuteUpdate();
\r
762 //IQuery insertCmd=session.CreateSQLQuery(
\r
763 IQuery insertCmd = session.CreateSQLQuery(
\r
764 "INSERT INTO FileState (Id,FilePath,Checksum,Version,VersionTimeStamp,ETag,LastMD5,FileStatus,OverlayStatus,ObjectID) VALUES (:id,:path,:checksum,:version,:versionTimeStamp,:etag,:etag,:fileStatus,:overlayStatus,:objectID)");
\r
765 insertCmd=setCriteria(insertCmd).SetGuid("id", Guid.NewGuid());
\r
766 affected = insertCmd.ExecuteUpdate();
\r
771 catch (Exception exc)
\r
773 Log.ErrorFormat("Failed to update [{0}]:[{1}]\r\n{2}",path,objectInfo.UUID, exc);
\r
778 private bool StateExists(string filePath,SQLiteConnection connection)
\r
780 using (var command = new SQLiteCommand("Select count(*) from FileState where FilePath=:path COLLATE NOCASE", connection))
\r
782 command.Parameters.AddWithValue("path", filePath);
\r
783 var result = command.ExecuteScalar();
\r
784 return ((long)result >= 1);
\r
789 private bool StateExistsByID(string objectId,SQLiteConnection connection)
\r
791 using (var command = new SQLiteCommand("Select count(*) from FileState where ObjectId=:id", connection))
\r
793 command.Parameters.AddWithValue("id", objectId);
\r
794 var result = command.ExecuteScalar();
\r
795 return ((long)result >= 1);
\r
800 public void SetFileStatus(string path, FileStatus status)
\r
802 if (String.IsNullOrWhiteSpace(path))
\r
803 throw new ArgumentNullException("path");
\r
804 if (!Path.IsPathRooted(path))
\r
805 throw new ArgumentException("The path must be rooted", "path");
\r
806 Contract.EndContractBlock();
\r
808 _persistenceAgent.Post(() => UpdateStatusDirect(path, status));
\r
811 public FileStatus GetFileStatus(string path)
\r
813 if (String.IsNullOrWhiteSpace(path))
\r
814 throw new ArgumentNullException("path");
\r
815 if (!Path.IsPathRooted(path))
\r
816 throw new ArgumentException("The path must be rooted", "path");
\r
817 Contract.EndContractBlock();
\r
820 using (var connection = GetConnection())
\r
822 var command = new SQLiteCommand("select FileStatus from FileState where FilePath=:path COLLATE NOCASE", connection);
\r
823 command.Parameters.AddWithValue("path", path);
\r
825 var statusValue = command.ExecuteScalar();
\r
826 if (statusValue==null)
\r
827 return FileStatus.Missing;
\r
828 return (FileStatus)Convert.ToInt32(statusValue);
\r
833 /// Deletes the status of the specified file
\r
835 /// <param name="path"></param>
\r
836 public void ClearFileStatus(string path)
\r
838 if (String.IsNullOrWhiteSpace(path))
\r
839 throw new ArgumentNullException("path");
\r
840 if (!Path.IsPathRooted(path))
\r
841 throw new ArgumentException("The path must be rooted", "path");
\r
842 Contract.EndContractBlock();
\r
844 _persistenceAgent.Post(() => DeleteDirect(path));
\r
848 /// Deletes the status of the specified folder and all its contents
\r
850 /// <param name="path"></param>
\r
851 public void ClearFolderStatus(string path)
\r
853 if (String.IsNullOrWhiteSpace(path))
\r
854 throw new ArgumentNullException("path");
\r
855 if (!Path.IsPathRooted(path))
\r
856 throw new ArgumentException("The path must be rooted", "path");
\r
857 Contract.EndContractBlock();
\r
858 //The agent may be null when removing an invalid/expired account from Settings, in which case processing hasn't yet started.
\r
859 if (_persistenceAgent == null)
\r
860 //In this case remove the folder immediatelly
\r
861 DeleteFolderDirect(path);
\r
863 //Otherwise schedule a delete
\r
864 _persistenceAgent.Post(() => DeleteFolderDirect(path));
\r
867 public IEnumerable<FileState> GetChildren(FileState fileState)
\r
869 if (fileState == null)
\r
870 throw new ArgumentNullException("fileState");
\r
871 Contract.EndContractBlock();
\r
873 var children = from state in FileState.Queryable
\r
874 where state.FilePath.StartsWith(fileState.FilePath + "\\")
\r
879 public void EnsureFileState(string path)
\r
881 var existingState = GetStateByFilePath(path);
\r
882 if (existingState != null)
\r
884 var fileInfo = FileInfoExtensions.FromPath(path);
\r
885 using (new SessionScope())
\r
887 var newState = FileState.CreateFor(fileInfo,StatusNotification);
\r
888 newState.FileStatus=FileStatus.Missing;
\r
889 _persistenceAgent.PostAndAwait(newState.CreateAndFlush).Wait();
\r
894 private int DeleteDirect(string filePath)
\r
896 using (log4net.ThreadContext.Stacks["StatusAgent"].Push("DeleteDirect"))
\r
903 using (var connection = GetConnection())
\r
905 var command = new SQLiteCommand("delete from FileState where FilePath = :path COLLATE NOCASE",
\r
908 command.Parameters.AddWithValue("path", filePath);
\r
910 var affected = command.ExecuteNonQuery();
\r
914 catch (Exception exc)
\r
916 Log.Error(exc.ToString());
\r
922 private int DeleteFolderDirect(string filePath)
\r
924 using (log4net.ThreadContext.Stacks["StatusAgent"].Push("DeleteDirect"))
\r
931 using (var connection = GetConnection())
\r
933 var command = new SQLiteCommand(@"delete from FileState where FilePath = :path or FilePath like :path || '\%' COLLATE NOCASE",
\r
936 command.Parameters.AddWithValue("path", filePath);
\r
938 var affected = command.ExecuteNonQuery();
\r
942 catch (Exception exc)
\r
944 Log.Error(exc.ToString());
\r
952 public void UpdateFileTreeHash(string path, TreeHash treeHash)
\r
954 if (String.IsNullOrWhiteSpace(path))
\r
955 throw new ArgumentNullException("path");
\r
956 if (!Path.IsPathRooted(path))
\r
957 throw new ArgumentException("The path must be rooted", "path");
\r
958 if (treeHash==null)
\r
959 throw new ArgumentNullException("treeHash");
\r
960 Contract.EndContractBlock();
\r
962 _persistenceAgent.Post(() => FileState.UpdateFileTreeHash(path, treeHash));
\r
965 public void UpdateFileChecksum(string path, string etag, string checksum)
\r
967 if (String.IsNullOrWhiteSpace(path))
\r
968 throw new ArgumentNullException("path");
\r
969 if (!Path.IsPathRooted(path))
\r
970 throw new ArgumentException("The path must be rooted", "path");
\r
971 Contract.EndContractBlock();
\r
973 _persistenceAgent.Post(() => FileState.UpdateChecksum(path, etag,checksum));
\r
976 public void UpdateLastMD5(FileInfo file, string etag)
\r
979 throw new ArgumentNullException("file");
\r
980 if (String.IsNullOrWhiteSpace(etag))
\r
981 throw new ArgumentNullException("etag");
\r
982 Contract.EndContractBlock();
\r
984 _persistenceAgent.Post(() => FileState.UpdateLastMD5(file, etag));
\r
988 public void CleanupOrphanStates()
\r
990 //Orphan states are those that do not correspond to an account, ie. their paths
\r
991 //do not start with the root path of any registered account
\r
993 var roots=(from account in Settings.Accounts
\r
994 select account.RootPath).ToList();
\r
996 var allStates = from state in FileState.Queryable
\r
997 select state.FilePath;
\r
999 foreach (var statePath in allStates)
\r
1001 if (!roots.Any(root=>statePath.StartsWith(root,StringComparison.InvariantCultureIgnoreCase)))
\r
1002 this.DeleteDirect(statePath);
\r
1006 public void CleanupStaleStates(AccountInfo accountInfo, List<ObjectInfo> objectInfos)
\r
1008 if (accountInfo == null)
\r
1009 throw new ArgumentNullException("accountInfo");
\r
1010 if (objectInfos == null)
\r
1011 throw new ArgumentNullException("objectInfos");
\r
1012 Contract.EndContractBlock();
\r
1016 //Stale states are those that have no corresponding local or server file
\r
1019 var agent=FileAgent.GetFileAgent(accountInfo);
\r
1021 var localFiles=agent.EnumerateFiles();
\r
1022 var localSet = new HashSet<string>(localFiles);
\r
1024 //RelativeUrlToFilePath will fail for
\r
1025 //infos of accounts, containers which have no Name
\r
1027 var serverFiles = from info in objectInfos
\r
1028 where info.Name != null
\r
1029 select Path.Combine(accountInfo.AccountPath,info.RelativeUrlToFilePath(accountInfo.UserName));
\r
1030 var serverSet = new HashSet<string>(serverFiles);
\r
1032 var allStates = from state in FileState.Queryable
\r
1033 where state.FilePath.StartsWith(agent.RootPath)
\r
1034 select state.FilePath;
\r
1035 var stateSet = new HashSet<string>(allStates);
\r
1036 stateSet.ExceptWith(serverSet);
\r
1037 stateSet.ExceptWith(localSet);
\r
1039 foreach (var remainder in stateSet)
\r
1041 DeleteDirect(remainder);
\r