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.SqlServerCe;
\r
47 using System.Diagnostics;
\r
48 using System.Diagnostics.Contracts;
\r
51 using System.Reflection;
\r
52 using System.Threading;
\r
54 using NHibernate.Cfg;
\r
55 using NHibernate.Cfg.MappingSchema;
\r
56 using NHibernate.Criterion;
\r
57 using NHibernate.Dialect;
\r
58 using NHibernate.Linq;
\r
59 using NHibernate.Mapping.ByCode;
\r
60 using NHibernate.Tool.hbm2ddl;
\r
61 using Pithos.Interfaces;
\r
62 using Pithos.Network;
\r
64 using Environment = System.Environment;
\r
66 namespace Pithos.Core.Agents
\r
68 [Export(typeof(IStatusChecker)),Export(typeof(IStatusKeeper))]
\r
69 public class StatusAgent:IStatusChecker,IStatusKeeper
\r
71 private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
\r
73 [System.ComponentModel.Composition.Import]
\r
74 public IPithosSettings Settings { get; set; }
\r
76 [System.ComponentModel.Composition.Import]
\r
77 public IStatusNotification StatusNotification { get; set; }
\r
79 //private Agent<Action> _persistenceAgent;
\r
83 public StatusAgent()
\r
85 var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
\r
87 _pithosDataPath = Path.Combine(appDataPath, "GRNET\\PITHOS");
\r
88 if (!Directory.Exists(_pithosDataPath))
\r
89 Directory.CreateDirectory(_pithosDataPath);
\r
91 var dbPath = Path.Combine(_pithosDataPath, "pithos.sdf");
\r
93 //MigrateOldDb(dbPath, appDataPath);
\r
96 var cfg = Configure(dbPath);
\r
98 var connectionString = "Data Source=" + dbPath;
\r
99 using (var sqlCeEngine = new SqlCeEngine(connectionString))
\r
101 if (!File.Exists(dbPath))
\r
103 sqlCeEngine.CreateDatabase();
\r
104 new SchemaExport(cfg).Execute(true, true, false);
\r
105 _factory = cfg.BuildSessionFactory();
\r
106 using (var session = _factory.OpenStatelessSession())
\r
108 session.Insert(new PithosVersion {Id = 1, Version = "0.0.0.0"});
\r
115 if (!sqlCeEngine.Verify(VerifyOption.Enhanced))
\r
116 sqlCeEngine.Repair(connectionString, RepairOption.RecoverAllOrFail);
\r
118 catch(SqlCeException ex)
\r
120 //Rethrow except for sharing errors while repairing
\r
121 if (ex.NativeError != 25035)
\r
124 var update = new SchemaUpdate(cfg);
\r
125 update.Execute(script=>Log.WarnFormat("[DBUPDATE] : {0}",script),true);
\r
126 _factory = cfg.BuildSessionFactory();
\r
132 private void UpgradeDatabase()
\r
134 using (var session = _factory.OpenSession())
\r
137 var storedVersion = session.Get<PithosVersion>(1);
\r
138 var actualVersion = Assembly.GetEntryAssembly().GetName().Version;
\r
140 if (actualVersion == new Version(storedVersion.Version))
\r
143 storedVersion.Version = actualVersion.ToString();
\r
144 session.Update(storedVersion);
\r
150 private static Configuration Configure(string pithosDbPath)
\r
152 if (String.IsNullOrWhiteSpace(pithosDbPath))
\r
153 throw new ArgumentNullException("pithosDbPath");
\r
154 if (!Path.IsPathRooted(pithosDbPath))
\r
155 throw new ArgumentException("path must be a rooted path", "pithosDbPath");
\r
156 Contract.EndContractBlock();
\r
159 var cfg = new Configuration();
\r
160 cfg.DataBaseIntegration(db=>
\r
162 db.Dialect<MsSqlCe40Dialect>();
\r
163 db.ConnectionString = "Data Source=" + pithosDbPath;
\r
164 db.AutoCommentSql = true;
\r
165 db.KeywordsAutoImport = Hbm2DDLKeyWords.AutoQuote;
\r
166 db.SchemaAction = SchemaAutoAction.Update;
\r
167 db.LogSqlInConsole = true;
\r
170 .GenerateStatistics()
\r
173 var mapping = GetMapping();
\r
174 cfg.AddMapping(mapping);
\r
179 private static HbmMapping GetMapping()
\r
181 var mapper = new ModelMapper();
\r
182 mapper.Class<FileState>(fm =>
\r
184 fm.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
\r
185 fm.Property(x => x.ObjectID, m =>
\r
187 m.Index("IX_FileState_ObjectID");
\r
189 fm.Property(x => x.FilePath, m =>
\r
192 m.UniqueKey("U_FileState_FilePath");
\r
193 m.Index("IX_FileState_FilePath");
\r
195 fm.Property(x => x.OverlayStatus);
\r
196 fm.Property(x => x.FileStatus);
\r
197 fm.Property(x => x.ConflictReason);
\r
198 fm.Property(x => x.Checksum, m => m.Length(64));
\r
199 fm.Property(x => x.ETag, m => m.Length(64));
\r
200 fm.Property(x => x.Hashes, m => m.Type(NHibernateUtil.StringClob));
\r
201 fm.Property(x => x.LastWriteDate);
\r
202 fm.Property(x => x.LastLength);
\r
203 fm.Property(x => x.Version);
\r
204 fm.Property(x => x.VersionTimeStamp);
\r
205 fm.Property(x => x.IsShared);
\r
206 fm.Property(x => x.SharedBy);
\r
207 fm.Property(x => x.ShareWrite);
\r
208 fm.Property(x => x.IsFolder);
\r
209 fm.Property(x => x.Modified);
\r
211 mapper.Class<PithosVersion>(fm =>
\r
213 fm.Id(x => x.Id, m => m.Generator(Generators.Assigned));
\r
214 fm.Property(x => x.Version, m => m.Length(20));
\r
218 var mapping = mapper.CompileMappingFor(new[] {typeof (FileState),typeof(PithosVersion)});
\r
222 public void StartProcessing(CancellationToken token)
\r
236 public void ProcessExistingFiles(IEnumerable<FileInfo> existingFiles)
\r
238 if (existingFiles == null)
\r
239 throw new ArgumentNullException("existingFiles");
\r
240 Contract.EndContractBlock();
\r
242 //Find new or matching files with a left join to the stored states
\r
243 using (var session = _factory.OpenSession())
\r
246 var fileStates = session.Query<FileState>().ToList();
\r
247 var currentFiles = from file in existingFiles
\r
248 join state in fileStates on file.FullName.ToLower() equals state.FilePath.ToLower()
\r
251 from substate in gs.DefaultIfEmpty()
\r
252 select Tuple.Create(file, substate);
\r
254 //To get the deleted files we must get the states that have no corresponding
\r
256 //We can't use the File.Exists method inside a query, so we get all file paths from the states
\r
257 var statePaths = (from state in fileStates
\r
258 select new {state.Id, state.FilePath}).ToList();
\r
259 //and check each one
\r
260 var missingStates = (from path in statePaths
\r
261 where !File.Exists(path.FilePath) && !Directory.Exists(path.FilePath)
\r
262 select path.Id).ToList();
\r
263 //Finally, retrieve the states that correspond to the deleted files
\r
264 var deletedFiles = from state in fileStates
\r
265 where missingStates.Contains(state.Id)
\r
266 select Tuple.Create(default(FileInfo), state);
\r
268 var pairs = currentFiles.Union(deletedFiles).ToList();
\r
271 var total = pairs.Count;
\r
272 foreach (var pair in pairs)
\r
274 ProcessFile(session,total, pair);
\r
282 private void ProcessFile(ISession session,int total, System.Tuple<FileInfo, FileState> pair)
\r
284 var idx = Interlocked.Increment(ref i);
\r
285 using (StatusNotification.GetNotifier("Indexing file {0} of {1}", "Indexed file {0} of {1} ", true,idx, total))
\r
287 var fileState = pair.Item2;
\r
288 var file = pair.Item1;
\r
289 if (fileState == null)
\r
291 //This is a new file
\r
292 var createState = FileState.CreateFor(file,StatusNotification);
\r
293 session.Save(createState);
\r
294 //_persistenceAgent.Post(createState.Create);
\r
296 else if (file == null)
\r
298 //This file was deleted while we were down. We should mark it as deleted
\r
299 //We have to go through UpdateStatus here because the state object we are using
\r
300 //was created by a different ORM session.
\r
301 UpdateStatusDirect(session,fileState.Id, FileStatus.Deleted);
\r
302 //_persistenceAgent.Post(() => UpdateStatusDirect((Guid) fileState.Id, FileStatus.Deleted));
\r
306 // //This file has a matching state. Need to check for possible changes
\r
307 // //To check for changes, we use the cheap (in CPU terms) MD5 algorithm
\r
308 // //on the entire file.
\r
310 // var hashString = file.ComputeShortHash(StatusNotification);
\r
311 // Debug.Assert(hashString.Length==32);
\r
314 // //TODO: Need a way to attach the hashes to the filestate so we don't
\r
315 // //recalculate them each time a call to calculate has is made
\r
316 // //We can either store them to the filestate or add them to a
\r
319 // //If the hashes don't match the file was changed
\r
320 // if (fileState.ETag != hashString)
\r
322 // _persistenceAgent.Post(() => UpdateStatusDirect((Guid) fileState.Id, FileStatus.Modified));
\r
329 private int UpdateStatusDirect(ISession session,Guid id, FileStatus status)
\r
331 using (ThreadContext.Stacks["StatusAgent"].Push("UpdateStatusDirect"))
\r
336 //var updatecmd = session.CreateSQLQuery(
\r
337 var updatecmd = session.CreateQuery(
\r
338 "update FileState set FileStatus= :fileStatus, Modified=:modified where Id = :id ")
\r
340 .SetEnum("fileStatus", status)
\r
341 .SetDateTime("modified",DateTime.Now);
\r
342 var affected = updatecmd.ExecuteUpdate();
\r
346 catch (Exception exc)
\r
348 Log.Error(exc.ToString());
\r
355 public string BlockHash { get; set; }
\r
357 public int BlockSize { get; set; }
\r
359 public void ChangeRoots(string oldPath, string newPath)
\r
361 if (String.IsNullOrWhiteSpace(oldPath))
\r
362 throw new ArgumentNullException("oldPath");
\r
363 if (!Path.IsPathRooted(oldPath))
\r
364 throw new ArgumentException("oldPath must be an absolute path", "oldPath");
\r
365 if (String.IsNullOrWhiteSpace(newPath))
\r
366 throw new ArgumentNullException("newPath");
\r
367 if (!Path.IsPathRooted(newPath))
\r
368 throw new ArgumentException("newPath must be an absolute path", "newPath");
\r
369 Contract.EndContractBlock();
\r
371 ChangeRootPath(oldPath,newPath);
\r
377 private readonly string _pithosDataPath;
\r
378 private readonly ISessionFactory _factory;
\r
380 public FileState GetStateByFilePath(string path)
\r
382 if (String.IsNullOrWhiteSpace(path))
\r
383 throw new ArgumentNullException("path");
\r
384 if (!Path.IsPathRooted(path))
\r
385 throw new ArgumentException("The path must be rooted", "path");
\r
386 Contract.EndContractBlock();
\r
391 using(var session=_factory.OpenStatelessSession())
\r
393 var state=session.Query<FileState>().SingleOrDefault(s => s.FilePath == path);
\r
396 state.FilePath=state.FilePath??String.Empty;
\r
397 state.OverlayStatus = state.OverlayStatus ??FileOverlayStatus.Unversioned;
\r
398 state.FileStatus = state.FileStatus ?? FileStatus.Missing;
\r
399 state.Checksum = state.Checksum ?? String.Empty;
\r
400 state.ETag = state.ETag ?? String.Empty;
\r
401 state.SharedBy = state.SharedBy ?? String.Empty;
\r
406 catch (Exception exc)
\r
408 Log.ErrorFormat(exc.ToString());
\r
413 public FileOverlayStatus GetFileOverlayStatus(string path)
\r
415 if (String.IsNullOrWhiteSpace(path))
\r
416 throw new ArgumentNullException("path");
\r
417 if (!Path.IsPathRooted(path))
\r
418 throw new ArgumentException("The path must be rooted", "path");
\r
419 Contract.EndContractBlock();
\r
424 using(var session=_factory.OpenStatelessSession())
\r
426 return (from state in session.Query<FileState>()
\r
427 where state.FilePath == path
\r
428 select state.OverlayStatus)
\r
430 .GetValueOrDefault(FileOverlayStatus.Unversioned);
\r
433 catch (Exception exc)
\r
435 Log.ErrorFormat(exc.ToString());
\r
436 return FileOverlayStatus.Unversioned;
\r
440 public void SetFileOverlayStatus(string path, FileOverlayStatus overlayStatus)
\r
442 if (String.IsNullOrWhiteSpace(path))
\r
443 throw new ArgumentNullException("path");
\r
444 if (!Path.IsPathRooted(path))
\r
445 throw new ArgumentException("The path must be rooted","path");
\r
446 Contract.EndContractBlock();
\r
448 StoreOverlayStatus(path,overlayStatus);
\r
451 public void SetFileState(string path, FileStatus fileStatus, FileOverlayStatus overlayStatus, string conflictReason)
\r
453 if (String.IsNullOrWhiteSpace(path))
\r
454 throw new ArgumentNullException("path");
\r
455 if (!Path.IsPathRooted(path))
\r
456 throw new ArgumentException("The path must be rooted", "path");
\r
457 Contract.EndContractBlock();
\r
459 Debug.Assert(!path.Contains(FolderConstants.CacheFolder));
\r
460 Debug.Assert(!path.EndsWith(".ignore"));
\r
461 using (ThreadContext.Stacks["StatusAgent"].Push("UpdateStatusDirect"))
\r
467 using (var session = _factory.OpenSession())
\r
468 using (var tx=session.BeginTransaction(IsolationLevel.ReadCommitted))
\r
471 //var updatecmd = session.CreateSQLQuery("update FileState set OverlayStatus= :overlayStatus, FileStatus= :fileStatus,ConflictReason= :conflictReason where FilePath = :path ")
\r
472 var updatecmd = session.CreateQuery("update FileState set OverlayStatus= :overlayStatus, FileStatus= :fileStatus,ConflictReason= :conflictReason, Modified=:modified where FilePath = :path")
\r
473 .SetString("path", path)
\r
474 .SetEnum("fileStatus", fileStatus)
\r
475 .SetEnum("overlayStatus", overlayStatus)
\r
476 .SetString("conflictReason", conflictReason)
\r
477 .SetDateTime("modified",DateTime.Now);
\r
478 var affected = updatecmd.ExecuteUpdate();
\r
482 //Can happen when downloading a new file
\r
483 var createdState = FileState.CreateFor(FileInfoExtensions.FromPath(path), StatusNotification);
\r
484 createdState.FileStatus = fileStatus;
\r
485 createdState.OverlayStatus = overlayStatus;
\r
486 createdState.ConflictReason = conflictReason;
\r
487 session.Save(createdState);
\r
488 //createdState.Create();
\r
494 catch (Exception exc)
\r
496 Log.Error(exc.ToString());
\r
503 public void StoreInfo(string path, ObjectInfo objectInfo, TreeHash treeHash)
\r
505 if (String.IsNullOrWhiteSpace(path))
\r
506 throw new ArgumentNullException("path");
\r
507 if (treeHash==null)
\r
508 throw new ArgumentNullException("treeHash");
\r
509 if (!Path.IsPathRooted(path))
\r
510 throw new ArgumentException("The path must be rooted", "path");
\r
511 if (objectInfo == null)
\r
512 throw new ArgumentNullException("objectInfo", "objectInfo can't be empty");
\r
513 Contract.EndContractBlock();
\r
515 StoreInfoDirect(path, objectInfo, treeHash);
\r
519 public void StoreInfo(string path, ObjectInfo objectInfo)
\r
521 if (String.IsNullOrWhiteSpace(path))
\r
522 throw new ArgumentNullException("path");
\r
523 if (!Path.IsPathRooted(path))
\r
524 throw new ArgumentException("The path must be rooted", "path");
\r
525 if (objectInfo == null)
\r
526 throw new ArgumentNullException("objectInfo", "objectInfo can't be empty");
\r
527 Contract.EndContractBlock();
\r
529 StoreInfoDirect(path, objectInfo, null);
\r
533 private void StoreInfoDirect(string path, ObjectInfo objectInfo,TreeHash treeHash)
\r
537 using (var session = _factory.OpenSession())
\r
538 using (var tx=session.BeginTransaction(IsolationLevel.ReadCommitted))
\r
541 //An entry for the new path may exist,
\r
542 IQuery deletecmd = session.CreateQuery(
\r
543 "delete from FileState where FilePath=:path and ObjectID is null")
\r
544 .SetString("path", path);
\r
545 deletecmd.ExecuteUpdate();
\r
547 //string md5=treeHash.NullSafe(t=>t.MD5);
\r
548 string hashes = treeHash.NullSafe(t => t.ToJson());
\r
550 var info = FileInfoExtensions.FromPath(path);
\r
551 var lastWriteTime = info.LastWriteTime;
\r
552 var isFolder = (info is DirectoryInfo);
\r
553 var lastLength = isFolder ? 0 : ((FileInfo) info).Length;
\r
555 var state = session.Query<FileState>().SingleOrDefault(s => s.ObjectID == (objectInfo.UUID??"§")) //Handle null UUIDs
\r
556 ?? session.Query<FileState>().SingleOrDefault(s => s.FilePath == path)
\r
557 ?? new FileState();
\r
558 state.FilePath = path;
\r
559 state.IsFolder = isFolder;
\r
560 state.LastWriteDate = lastWriteTime;
\r
561 state.LastLength = lastLength;
\r
562 state.Checksum = objectInfo.X_Object_Hash;
\r
563 state.Hashes = hashes;
\r
564 state.Version = objectInfo.Version.GetValueOrDefault();
\r
565 state.VersionTimeStamp = objectInfo.VersionTimestamp;
\r
566 state.ETag = objectInfo.ETag;
\r
567 state.FileStatus = FileStatus.Unchanged;
\r
568 state.OverlayStatus = FileOverlayStatus.Normal;
\r
569 state.ObjectID = objectInfo.UUID;
\r
570 state.Modified = DateTime.Now;
\r
571 session.SaveOrUpdate(state);
\r
577 Log.ErrorFormat("DebugDB [{0}]:[{1}]\r\n{2}", path, objectInfo.UUID, objectInfo.X_Object_Hash);
\r
580 catch (Exception exc)
\r
582 Log.ErrorFormat("Failed to update [{0}]:[{1}]\r\n{2}",path,objectInfo.UUID, exc);
\r
589 public void SetFileStatus(string path, FileStatus status)
\r
591 if (String.IsNullOrWhiteSpace(path))
\r
592 throw new ArgumentNullException("path");
\r
593 if (!Path.IsPathRooted(path))
\r
594 throw new ArgumentException("The path must be rooted", "path");
\r
595 Contract.EndContractBlock();
\r
597 using (ThreadContext.Stacks["StatusAgent"].Push("UpdateStatusDirect"))
\r
602 using (var session = _factory.OpenSession())
\r
603 using (var tx=session.BeginTransaction(IsolationLevel.ReadCommitted))
\r
606 //var updatecmd = session.CreateSQLQuery(
\r
607 var updatecmd = session.CreateQuery(
\r
608 "update FileState set FileStatus= :fileStatus, Modified=:modified where FilePath = :path ")
\r
609 .SetString("path", path)
\r
610 .SetEnum("fileStatus", status)
\r
611 .SetDateTime("modified",DateTime.Now);
\r
612 var affected = updatecmd.ExecuteUpdate();
\r
616 var createdState = FileState.CreateFor(FileInfoExtensions.FromPath(path), StatusNotification);
\r
617 createdState.FileStatus = status;
\r
618 session.Save(createdState);
\r
624 catch (Exception exc)
\r
626 Log.Error(exc.ToString());
\r
632 public FileStatus GetFileStatus(string path)
\r
634 if (String.IsNullOrWhiteSpace(path))
\r
635 throw new ArgumentNullException("path");
\r
636 if (!Path.IsPathRooted(path))
\r
637 throw new ArgumentException("The path must be rooted", "path");
\r
638 Contract.EndContractBlock();
\r
641 using(var session=_factory.OpenStatelessSession())
\r
642 return (from state in session.Query<FileState>()
\r
643 select state.FileStatus).SingleOrDefault()??FileStatus.Missing;
\r
647 /// Deletes the status of the specified file
\r
649 /// <param name="path"></param>
\r
650 public void ClearFileStatus(string path)
\r
652 if (String.IsNullOrWhiteSpace(path))
\r
653 throw new ArgumentNullException("path");
\r
654 if (!Path.IsPathRooted(path))
\r
655 throw new ArgumentException("The path must be rooted", "path");
\r
656 Contract.EndContractBlock();
\r
657 using(var session=_factory.OpenSession())
\r
659 DeleteDirect(session,path);
\r
665 /// Deletes the status of the specified folder and all its contents
\r
667 /// <param name="path"></param>
\r
668 public void ClearFolderStatus(string path)
\r
670 if (String.IsNullOrWhiteSpace(path))
\r
671 throw new ArgumentNullException("path");
\r
672 if (!Path.IsPathRooted(path))
\r
673 throw new ArgumentException("The path must be rooted", "path");
\r
674 Contract.EndContractBlock();
\r
675 using (ThreadContext.Stacks["StatusAgent"].Push("DeleteDirect"))
\r
680 using (var session = _factory.OpenSession())
\r
682 DeleteDirect(session,path);
\r
686 catch (Exception exc)
\r
688 Log.Error(exc.ToString());
\r
694 public IEnumerable<FileState> GetChildren(FileState fileState)
\r
696 if (fileState == null)
\r
697 throw new ArgumentNullException("fileState");
\r
698 Contract.EndContractBlock();
\r
700 var session = _factory.GetCurrentSession();
\r
701 var children = from state in session.Query<FileState>()
\r
702 where state.FilePath.StartsWith(fileState.FilePath + "\\")
\r
707 public void EnsureFileState(string path)
\r
709 var existingState = GetStateByFilePath(path);
\r
710 if (existingState != null)
\r
712 var fileInfo = FileInfoExtensions.FromPath(path);
\r
714 using (var session=_factory.OpenSession())
\r
716 var newState = FileState.CreateFor(fileInfo,StatusNotification);
\r
717 newState.FileStatus=FileStatus.Missing;
\r
718 session.SaveOrUpdate(newState);
\r
720 //_persistenceAgent.PostAndAwait(newState.CreateAndFlush).Wait();
\r
725 private void DeleteDirect(ISession session,string filePath)
\r
727 using (ThreadContext.Stacks["StatusAgent"].Push("DeleteDirect"))
\r
732 var deletes= session.CreateQuery("delete from FileState where FilePath = :path")
\r
733 .SetParameter("path", filePath)
\r
736 catch (Exception exc)
\r
738 Log.Error(exc.ToString());
\r
747 public void CleanupOrphanStates()
\r
749 //Orphan states are those that do not correspond to an account, ie. their paths
\r
750 //do not start with the root path of any registered account
\r
752 var roots=(from account in Settings.Accounts
\r
753 select account.RootPath).ToList();
\r
755 using (var session = _factory.OpenSession())
\r
757 var allStates = from state in session.Query<FileState>()
\r
758 select state.FilePath;
\r
760 foreach (var statePath in allStates)
\r
762 if (!roots.Any(root => statePath.StartsWith(root, StringComparison.InvariantCultureIgnoreCase)))
\r
763 this.DeleteDirect(session,statePath);
\r
769 public void SaveCopy<T>(T state) where T:class
\r
771 using (var session = _factory.OpenSession())
\r
773 session.Merge(state);
\r
778 public void CleanupStaleStates(AccountInfo accountInfo, List<ObjectInfo> objectInfos)
\r
780 if (accountInfo == null)
\r
781 throw new ArgumentNullException("accountInfo");
\r
782 if (objectInfos == null)
\r
783 throw new ArgumentNullException("objectInfos");
\r
784 Contract.EndContractBlock();
\r
788 //Stale states are those that have no corresponding local or server file
\r
791 var agent=FileAgent.GetFileAgent(accountInfo);
\r
793 var localFiles=agent.EnumerateFiles();
\r
794 var localSet = new HashSet<string>(localFiles);
\r
796 //RelativeUrlToFilePath will fail for
\r
797 //infos of accounts, containers which have no Name
\r
799 var serverFiles = from info in objectInfos
\r
800 where info.Name != null
\r
801 select Path.Combine(accountInfo.AccountPath,info.RelativeUrlToFilePath(accountInfo.UserName));
\r
802 var serverSet = new HashSet<string>(serverFiles);
\r
804 using (var session = _factory.OpenSession())
\r
807 var allStates = from state in session.Query<FileState>()
\r
808 where state.FilePath.StartsWith(agent.RootPath)
\r
809 select state.FilePath;
\r
810 var stateSet = new HashSet<string>(allStates);
\r
811 stateSet.ExceptWith(serverSet);
\r
812 stateSet.ExceptWith(localSet);
\r
814 foreach (var remainder in stateSet)
\r
816 DeleteDirect(session,remainder);
\r
822 public static TreeHash CalculateTreeHash(FileSystemInfo fileInfo, AccountInfo accountInfo, FileState fileState, byte hashingParallelism, CancellationToken cancellationToken, IProgress<HashProgress> progress)
\r
824 fileInfo.Refresh();
\r
825 //If the file doesn't exist, return the empty treehash
\r
826 if (!fileInfo.Exists)
\r
827 return TreeHash.Empty;
\r
829 //FileState may be null if there is no stored state for this file
\r
830 //if (fileState==null)
\r
831 return Signature.CalculateTreeHashAsync(fileInfo,
\r
832 accountInfo.BlockSize,
\r
833 accountInfo.BlockHash,
\r
834 hashingParallelism,
\r
835 cancellationToken, progress);
\r
836 //Can we use the stored hashes?
\r
837 //var localTreeHash = fileState.LastMD5 == Signature.CalculateMD5(fileInfo)
\r
838 // ? TreeHash.Parse(fileState.Hashes)
\r
839 // : Signature.CalculateTreeHashAsync(fileInfo,
\r
840 // accountInfo.BlockSize,
\r
841 // accountInfo.BlockHash,
\r
842 // hashingParallelism,
\r
843 // cancellationToken, progress);
\r
844 //return localTreeHash;
\r
849 private object ExecuteWithRetry(Func<ISession, object, object> call, object state)
\r
852 while (retries > 0)
\r
855 using (var session=_factory.OpenSession())
\r
857 var result=call(session, state);
\r
862 catch (Exception/* ActiveRecordException */)
\r
871 //TODO: Must separate between UpdateChecksum and UpdateFileHashes
\r
873 public void ChangeRootPath(string oldPath, string newPath)
\r
875 if (String.IsNullOrWhiteSpace(oldPath))
\r
876 throw new ArgumentNullException("oldPath");
\r
877 if (!Path.IsPathRooted(oldPath))
\r
878 throw new ArgumentException("oldPath must be an absolute path", "oldPath");
\r
879 if (string.IsNullOrWhiteSpace(newPath))
\r
880 throw new ArgumentNullException("newPath");
\r
881 if (!Path.IsPathRooted(newPath))
\r
882 throw new ArgumentException("newPath must be an absolute path", "newPath");
\r
883 Contract.EndContractBlock();
\r
885 //Ensure the paths end with the same character
\r
886 if (!oldPath.EndsWith("\\"))
\r
887 oldPath = oldPath + "\\";
\r
888 if (!newPath.EndsWith("\\"))
\r
889 newPath = newPath + "\\";
\r
891 ExecuteWithRetry((session, instance) =>
\r
893 const string hqlUpdate =
\r
894 "update FileState set FilePath = replace(FilePath,:oldPath,:newPath), Modified=:modified where FilePath like :oldPath || '%' ";
\r
895 var renames = session.CreateQuery(hqlUpdate)
\r
896 .SetString("oldPath", oldPath)
\r
897 .SetString("newPath", newPath)
\r
898 .SetDateTime("modified",DateTime.Now)
\r
906 /// Mark Unversioned all FileState rows from the database whose path
\r
907 /// starts with one of the removed paths
\r
909 /// <param name="removed"></param>
\r
910 public void UnversionPaths(List<string> removed)
\r
912 if (removed == null)
\r
914 if (removed.Count == 0)
\r
917 //Create a disjunction (list of OR statements
\r
918 var disjunction = new Disjunction();
\r
919 foreach (var path in removed)
\r
921 //with the restriction FileState.FilePath like '@path%'
\r
922 disjunction.Add(Restrictions.On<FileState>(s => s.FilePath)
\r
923 .IsLike(path, MatchMode.Start));
\r
926 //Generate a query from the disjunction
\r
927 var query = QueryOver.Of<FileState>().Where(disjunction);
\r
929 ExecuteWithRetry((session, instance) =>
\r
931 using (var tx = session.BeginTransaction())
\r
933 var states = query.GetExecutableQueryOver(session).List();
\r
934 foreach (var state in states)
\r
936 state.FileStatus = FileStatus.Unversioned;
\r
937 state.OverlayStatus = FileOverlayStatus.Unversioned;
\r
938 session.Update(session);
\r
946 public List<FileState> GetAllStates()
\r
948 using(var session=_factory.OpenSession())
\r
950 return session.Query<FileState>().ToList();
\r
954 public List<string> GetAllStatePaths()
\r
956 using (var session = _factory.OpenSession())
\r
958 return session.Query<FileState>().Select(state => state.FilePath).ToList();
\r
962 public List<FileState> GetConflictStates()
\r
964 using (var session = _factory.OpenSession())
\r
966 var fileStates = from state in session.Query<FileState>()
\r
967 where state.FileStatus == FileStatus.Conflict ||
\r
968 state.OverlayStatus == FileOverlayStatus.Conflict
\r
970 return fileStates.ToList();
\r
974 public void UpdateFileChecksum(string path, string etag, TreeHash treeHash)
\r
976 if (String.IsNullOrWhiteSpace(path))
\r
977 throw new ArgumentNullException("path");
\r
978 if (!Path.IsPathRooted(path))
\r
979 throw new ArgumentException("The path must be rooted", "path");
\r
980 Contract.EndContractBlock();
\r
982 if (string.IsNullOrWhiteSpace(path))
\r
983 throw new ArgumentNullException("absolutePath");
\r
984 Contract.EndContractBlock();
\r
986 var hashes = treeHash.ToJson();
\r
987 var topHash = treeHash.TopHash.ToHashString();
\r
989 ExecuteWithRetry((session, instance) =>
\r
991 const string hqlUpdate = "update FileState set Checksum= :checksum,Hashes=:hashes,ETag=:etag, Modified=:modified where FilePath = :path ";
\r
992 var updatedEntities = session.CreateQuery(hqlUpdate)
\r
993 .SetString("path", path)
\r
994 .SetString("checksum", topHash)
\r
995 .SetString("hashes", hashes)
\r
996 .SetString("etag", etag)
\r
997 .SetDateTime("modified", DateTime.Now)
\r
999 return updatedEntities;
\r
1003 //Store only the hashes
\r
1004 public void UpdateFileHashes(string absolutePath, TreeHash treeHash)
\r
1006 if (string.IsNullOrWhiteSpace(absolutePath))
\r
1007 throw new ArgumentNullException("absolutePath");
\r
1008 Contract.EndContractBlock();
\r
1010 var hashes = treeHash.ToJson();
\r
1011 var topHash = treeHash.TopHash.ToHashString();
\r
1013 ExecuteWithRetry((session, instance) =>
\r
1016 const string hqlUpdate = "update FileState set Hashes=:hashes,Modified=:modified where FilePath = :path ";
\r
1018 const string hqlUpdate = "update FileState set Checksum= :checksum,Hashes=:hashes where FilePath = :path ";
\r
1020 var updatedEntities = session.CreateQuery(hqlUpdate)
\r
1021 .SetString("path", absolutePath)
\r
1022 //.SetString("checksum", topHash)
\r
1023 // .SetString("md5",treeHash.MD5)
\r
1024 .SetString("hashes", hashes)
\r
1025 .SetDateTime("modified", DateTime.Now)
\r
1027 return updatedEntities;
\r
1032 public void RenameState(string oldPath, string newPath)
\r
1034 if (string.IsNullOrWhiteSpace(oldPath))
\r
1035 throw new ArgumentNullException("oldPath");
\r
1036 Contract.EndContractBlock();
\r
1038 ExecuteWithRetry((session, instance) =>
\r
1040 const string hqlUpdate =
\r
1041 "update FileState set FilePath= :newPath, Modified=:modified where FilePath = :oldPath ";
\r
1042 var updatedEntities = session.CreateQuery(hqlUpdate)
\r
1043 .SetString("oldPath", oldPath)
\r
1044 .SetString("newPath", newPath)
\r
1045 .SetDateTime("modified", DateTime.Now)
\r
1047 return updatedEntities;
\r
1052 public void StoreOverlayStatus(string absolutePath, FileOverlayStatus newStatus)
\r
1054 if (string.IsNullOrWhiteSpace(absolutePath))
\r
1055 throw new ArgumentNullException("absolutePath");
\r
1056 Contract.EndContractBlock();
\r
1058 using(var session=_factory.OpenSession())
\r
1060 using (var tx = session.BeginTransaction())
\r
1062 var state = session.Query<FileState>().SingleOrDefault(s => s.FilePath == absolutePath)
\r
1064 FilePath = absolutePath,
\r
1065 OverlayStatus = newStatus,
\r
1066 ETag = Signature.MERKLE_EMPTY,
\r
1067 //LastMD5=String.Empty,
\r
1068 IsFolder = Directory.Exists(absolutePath),
\r
1069 Modified=DateTime.Now
\r
1071 state.OverlayStatus = newStatus;
\r
1072 session.SaveOrUpdate(state);
\r