Added server move detection when hashes match or downloading
[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         private string _shortHash;
114
115         /// <summary>
116         /// An easy to calcualte hash over the entire file, used to detect file changes
117         /// </summary>
118         /// <remarks>The algorithm used to calculate this hash should be cheap</remarks>
119         [Property(NotNull=true,Default="")]
120         public string ShortHash
121         {
122             get
123             {
124                 return _shortHash;
125             }
126             set
127             {
128                 _shortHash = value;
129             }
130         }
131
132
133         [Property]
134         public long? Version { get; set; }
135
136         [Property]
137         public DateTime? VersionTimeStamp { get; set; }
138
139
140         [Property]
141         public bool IsShared { get; set; }
142
143         [Property]
144         public string SharedBy { get; set; }
145
146         [Property]
147         public bool ShareWrite { get; set; }
148
149         [Property]
150         public bool IsFolder{ get; set; }
151
152         [HasMany(Cascade = ManyRelationCascadeEnum.AllDeleteOrphan, Lazy = true, Inverse = true)]
153         public IList<FileTag> Tags
154         {
155             get { return _tags; }
156             set { _tags = value; }
157         }
158
159         [Property]
160         public DateTime Modified { get; set; }
161
162
163         public FileSystemInfo GetFileSystemInfo()
164         {
165             if (String.IsNullOrWhiteSpace(FilePath))
166                 throw new InvalidOperationException();
167             Contract.EndContractBlock();
168
169             return Directory.Exists(FilePath) ?
170                 (FileSystemInfo)new DirectoryInfo(FilePath)
171                 : new FileInfo(FilePath);
172         }
173
174         public string GetRelativeUrl(AccountInfo accountInfo)
175         {
176             if (accountInfo==null)
177                 throw new ArgumentNullException("accountInfo");
178             Contract.EndContractBlock();
179
180             var fsi=GetFileSystemInfo();
181             return fsi.AsRelativeUrlTo(accountInfo.AccountPath);
182         }
183         /*public static FileState FindByFilePath(string absolutePath)
184         {
185             if (string.IsNullOrWhiteSpace(absolutePath))
186                 throw new ArgumentNullException("absolutePath");
187             Contract.EndContractBlock();
188             try
189             {
190
191                 
192
193                 return Queryable.FirstOrDefault(s => s.FilePath == absolutePath);
194             }
195             catch (Exception ex)
196             {
197                 Log.Error(ex.ToString());
198                 throw;
199             }
200
201
202         }*/
203
204        /* public static void DeleteByFilePath(string absolutePath)
205         {
206             if (string.IsNullOrWhiteSpace(absolutePath))
207                 throw new ArgumentNullException("absolutePath");
208             Contract.EndContractBlock();
209
210             ExecuteWithRetry((session, instance) =>
211                         {
212                             const string hqlDelete = "delete FileState where FilePath = :path";
213                             var deletedEntities = session.CreateQuery(hqlDelete)
214                                 .SetString("path", absolutePath)
215                                 .ExecuteUpdate();
216                             return deletedEntities;
217                         }, null);
218
219         }*/
220
221         public static void StoreFileStatus(string absolutePath, FileStatus newStatus)
222         {
223             if (string.IsNullOrWhiteSpace(absolutePath))
224                 throw new ArgumentNullException("absolutePath");
225             Contract.EndContractBlock();
226
227             ExecuteWithRetry((session, instance) =>
228                         {
229                             const string hqlUpdate = "update FileState set FileStatus= :status where FilePath = :path ";
230                             var updatedEntities = session.CreateQuery(hqlUpdate)
231                                 .SetString("path", absolutePath)
232                                 .SetEnum("status", newStatus)
233                                 .ExecuteUpdate();
234                             if (updatedEntities == 0)
235                             {
236                                 var newState = new FileState
237                                                    {
238                                                        FilePath = absolutePath,
239                                                        Id = Guid.NewGuid(),
240                                                        FileStatus = newStatus,
241                                                        IsFolder=Directory.Exists(absolutePath)
242                                                    };
243                                 newState.CreateAndFlush();
244                             }
245                             return null;
246                         }, null);
247
248         }
249
250         /*public static void StoreOverlayStatus(string absolutePath, FileOverlayStatus newStatus)
251         {
252             if (string.IsNullOrWhiteSpace(absolutePath))
253                 throw new ArgumentNullException("absolutePath");
254             Contract.EndContractBlock();
255
256             ExecuteWithRetry((session, instance) =>
257                         {
258                             const string hqlUpdate =
259                                 "update FileState set OverlayStatus= :status where FilePath = :path ";
260                             var updatedEntities = session.CreateQuery(hqlUpdate)                                
261                                 .SetString("path", absolutePath)
262                                 .SetEnum("status", newStatus)                                
263                                 .ExecuteUpdate();
264                             if (updatedEntities == 0)
265                             {
266                                 var newState = new FileState
267                                                    {
268                                                        FilePath = absolutePath,
269                                                        Id = Guid.NewGuid(),
270                                                        OverlayStatus = newStatus,
271                                                        ShortHash = String.Empty,
272                                                        IsFolder=Directory.Exists(absolutePath)
273                                                    };
274                                 newState.CreateAndFlush();
275                             }
276                             return null;
277                         }, null);
278
279         }
280 */
281         public static void StoreOverlayStatus(string absolutePath, FileOverlayStatus newStatus,string shortHash)
282         {
283             if (string.IsNullOrWhiteSpace(absolutePath))
284                 throw new ArgumentNullException("absolutePath");
285             Contract.EndContractBlock();
286
287             ExecuteWithRetry((session, instance) =>
288                         {
289                             const string hqlUpdate =
290                                 "update FileState set OverlayStatus= :status where FilePath = :path ";
291                             var updatedEntities = session.CreateQuery(hqlUpdate)                                
292                                 .SetString("path", absolutePath)
293                                 .SetEnum("status", newStatus)                                
294                                 .ExecuteUpdate();
295                             if (updatedEntities == 0)
296                             {
297                                 var newState = new FileState
298                                                    {
299                                                        FilePath = absolutePath,
300                                                        Id = Guid.NewGuid(),
301                                                        OverlayStatus = newStatus,
302                                                        ShortHash = shortHash??String.Empty,
303                                                        IsFolder=Directory.Exists(absolutePath)
304                                                    };
305                                 newState.CreateAndFlush();
306                             }
307                             return null;
308                         }, null);
309
310         }
311
312 /*
313         public static void UpdateStatus(string absolutePath, FileStatus fileStatus, FileOverlayStatus overlayStatus)
314         {
315             if (string.IsNullOrWhiteSpace(absolutePath))
316                 throw new ArgumentNullException("absolutePath");
317             Contract.EndContractBlock();
318
319             ExecuteWithRetry((session, instance) =>
320                         {
321                             const string hqlUpdate =
322                                 "update FileState set OverlayStatus= :overlayStatus, FileStatus= :fileStatus where FilePath = :path  ";
323                             var updatedEntities = session.CreateQuery(hqlUpdate)
324                                 .SetString("path", absolutePath)
325                                 .SetEnum("fileStatus", fileStatus)
326                                 .SetEnum("overlayStatus", overlayStatus)
327                                 .ExecuteUpdate();
328                             return updatedEntities;
329                         }, null);
330
331         }
332 */
333
334 /*
335         public static void UpdateStatus(string absolutePath, FileStatus fileStatus)
336         {
337             if (string.IsNullOrWhiteSpace(absolutePath))
338                 throw new ArgumentNullException("absolutePath");
339             Contract.EndContractBlock();
340
341             ExecuteWithRetry((session, instance) =>
342                         {
343                             const string hqlUpdate =
344                                 "update FileState set FileStatus= :fileStatus where FilePath = :path  ";
345                             var updatedEntities = session.CreateQuery(hqlUpdate)
346                                 .SetString("path", absolutePath)
347                                 .SetEnum("fileStatus", fileStatus)
348                                 .ExecuteUpdate();
349                             return updatedEntities;
350                         }, null);
351
352         }
353
354 */
355         public static void RenameState(string oldPath, string newPath)
356         {
357             if (string.IsNullOrWhiteSpace(oldPath))
358                 throw new ArgumentNullException("oldPath");
359             Contract.EndContractBlock();
360
361             ExecuteWithRetry((session, instance) =>
362                         {
363                             const string hqlUpdate =
364                                 "update FileState set FilePath= :newPath where FilePath = :oldPath ";
365                             var updatedEntities = session.CreateQuery(hqlUpdate)
366                                 .SetString("oldPath", oldPath)
367                                 .SetString("newPath", newPath)
368                                 .ExecuteUpdate();
369                             return updatedEntities;
370                         }, null);
371
372         }
373
374      /*   public static void UpdateStatus(Guid id, FileStatus fileStatus)
375         {
376
377             ExecuteWithRetry((session, instance) =>
378             {
379                 const string hqlUpdate =
380                     "update FileState set FileStatus= :fileStatus where Id = :id  ";
381                 var updatedEntities = session.CreateQuery(hqlUpdate)
382                     .SetGuid("id", id)
383                     .SetEnum("fileStatus", fileStatus)
384                     .ExecuteUpdate();
385                 return updatedEntities;
386             }, null);
387         }*/
388
389         public static void UpdateChecksum(string absolutePath, string shortHash, string checksum)
390         {
391             if (string.IsNullOrWhiteSpace(absolutePath))
392                 throw new ArgumentNullException("absolutePath");
393             Contract.EndContractBlock();
394
395             ExecuteWithRetry((session, instance) =>
396                         {
397                             const string hqlUpdate = "update FileState set Checksum= :checksum,ShortHash=:shortHash where FilePath = :path ";
398                             var updatedEntities = session.CreateQuery(hqlUpdate)
399                                 .SetString("path", absolutePath)
400                                 .SetString("checksum", checksum)
401                                 .SetString("shortHash", shortHash)
402                                 .ExecuteUpdate();
403                             return updatedEntities;
404                         }, null);
405
406         }
407
408         public static void ChangeRootPath(string oldPath, string newPath)
409         {
410             if (String.IsNullOrWhiteSpace(oldPath))
411                 throw new ArgumentNullException("oldPath");
412             if (!Path.IsPathRooted(oldPath))
413                 throw new ArgumentException("oldPath must be an absolute path", "oldPath");
414             if (string.IsNullOrWhiteSpace(newPath))
415                 throw new ArgumentNullException("newPath");
416             if (!Path.IsPathRooted(newPath))
417                 throw new ArgumentException("newPath must be an absolute path", "newPath");
418             Contract.EndContractBlock();
419
420             //Ensure the paths end with the same character
421             if (!oldPath.EndsWith("\\"))
422                 oldPath = oldPath + "\\";
423             if (!newPath.EndsWith("\\"))
424                 newPath = newPath + "\\";
425
426                 ExecuteWithRetry((session, instance) =>
427                             {
428                                 const string hqlUpdate =
429                                     "update FileState set FilePath = replace(FilePath,:oldPath,:newPath) where FilePath like :oldPath || '%' ";
430                                 var renames = session.CreateQuery(hqlUpdate)
431                                     .SetString("oldPath", oldPath)
432                                     .SetString("newPath", newPath)
433                                     .ExecuteUpdate();
434                                 return renames;
435                             }, null);
436         }
437
438         public static FileState CreateFor(FileSystemInfo info)
439         {
440             if(info==null)
441                 throw new ArgumentNullException("info");
442             Contract.EndContractBlock();
443             
444             if (info is DirectoryInfo)
445                 return new FileState
446                 {
447                     FilePath = info.FullName,
448                     OverlayStatus = FileOverlayStatus.Unversioned,
449                     FileStatus = FileStatus.Created,
450                     ShortHash=String.Empty,
451                     Id = Guid.NewGuid()
452                 };
453
454
455             var shortHash = ((FileInfo)info).ComputeShortHash();
456             var fileState = new FileState
457                                 {
458                                     FilePath = info.FullName,
459                                     OverlayStatus = FileOverlayStatus.Unversioned,
460                                     FileStatus = FileStatus.Created,               
461                                     ShortHash=shortHash,
462                                     Id = Guid.NewGuid()
463                                 };
464             return fileState;
465         }
466
467
468         private static void ExecuteWithRetry(NHibernateDelegate call, object state)
469         {
470             int retries = 3;
471             while (retries > 0)
472                 try
473                 {
474                     using (new SessionScope())
475                     {
476                         Execute(call, state);
477                         return;
478                     }
479                 }
480                 catch (ActiveRecordException )
481                 {
482                     retries--;
483                     if (retries <= 0)
484                         throw;
485                 }
486         }
487
488         /// <summary>
489         /// Mark Unversioned all FileState rows from the database whose path
490         /// starts with one of the removed paths
491         /// </summary>
492         /// <param name="removed"></param>
493         public static void UnversionPaths(List<string> removed)
494         {
495             if (removed == null)
496                 return;
497             if (removed.Count == 0)
498                 return;
499
500             //Create a disjunction (list of OR statements
501             var disjunction = new Disjunction();            
502             foreach (var path in removed)
503             {
504                 //with the restriction FileState.FilePath like '@path%'
505                 disjunction.Add(Restrictions.On<FileState>(s => s.FilePath)
506                     .IsLike(path, MatchMode.Start));
507             }
508
509             //Generate a query from the disjunction
510             var query=QueryOver.Of<FileState>().Where(disjunction);
511                         
512             ExecuteWithRetry((session,instance)=>
513                                  {
514                                      using (var t=session.BeginTransaction())
515                                      {
516                                          var states = query.GetExecutableQueryOver(session).List();
517                                          foreach (var state in states)
518                                          {
519                                              state.FileStatus = FileStatus.Unversioned;
520                                              state.OverlayStatus = FileOverlayStatus.Unversioned;
521                                              state.Update();
522                                          }
523                                          t.Commit();
524                                      }
525                                      return null;
526                                  },null);
527         }
528
529     }
530
531     [ActiveRecord("Tags")]
532     public class FileTag : ActiveRecordLinqBase<FileTag>
533     {
534         [PrimaryKey]
535         public int Id { get; set; }
536
537         [Property]
538         public string Name { get; set; }
539
540         [Property]
541         public string Value { get; set; }
542
543         [BelongsTo("FileStateId")]
544         public FileState FileState { get; set; }
545
546     }
547    
548 }