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 = ActiveRecordLinqBase<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 (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 (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 (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 //Can happen when downloading a new file
\r
499 var createdState = FileState.CreateFor(FileInfoExtensions.FromPath(absolutePath), StatusNotification);
\r
500 createdState.FileStatus = fileStatus;
\r
501 createdState.OverlayStatus = overlayStatus;
\r
502 createdState.ConflictReason = conflictReason;
\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)
\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));
\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, TreeHash treeHash)
\r
703 if (String.IsNullOrWhiteSpace(path))
\r
704 throw new ArgumentNullException("path");
\r
705 if (treeHash==null)
\r
706 throw new ArgumentNullException("treeHash");
\r
707 if (!Path.IsPathRooted(path))
\r
708 throw new ArgumentException("The path must be rooted", "path");
\r
709 if (objectInfo == null)
\r
710 throw new ArgumentNullException("objectInfo", "objectInfo can't be empty");
\r
711 Contract.EndContractBlock();
\r
713 _persistenceAgent.Post(() => StoreInfoDirect(path, objectInfo, treeHash));
\r
717 public void StoreInfo(string path, ObjectInfo objectInfo)
\r
719 if (String.IsNullOrWhiteSpace(path))
\r
720 throw new ArgumentNullException("path");
\r
721 if (!Path.IsPathRooted(path))
\r
722 throw new ArgumentException("The path must be rooted", "path");
\r
723 if (objectInfo == null)
\r
724 throw new ArgumentNullException("objectInfo", "objectInfo can't be empty");
\r
725 Contract.EndContractBlock();
\r
727 _persistenceAgent.Post(() => StoreInfoDirect(path, objectInfo, null));
\r
731 private void StoreInfoDirect(string path, ObjectInfo objectInfo,TreeHash treeHash)
\r
735 using (var session = ActiveRecordMediator.GetSessionFactoryHolder().CreateSession(typeof(FileState)))
\r
736 using (var tx=session.BeginTransaction(IsolationLevel.ReadCommitted))
\r
739 //var walquery = session.CreateSQLQuery("PRAGMA journal_mode=WAL");
\r
740 var walquery = session.CreateSQLQuery("PRAGMA journal_mode=WAL");
\r
743 //An entry for the new path may exist,
\r
744 IQuery deletecmd = session.CreateQuery(
\r
745 "delete from FileState where FilePath=:path and ObjectID is null")
\r
746 .SetString("path",path);
\r
747 deletecmd.ExecuteUpdate();
\r
749 string md5=treeHash.NullSafe(t=>t.MD5);
\r
750 string hashes=treeHash.NullSafe(t=>t.ToJson());
\r
752 var info = FileInfoExtensions.FromPath(path);
\r
753 var lastWriteTime = info.LastWriteTime;
\r
754 var isFolder = (info is DirectoryInfo);
\r
755 var lastLength=isFolder ? 0:((FileInfo) info).Length;
\r
757 Func<IQuery, IQuery> setCriteria = q => {
\r
758 var q1=q.SetString("path", path)
\r
759 .SetBoolean("isFolder",isFolder)
\r
760 .SetDateTime("lastWrite",lastWriteTime)
\r
761 .SetInt64("lastLength",lastLength)
\r
762 .SetString("checksum", objectInfo.X_Object_Hash)
\r
763 .SetString("etag", objectInfo.ETag)
\r
764 .SetInt64("version",objectInfo.Version.GetValueOrDefault())
\r
765 .SetDateTime("versionTimeStamp",objectInfo.VersionTimestamp.GetValueOrDefault())
\r
766 .SetEnum("fileStatus", FileStatus.Unchanged)
\r
767 .SetEnum("overlayStatus",FileOverlayStatus.Normal)
\r
768 .SetString("objectID", objectInfo.UUID);
\r
769 if (treeHash!=null)
\r
771 q1=q1.SetString("hashes", hashes)
\r
772 .SetString("md5", md5);
\r
777 var updateStatement=(treeHash!=null)
\r
778 ? "update FileState set FilePath=:path,IsFolder=:isFolder,LastWriteDate=:lastWrite,LastLength=:lastLength,FileStatus= :fileStatus,OverlayStatus= :overlayStatus, Checksum=:checksum,Hashes=:hashes, ETag=:etag,LastMD5=:md5,Version=:version,VersionTimeStamp=:versionTimeStamp "
\r
779 : "update FileState set FilePath=:path,IsFolder=:isFolder,LastWriteDate=:lastWrite,LastLength=:lastLength,FileStatus= :fileStatus,OverlayStatus= :overlayStatus, Checksum=:checksum, ETag=:etag,Version=:version,VersionTimeStamp=:versionTimeStamp ";
\r
781 IQuery updatecmd = session.CreateQuery(updateStatement + " where ObjectID = :objectID ");
\r
784 updatecmd = setCriteria(updatecmd);
\r
785 //If the ID exists, update the status
\r
786 var affected = updatecmd.ExecuteUpdate();
\r
788 //If the ID doesn't exist, try to update using the path, and store the ID as well.
\r
791 updateStatement=(treeHash!=null)
\r
792 ? "update FileState set IsFolder=:isFolder,LastWriteDate=:lastWrite,LastLength=:lastLength,FileStatus= :fileStatus,OverlayStatus= :overlayStatus, ObjectID=:objectID, Checksum=:checksum,Hashes=:hashes, ETag=:etag,LastMD5=:md5,Version=:version,VersionTimeStamp=:versionTimeStamp "
\r
793 : "update FileState set IsFolder=:isFolder,LastWriteDate=:lastWrite,LastLength=:lastLength,FileStatus= :fileStatus,OverlayStatus= :overlayStatus, ObjectID=:objectID, Checksum=:checksum, ETag=:etag,Version=:version,VersionTimeStamp=:versionTimeStamp ";
\r
795 updatecmd = session.CreateQuery(updateStatement + " where FilePath = :path");
\r
796 updatecmd=setCriteria(updatecmd);
\r
797 affected = updatecmd.ExecuteUpdate();
\r
799 //If the record can't be located, create a new one
\r
802 IQuery insertCmd = session.CreateSQLQuery("INSERT INTO FileState (Id,FilePath,IsFolder,LastWriteDate,LastLength,Checksum,Hashes,Version,VersionTimeStamp,ETag,LastMD5,FileStatus,OverlayStatus,ObjectID) " +
\r
803 "VALUES (:id,:path,:isFolder,:lastWrite,:lastLength,:checksum,:hashes,:version,:versionTimeStamp,:etag,:md5,:fileStatus,:overlayStatus,:objectID)");
\r
804 insertCmd=setCriteria(insertCmd)
\r
805 .SetGuid("id", Guid.NewGuid());
\r
806 affected = insertCmd.ExecuteUpdate();
\r
811 catch (Exception exc)
\r
813 Log.ErrorFormat("Failed to update [{0}]:[{1}]\r\n{2}",path,objectInfo.UUID, exc);
\r
818 private bool StateExists(string filePath,SQLiteConnection connection)
\r
820 using (var command = new SQLiteCommand("Select count(*) from FileState where FilePath=:path COLLATE NOCASE", connection))
\r
822 command.Parameters.AddWithValue("path", filePath);
\r
823 var result = command.ExecuteScalar();
\r
824 return ((long)result >= 1);
\r
829 private bool StateExistsByID(string objectId,SQLiteConnection connection)
\r
831 using (var command = new SQLiteCommand("Select count(*) from FileState where ObjectId=:id", connection))
\r
833 command.Parameters.AddWithValue("id", objectId);
\r
834 var result = command.ExecuteScalar();
\r
835 return ((long)result >= 1);
\r
840 public void SetFileStatus(string path, FileStatus status)
\r
842 if (String.IsNullOrWhiteSpace(path))
\r
843 throw new ArgumentNullException("path");
\r
844 if (!Path.IsPathRooted(path))
\r
845 throw new ArgumentException("The path must be rooted", "path");
\r
846 Contract.EndContractBlock();
\r
848 _persistenceAgent.Post(() => UpdateStatusDirect(path, status));
\r
851 public FileStatus GetFileStatus(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
860 using (var connection = GetConnection())
\r
862 var command = new SQLiteCommand("select FileStatus from FileState where FilePath=:path COLLATE NOCASE", connection);
\r
863 command.Parameters.AddWithValue("path", path);
\r
865 var statusValue = command.ExecuteScalar();
\r
866 if (statusValue==null)
\r
867 return FileStatus.Missing;
\r
868 return (FileStatus)Convert.ToInt32(statusValue);
\r
873 /// Deletes the status of the specified file
\r
875 /// <param name="path"></param>
\r
876 public void ClearFileStatus(string path)
\r
878 if (String.IsNullOrWhiteSpace(path))
\r
879 throw new ArgumentNullException("path");
\r
880 if (!Path.IsPathRooted(path))
\r
881 throw new ArgumentException("The path must be rooted", "path");
\r
882 Contract.EndContractBlock();
\r
884 _persistenceAgent.Post(() => DeleteDirect(path));
\r
888 /// Deletes the status of the specified folder and all its contents
\r
890 /// <param name="path"></param>
\r
891 public void ClearFolderStatus(string path)
\r
893 if (String.IsNullOrWhiteSpace(path))
\r
894 throw new ArgumentNullException("path");
\r
895 if (!Path.IsPathRooted(path))
\r
896 throw new ArgumentException("The path must be rooted", "path");
\r
897 Contract.EndContractBlock();
\r
898 //The agent may be null when removing an invalid/expired account from Settings, in which case processing hasn't yet started.
\r
899 if (_persistenceAgent == null)
\r
900 //In this case remove the folder immediatelly
\r
901 DeleteFolderDirect(path);
\r
903 //Otherwise schedule a delete
\r
904 _persistenceAgent.Post(() => DeleteFolderDirect(path));
\r
907 public IEnumerable<FileState> GetChildren(FileState fileState)
\r
909 if (fileState == null)
\r
910 throw new ArgumentNullException("fileState");
\r
911 Contract.EndContractBlock();
\r
913 var children = from state in ActiveRecordLinqBase<FileState>.Queryable
\r
914 where state.FilePath.StartsWith(fileState.FilePath + "\\")
\r
919 public void EnsureFileState(string path)
\r
921 var existingState = GetStateByFilePath(path);
\r
922 if (existingState != null)
\r
924 var fileInfo = FileInfoExtensions.FromPath(path);
\r
925 using (new SessionScope())
\r
927 var newState = FileState.CreateFor(fileInfo,StatusNotification);
\r
928 newState.FileStatus=FileStatus.Missing;
\r
929 _persistenceAgent.PostAndAwait(newState.CreateAndFlush).Wait();
\r
934 private int DeleteDirect(string filePath)
\r
936 using (ThreadContext.Stacks["StatusAgent"].Push("DeleteDirect"))
\r
943 using (var connection = GetConnection())
\r
945 var command = new SQLiteCommand("delete from FileState where FilePath = :path COLLATE NOCASE",
\r
948 command.Parameters.AddWithValue("path", filePath);
\r
950 var affected = command.ExecuteNonQuery();
\r
954 catch (Exception exc)
\r
956 Log.Error(exc.ToString());
\r
962 private int DeleteFolderDirect(string filePath)
\r
964 using (ThreadContext.Stacks["StatusAgent"].Push("DeleteDirect"))
\r
971 using (var connection = GetConnection())
\r
973 var command = new SQLiteCommand(@"delete from FileState where FilePath = :path or FilePath like :path || '\%' COLLATE NOCASE",
\r
976 command.Parameters.AddWithValue("path", filePath);
\r
978 var affected = command.ExecuteNonQuery();
\r
982 catch (Exception exc)
\r
984 Log.Error(exc.ToString());
\r
992 public void UpdateFileTreeHash(string path, TreeHash treeHash)
\r
994 if (String.IsNullOrWhiteSpace(path))
\r
995 throw new ArgumentNullException("path");
\r
996 if (!Path.IsPathRooted(path))
\r
997 throw new ArgumentException("The path must be rooted", "path");
\r
998 if (treeHash==null)
\r
999 throw new ArgumentNullException("treeHash");
\r
1000 Contract.EndContractBlock();
\r
1002 _persistenceAgent.Post(() => FileState.UpdateFileTreeHash(path, treeHash));
\r
1005 public void UpdateFileChecksum(string path, string etag, TreeHash treeHash)
\r
1007 if (String.IsNullOrWhiteSpace(path))
\r
1008 throw new ArgumentNullException("path");
\r
1009 if (!Path.IsPathRooted(path))
\r
1010 throw new ArgumentException("The path must be rooted", "path");
\r
1011 Contract.EndContractBlock();
\r
1013 _persistenceAgent.Post(() => FileState.UpdateChecksum(path, etag,treeHash));
\r
1016 /* public void UpdateLastMD5(FileInfo file, string etag)
\r
1019 throw new ArgumentNullException("file");
\r
1020 if (String.IsNullOrWhiteSpace(etag))
\r
1021 throw new ArgumentNullException("etag");
\r
1022 Contract.EndContractBlock();
\r
1024 _persistenceAgent.Post(() => FileState.UpdateLastMD5(file, etag));
\r
1028 public void CleanupOrphanStates()
\r
1030 //Orphan states are those that do not correspond to an account, ie. their paths
\r
1031 //do not start with the root path of any registered account
\r
1033 var roots=(from account in Settings.Accounts
\r
1034 select account.RootPath).ToList();
\r
1036 var allStates = from state in ActiveRecordLinqBase<FileState>.Queryable
\r
1037 select state.FilePath;
\r
1039 foreach (var statePath in allStates)
\r
1041 if (!roots.Any(root=>statePath.StartsWith(root,StringComparison.InvariantCultureIgnoreCase)))
\r
1042 this.DeleteDirect(statePath);
\r
1046 public void CleanupStaleStates(AccountInfo accountInfo, List<ObjectInfo> objectInfos)
\r
1048 if (accountInfo == null)
\r
1049 throw new ArgumentNullException("accountInfo");
\r
1050 if (objectInfos == null)
\r
1051 throw new ArgumentNullException("objectInfos");
\r
1052 Contract.EndContractBlock();
\r
1056 //Stale states are those that have no corresponding local or server file
\r
1059 var agent=FileAgent.GetFileAgent(accountInfo);
\r
1061 var localFiles=agent.EnumerateFiles();
\r
1062 var localSet = new HashSet<string>(localFiles);
\r
1064 //RelativeUrlToFilePath will fail for
\r
1065 //infos of accounts, containers which have no Name
\r
1067 var serverFiles = from info in objectInfos
\r
1068 where info.Name != null
\r
1069 select Path.Combine(accountInfo.AccountPath,info.RelativeUrlToFilePath(accountInfo.UserName));
\r
1070 var serverSet = new HashSet<string>(serverFiles);
\r
1072 var allStates = from state in ActiveRecordLinqBase<FileState>.Queryable
\r
1073 where state.FilePath.StartsWith(agent.RootPath)
\r
1074 select state.FilePath;
\r
1075 var stateSet = new HashSet<string>(allStates);
\r
1076 stateSet.ExceptWith(serverSet);
\r
1077 stateSet.ExceptWith(localSet);
\r
1079 foreach (var remainder in stateSet)
\r
1081 DeleteDirect(remainder);
\r
1087 public static TreeHash CalculateTreeHash(FileSystemInfo fileInfo, AccountInfo accountInfo, FileState fileState, byte hashingParallelism, CancellationToken cancellationToken, Progress<double> progress)
\r
1089 //FileState may be null if there is no stored state for this file
\r
1090 if (fileState==null)
\r
1091 return Signature.CalculateTreeHashAsync(fileInfo,
\r
1092 accountInfo.BlockSize,
\r
1093 accountInfo.BlockHash,
\r
1094 hashingParallelism,
\r
1095 cancellationToken, progress);
\r
1096 //Can we use the stored hashes?
\r
1097 var localTreeHash = fileState.LastMD5 == Signature.CalculateMD5(fileInfo)
\r
1098 ? TreeHash.Parse(fileState.Hashes)
\r
1099 : Signature.CalculateTreeHashAsync(fileInfo,
\r
1100 accountInfo.BlockSize,
\r
1101 accountInfo.BlockHash,
\r
1102 hashingParallelism,
\r
1103 cancellationToken, progress);
\r
1104 return localTreeHash;
\r