Replaced Merkle hash with MD5 for change checking
[pithos-ms-client] / trunk / Pithos.Core / FileState.cs
index f0239cc..3995cac 100644 (file)
@@ -1,27 +1,82 @@
-// -----------------------------------------------------------------------
-// <copyright file="FileState.cs" company="Microsoft">
-// TODO: Update copyright text.
-// </copyright>
-// -----------------------------------------------------------------------
-
+#region
+/* -----------------------------------------------------------------------
+ * <copyright file="FileState.cs" company="GRNet">
+ * 
+ * Copyright 2011-2012 GRNET S.A. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above
+ *      copyright notice, this list of conditions and the following
+ *      disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above
+ *      copyright notice, this list of conditions and the following
+ *      disclaimer in the documentation and/or other materials
+ *      provided with the distribution.
+ *
+ *
+ * THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
+ * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+ * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+ * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ *
+ * The views and conclusions contained in the software and
+ * documentation are those of the authors and should not be
+ * interpreted as representing official policies, either expressed
+ * or implied, of GRNET S.A.
+ * </copyright>
+ * -----------------------------------------------------------------------
+ */
+#endregion
+using System.Diagnostics.Contracts;
+using System.IO;
+using System.Reflection;
+using System.Threading.Tasks;
 using Castle.ActiveRecord;
 using Castle.ActiveRecord.Framework;
+using Castle.ActiveRecord.Queries;
+using NHibernate.Criterion;
+using Pithos.Core.Agents;
 using Pithos.Interfaces;
