root / trunk / Pithos.Core / FileState.cs @ 12c87c0e
History | View | Annotate | Download (24.3 kB)
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 string Hashes { get; set; } |
125 |
|
126 |
[Property] |
127 |
public DateTime? LastWriteDate { get; set; } |
128 |
|
129 |
[Property] |
130 |
public long? LastLength { get; set; } |
131 |
|
132 |
[Property] |
133 |
public long? Version { get; set; } |
134 |
|
135 |
[Property] |
136 |
public DateTime? VersionTimeStamp { get; set; } |
137 |
|
138 |
|
139 |
[Property] |
140 |
public bool IsShared { get; set; } |
141 |
|
142 |
[Property] |
143 |
public string SharedBy { get; set; } |
144 |
|
145 |
[Property] |
146 |
public bool ShareWrite { get; set; } |
147 |
|
148 |
[Property] |
149 |
public bool IsFolder{ get; set; } |
150 |
|
151 |
[HasMany(Cascade = ManyRelationCascadeEnum.AllDeleteOrphan, Lazy = true, Inverse = true)] |
152 |
public IList<FileTag> Tags |
153 |
{ |
154 |
get { return _tags; } |
155 |
set { _tags = value; } |
156 |
} |
157 |
|
158 |
[Property] |
159 |
public DateTime Modified { get; set; } |
160 |
|
161 |
|
162 |
public FileSystemInfo GetFileSystemInfo() |
163 |
{ |
164 |
if (String.IsNullOrWhiteSpace(FilePath)) |
165 |
throw new InvalidOperationException(); |
166 |
Contract.EndContractBlock(); |
167 |
|
168 |
return Directory.Exists(FilePath) ? |
169 |
(FileSystemInfo)new DirectoryInfo(FilePath) |
170 |
: new FileInfo(FilePath); |
171 |
} |
172 |
|
173 |
public string GetRelativeUrl(AccountInfo accountInfo) |
174 |
{ |
175 |
if (accountInfo==null) |
176 |
throw new ArgumentNullException("accountInfo"); |
177 |
Contract.EndContractBlock(); |
178 |
|
179 |
var fsi=GetFileSystemInfo(); |
180 |
return fsi.AsRelativeUrlTo(accountInfo.AccountPath); |
181 |
} |
182 |
/*public static FileState FindByFilePath(string absolutePath) |
183 |
{ |
184 |
if (string.IsNullOrWhiteSpace(absolutePath)) |
185 |
throw new ArgumentNullException("absolutePath"); |
186 |
Contract.EndContractBlock(); |
187 |
try |
188 |
{ |
189 |
|
190 |
|
191 |
|
192 |
return Queryable.FirstOrDefault(s => s.FilePath == absolutePath); |
193 |
} |
194 |
catch (Exception ex) |
195 |
{ |
196 |
Log.Error(ex.ToString()); |
197 |
throw; |
198 |
} |
199 |
|
200 |
|
201 |
}*/ |
202 |
|
203 |
/* public static void DeleteByFilePath(string absolutePath) |
204 |
{ |
205 |
if (string.IsNullOrWhiteSpace(absolutePath)) |
206 |
throw new ArgumentNullException("absolutePath"); |
207 |
Contract.EndContractBlock(); |
208 |
|
209 |
ExecuteWithRetry((session, instance) => |
210 |
{ |
211 |
const string hqlDelete = "delete FileState where FilePath = :path"; |
212 |
var deletedEntities = session.CreateQuery(hqlDelete) |
213 |
.SetString("path", absolutePath) |
214 |
.ExecuteUpdate(); |
215 |
return deletedEntities; |
216 |
}, null); |
217 |
|
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 = |
230 |
"update FileState set FileStatus= :status where FilePath = :path "; |
231 |
using (session.BeginTransaction()) |
232 |
{ |
233 |
var updatedEntities = session.CreateQuery(hqlUpdate) |
234 |
.SetString("path", absolutePath) |
235 |
.SetEnum("status", newStatus) |
236 |
.ExecuteUpdate(); |
237 |
if (updatedEntities == 0) |
238 |
{ |
239 |
var newState = new FileState |
240 |
{ |
241 |
FilePath = absolutePath, |
242 |
Id = Guid.NewGuid(), |
243 |
FileStatus = newStatus, |
244 |
IsFolder = Directory.Exists(absolutePath) |
245 |
}; |
246 |
newState.CreateAndFlush(); |
247 |
} |
248 |
return null; |
249 |
} |
250 |
}, null); |
251 |
|
252 |
} |
253 |
*/ |
254 |
|
255 |
public static void StoreOverlayStatus(string absolutePath, FileOverlayStatus newStatus,string etag) |
256 |
{ |
257 |
if (string.IsNullOrWhiteSpace(absolutePath)) |
258 |
throw new ArgumentNullException("absolutePath"); |
259 |
Contract.EndContractBlock(); |
260 |
|
261 |
ExecuteWithRetry((session, instance) => |
262 |
{ |
263 |
const string hqlUpdate = |
264 |
"update FileState set OverlayStatus= :status where FilePath = :path "; |
265 |
using (var tx=session.BeginTransaction()) |
266 |
{ |
267 |
var updatedEntities = session.CreateQuery(hqlUpdate) |
268 |
.SetString("path", absolutePath) |
269 |
.SetEnum("status", newStatus) |
270 |
.ExecuteUpdate(); |
271 |
if (updatedEntities == 0) |
272 |
{ |
273 |
var newState = new FileState |
274 |
{ |
275 |
FilePath = absolutePath, |
276 |
Id = Guid.NewGuid(), |
277 |
OverlayStatus = newStatus, |
278 |
ETag = etag ?? String.Empty, |
279 |
LastMD5=String.Empty, |
280 |
IsFolder = Directory.Exists(absolutePath) |
281 |
}; |
282 |
newState.CreateAndFlush(); |
283 |
} |
284 |
tx.Commit(); |
285 |
return null; |
286 |
} |
287 |
}, null); |
288 |
|
289 |
} |
290 |
|
291 |
/* |
292 |
public static void UpdateStatus(string absolutePath, FileStatus fileStatus, FileOverlayStatus overlayStatus) |
293 |
{ |
294 |
if (string.IsNullOrWhiteSpace(absolutePath)) |
295 |
throw new ArgumentNullException("absolutePath"); |
296 |
Contract.EndContractBlock(); |
297 |
|
298 |
ExecuteWithRetry((session, instance) => |
299 |
{ |
300 |
const string hqlUpdate = |
301 |
"update FileState set OverlayStatus= :overlayStatus, FileStatus= :fileStatus where FilePath = :path "; |
302 |
var updatedEntities = session.CreateQuery(hqlUpdate) |
303 |
.SetString("path", absolutePath) |
304 |
.SetEnum("fileStatus", fileStatus) |
305 |
.SetEnum("overlayStatus", overlayStatus) |
306 |
.ExecuteUpdate(); |
307 |
return updatedEntities; |
308 |
}, null); |
309 |
|
310 |
} |
311 |
*/ |
312 |
|
313 |
/* |
314 |
public static void UpdateStatus(string absolutePath, FileStatus fileStatus) |
315 |
{ |
316 |
if (string.IsNullOrWhiteSpace(absolutePath)) |
317 |
throw new ArgumentNullException("absolutePath"); |
318 |
Contract.EndContractBlock(); |
319 |
|
320 |
ExecuteWithRetry((session, instance) => |
321 |
{ |
322 |
const string hqlUpdate = |
323 |
"update FileState set FileStatus= :fileStatus where FilePath = :path "; |
324 |
var updatedEntities = session.CreateQuery(hqlUpdate) |
325 |
.SetString("path", absolutePath) |
326 |
.SetEnum("fileStatus", fileStatus) |
327 |
.ExecuteUpdate(); |
328 |
return updatedEntities; |
329 |
}, null); |
330 |
|
331 |
} |
332 |
|
333 |
*/ |
334 |
public static void RenameState(string oldPath, string newPath) |
335 |
{ |
336 |
if (string.IsNullOrWhiteSpace(oldPath)) |
337 |
throw new ArgumentNullException("oldPath"); |
338 |
Contract.EndContractBlock(); |
339 |
|
340 |
ExecuteWithRetry((session, instance) => |
341 |
{ |
342 |
const string hqlUpdate = |
343 |
"update FileState set FilePath= :newPath where FilePath = :oldPath "; |
344 |
var updatedEntities = session.CreateQuery(hqlUpdate) |
345 |
.SetString("oldPath", oldPath) |
346 |
.SetString("newPath", newPath) |
347 |
.ExecuteUpdate(); |
348 |
return updatedEntities; |
349 |
}, null); |
350 |
|
351 |
} |
352 |
|
353 |
/* public static void UpdateStatus(Guid id, FileStatus fileStatus) |
354 |
{ |
355 |
|
356 |
ExecuteWithRetry((session, instance) => |
357 |
{ |
358 |
const string hqlUpdate = |
359 |
"update FileState set FileStatus= :fileStatus where Id = :id "; |
360 |
var updatedEntities = session.CreateQuery(hqlUpdate) |
361 |
.SetGuid("id", id) |
362 |
.SetEnum("fileStatus", fileStatus) |
363 |
.ExecuteUpdate(); |
364 |
return updatedEntities; |
365 |
}, null); |
366 |
}*/ |
367 |
|
368 |
public static void UpdateFileTreeHash(string absolutePath, TreeHash treeHash) |
369 |
{ |
370 |
if (string.IsNullOrWhiteSpace(absolutePath)) |
371 |
throw new ArgumentNullException("absolutePath"); |
372 |
Contract.EndContractBlock(); |
373 |
|
374 |
ExecuteWithRetry((session, instance) => |
375 |
{ |
376 |
|
377 |
var hashes=treeHash.ToJson(); |
378 |
const string hqlUpdate = "update FileState set Checksum= :checksum,ETag=:etag, Hashes=:hashes where FilePath = :path "; |
379 |
var updatedEntities = session.CreateQuery(hqlUpdate) |
380 |
.SetString("path", absolutePath) |
381 |
.SetString("checksum", treeHash.TopHash.ToHashString()) |
382 |
.SetString("etag", treeHash.MD5) |
383 |
.SetString("hashes",hashes) |
384 |
.ExecuteUpdate(); |
385 |
return updatedEntities; |
386 |
}, null); |
387 |
} |
388 |
|
389 |
public static void UpdateChecksum(string absolutePath, string etag, 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,ETag=:etag where FilePath = :path "; |
398 |
var updatedEntities = session.CreateQuery(hqlUpdate) |
399 |
.SetString("path", absolutePath) |
400 |
.SetString("checksum", checksum) |
401 |
.SetString("etag", etag) |
402 |
.ExecuteUpdate(); |
403 |
return updatedEntities; |
404 |
}, null); |
405 |
|
406 |
} |
407 |
|
408 |
|
409 |
public static void UpdateLastMD5(FileInfo file, string md5) |
410 |
{ |
411 |
if (file==null) |
412 |
throw new ArgumentNullException("file"); |
413 |
Contract.EndContractBlock(); |
414 |
|
415 |
ExecuteWithRetry((session, instance) => |
416 |
{ |
417 |
const string hqlUpdate = "update FileState set LastMD5=:md5, LastWriteDate=:date,LastLength=:length where FilePath = :path "; |
418 |
|
419 |
file.Refresh(); |
420 |
if (!file.Exists) |
421 |
return 0; |
422 |
var fullName = file.WithProperCapitalization().FullName; |
423 |
|
424 |
using (var tx=session.BeginTransaction()) |
425 |
{ |
426 |
var updatedEntities = session.CreateQuery(hqlUpdate) |
427 |
.SetDateTime("date", file.LastWriteTime) |
428 |
.SetInt64("length", file.Length) |
429 |
.SetString("md5", md5) |
430 |
.SetString("path", fullName) |
431 |
.ExecuteUpdate(); |
432 |
if (updatedEntities == 0) |
433 |
{ |
434 |
var newState = new FileState |
435 |
{ |
436 |
FilePath = fullName, |
437 |
Id = Guid.NewGuid(), |
438 |
OverlayStatus = FileOverlayStatus.Normal, |
439 |
FileStatus = FileStatus.Unchanged, |
440 |
IsFolder = false, |
441 |
LastLength = file.Length, |
442 |
LastWriteDate = file.LastWriteTime, |
443 |
LastMD5 = md5 ?? String.Empty, |
444 |
ETag = String.Empty |
445 |
}; |
446 |
newState.CreateAndFlush(); |
447 |
} |
448 |
tx.Commit(); |
449 |
return updatedEntities; |
450 |
} |
451 |
}, null); |
452 |
|
453 |
} |
454 |
|
455 |
public static void ChangeRootPath(string oldPath, string newPath) |
456 |
{ |
457 |
if (String.IsNullOrWhiteSpace(oldPath)) |
458 |
throw new ArgumentNullException("oldPath"); |
459 |
if (!Path.IsPathRooted(oldPath)) |
460 |
throw new ArgumentException("oldPath must be an absolute path", "oldPath"); |
461 |
if (string.IsNullOrWhiteSpace(newPath)) |
462 |
throw new ArgumentNullException("newPath"); |
463 |
if (!Path.IsPathRooted(newPath)) |
464 |
throw new ArgumentException("newPath must be an absolute path", "newPath"); |
465 |
Contract.EndContractBlock(); |
466 |
|
467 |
//Ensure the paths end with the same character |
468 |
if (!oldPath.EndsWith("\\")) |
469 |
oldPath = oldPath + "\\"; |
470 |
if (!newPath.EndsWith("\\")) |
471 |
newPath = newPath + "\\"; |
472 |
|
473 |
ExecuteWithRetry((session, instance) => |
474 |
{ |
475 |
const string hqlUpdate = |
476 |
"update FileState set FilePath = replace(FilePath,:oldPath,:newPath) where FilePath like :oldPath || '%' "; |
477 |
var renames = session.CreateQuery(hqlUpdate) |
478 |
.SetString("oldPath", oldPath) |
479 |
.SetString("newPath", newPath) |
480 |
.ExecuteUpdate(); |
481 |
return renames; |
482 |
}, null); |
483 |
} |
484 |
|
485 |
public static FileState CreateFor(FileSystemInfo info,IStatusNotification notification) |
486 |
{ |
487 |
if(info==null) |
488 |
throw new ArgumentNullException("info"); |
489 |
Contract.EndContractBlock(); |
490 |
|
491 |
if (info is DirectoryInfo) |
492 |
return new FileState |
493 |
{ |
494 |
FilePath = info.FullName, |
495 |
OverlayStatus = FileOverlayStatus.Unversioned, |
496 |
FileStatus = FileStatus.Created, |
497 |
ETag=String.Empty, |
498 |
LastMD5=String.Empty, |
499 |
Id = Guid.NewGuid() |
500 |
}; |
501 |
|
502 |
|
503 |
var etag = ((FileInfo)info).ComputeShortHash(notification); |
504 |
var fileState = new FileState |
505 |
{ |
506 |
FilePath = info.FullName, |
507 |
OverlayStatus = FileOverlayStatus.Unversioned, |
508 |
FileStatus = FileStatus.Created, |
509 |
ETag=etag, |
510 |
LastMD5=String.Empty, |
511 |
Id = Guid.NewGuid() |
512 |
}; |
513 |
return fileState; |
514 |
} |
515 |
|
516 |
|
517 |
private static void ExecuteWithRetry(NHibernateDelegate call, object state) |
518 |
{ |
519 |
int retries = 3; |
520 |
while (retries > 0) |
521 |
try |
522 |
{ |
523 |
using (new SessionScope()) |
524 |
{ |
525 |
Execute(call, state); |
526 |
return; |
527 |
} |
528 |
} |
529 |
catch (ActiveRecordException ) |
530 |
{ |
531 |
retries--; |
532 |
if (retries <= 0) |
533 |
throw; |
534 |
} |
535 |
} |
536 |
|
537 |
/// <summary> |
538 |
/// Mark Unversioned all FileState rows from the database whose path |
539 |
/// starts with one of the removed paths |
540 |
/// </summary> |
541 |
/// <param name="removed"></param> |
542 |
public static void UnversionPaths(List<string> removed) |
543 |
{ |
544 |
if (removed == null) |
545 |
return; |
546 |
if (removed.Count == 0) |
547 |
return; |
548 |
|
549 |
//Create a disjunction (list of OR statements |
550 |
var disjunction = new Disjunction(); |
551 |
foreach (var path in removed) |
552 |
{ |
553 |
//with the restriction FileState.FilePath like '@path%' |
554 |
disjunction.Add(Restrictions.On<FileState>(s => s.FilePath) |
555 |
.IsLike(path, MatchMode.Start)); |
556 |
} |
557 |
|
558 |
//Generate a query from the disjunction |
559 |
var query=QueryOver.Of<FileState>().Where(disjunction); |
560 |
|
561 |
ExecuteWithRetry((session,instance)=> |
562 |
{ |
563 |
using (var tx=session.BeginTransaction()) |
564 |
{ |
565 |
var states = query.GetExecutableQueryOver(session).List(); |
566 |
foreach (var state in states) |
567 |
{ |
568 |
state.FileStatus = FileStatus.Unversioned; |
569 |
state.OverlayStatus = FileOverlayStatus.Unversioned; |
570 |
state.Update(); |
571 |
} |
572 |
tx.Commit(); |
573 |
} |
574 |
return null; |
575 |
},null); |
576 |
} |
577 |
|
578 |
public string ToDebugString() |
579 |
{ |
580 |
return String.Format("[STATE] FilePath:[{0}] ObjectID:[{1}], ETAG: [{2}], LastMD5:[{3}]", FilePath, ObjectID, |
581 |
ETag, LastMD5); |
582 |
} |
583 |
} |
584 |
|
585 |
[ActiveRecord("Tags")] |
586 |
public class FileTag : ActiveRecordLinqBase<FileTag> |
587 |
{ |
588 |
[PrimaryKey] |
589 |
public int Id { get; set; } |
590 |
|
591 |
[Property] |
592 |
public string Name { get; set; } |
593 |
|
594 |
[Property] |
595 |
public string Value { get; set; } |
596 |
|
597 |
[BelongsTo("FileStateId")] |
598 |
public FileState FileState { get; set; } |
599 |
|
600 |
} |
601 |
|
602 |
} |