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
53 using System.Threading.Tasks;
\r
55 using NHibernate.Cfg;
\r
56 using NHibernate.Cfg.MappingSchema;
\r
57 using NHibernate.Criterion;
\r
58 using NHibernate.Dialect;
\r
59 using NHibernate.Linq;
\r
60 using NHibernate.Mapping.ByCode;
\r
61 using NHibernate.Tool.hbm2ddl;
\r
62 using Pithos.Interfaces;
\r
63 using Pithos.Network;
\r
65 using Environment = System.Environment;
\r
67 namespace Pithos.Core.Agents
\r
69 [Export(typeof(IStatusChecker)),Export(typeof(IStatusKeeper))]
\r
70 public class StatusAgent:IStatusChecker,IStatusKeeper
\r
72 private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
\r
74 [System.ComponentModel.Composition.Import]
\r
75 public IPithosSettings Settings { get; set; }
\r
77 [System.ComponentModel.Composition.Import]
\r
78 public IStatusNotification StatusNotification { get; set; }
\r
80 //private Agent<Action> _persistenceAgent;
\r
84 public StatusAgent()
\r
86 var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
\r
88 _pithosDataPath = Path.Combine(appDataPath, "GRNET\\PITHOS");
\r
89 if (!Directory.Exists(_pithosDataPath))
\r
90 Directory.CreateDirectory(_pithosDataPath);
\r
92 var dbPath = Path.Combine(_pithosDataPath, "pithos.sdf");
\r
94 //MigrateOldDb(dbPath, appDataPath);
\r
97 var cfg = Configure(dbPath);
\r
99 var connectionString = "Data Source=" + dbPath;
\r
100 using (var sqlCeEngine = new SqlCeEngine(connectionString))
\r
102 if (!File.Exists(dbPath))
\r
104 sqlCeEngine.CreateDatabase();
\r
105 new SchemaExport(cfg).Execute(true, true, false);
\r
106 _factory = cfg.BuildSessionFactory();
\r
107 using (var session = _factory.OpenStatelessSession())
\r
109 session.Insert(new PithosVersion {Id = 1, Version = "0.0.0.0"});
\r
116 if (!sqlCeEngine.Verify(VerifyOption.Enhanced))
\r
117 sqlCeEngine.Repair(connectionString, RepairOption.RecoverAllOrFail);
\r
119 catch(SqlCeException ex)
\r
121 //Rethrow except for sharing errors while repairing
\r
122 if (ex.NativeError != 25035)
\r
127 var update = new SchemaUpdate(cfg);
\r
128 update.Execute(script=>Log.WarnFormat("[DBUPDATE] : {0}",script),true);
\r
129 _factory = cfg.BuildSessionFactory();
\r
135 private void UpgradeDatabase()
\r
137 using (var session = _factory.OpenSession())
\r
140 var storedVersion = session.Get<PithosVersion>(1);
\r
141 var actualVersion = Assembly.GetEntryAssembly().GetName().Version;
\r
143 if (actualVersion == new Version(storedVersion.Version))
\r
146 storedVersion.Version = actualVersion.ToString();
\r
147 session.Update(storedVersion);
\r
153 private static Configuration Configure(string pithosDbPath)
\r
155 if (String.IsNullOrWhiteSpace(pithosDbPath))
\r
156 throw new ArgumentNullException("pithosDbPath");
\r
157 if (!Path.IsPathRooted(pithosDbPath))
\r
158 throw new ArgumentException("path must be a rooted path", "pithosDbPath");
\r
159 Contract.EndContractBlock();
\r
162 var cfg = new Configuration();
\r
163 cfg.DataBaseIntegration(db=>
\r
165 db.Dialect<MsSqlCe40Dialect>();
\r
166 db.ConnectionString = "Data Source=" + pithosDbPath;
\r
167 db.AutoCommentSql = true;
\r
168 db.KeywordsAutoImport = Hbm2DDLKeyWords.AutoQuote;
\r
169 db.SchemaAction = SchemaAutoAction.Update;
\r
170 db.LogSqlInConsole = false;
\r
173 .GenerateStatistics()
\r
176 var mapping = GetMapping();
\r
177 cfg.AddMapping(mapping);
\r
182 private static HbmMapping GetMapping()
\r
184 var mapper = new ModelMapper();
\r
185 mapper.Class<FileState>(fm =>
\r
187 fm.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
\r
188 fm.Property(x => x.ObjectID, m =>
\r
190 m.Index("IX_FileState_ObjectID");
\r
192 fm.Property(x => x.FilePath, m =>
\r
195 m.UniqueKey("U_FileState_FilePath");
\r
196 m.Index("IX_FileState_FilePath");
\r
199 fm.Property(x => x.OverlayStatus);
\r
200 fm.Property(x => x.FileStatus);
\r
201 fm.Property(x => x.ConflictReason);
\r
202 fm.Property(x => x.Checksum, m => m.Length(64));
\r
203 fm.Property(x => x.ETag, m => m.Length(64));
\r
204 fm.Property(x => x.Hashes, m => m.Type(NHibernateUtil.StringClob));
\r
205 fm.Property(x => x.LastWriteDate);
\r
206 fm.Property(x => x.LastLength);
\r
207 fm.Property(x => x.Version);
\r
208 fm.Property(x => x.VersionTimeStamp);
\r
209 fm.Property(x => x.IsShared);
\r
210 fm.Property(x => x.SharedBy);
\r
211 fm.Property(x => x.ShareWrite);
\r
212 fm.Property(x => x.IsFolder);
\r
213 fm.Property(x => x.Modified);
\r
215 mapper.Class<PithosVersion>(fm =>
\r
217 fm.Id(x => x.Id, m => m.Generator(Generators.Assigned));
\r
218 fm.Property(x => x.Version, m => m.Length(20));
\r
222 var mapping = mapper.CompileMappingFor(new[] {typeof (FileState),typeof(PithosVersion)});
\r
226 public void StartProcessing(CancellationToken token)
\r
240 public void ProcessExistingFiles(IEnumerable<FileInfo> existingFiles)
\r
242 if (existingFiles == null)
\r
243 throw new ArgumentNullException("existingFiles");
\r
244 Contract.EndContractBlock();
\r
246 //Find new or matching files with a left join to the stored states
\r
247 using (var session = _factory.OpenSession())
\r
250 var fileStates = session.Query<FileState>().ToList();
\r
251 var currentFiles = from file in existingFiles
\r
252 join state in fileStates on file.FullName.ToLower() equals state.FilePath.ToLower()
\r
255 from substate in gs.DefaultIfEmpty()
\r
256 select Tuple.Create(file, substate);
\r
258 //To get the deleted files we must get the states that have no corresponding
\r
260 //We can't use the File.Exists method inside a query, so we get all file paths from the states
\r
261 var statePaths = (from state in fileStates
\r
262 select new {state.Id, state.FilePath}).ToList();
\r
263 //and check each one
\r
264 var missingStates = (from path in statePaths
\r
265 where !File.Exists(path.FilePath) && !Directory.Exists(path.FilePath)
\r
266 select path.Id).ToList();
\r
267 //Finally, retrieve the states that correspond to the deleted files
\r
268 var deletedFiles = from state in fileStates
\r
269 where missingStates.Contains(state.Id)
\r
270 select Tuple.Create(default(FileInfo), state);
\r
272 var pairs = currentFiles.Union(deletedFiles).ToList();
\r
275 var total = pairs.Count;
\r
276 foreach (var pair in pairs)
\r
278 ProcessFile(session,total, pair);
\r
286 private void ProcessFile(ISession session,int total, System.Tuple<FileInfo, FileState> pair)
\r
288 var idx = Interlocked.Increment(ref i);
\r
289 using (StatusNotification.GetNotifier("Indexing file {0} of {1}", "Indexed file {0} of {1} ", true,idx, total))
\r
291 var fileState = pair.Item2;
\r
292 var file = pair.Item1;
\r
293 if (fileState == null)
\r
295 //This is a new file
\r
296 var createState = FileState.CreateFor(file,StatusNotification);
\r
297 session.Save(createState);
\r
298 //_persistenceAgent.Post(createState.Create);
\r
300 else if (file == null)
\r
302 //This file was deleted while we were down. We should mark it as deleted
\r
303 //We have to go through UpdateStatus here because the state object we are using
\r
304 //was created by a different ORM session.
\r
305 UpdateStatusDirect(session,fileState.Id, FileStatus.Deleted);
\r
306 //_persistenceAgent.Post(() => UpdateStatusDirect((Guid) fileState.Id, FileStatus.Deleted));
\r
310 // //This file has a matching state. Need to check for possible changes
\r
311 // //To check for changes, we use the cheap (in CPU terms) MD5 algorithm
\r
312 // //on the entire file.
\r
314 // var hashString = file.ComputeShortHash(StatusNotification);
\r
315 // Debug.Assert(hashString.Length==32);
\r
318 // //TODO: Need a way to attach the hashes to the filestate so we don't
\r
319 // //recalculate them each time a call to calculate has is made
\r
320 // //We can either store them to the filestate or add them to a
\r
323 // //If the hashes don't match the file was changed
\r
324 // if (fileState.ETag != hashString)
\r
326 // _persistenceAgent.Post(() => UpdateStatusDirect((Guid) fileState.Id, FileStatus.Modified));
\r
333 private int UpdateStatusDirect(ISession session,Guid id, FileStatus status)
\r
335 using (ThreadContext.Stacks["StatusAgent"].Push("UpdateStatusDirect"))
\r
340 //var updatecmd = session.CreateSQLQuery(
\r
341 var updatecmd = session.CreateQuery(
\r
342 "update FileState set FileStatus= :fileStatus, Modified=:modified where Id = :id ")
\r
344 .SetEnum("fileStatus", status)
\r
345 .SetDateTime("modified",DateTime.Now);
\r
346 var affected = updatecmd.ExecuteUpdate();
\r
350 catch (Exception exc)
\r
352 Log.Error(exc.ToString());
\r
359 public string BlockHash { get; set; }
\r
361 public int BlockSize { get; set; }
\r
363 public void ChangeRoots(string oldPath, string newPath)
\r
365 if (String.IsNullOrWhiteSpace(oldPath))
\r
366 throw new ArgumentNullException("oldPath");
\r
367 if (!Path.IsPathRooted(oldPath))
\r
368 throw new ArgumentException("oldPath must be an absolute path", "oldPath");
\r
369 if (String.IsNullOrWhiteSpace(newPath))
\r
370 throw new ArgumentNullException("newPath");
\r
371 if (!Path.IsPathRooted(newPath))
\r
372 throw new ArgumentException("newPath must be an absolute path", "newPath");
\r
373 Contract.EndContractBlock();
\r
375 ChangeRootPath(oldPath,newPath);
\r
381 private readonly string _pithosDataPath;
\r
382 private readonly ISessionFactory _factory;
\r
384 public FileState GetStateByFilePath(string path)
\r
386 if (String.IsNullOrWhiteSpace(path))
\r
387 throw new ArgumentNullException("path");
\r
388 if (!Path.IsPathRooted(path))
\r
389 throw new ArgumentException("The path must be rooted", "path");
\r
390 Contract.EndContractBlock();
\r
395 using(var session=_factory.OpenStatelessSession())
\r
397 var state=session.Query<FileState>().SingleOrDefault(s => s.FilePath == path);
\r
400 state.FilePath=state.FilePath??String.Empty;
\r
401 state.OverlayStatus = state.OverlayStatus ??FileOverlayStatus.Unversioned;
\r
402 state.FileStatus = state.FileStatus ?? FileStatus.Missing;
\r
403 state.Checksum = state.Checksum ?? String.Empty;
\r
404 state.ETag = state.ETag ?? String.Empty;
\r
405 state.SharedBy = state.SharedBy ?? String.Empty;
\r
410 catch (Exception exc)
\r
412 Log.ErrorFormat(exc.ToString());
\r
417 public FileOverlayStatus GetFileOverlayStatus(string path)
\r
419 if (String.IsNullOrWhiteSpace(path))
\r
420 throw new ArgumentNullException("path");
\r
421 if (!Path.IsPathRooted(path))
\r
422 throw new ArgumentException("The path must be rooted", "path");
\r
423 Contract.EndContractBlock();
\r
428 using(var session=_factory.OpenStatelessSession())
\r
430 return (from state in session.Query<FileState>()
\r
431 where state.FilePath == path
\r
432 select state.OverlayStatus)
\r
434 .GetValueOrDefault(FileOverlayStatus.Unversioned);
\r
437 catch (Exception exc)
\r
439 Log.ErrorFormat(exc.ToString());
\r
440 return FileOverlayStatus.Unversioned;
\r
444 public void SetFileOverlayStatus(string path, FileOverlayStatus overlayStatus)
\r
446 if (String.IsNullOrWhiteSpace(path))
\r
447 throw new ArgumentNullException("path");
\r
448 if (!Path.IsPathRooted(path))
\r
449 throw new ArgumentException("The path must be rooted","path");
\r
450 Contract.EndContractBlock();
\r
452 StoreOverlayStatus(path,overlayStatus);
\r
455 public void SetFileState(string path, FileStatus fileStatus, FileOverlayStatus overlayStatus, string conflictReason)
\r
457 if (String.IsNullOrWhiteSpace(path))
\r
458 throw new ArgumentNullException("path");
\r
459 if (!Path.IsPathRooted(path))
\r
460 throw new ArgumentException("The path must be rooted", "path");
\r
461 Contract.EndContractBlock();
\r
463 Debug.Assert(!path.Contains(FolderConstants.CacheFolder));
\r
464 Debug.Assert(!path.EndsWith(".ignore"));
\r
465 using (ThreadContext.Stacks["StatusAgent"].Push("SetFileState"))
\r
471 using (var session = _factory.OpenSession())
\r
472 using (var tx=session.BeginTransaction(IsolationLevel.ReadCommitted))
\r
474 var state = session.Query<FileState>().FirstOrDefault(s => s.FilePath == path)
\r
476 ((path.Length<=260)
\r
477 ?FileState.CreateFor(FileInfoExtensions.FromPath(path), StatusNotification)
\r
480 OverlayStatus = FileOverlayStatus.Conflict,
\r
481 FileStatus = Pithos.Core.FileStatus.Conflict,
\r
484 LastWriteDate = DateTime.Today,
\r
486 IsFolder=Directory.Exists(path),
\r
487 Modified=DateTime.Now
\r
489 state.FileStatus = fileStatus;
\r
490 state.OverlayStatus = overlayStatus;
\r
491 state.ConflictReason = conflictReason;
\r
492 state.Modified = DateTime.Now;
\r
493 session.SaveOrUpdate(state);
\r
495 //var updatecmd = session.CreateSQLQuery("update FileState set OverlayStatus= :overlayStatus, FileStatus= :fileStatus,ConflictReason= :conflictReason where FilePath = :path ")
\r
496 var updatecmd = session.CreateQuery("update FileState set OverlayStatus= :overlayStatus, FileStatus= :fileStatus,ConflictReason= :conflictReason, Modified=:modified where FilePath = :path")
\r
497 .SetString("path", path)
\r
498 .SetEnum("fileStatus", fileStatus)
\r
499 .SetEnum("overlayStatus", overlayStatus)
\r
500 .SetString("conflictReason", conflictReason)
\r
501 .SetDateTime("modified",DateTime.Now);
\r
502 var affected = updatecmd.ExecuteUpdate();
\r
506 //Can happen when downloading a new file
\r
507 var createdState = FileState.CreateFor(FileInfoExtensions.FromPath(path), StatusNotification);
\r
508 createdState.FileStatus = fileStatus;
\r
509 createdState.OverlayStatus = overlayStatus;
\r
510 createdState.ConflictReason = conflictReason;
\r
511 session.Save(createdState);
\r
512 //createdState.Create();
\r
518 catch (Exception exc)
\r
520 Log.Error(exc.ToString());
\r
527 public void StoreInfo(string path, ObjectInfo objectInfo, TreeHash treeHash)
\r
529 if (String.IsNullOrWhiteSpace(path))
\r
530 throw new ArgumentNullException("path");
\r
531 if (treeHash==null)
\r
532 throw new ArgumentNullException("treeHash");
\r
533 if (!Path.IsPathRooted(path))
\r
534 throw new ArgumentException("The path must be rooted", "path");
\r
535 if (objectInfo == null)
\r
536 throw new ArgumentNullException("objectInfo", "objectInfo can't be empty");
\r
537 Contract.EndContractBlock();
\r
539 StoreInfoDirect(path, objectInfo, treeHash);
\r
543 public void StoreInfo(string path, ObjectInfo objectInfo)
\r
545 if (String.IsNullOrWhiteSpace(path))
\r
546 throw new ArgumentNullException("path");
\r
547 if (!Path.IsPathRooted(path))
\r
548 throw new ArgumentException("The path must be rooted", "path");
\r
549 if (objectInfo == null)
\r
550 throw new ArgumentNullException("objectInfo", "objectInfo can't be empty");
\r
551 Contract.EndContractBlock();
\r
553 StoreInfoDirect(path, objectInfo, null);
\r
557 private void StoreInfoDirect(string path, ObjectInfo objectInfo,TreeHash treeHash)
\r
561 using (var session = _factory.OpenSession())
\r
562 using (var tx=session.BeginTransaction(IsolationLevel.ReadCommitted))
\r
564 //An entry for the new path may exist,
\r
565 IQuery deletecmd = session.CreateQuery(
\r
566 "delete from FileState where FilePath=:path and ObjectID is null")
\r
567 .SetString("path", path);
\r
568 deletecmd.ExecuteUpdate();
\r
570 //string md5=treeHash.NullSafe(t=>t.MD5);
\r
571 string hashes = treeHash.NullSafe(t => t.ToJson());
\r
573 var info = FileInfoExtensions.FromPath(path);
\r
574 var lastWriteTime = info.LastWriteTime;
\r
575 var isFolder = (info is DirectoryInfo);
\r
576 var lastLength = isFolder ? 0 : ((FileInfo) info).Length;
\r
578 var state = session.Query<FileState>().SingleOrDefault(s => s.ObjectID == (objectInfo.UUID??"ยง")) //Handle null UUIDs
\r
579 ?? session.Query<FileState>().SingleOrDefault(s => s.FilePath == path)
\r
580 ?? new FileState();
\r
581 state.FilePath = path;
\r
582 state.IsFolder = isFolder;
\r
583 state.LastWriteDate = lastWriteTime;
\r
584 state.LastLength = lastLength;
\r
585 state.Checksum = objectInfo.X_Object_Hash;
\r
586 state.Hashes = hashes;
\r
587 state.Version = objectInfo.Version.GetValueOrDefault();
\r
588 state.VersionTimeStamp = objectInfo.VersionTimestamp;
\r
589 state.ETag = objectInfo.ETag;
\r
590 state.FileStatus = FileStatus.Unchanged;
\r
591 state.OverlayStatus = FileOverlayStatus.Normal;
\r
592 state.ObjectID = objectInfo.UUID;
\r
593 state.Modified = DateTime.Now;
\r
594 session.SaveOrUpdate(state);
\r
600 if (Log.IsDebugEnabled)
\r
601 Log.DebugFormat("DebugDB [{0}]:[{1}]\r\n{2}", path, objectInfo.UUID, objectInfo.X_Object_Hash);
\r
604 catch (Exception exc)
\r
606 Log.ErrorFormat("Failed to update [{0}]:[{1}]\r\n{2}",path,objectInfo.UUID, exc);
\r
613 public void SetFileStatus(string path, FileStatus status)
\r
615 if (String.IsNullOrWhiteSpace(path))
\r
616 throw new ArgumentNullException("path");
\r
617 if (!Path.IsPathRooted(path))
\r
618 throw new ArgumentException("The path must be rooted", "path");
\r
619 Contract.EndContractBlock();
\r
621 using (ThreadContext.Stacks["StatusAgent"].Push("SetFileStatus"))
\r
626 using (var session = _factory.OpenSession())
\r
627 using (var tx=session.BeginTransaction(IsolationLevel.ReadCommitted))
\r
630 //var updatecmd = session.CreateSQLQuery(
\r
631 var updatecmd = session.CreateQuery(
\r
632 "update FileState set FileStatus= :fileStatus, Modified=:modified where FilePath = :path ")
\r
633 .SetString("path", path)
\r
634 .SetEnum("fileStatus", status)
\r
635 .SetDateTime("modified",DateTime.Now);
\r
636 var affected = updatecmd.ExecuteUpdate();
\r
640 var createdState = FileState.CreateFor(FileInfoExtensions.FromPath(path), StatusNotification);
\r
641 createdState.FileStatus = status;
\r
642 session.Save(createdState);
\r
648 catch (Exception exc)
\r
650 Log.Error(exc.ToString());
\r
656 public FileStatus GetFileStatus(string path)
\r
658 if (String.IsNullOrWhiteSpace(path))
\r
659 throw new ArgumentNullException("path");
\r
660 if (!Path.IsPathRooted(path))
\r
661 throw new ArgumentException("The path must be rooted", "path");
\r
662 Contract.EndContractBlock();
\r
665 using(var session=_factory.OpenStatelessSession())
\r
666 return (from state in session.Query<FileState>()
\r
667 select state.FileStatus).SingleOrDefault()??FileStatus.Missing;
\r
671 /// Deletes the status of the specified file
\r
673 /// <param name="path"></param>
\r
674 public void ClearFileStatus(string path)
\r
676 if (String.IsNullOrWhiteSpace(path))
\r
677 throw new ArgumentNullException("path");
\r
678 if (!Path.IsPathRooted(path))
\r
679 throw new ArgumentException("The path must be rooted", "path");
\r
680 Contract.EndContractBlock();
\r
681 using(var session=_factory.OpenSession())
\r
683 DeleteDirect(session,path);
\r
689 /// Deletes the status of the specified folder and all its contents
\r
691 /// <param name="path"></param>
\r
692 public void ClearFolderStatus(string path)
\r
694 if (String.IsNullOrWhiteSpace(path))
\r
695 throw new ArgumentNullException("path");
\r
696 if (!Path.IsPathRooted(path))
\r
697 throw new ArgumentException("The path must be rooted", "path");
\r
698 Contract.EndContractBlock();
\r
699 using (ThreadContext.Stacks["StatusAgent"].Push("DeleteDirect"))
\r
704 using (var session = _factory.OpenSession())
\r
706 DeleteDirect(session,path);
\r
710 catch (Exception exc)
\r
712 Log.Error(exc.ToString());
\r
718 public IEnumerable<FileState> GetChildren(FileState fileState)
\r
720 if (fileState == null)
\r
721 throw new ArgumentNullException("fileState");
\r
722 Contract.EndContractBlock();
\r
724 var session = _factory.GetCurrentSession();
\r
725 var children = from state in session.Query<FileState>()
\r
726 where state.FilePath.StartsWith(fileState.FilePath + "\\")
\r
731 public void EnsureFileState(string path)
\r
733 var existingState = GetStateByFilePath(path);
\r
734 if (existingState != null)
\r
736 var fileInfo = FileInfoExtensions.FromPath(path);
\r
738 using (var session=_factory.OpenSession())
\r
740 var newState = FileState.CreateFor(fileInfo,StatusNotification);
\r
741 newState.FileStatus=FileStatus.Missing;
\r
742 session.SaveOrUpdate(newState);
\r
744 //_persistenceAgent.PostAndAwait(newState.CreateAndFlush).Wait();
\r
749 private void DeleteDirect(ISession session,string filePath)
\r
751 using (ThreadContext.Stacks["StatusAgent"].Push("DeleteDirect"))
\r
756 var deletes= session.CreateQuery("delete from FileState where FilePath = :path")
\r
757 .SetParameter("path", filePath)
\r
760 catch (Exception exc)
\r
762 Log.Error(exc.ToString());
\r
771 public void CleanupOrphanStates()
\r
773 //Orphan states are those that do not correspond to an account, ie. their paths
\r
774 //do not start with the root path of any registered account
\r
776 var roots=(from account in Settings.Accounts
\r
777 select account.RootPath).ToList();
\r
779 using (var session = _factory.OpenSession())
\r
781 var allStates = from state in session.Query<FileState>()
\r
782 select state.FilePath;
\r
784 foreach (var statePath in allStates)
\r
786 if (!roots.Any(root => statePath.StartsWith(root, StringComparison.InvariantCultureIgnoreCase)))
\r
787 this.DeleteDirect(session,statePath);
\r
793 public void SaveCopy<T>(T state) where T:class
\r
795 using (var session = _factory.OpenSession())
\r
797 session.Merge(state);
\r
802 public void CleanupStaleStates(AccountInfo accountInfo, List<ObjectInfo> objectInfos)
\r
804 if (accountInfo == null)
\r
805 throw new ArgumentNullException("accountInfo");
\r
806 if (objectInfos == null)
\r
807 throw new ArgumentNullException("objectInfos");
\r
808 Contract.EndContractBlock();
\r
812 //Stale states are those that have no corresponding local or server file
\r
815 var agent=FileAgent.GetFileAgent(accountInfo);
\r
817 var localFiles=agent.EnumerateFiles();
\r
818 var localSet = new HashSet<string>(localFiles);
\r
820 //RelativeUrlToFilePath will fail for
\r
821 //infos of accounts, containers which have no Name
\r
823 var serverFiles = from info in objectInfos
\r
824 where info.Name != null
\r
825 select Path.Combine(accountInfo.AccountPath,info.RelativeUrlToFilePath(accountInfo.UserName));
\r
826 var serverSet = new HashSet<string>(serverFiles);
\r
828 using (var session = _factory.OpenSession())
\r
831 var allStates = from state in session.Query<FileState>()
\r
832 where state.FilePath.StartsWith(agent.RootPath)
\r
833 select state.FilePath;
\r
834 var stateSet = new HashSet<string>(allStates);
\r
835 stateSet.ExceptWith(serverSet);
\r
836 stateSet.ExceptWith(localSet);
\r
838 foreach (var remainder in stateSet)
\r
840 DeleteDirect(session,remainder);
\r
846 public static TreeHash CalculateTreeHash(FileSystemInfo fileInfo, AccountInfo accountInfo, FileState fileState, byte hashingParallelism, CancellationToken cancellationToken, IProgress<HashProgress> progress)
\r
848 fileInfo.Refresh();
\r
849 //If the file doesn't exist, return the empty treehash
\r
850 if (!fileInfo.Exists)
\r
851 return TreeHash.Empty;
\r
853 //FileState may be null if there is no stored state for this file
\r
854 //if (fileState==null)
\r
855 return TaskEx.Run(async () =>await Signature.CalculateTreeHashAsync(fileInfo,
\r
856 accountInfo.BlockSize,
\r
857 accountInfo.BlockHash,
\r
858 hashingParallelism,
\r
859 cancellationToken, progress).ConfigureAwait(false)).Result;
\r
860 //Can we use the stored hashes?
\r
861 //var localTreeHash = fileState.LastMD5 == Signature.CalculateMD5(fileInfo)
\r
862 // ? TreeHash.Parse(fileState.Hashes)
\r
863 // : Signature.CalculateTreeHashAsync(fileInfo,
\r
864 // accountInfo.BlockSize,
\r
865 // accountInfo.BlockHash,
\r
866 // hashingParallelism,
\r
867 // cancellationToken, progress);
\r
868 //return localTreeHash;
\r
873 private object ExecuteWithRetry(Func<ISession, object, object> call, object state)
\r
876 while (retries > 0)
\r
879 using (var session=_factory.OpenSession())
\r
881 var result=call(session, state);
\r
886 catch (Exception/* ActiveRecordException */)
\r
895 //TODO: Must separate between UpdateChecksum and UpdateFileHashes
\r
897 public void ChangeRootPath(string oldPath, string newPath)
\r
899 if (String.IsNullOrWhiteSpace(oldPath))
\r
900 throw new ArgumentNullException("oldPath");
\r
901 if (!Path.IsPathRooted(oldPath))
\r
902 throw new ArgumentException("oldPath must be an absolute path", "oldPath");
\r
903 if (string.IsNullOrWhiteSpace(newPath))
\r
904 throw new ArgumentNullException("newPath");
\r
905 if (!Path.IsPathRooted(newPath))
\r
906 throw new ArgumentException("newPath must be an absolute path", "newPath");
\r
907 Contract.EndContractBlock();
\r
909 //Ensure the paths end with the same character
\r
910 if (!oldPath.EndsWith("\\"))
\r
911 oldPath = oldPath + "\\";
\r
912 if (!newPath.EndsWith("\\"))
\r
913 newPath = newPath + "\\";
\r
915 ExecuteWithRetry((session, instance) =>
\r
917 const string hqlUpdate =
\r
918 "update FileState set FilePath = replace(FilePath,:oldPath,:newPath), Modified=:modified where FilePath like :oldPath || '%' ";
\r
919 var renames = session.CreateQuery(hqlUpdate)
\r
920 .SetString("oldPath", oldPath)
\r
921 .SetString("newPath", newPath)
\r
922 .SetDateTime("modified",DateTime.Now)
\r
930 /// Mark Unversioned all FileState rows from the database whose path
\r
931 /// starts with one of the removed paths
\r
933 /// <param name="removed"></param>
\r
934 public void UnversionPaths(List<string> removed)
\r
936 if (removed == null)
\r
938 if (removed.Count == 0)
\r
941 //Create a disjunction (list of OR statements
\r
942 var disjunction = new Disjunction();
\r
943 foreach (var path in removed)
\r
945 //with the restriction FileState.FilePath like '@path%'
\r
946 disjunction.Add(Restrictions.On<FileState>(s => s.FilePath)
\r
947 .IsLike(path, MatchMode.Start));
\r
950 //Generate a query from the disjunction
\r
951 var query = QueryOver.Of<FileState>().Where(disjunction);
\r
953 using (var session = _factory.OpenSession())
\r
954 using (var tx = session.BeginTransaction())
\r
957 var states = query.GetExecutableQueryOver(session).List();
\r
958 foreach (var state in states)
\r
960 state.FileStatus = FileStatus.Unversioned;
\r
961 state.OverlayStatus = FileOverlayStatus.Unversioned;
\r
962 session.Update(state);
\r
971 public List<FileState> GetAllStates()
\r
973 using(var session=_factory.OpenSession())
\r
975 return session.Query<FileState>().ToList();
\r
979 public List<string> GetAllStatePaths()
\r
981 using (var session = _factory.OpenSession())
\r
983 return session.Query<FileState>().Select(state => state.FilePath).ToList();
\r
987 public List<FileState> GetConflictStates()
\r
989 using (var session = _factory.OpenSession())
\r
991 var fileStates = from state in session.Query<FileState>()
\r
992 where state.FileStatus == FileStatus.Conflict ||
\r
993 state.OverlayStatus == FileOverlayStatus.Conflict
\r
995 return fileStates.ToList();
\r
999 public void MoveFileState(string oldFullPath, string newFullPath, ObjectInfo objectInfo, TreeHash treeHash)
\r
1001 if (String.IsNullOrWhiteSpace(oldFullPath))
\r
1002 throw new ArgumentNullException("oldFullPath");
\r
1003 if (!Path.IsPathRooted(oldFullPath))
\r
1004 throw new ArgumentException("The path must be rooted", "oldFullPath");
\r
1005 if (String.IsNullOrWhiteSpace(newFullPath))
\r
1006 throw new ArgumentNullException("newFullPath");
\r
1007 if (!Path.IsPathRooted(newFullPath))
\r
1008 throw new ArgumentException("The path must be rooted", "newFullPath");
\r
1009 if (treeHash == null)
\r
1010 throw new ArgumentNullException("treeHash");
\r
1011 if (objectInfo == null)
\r
1012 throw new ArgumentNullException("objectInfo", "objectInfo can't be empty");
\r
1013 Contract.EndContractBlock();
\r
1018 using (var session = _factory.OpenSession())
\r
1019 using (var tx=session.BeginTransaction(IsolationLevel.ReadCommitted))
\r
1021 //An entry for the new path may exist,
\r
1022 IQuery deletecmd = session.CreateQuery(
\r
1023 "delete from FileState where FilePath=:path and ObjectID is null")
\r
1024 .SetString("path", newFullPath);
\r
1025 deletecmd.ExecuteUpdate();
\r
1027 //string md5=treeHash.NullSafe(t=>t.MD5);
\r
1028 string hashes = treeHash.NullSafe(t => t.ToJson());
\r
1030 var info = FileInfoExtensions.FromPath(newFullPath);
\r
1031 var lastWriteTime = info.LastWriteTime;
\r
1032 var isFolder = (info is DirectoryInfo);
\r
1033 var lastLength = isFolder ? 0 : ((FileInfo) info).Length;
\r
1035 var state = session.Query<FileState>().SingleOrDefault(s => s.ObjectID == (objectInfo.UUID??"ยง")) //Handle null UUIDs
\r
1036 ?? session.Query<FileState>().SingleOrDefault(s => s.FilePath == newFullPath)
\r
1037 ?? new FileState();
\r
1038 state.FilePath = newFullPath;
\r
1039 state.IsFolder = isFolder;
\r
1040 state.LastWriteDate = lastWriteTime;
\r
1041 state.LastLength = lastLength;
\r
1042 state.Checksum = objectInfo.X_Object_Hash;
\r
1043 state.Hashes = hashes;
\r
1044 state.Version = objectInfo.Version.GetValueOrDefault();
\r
1045 state.VersionTimeStamp = objectInfo.VersionTimestamp;
\r
1046 state.ETag = objectInfo.ETag;
\r
1047 state.FileStatus = FileStatus.Unchanged;
\r
1048 state.OverlayStatus = FileOverlayStatus.Normal;
\r
1049 state.ObjectID = objectInfo.UUID;
\r
1050 state.Modified = DateTime.Now;
\r
1051 session.SaveOrUpdate(state);
\r
1054 //Delete the old path entry if it still exists (eg. for folders)
\r
1055 session.CreateQuery("delete from FileState where FilePath = :path")
\r
1056 .SetParameter("path", oldFullPath)
\r
1062 if (Log.IsDebugEnabled)
\r
1063 Log.DebugFormat("DebugDB [{0}]:[{1}]\r\n{2}", newFullPath, objectInfo.UUID, objectInfo.X_Object_Hash);
\r
1066 catch (Exception exc)
\r
1068 Log.ErrorFormat("Failed to update [{0}]:[{1}]\r\n{2}",newFullPath,objectInfo.UUID, exc);
\r
1073 public void UpdateFileChecksum(string path, string etag, TreeHash treeHash)
\r
1075 if (String.IsNullOrWhiteSpace(path))
\r
1076 throw new ArgumentNullException("path");
\r
1077 if (!Path.IsPathRooted(path))
\r
1078 throw new ArgumentException("The path must be rooted", "path");
\r
1081 var hashes = treeHash.ToJson();
\r
1082 var topHash = treeHash.TopHash.ToHashString();
\r
1084 ExecuteWithRetry((session, instance) =>
\r
1086 const string hqlUpdate = "update FileState set Checksum= :checksum,Hashes=:hashes,ETag=:etag, Modified=:modified where FilePath = :path ";
\r
1087 var updatedEntities = session.CreateQuery(hqlUpdate)
\r
1088 .SetString("path", path)
\r
1089 .SetString("checksum", topHash)
\r
1090 .SetString("hashes", hashes)
\r
1091 .SetString("etag", etag)
\r
1092 .SetDateTime("modified", DateTime.Now)
\r
1094 return updatedEntities;
\r
1098 //Store only the hashes
\r
1099 public void UpdateFileHashes(FileInfo file, TreeHash treeHash)
\r
1102 throw new ArgumentNullException("file");
\r
1103 Contract.EndContractBlock();
\r
1105 var hashes = treeHash.ToJson();
\r
1106 var topHash = treeHash.TopHash.ToHashString();
\r
1108 ExecuteWithRetry((session, instance) =>
\r
1111 const string hqlUpdate = "update FileState set Hashes=:hashes,Modified=:modified,LastWriteDate=:date, LastLength=:length where FilePath = :path ";
\r
1113 const string hqlUpdate = "update FileState set Checksum= :checksum,Hashes=:hashes where FilePath = :path ";
\r
1115 var updatedEntities = session.CreateQuery(hqlUpdate)
\r
1116 .SetString("path", file.FullName)
\r
1117 .SetDateTime("date",file.LastWriteTime)
\r
1118 .SetInt64("length",file.Length)
\r
1119 //.SetString("checksum", topHash)
\r
1120 // .SetString("md5",treeHash.MD5)
\r
1121 .SetString("hashes", hashes)
\r
1122 .SetDateTime("modified", DateTime.Now)
\r
1124 return updatedEntities;
\r
1129 public void RenameState(string oldPath, string newPath)
\r
1131 if (string.IsNullOrWhiteSpace(oldPath))
\r
1132 throw new ArgumentNullException("oldPath");
\r
1133 Contract.EndContractBlock();
\r
1135 ExecuteWithRetry((session, instance) =>
\r
1137 const string hqlUpdate =
\r
1138 "update FileState set FilePath= :newPath, Modified=:modified where FilePath = :oldPath ";
\r
1139 var updatedEntities = session.CreateQuery(hqlUpdate)
\r
1140 .SetString("oldPath", oldPath)
\r
1141 .SetString("newPath", newPath)
\r
1142 .SetDateTime("modified", DateTime.Now)
\r
1144 return updatedEntities;
\r
1149 public void StoreOverlayStatus(string absolutePath, FileOverlayStatus newStatus)
\r
1151 if (string.IsNullOrWhiteSpace(absolutePath))
\r
1152 throw new ArgumentNullException("absolutePath");
\r
1153 Contract.EndContractBlock();
\r
1155 using(var session=_factory.OpenSession())
\r
1157 using (var tx = session.BeginTransaction())
\r
1159 var state = session.Query<FileState>().SingleOrDefault(s => s.FilePath == absolutePath)
\r
1161 FilePath = absolutePath,
\r
1162 OverlayStatus = newStatus,
\r
1163 ETag = Signature.MERKLE_EMPTY,
\r
1164 //LastMD5=String.Empty,
\r
1165 IsFolder = Directory.Exists(absolutePath),
\r
1166 Modified=DateTime.Now
\r
1168 state.OverlayStatus = newStatus;
\r
1169 session.SaveOrUpdate(state);
\r