+using Pithos.Network;
+using log4net;
 
 namespace Pithos.Core
 {
     using System;
-    using System.Collections.Generic;
-    using System.Linq;
-    using System.Text;
+    using System.Collections.Generic;    
 
     /// <summary>
     /// TODO: Update summary.
     /// </summary>
     [ActiveRecord]
-    public class FileState:ActiveRecordLinqBase<FileState>
+    public class FileState : ActiveRecordLinqBase<FileState>
     {
-        [PrimaryKey]
+        private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
+
+
+        private IList<FileTag> _tags = new List<FileTag>();
+
+        [PrimaryKey(PrimaryKeyType.Guid)]
+        public Guid Id { get; set; }
+
+        
+        //[Property(Unique = true, UniqueKey = "IX_FileState_ObjectID")]
+        [Property]
+        public string ObjectID { get; set; }
+
+        [Property(Unique = true, UniqueKey = "IX_FileState_FilePath")]
         public string FilePath { get; set; }
 
         [Property]
@@ -31,6 +86,463 @@ namespace Pithos.Core
         public FileStatus FileStatus { get; set; }
 
         [Property]
-        public string Checksum { get; set; }
+        public string ConflictReason { get; set; }
+
+        private string _checksum;
+
+        /// <summary>
+        /// The tophash value of the file, calculated by using a Merkle hash with the SHA256 algorithm
+        /// </summary>
+        /// <remarks>
+        /// The SHA256 algorithm is substantially more expenive than other algorithms.
+        /// Recalculating the Checksum should be avoided whenever possible.
+        /// </remarks>
+        [Property]
+        public string Checksum
+        {
+            get
+            {
+                return _checksum;
+            }
+            set
+            {
+                _checksum = value;
+            }
+        }
+
+        private string _shortHash;
+
+        /// <summary>
+        /// An easy to calcualte hash over the entire file, used to detect file changes
+        /// </summary>
+        /// <remarks>The algorithm used to calculate this hash should be cheap</remarks>
+        [Property(NotNull=true,Default="")]
+        public string ShortHash
+        {
+            get
+            {
+                return _shortHash;
+            }
+            set
+            {
+                _shortHash = value;
+            }
+        }
+
+
+        [Property]
+        public long? Version { get; set; }
+
+        [Property]
+        public DateTime? VersionTimeStamp { get; set; }
+
+
+        [Property]
+        public bool IsShared { get; set; }
+
+        [Property]
+        public string SharedBy { get; set; }
+
+        [Property]
+        public bool ShareWrite { get; set; }
+
+        [Property]
+        public bool IsFolder{ get; set; }
+
+        [HasMany(Cascade = ManyRelationCascadeEnum.AllDeleteOrphan, Lazy = true, Inverse = true)]
+        public IList<FileTag> Tags
+        {
+            get { return _tags; }
+            set { _tags = value; }
+        }
+
+        [Property]
+        public DateTime Modified { get; set; }
+
+
+        public FileSystemInfo GetFileSystemInfo()
+        {
+            if (String.IsNullOrWhiteSpace(FilePath))
+                throw new InvalidOperationException();
+            Contract.EndContractBlock();
+
+            return Directory.Exists(FilePath) ?
+                (FileSystemInfo)new DirectoryInfo(FilePath)
+                : new FileInfo(FilePath);
+        }
+
+        public string GetRelativeUrl(AccountInfo accountInfo)
+        {
+            if (accountInfo==null)
+                throw new ArgumentNullException("accountInfo");
+            Contract.EndContractBlock();
+
+            var fsi=GetFileSystemInfo();
+            return fsi.AsRelativeUrlTo(accountInfo.AccountPath);
+        }
+        /*public static FileState FindByFilePath(string absolutePath)
+        {
+            if (string.IsNullOrWhiteSpace(absolutePath))
+                throw new ArgumentNullException("absolutePath");
+            Contract.EndContractBlock();
+            try
+            {
+
+                
+
+                return Queryable.FirstOrDefault(s => s.FilePath == absolutePath);
+            }
+            catch (Exception ex)
+            {
+                Log.Error(ex.ToString());
+                throw;
+            }
+
+
+        }*/
+
+       /* public static void DeleteByFilePath(string absolutePath)
+        {
+            if (string.IsNullOrWhiteSpace(absolutePath))
+                throw new ArgumentNullException("absolutePath");
+            Contract.EndContractBlock();
+
+            ExecuteWithRetry((session, instance) =>
+                        {
+                            const string hqlDelete = "delete FileState where FilePath = :path";
+                            var deletedEntities = session.CreateQuery(hqlDelete)
+                                .SetString("path", absolutePath)
+                                .ExecuteUpdate();
+                            return deletedEntities;
+                        }, null);
+
+        }*/
+
+        public static void StoreFileStatus(string absolutePath, FileStatus newStatus)
+        {
+            if (string.IsNullOrWhiteSpace(absolutePath))
+                throw new ArgumentNullException("absolutePath");
+            Contract.EndContractBlock();
+
+            ExecuteWithRetry((session, instance) =>
+                        {
+                            const string hqlUpdate = "update FileState set FileStatus= :status where FilePath = :path ";
+                            var updatedEntities = session.CreateQuery(hqlUpdate)
+                                .SetString("path", absolutePath)
+                                .SetEnum("status", newStatus)
+                                .ExecuteUpdate();
+                            if (updatedEntities == 0)
+                            {
+                                var newState = new FileState
+                                                   {
+                                                       FilePath = absolutePath,
+                                                       Id = Guid.NewGuid(),
+                                                       FileStatus = newStatus,
+                                                       IsFolder=Directory.Exists(absolutePath)
+                                                   };
+                                newState.CreateAndFlush();
+                            }
+                            return null;
+                        }, null);
+
+        }
+
+        /*public static void StoreOverlayStatus(string absolutePath, FileOverlayStatus newStatus)
+        {
+            if (string.IsNullOrWhiteSpace(absolutePath))
+                throw new ArgumentNullException("absolutePath");
+            Contract.EndContractBlock();
+
+            ExecuteWithRetry((session, instance) =>
+                        {
+                            const string hqlUpdate =
+                                "update FileState set OverlayStatus= :status where FilePath = :path ";
+                            var updatedEntities = session.CreateQuery(hqlUpdate)                                
+                                .SetString("path", absolutePath)
+                                .SetEnum("status", newStatus)                                
+                                .ExecuteUpdate();
+                            if (updatedEntities == 0)
+                            {
+                                var newState = new FileState
+                                                   {
+                                                       FilePath = absolutePath,
+                                                       Id = Guid.NewGuid(),
+                                                       OverlayStatus = newStatus,
+                                                       ShortHash = String.Empty,
+                                                       IsFolder=Directory.Exists(absolutePath)
+                                                   };
+                                newState.CreateAndFlush();
+                            }
+                            return null;
+                        }, null);
+
+        }
+*/
+        public static void StoreOverlayStatus(string absolutePath, FileOverlayStatus newStatus,string shortHash)
+        {
+            if (string.IsNullOrWhiteSpace(absolutePath))
+                throw new ArgumentNullException("absolutePath");
+            Contract.EndContractBlock();
+
+            ExecuteWithRetry((session, instance) =>
+                        {
+                            const string hqlUpdate =
+                                "update FileState set OverlayStatus= :status where FilePath = :path ";
+                            var updatedEntities = session.CreateQuery(hqlUpdate)                                
+                                .SetString("path", absolutePath)
+                                .SetEnum("status", newStatus)                                
+                                .ExecuteUpdate();
+                            if (updatedEntities == 0)
+                            {
+                                var newState = new FileState
+                                                   {
+                                                       FilePath = absolutePath,
+                                                       Id = Guid.NewGuid(),
+                                                       OverlayStatus = newStatus,
+                                                       ShortHash = shortHash??String.Empty,
+                                                       IsFolder=Directory.Exists(absolutePath)
+                                                   };
+                                newState.CreateAndFlush();
+                            }
+                            return null;
+                        }, null);
+
+        }
+
+/*
+        public static void UpdateStatus(string absolutePath, FileStatus fileStatus, FileOverlayStatus overlayStatus)
+        {
+            if (string.IsNullOrWhiteSpace(absolutePath))
+                throw new ArgumentNullException("absolutePath");
+            Contract.EndContractBlock();
+
+            ExecuteWithRetry((session, instance) =>
+                        {
+                            const string hqlUpdate =
+                                "update FileState set OverlayStatus= :overlayStatus, FileStatus= :fileStatus where FilePath = :path  ";
+                            var updatedEntities = session.CreateQuery(hqlUpdate)
+                                .SetString("path", absolutePath)
+                                .SetEnum("fileStatus", fileStatus)
+                                .SetEnum("overlayStatus", overlayStatus)
+                                .ExecuteUpdate();
+                            return updatedEntities;
+                        }, null);
+
+        }
+*/
+
+/*
+        public static void UpdateStatus(string absolutePath, FileStatus fileStatus)
+        {
+            if (string.IsNullOrWhiteSpace(absolutePath))
+                throw new ArgumentNullException("absolutePath");
+            Contract.EndContractBlock();
+
+            ExecuteWithRetry((session, instance) =>
+                        {
+                            const string hqlUpdate =
+                                "update FileState set FileStatus= :fileStatus where FilePath = :path  ";
+                            var updatedEntities = session.CreateQuery(hqlUpdate)
+                                .SetString("path", absolutePath)
+                                .SetEnum("fileStatus", fileStatus)
+                                .ExecuteUpdate();
+                            return updatedEntities;
+                        }, null);
+
+        }
+
+*/
+        public static void RenameState(string oldPath, string newPath)
+        {
+            if (string.IsNullOrWhiteSpace(oldPath))
+                throw new ArgumentNullException("oldPath");
+            Contract.EndContractBlock();
+
+            ExecuteWithRetry((session, instance) =>
+                        {
+                            const string hqlUpdate =
+                                "update FileState set FilePath= :newPath where FilePath = :oldPath ";
+                            var updatedEntities = session.CreateQuery(hqlUpdate)
+                                .SetString("oldPath", oldPath)
+                                .SetString("newPath", newPath)
+                                .ExecuteUpdate();
+                            return updatedEntities;
+                        }, null);
+
+        }
+
+     /*   public static void UpdateStatus(Guid id, FileStatus fileStatus)
+        {
+
+            ExecuteWithRetry((session, instance) =>
+            {
+                const string hqlUpdate =
+                    "update FileState set FileStatus= :fileStatus where Id = :id  ";
+                var updatedEntities = session.CreateQuery(hqlUpdate)
+                    .SetGuid("id", id)
+                    .SetEnum("fileStatus", fileStatus)
+                    .ExecuteUpdate();
+                return updatedEntities;
+            }, null);
+        }*/
+
+        public static void UpdateChecksum(string absolutePath, string shortHash, string checksum)
+        {
+            if (string.IsNullOrWhiteSpace(absolutePath))
+                throw new ArgumentNullException("absolutePath");
+            Contract.EndContractBlock();
+
+            ExecuteWithRetry((session, instance) =>
+                        {
+                            const string hqlUpdate = "update FileState set Checksum= :checksum,ShortHash=:shortHash where FilePath = :path ";
+                            var updatedEntities = session.CreateQuery(hqlUpdate)
+                                .SetString("path", absolutePath)
+                                .SetString("checksum", checksum)
+                                .SetString("shortHash", shortHash)
+                                .ExecuteUpdate();
+                            return updatedEntities;
+                        }, null);
+
+        }
+
+        public static void ChangeRootPath(string oldPath, string newPath)
+        {
+            if (String.IsNullOrWhiteSpace(oldPath))
+                throw new ArgumentNullException("oldPath");
+            if (!Path.IsPathRooted(oldPath))
+                throw new ArgumentException("oldPath must be an absolute path", "oldPath");
+            if (string.IsNullOrWhiteSpace(newPath))
+                throw new ArgumentNullException("newPath");
+            if (!Path.IsPathRooted(newPath))
+                throw new ArgumentException("newPath must be an absolute path", "newPath");
+            Contract.EndContractBlock();
+
+            //Ensure the paths end with the same character
+            if (!oldPath.EndsWith("\\"))
+                oldPath = oldPath + "\\";
+            if (!newPath.EndsWith("\\"))
+                newPath = newPath + "\\";
+
+                ExecuteWithRetry((session, instance) =>
+                            {
+                                const string hqlUpdate =
+                                    "update FileState set FilePath = replace(FilePath,:oldPath,:newPath) where FilePath like :oldPath || '%' ";
+                                var renames = session.CreateQuery(hqlUpdate)
+                                    .SetString("oldPath", oldPath)
+                                    .SetString("newPath", newPath)
+                                    .ExecuteUpdate();
+                                return renames;
+                            }, null);
+        }
+
+        public static FileState CreateFor(FileSystemInfo info,IStatusNotification notification)
+        {
+            if(info==null)
+                throw new ArgumentNullException("info");
+            Contract.EndContractBlock();
+            
+            if (info is DirectoryInfo)
+                return new FileState
+                {
+                    FilePath = info.FullName,
+                    OverlayStatus = FileOverlayStatus.Unversioned,
+                    FileStatus = FileStatus.Created,
+                    ShortHash=String.Empty,
+                    Id = Guid.NewGuid()
+                };
+
+            
+            var shortHash = ((FileInfo)info).ComputeShortHash(notification);
+            var fileState = new FileState
+                                {
+                                    FilePath = info.FullName,
+                                    OverlayStatus = FileOverlayStatus.Unversioned,
+                                    FileStatus = FileStatus.Created,               
+                                    ShortHash=shortHash,
+                                    Id = Guid.NewGuid()
+                                };
+            return fileState;
+        }
+
+
+        private static void ExecuteWithRetry(NHibernateDelegate call, object state)
+        {
+            int retries = 3;
+            while (retries > 0)
+                try
+                {
+                    using (new SessionScope())
+                    {
+                        Execute(call, state);
+                        return;
+                    }
+                }
+                catch (ActiveRecordException )
+                {
+                    retries--;
+                    if (retries <= 0)
+                        throw;
+                }
+        }
+
+        /// <summary>
+        /// Mark Unversioned all FileState rows from the database whose path
+        /// starts with one of the removed paths
+        /// </summary>
+        /// <param name="removed"></param>
+        public static void UnversionPaths(List<string> removed)
+        {
+            if (removed == null)
+                return;
+            if (removed.Count == 0)
+                return;
+
+            //Create a disjunction (list of OR statements
+            var disjunction = new Disjunction();            
+            foreach (var path in removed)
+            {
+                //with the restriction FileState.FilePath like '@path%'
+                disjunction.Add(Restrictions.On<FileState>(s => s.FilePath)
+                    .IsLike(path, MatchMode.Start));
+            }
+
+            //Generate a query from the disjunction
+            var query=QueryOver.Of<FileState>().Where(disjunction);
+                        
+            ExecuteWithRetry((session,instance)=>
+                                 {
+                                     using (var t=session.BeginTransaction())
+                                     {
+                                         var states = query.GetExecutableQueryOver(session).List();
+                                         foreach (var state in states)
+                                         {
+                                             state.FileStatus = FileStatus.Unversioned;
+                                             state.OverlayStatus = FileOverlayStatus.Unversioned;
+                                             state.Update();
+                                         }
+                                         t.Commit();
+                                     }
+                                     return null;
+                                 },null);
+        }
+
+    }
+
+    [ActiveRecord("Tags")]
+    public class FileTag : ActiveRecordLinqBase<FileTag>
+    {
+        [PrimaryKey]
+        public int Id { get; set; }
+
+        [Property]
+        public string Name { get; set; }
+
+        [Property]
+        public string Value { get; set; }
+
+        [BelongsTo("FileStateId")]
+        public FileState FileState { get; set; }
+
     }
+   
 }