Added checks for hashing of cache files
[pithos-ms-client] / trunk / Pithos.Core / FileState.cs
1 #region
2 /* -----------------------------------------------------------------------
3  * <copyright file="FileState.cs" company="GRNet">
4  * 
5  * Copyright 2011-2012 GRNET S.A. All rights reserved.
6  *
7  * Redistribution and use in source and binary forms, with or
8  * without modification, are permitted provided that the following
9  * conditions are met:
10  *
11  *   1. Redistributions of source code must retain the above
12  *      copyright notice, this list of conditions and the following
13  *      disclaimer.
14  *
15  *   2. Redistributions in binary form must reproduce the above
16  *      copyright notice, this list of conditions and the following
17  *      disclaimer in the documentation and/or other materials
18  *      provided with the distribution.
19  *
20  *
21  * THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
22  * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
23  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
24  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
25  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
26  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
27  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
28  * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
29  * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
30  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
31  * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
32  * POSSIBILITY OF SUCH DAMAGE.
33  *
34  * The views and conclusions contained in the software and
35  * documentation are those of the authors and should not be
36  * interpreted as representing official policies, either expressed
37  * or implied, of GRNET S.A.
38  * </copyright>
39  * -----------------------------------------------------------------------
40  */
41 #endregion
42 using System.Diagnostics.Contracts;
43 using System.IO;
44 using System.Reflection;
45 using System.Threading.Tasks;
46 using Castle.ActiveRecord;
47 using Castle.ActiveRecord.Framework;
48 using Castle.ActiveRecord.Queries;
49 using NHibernate.Criterion;
50 using Pithos.Core.Agents;
51 using Pithos.Interfaces;
52 using Pithos.Network;
53 using log4net;
54
55 namespace Pithos.Core
56 {
57     using System;
58     using System.Collections.Generic;    
59
60     /// <summary>
61     /// TODO: Update summary.
62     /// </summary>
63     [ActiveRecord]
64     public class FileState : ActiveRecordLinqBase<FileState>
65     {
66         private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
67
68
69         private IList<FileTag> _tags = new List<FileTag>();
70
71         [PrimaryKey(PrimaryKeyType.Guid)]
72         public Guid Id { get; set; }
73
74         
75         //[Property(Unique = true, UniqueKey = "IX_FileState_ObjectID")]
76         [Property]
77         public string ObjectID { get; set; }
78
79         [Property(Unique = true, UniqueKey = "IX_FileState_FilePath")]
80         public string FilePath { get; set; }
81
82         [Property]
83         public FileOverlayStatus OverlayStatus { get; set; }
84
85         [Property]
86         public FileStatus FileStatus { get; set; }
87
88         [Property]
89         public string ConflictReason { get; set; }
90
91         private string _checksum;
92
93         /// <summary>
94         /// The tophash value of the file, calculated by using a Merkle hash with the SHA256 algorithm
95         /// </summary>
96         /// <remarks>
97         /// The SHA256 algorithm is substantially more expenive than other algorithms.
98         /// Recalculating the Checksum should be avoided whenever possible.
99         /// </remarks>
100         [Property]
101         public string Checksum
102         {
103             get
104             {
105                 return _checksum;
106             }
107             set
108             {
109                 _checksum = value;
110             }
111         }
112         
113         /// <summary>
114         /// An easy to calcualte hash over the entire file, used to detect file changes
115         /// </summary>
116         /// <remarks>The algorithm used to calculate this hash should be cheap</remarks>
117         [Property(NotNull = true, Default = "")]
118         public string ETag { get; set; }
119
120         [Property(NotNull = true, Default = "")]
121         public string LastMD5 { get; set; }
122
123         [Property]
124         public DateTime? LastWriteDate { get; set; }
125
126         [Property]
127         public long? LastLength { get; set; }
128         
129         [Property]
130         public long? Version { get; set; }
131
132         [Property]
133         public DateTime? VersionTimeStamp { get; set; }
134
135
136         [Property]
137         public bool IsShared { get; set; }
138
139         [Property]
140         public string SharedBy { get; set; }
141
142         [Property]
143         public bool ShareWrite { get; set; }
144
145         [Property]
146         public bool IsFolder{ get; set; }
147
148         [HasMany(Cascade = ManyRelationCascadeEnum.AllDeleteOrphan, Lazy = true, Inverse = true)]
149         public IList<FileTag> Tags
150         {
151             get { return _tags; }
152             set { _tags = value; }
153         }
154
155         [Property]
156         public DateTime Modified { get; set; }
157
158
159         public FileSystemInfo GetFileSystemInfo()
160         {
161             if (String.IsNullOrWhiteSpace(FilePath))
162                 throw new InvalidOperationException();
163             Contract.EndContractBlock();
164
165             return Directory.Exists(FilePath) ?
166                 (FileSystemInfo)new DirectoryInfo(FilePath)
167                 : new FileInfo(FilePath);
168         }
169
170         public string GetRelativeUrl(AccountInfo accountInfo)
171         {
172             if (accountInfo==null)
173                 throw new ArgumentNullException("accountInfo");
174             Contract.EndContractBlock();
175
176             var fsi=GetFileSystemInfo();
177             return fsi.AsRelativeUrlTo(accountInfo.AccountPath);
178         }
179         /*public static FileState FindByFilePath(string absolutePath)
180         {
181             if (string.IsNullOrWhiteSpace(absolutePath))
182                 throw new ArgumentNullException("absolutePath");
183             Contract.EndContractBlock();
184             try
185             {
186
187                 
188
189                 return Queryable.FirstOrDefault(s => s.FilePath == absolutePath);
190             }
191             catch (Exception ex)
192             {
193                 Log.Error(ex.ToString());
194                 throw;
195             }
196
197
198         }*/
199
200        /* public static void DeleteByFilePath(string absolutePath)
201         {
202             if (string.IsNullOrWhiteSpace(absolutePath))
203                 throw new ArgumentNullException("absolutePath");
204             Contract.EndContractBlock();
205
206             ExecuteWithRetry((session, instance) =>
207                         {
208                             const string hqlDelete = "delete FileState where FilePath = :path";
209                             var deletedEntities = session.CreateQuery(hqlDelete)
210                                 .SetString("path", absolutePath)
211                                 .ExecuteUpdate();
212                             return deletedEntities;
213                         }, null);
214
215         }*/
216
217         public static void StoreFileStatus(string absolutePath, FileStatus newStatus)
218         {
219             if (string.IsNullOrWhiteSpace(absolutePath))
220                 throw new ArgumentNullException("absolutePath");
221             Contract.EndContractBlock();
222
223             ExecuteWithRetry((session, instance) =>
224                         {
225                             const string hqlUpdate = "update FileState set FileStatus= :status where FilePath = :path ";
226                             var updatedEntities = session.CreateQuery(hqlUpdate)
227                                 .SetString("path", absolutePath)
228                                 .SetEnum("status", newStatus)
229                                 .ExecuteUpdate();
230                             if (updatedEntities == 0)
231                             {
232                                 var newState = new FileState
233                                                    {
234                                                        FilePath = absolutePath,
235                                                        Id = Guid.NewGuid(),
236                                                        FileStatus = newStatus,
237                                                        IsFolder=Directory.Exists(absolutePath)
238                                                    };
239                                 newState.CreateAndFlush();
240                             }
241                             return null;
242                         }, null);
243
244         }
245
246         /*public static void StoreOverlayStatus(string absolutePath, FileOverlayStatus newStatus)
247         {
248             if (string.IsNullOrWhiteSpace(absolutePath))
249                 throw new ArgumentNullException("absolutePath");
250             Contract.EndContractBlock();
251
252             ExecuteWithRetry((session, instance) =>
253                         {
254                             const string hqlUpdate =
255                                 "update FileState set OverlayStatus= :status where FilePath = :path ";
256                             var updatedEntities = session.CreateQuery(hqlUpdate)                                
257                                 .SetString("path", absolutePath)
258                                 .SetEnum("status", newStatus)                                
259                                 .ExecuteUpdate();
260                             if (updatedEntities == 0)
261                             {
262                                 var newState = new FileState
263                                                    {
264                                                        FilePath = absolutePath,
265                                                        Id = Guid.NewGuid(),
266                                                        OverlayStatus = newStatus,
267                                                        ETag = String.Empty,
268                                                        IsFolder=Directory.Exists(absolutePath)
269                                                    };
270                                 newState.CreateAndFlush();
271                             }
272                             return null;
273                         }, null);
274
275         }
276 */
277         public static void StoreOverlayStatus(string absolutePath, FileOverlayStatus newStatus,string etag)
278         {
279             if (string.IsNullOrWhiteSpace(absolutePath))
280                 throw new ArgumentNullException("absolutePath");
281             Contract.EndContractBlock();
282
283             ExecuteWithRetry((session, instance) =>
284                         {
285                             const string hqlUpdate =
286                                 "update FileState set OverlayStatus= :status where FilePath = :path ";
287                             var updatedEntities = session.CreateQuery(hqlUpdate)                                
288                                 .SetString("path", absolutePath)
289                                 .SetEnum("status", newStatus)                                
290                                 .ExecuteUpdate();
291                             if (updatedEntities == 0)
292                             {
293                                 var newState = new FileState
294                                                    {
295                                                        FilePath = absolutePath,
296                                                        Id = Guid.NewGuid(),
297                                                        OverlayStatus = newStatus,
298                                                        ETag = etag??String.Empty,
299                                                        IsFolder=Directory.Exists(absolutePath)
300                                                    };
301                                 newState.CreateAndFlush();
302                             }
303                             return null;
304                         }, null);
305
306         }
307
308 /*
309         public static void UpdateStatus(string absolutePath, FileStatus fileStatus, FileOverlayStatus overlayStatus)
310         {
311             if (string.IsNullOrWhiteSpace(absolutePath))
312                 throw new ArgumentNullException("absolutePath");
313             Contract.EndContractBlock();
314
315             ExecuteWithRetry((session, instance) =>
316                         {
317                             const string hqlUpdate =
318                                 "update FileState set OverlayStatus= :overlayStatus, FileStatus= :fileStatus where FilePath = :path  ";
319                             var updatedEntities = session.CreateQuery(hqlUpdate)
320                                 .SetString("path", absolutePath)
321                                 .SetEnum("fileStatus", fileStatus)
322                                 .SetEnum("overlayStatus", overlayStatus)
323                                 .ExecuteUpdate();
324                             return updatedEntities;
325                         }, null);
326
327         }
328 */
329
330 /*
331         public static void UpdateStatus(string absolutePath, FileStatus fileStatus)
332         {
333             if (string.IsNullOrWhiteSpace(absolutePath))
334                 throw new ArgumentNullException("absolutePath");
335             Contract.EndContractBlock();
336
337             ExecuteWithRetry((session, instance) =>
338                         {
339                             const string hqlUpdate =
340                                 "update FileState set FileStatus= :fileStatus where FilePath = :path  ";
341                             var updatedEntities = session.CreateQuery(hqlUpdate)
342                                 .SetString("path", absolutePath)
343                                 .SetEnum("fileStatus", fileStatus)
344                                 .ExecuteUpdate();
345                             return updatedEntities;
346                         }, null);
347
348         }
349
350 */
351         public static void RenameState(string oldPath, string newPath)
352         {
353             if (string.IsNullOrWhiteSpace(oldPath))
354                 throw new ArgumentNullException("oldPath");
355             Contract.EndContractBlock();
356
357             ExecuteWithRetry((session, instance) =>
358                         {
359                             const string hqlUpdate =
360                                 "update FileState set FilePath= :newPath where FilePath = :oldPath ";
361                             var updatedEntities = session.CreateQuery(hqlUpdate)
362                                 .SetString("oldPath", oldPath)
363                                 .SetString("newPath", newPath)
364                                 .ExecuteUpdate();
365                             return updatedEntities;
366                         }, null);
367
368         }
369
370      /*   public static void UpdateStatus(Guid id, FileStatus fileStatus)
371         {
372
373             ExecuteWithRetry((session, instance) =>
374             {
375                 const string hqlUpdate =
376                     "update FileState set FileStatus= :fileStatus where Id = :id  ";
377                 var updatedEntities = session.CreateQuery(hqlUpdate)
378                     .SetGuid("id", id)
379                     .SetEnum("fileStatus", fileStatus)
380                     .ExecuteUpdate();
381                 return updatedEntities;
382             }, null);
383         }*/
384
385         public static void UpdateChecksum(string absolutePath, string etag, string checksum)
386         {
387             if (string.IsNullOrWhiteSpace(absolutePath))
388                 throw new ArgumentNullException("absolutePath");
389             Contract.EndContractBlock();
390
391             ExecuteWithRetry((session, instance) =>
392                         {
393                             const string hqlUpdate = "update FileState set Checksum= :checksum,ETag=:etag where FilePath = :path ";
394                             var updatedEntities = session.CreateQuery(hqlUpdate)
395                                 .SetString("path", absolutePath)
396                                 .SetString("checksum", checksum)
397                                 .SetString("etag", etag)
398                                 .ExecuteUpdate();
399                             return updatedEntities;
400                         }, null);
401
402         }
403
404         public static void UpdateLastMD5(FileInfo file, string md5)
405         {
406             if (file==null)
407                 throw new ArgumentNullException("file");
408             Contract.EndContractBlock();
409
410             ExecuteWithRetry((session, instance) =>
411                         {
412                             const string hqlUpdate = "update FileState set LastMD5=:md5, LastWriteDate=:date,LastLength=:length where FilePath = :path ";
413                             var fullName = file.WithProperCapitalization().FullName;
414
415                             var updatedEntities = session.CreateQuery(hqlUpdate)
416                                 .SetDateTime("date", file.LastWriteTime)
417                                 .SetInt64("length", file.Length)
418                                 .SetString("md5", md5)
419                                 .SetString("path", fullName)
420                                 .ExecuteUpdate();
421                             if (updatedEntities == 0)
422                             {
423                                 var newState = new FileState
424                                 {
425                                     FilePath = fullName,
426                                     Id = Guid.NewGuid(),
427                                     OverlayStatus = FileOverlayStatus.Normal,
428                                     FileStatus=FileStatus.Unchanged,
429                                     IsFolder = false,
430                                     LastLength=file.Length,
431                                     LastWriteDate=file.LastWriteTime,
432                                     LastMD5 = md5 ?? String.Empty,
433                                     ETag=String.Empty
434                                 };
435                                 newState.CreateAndFlush();
436                             }
437                             return updatedEntities;
438                         }, null);
439
440         }
441
442         public static void ChangeRootPath(string oldPath, string newPath)
443         {
444             if (String.IsNullOrWhiteSpace(oldPath))
445                 throw new ArgumentNullException("oldPath");
446             if (!Path.IsPathRooted(oldPath))
447                 throw new ArgumentException("oldPath must be an absolute path", "oldPath");
448             if (string.IsNullOrWhiteSpace(newPath))
449                 throw new ArgumentNullException("newPath");
450             if (!Path.IsPathRooted(newPath))
451                 throw new ArgumentException("newPath must be an absolute path", "newPath");
452             Contract.EndContractBlock();
453
454             //Ensure the paths end with the same character
455             if (!oldPath.EndsWith("\\"))
456                 oldPath = oldPath + "\\";
457             if (!newPath.EndsWith("\\"))
458                 newPath = newPath + "\\";
459
460                 ExecuteWithRetry((session, instance) =>
461                             {
462                                 const string hqlUpdate =
463                                     "update FileState set FilePath = replace(FilePath,:oldPath,:newPath) where FilePath like :oldPath || '%' ";
464                                 var renames = session.CreateQuery(hqlUpdate)
465                                     .SetString("oldPath", oldPath)
466                                     .SetString("newPath", newPath)
467                                     .ExecuteUpdate();
468                                 return renames;
469                             }, null);
470         }
471
472         public static FileState CreateFor(FileSystemInfo info,IStatusNotification notification)
473         {
474             if(info==null)
475                 throw new ArgumentNullException("info");
476             Contract.EndContractBlock();
477             
478             if (info is DirectoryInfo)
479                 return new FileState
480                 {
481                     FilePath = info.FullName,
482                     OverlayStatus = FileOverlayStatus.Unversioned,
483                     FileStatus = FileStatus.Created,
484                     ETag=String.Empty,
485                     LastMD5=String.Empty,
486                     Id = Guid.NewGuid()
487                 };
488
489             
490             var etag = ((FileInfo)info).ComputeShortHash(notification);
491             var fileState = new FileState
492                                 {
493                                     FilePath = info.FullName,
494                                     OverlayStatus = FileOverlayStatus.Unversioned,
495                                     FileStatus = FileStatus.Created,               
496                                     ETag=etag,
497                                     LastMD5=String.Empty,
498                                     Id = Guid.NewGuid()
499                                 };
500             return fileState;
501         }
502
503
504         private static void ExecuteWithRetry(NHibernateDelegate call, object state)
505         {
506             int retries = 3;
507             while (retries > 0)
508                 try
509                 {
510                     using (new SessionScope())
511                     {
512                         Execute(call, state);
513                         return;
514                     }
515                 }
516                 catch (ActiveRecordException )
517                 {
518                     retries--;
519                     if (retries <= 0)
520                         throw;
521                 }
522         }
523
524         /// <summary>
525         /// Mark Unversioned all FileState rows from the database whose path
526         /// starts with one of the removed paths
527         /// </summary>
528         /// <param name="removed"></param>
529         public static void UnversionPaths(List<string> removed)
530         {
531             if (removed == null)
532                 return;
533             if (removed.Count == 0)
534                 return;
535
536             //Create a disjunction (list of OR statements
537             var disjunction = new Disjunction();            
538             foreach (var path in removed)
539             {
540                 //with the restriction FileState.FilePath like '@path%'
541                 disjunction.Add(Restrictions.On<FileState>(s => s.FilePath)
542                     .IsLike(path, MatchMode.Start));
543             }
544
545             //Generate a query from the disjunction
546             var query=QueryOver.Of<FileState>().Where(disjunction);
547                         
548             ExecuteWithRetry((session,instance)=>
549                                  {
550                                      using (var t=session.BeginTransaction())
551                                      {
552                                          var states = query.GetExecutableQueryOver(session).List();
553                                          foreach (var state in states)
554                                          {
555                                              state.FileStatus = FileStatus.Unversioned;
556                                              state.OverlayStatus = FileOverlayStatus.Unversioned;
557                                              state.Update();
558                                          }
559                                          t.Commit();
560                                      }
561                                      return null;
562                                  },null);
563         }
564
565     }
566
567     [ActiveRecord("Tags")]
568     public class FileTag : ActiveRecordLinqBase<FileTag>
569     {
570         [PrimaryKey]
571         public int Id { get; set; }
572
573         [Property]
574         public string Name { get; set; }
575
576         [Property]
577         public string Value { get; set; }
578
579         [BelongsTo("FileStateId")]
580         public FileState FileState { get; set; }
581
582     }
583    
584 }