root / trunk / Pithos.Core / Agents / StatusAgent.cs @ cfb09103
History | View | Annotate | Download (44.8 kB)
1 |
#region |
---|---|
2 |
/* ----------------------------------------------------------------------- |
3 |
* <copyright file="StatusAgent.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; |
43 |
using System.Collections.Generic; |
44 |
using System.ComponentModel.Composition; |
45 |
using System.Data; |
46 |
using System.Data.SqlServerCe; |
47 |
using System.Diagnostics; |
48 |
using System.Diagnostics.Contracts; |
49 |
using System.IO; |
50 |
using System.Linq; |
51 |
using System.Reflection; |
52 |
using System.Threading; |
53 |
using NHibernate; |
54 |
using NHibernate.Cfg; |
55 |
using NHibernate.Cfg.MappingSchema; |
56 |
using NHibernate.Criterion; |
57 |
using NHibernate.Dialect; |
58 |
using NHibernate.Linq; |
59 |
using NHibernate.Mapping.ByCode; |
60 |
using NHibernate.Tool.hbm2ddl; |
61 |
using Pithos.Interfaces; |
62 |
using Pithos.Network; |
63 |
using log4net; |
64 |
using Environment = System.Environment; |
65 |
|
66 |
namespace Pithos.Core.Agents |
67 |
{ |
68 |
[Export(typeof(IStatusChecker)),Export(typeof(IStatusKeeper))] |
69 |
public class StatusAgent:IStatusChecker,IStatusKeeper |
70 |
{ |
71 |
private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); |
72 |
|
73 |
[System.ComponentModel.Composition.Import] |
74 |
public IPithosSettings Settings { get; set; } |
75 |
|
76 |
[System.ComponentModel.Composition.Import] |
77 |
public IStatusNotification StatusNotification { get; set; } |
78 |
|
79 |
//private Agent<Action> _persistenceAgent; |
80 |
|
81 |
|
82 |
|
83 |
public StatusAgent() |
84 |
{ |
85 |
var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); |
86 |
|
87 |
_pithosDataPath = Path.Combine(appDataPath, "GRNET\\PITHOS"); |
88 |
if (!Directory.Exists(_pithosDataPath)) |
89 |
Directory.CreateDirectory(_pithosDataPath); |
90 |
|
91 |
var dbPath = Path.Combine(_pithosDataPath, "pithos.sdf"); |
92 |
|
93 |
//MigrateOldDb(dbPath, appDataPath); |
94 |
|
95 |
|
96 |
var cfg = Configure(dbPath); |
97 |
|
98 |
var connectionString = "Data Source=" + dbPath; |
99 |
using (var sqlCeEngine = new SqlCeEngine(connectionString)) |
100 |
{ |
101 |
if (!File.Exists(dbPath)) |
102 |
{ |
103 |
sqlCeEngine.CreateDatabase(); |
104 |
new SchemaExport(cfg).Execute(true, true, false); |
105 |
_factory = cfg.BuildSessionFactory(); |
106 |
using (var session = _factory.OpenStatelessSession()) |
107 |
{ |
108 |
session.Insert(new PithosVersion {Id = 1, Version = "0.0.0.0"}); |
109 |
} |
110 |
} |
111 |
else |
112 |
{ |
113 |
try |
114 |
{ |
115 |
if (!sqlCeEngine.Verify(VerifyOption.Enhanced)) |
116 |
sqlCeEngine.Repair(connectionString, RepairOption.RecoverAllOrFail); |
117 |
} |
118 |
catch(SqlCeException ex) |
119 |
{ |
120 |
//Rethrow except for sharing errors while repairing |
121 |
if (ex.NativeError != 25035) |
122 |
throw; |
123 |
} |
124 |
var update = new SchemaUpdate(cfg); |
125 |
update.Execute(script=>Log.WarnFormat("[DBUPDATE] : {0}",script),true); |
126 |
_factory = cfg.BuildSessionFactory(); |
127 |
} |
128 |
UpgradeDatabase(); |
129 |
} |
130 |
} |
131 |
|
132 |
private void UpgradeDatabase() |
133 |
{ |
134 |
using (var session = _factory.OpenSession()) |
135 |
{ |
136 |
|
137 |
var storedVersion = session.Get<PithosVersion>(1); |
138 |
var actualVersion = Assembly.GetEntryAssembly().GetName().Version; |
139 |
|
140 |
if (actualVersion == new Version(storedVersion.Version)) |
141 |
return; |
142 |
|
143 |
storedVersion.Version = actualVersion.ToString(); |
144 |
session.Update(storedVersion); |
145 |
session.Flush(); |
146 |
} |
147 |
} |
148 |
|
149 |
|
150 |
private static Configuration Configure(string pithosDbPath) |
151 |
{ |
152 |
if (String.IsNullOrWhiteSpace(pithosDbPath)) |
153 |
throw new ArgumentNullException("pithosDbPath"); |
154 |
if (!Path.IsPathRooted(pithosDbPath)) |
155 |
throw new ArgumentException("path must be a rooted path", "pithosDbPath"); |
156 |
Contract.EndContractBlock(); |
157 |
|
158 |
|
159 |
var cfg = new Configuration(); |
160 |
cfg.DataBaseIntegration(db=> |
161 |
{ |
162 |
db.Dialect<MsSqlCe40Dialect>(); |
163 |
db.ConnectionString = "Data Source=" + pithosDbPath; |
164 |
db.AutoCommentSql = true; |
165 |
db.KeywordsAutoImport = Hbm2DDLKeyWords.AutoQuote; |
166 |
db.SchemaAction = SchemaAutoAction.Update; |
167 |
db.LogSqlInConsole = true; |
168 |
}) |
169 |
.SessionFactory() |
170 |
.GenerateStatistics() |
171 |
.Integrate.Schema |
172 |
.Updating(); |
173 |
var mapping = GetMapping(); |
174 |
cfg.AddMapping(mapping); |
175 |
|
176 |
return cfg; |
177 |
} |
178 |
|
179 |
private static HbmMapping GetMapping() |
180 |
{ |
181 |
var mapper = new ModelMapper(); |
182 |
mapper.Class<FileState>(fm => |
183 |
{ |
184 |
fm.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); |
185 |
fm.Property(x => x.ObjectID, m => |
186 |
{ |
187 |
m.Index("IX_FileState_ObjectID"); |
188 |
}); |
189 |
fm.Property(x => x.FilePath, m => |
190 |
{ |
191 |
m.Unique(true); |
192 |
m.UniqueKey("U_FileState_FilePath"); |
193 |
m.Index("IX_FileState_FilePath"); |
194 |
}); |
195 |
fm.Property(x => x.OverlayStatus); |
196 |
fm.Property(x => x.FileStatus); |
197 |
fm.Property(x => x.ConflictReason); |
198 |
fm.Property(x => x.Checksum, m => m.Length(64)); |
199 |
fm.Property(x => x.ETag, m => m.Length(64)); |
200 |
fm.Property(x => x.Hashes, m => m.Type(NHibernateUtil.StringClob)); |
201 |
fm.Property(x => x.LastWriteDate); |
202 |
fm.Property(x => x.LastLength); |
203 |
fm.Property(x => x.Version); |
204 |
fm.Property(x => x.VersionTimeStamp); |
205 |
fm.Property(x => x.IsShared); |
206 |
fm.Property(x => x.SharedBy); |
207 |
fm.Property(x => x.ShareWrite); |
208 |
fm.Property(x => x.IsFolder); |
209 |
fm.Property(x => x.Modified); |
210 |
}); |
211 |
mapper.Class<PithosVersion>(fm => |
212 |
{ |
213 |
fm.Id(x => x.Id, m => m.Generator(Generators.Assigned)); |
214 |
fm.Property(x => x.Version, m => m.Length(20)); |
215 |
}); |
216 |
|
217 |
|
218 |
var mapping = mapper.CompileMappingFor(new[] {typeof (FileState),typeof(PithosVersion)}); |
219 |
return mapping; |
220 |
} |
221 |
|
222 |
public void StartProcessing(CancellationToken token) |
223 |
{ |
224 |
|
225 |
|
226 |
} |
227 |
|
228 |
|
229 |
|
230 |
public void Stop() |
231 |
{ |
232 |
|
233 |
} |
234 |
|
235 |
|
236 |
public void ProcessExistingFiles(IEnumerable<FileInfo> existingFiles) |
237 |
{ |
238 |
if (existingFiles == null) |
239 |
throw new ArgumentNullException("existingFiles"); |
240 |
Contract.EndContractBlock(); |
241 |
|
242 |
//Find new or matching files with a left join to the stored states |
243 |
using (var session = _factory.OpenSession()) |
244 |
{ |
245 |
|
246 |
var fileStates = session.Query<FileState>().ToList(); |
247 |
var currentFiles = from file in existingFiles |
248 |
join state in fileStates on file.FullName.ToLower() equals state.FilePath.ToLower() |
249 |
into |
250 |
gs |
251 |
from substate in gs.DefaultIfEmpty() |
252 |
select Tuple.Create(file, substate); |
253 |
|
254 |
//To get the deleted files we must get the states that have no corresponding |
255 |
//files. |
256 |
//We can't use the File.Exists method inside a query, so we get all file paths from the states |
257 |
var statePaths = (from state in fileStates |
258 |
select new {state.Id, state.FilePath}).ToList(); |
259 |
//and check each one |
260 |
var missingStates = (from path in statePaths |
261 |
where !File.Exists(path.FilePath) && !Directory.Exists(path.FilePath) |
262 |
select path.Id).ToList(); |
263 |
//Finally, retrieve the states that correspond to the deleted files |
264 |
var deletedFiles = from state in fileStates |
265 |
where missingStates.Contains(state.Id) |
266 |
select Tuple.Create(default(FileInfo), state); |
267 |
|
268 |
var pairs = currentFiles.Union(deletedFiles).ToList(); |
269 |
|
270 |
i = 1; |
271 |
var total = pairs.Count; |
272 |
foreach (var pair in pairs) |
273 |
{ |
274 |
ProcessFile(session,total, pair); |
275 |
} |
276 |
session.Flush(); |
277 |
} |
278 |
} |
279 |
|
280 |
int i = 1; |
281 |
|
282 |
private void ProcessFile(ISession session,int total, System.Tuple<FileInfo, FileState> pair) |
283 |
{ |
284 |
var idx = Interlocked.Increment(ref i); |
285 |
using (StatusNotification.GetNotifier("Indexing file {0} of {1}", "Indexed file {0} of {1} ", true,idx, total)) |
286 |
{ |
287 |
var fileState = pair.Item2; |
288 |
var file = pair.Item1; |
289 |
if (fileState == null) |
290 |
{ |
291 |
//This is a new file |
292 |
var createState = FileState.CreateFor(file,StatusNotification); |
293 |
session.Save(createState); |
294 |
//_persistenceAgent.Post(createState.Create); |
295 |
} |
296 |
else if (file == null) |
297 |
{ |
298 |
//This file was deleted while we were down. We should mark it as deleted |
299 |
//We have to go through UpdateStatus here because the state object we are using |
300 |
//was created by a different ORM session. |
301 |
UpdateStatusDirect(session,fileState.Id, FileStatus.Deleted); |
302 |
//_persistenceAgent.Post(() => UpdateStatusDirect((Guid) fileState.Id, FileStatus.Deleted)); |
303 |
} |
304 |
//else |
305 |
//{ |
306 |
// //This file has a matching state. Need to check for possible changes |
307 |
// //To check for changes, we use the cheap (in CPU terms) MD5 algorithm |
308 |
// //on the entire file. |
309 |
|
310 |
// var hashString = file.ComputeShortHash(StatusNotification); |
311 |
// Debug.Assert(hashString.Length==32); |
312 |
|
313 |
|
314 |
// //TODO: Need a way to attach the hashes to the filestate so we don't |
315 |
// //recalculate them each time a call to calculate has is made |
316 |
// //We can either store them to the filestate or add them to a |
317 |
// //dictionary |
318 |
|
319 |
// //If the hashes don't match the file was changed |
320 |
// if (fileState.ETag != hashString) |
321 |
// { |
322 |
// _persistenceAgent.Post(() => UpdateStatusDirect((Guid) fileState.Id, FileStatus.Modified)); |
323 |
// } |
324 |
//} |
325 |
} |
326 |
} |
327 |
|
328 |
|
329 |
private int UpdateStatusDirect(ISession session,Guid id, FileStatus status) |
330 |
{ |
331 |
using (ThreadContext.Stacks["StatusAgent"].Push("UpdateStatusDirect")) |
332 |
{ |
333 |
|
334 |
try |
335 |
{ |
336 |
//var updatecmd = session.CreateSQLQuery( |
337 |
var updatecmd = session.CreateQuery( |
338 |
"update FileState set FileStatus= :fileStatus, Modified=:modified where Id = :id ") |
339 |
.SetGuid("id", id) |
340 |
.SetEnum("fileStatus", status) |
341 |
.SetDateTime("modified",DateTime.Now); |
342 |
var affected = updatecmd.ExecuteUpdate(); |
343 |
session.Flush(); |
344 |
return affected; |
345 |
} |
346 |
catch (Exception exc) |
347 |
{ |
348 |
Log.Error(exc.ToString()); |
349 |
throw; |
350 |
} |
351 |
} |
352 |
} |
353 |
|
354 |
|
355 |
public string BlockHash { get; set; } |
356 |
|
357 |
public int BlockSize { get; set; } |
358 |
|
359 |
public void ChangeRoots(string oldPath, string newPath) |
360 |
{ |
361 |
if (String.IsNullOrWhiteSpace(oldPath)) |
362 |
throw new ArgumentNullException("oldPath"); |
363 |
if (!Path.IsPathRooted(oldPath)) |
364 |
throw new ArgumentException("oldPath must be an absolute path", "oldPath"); |
365 |
if (String.IsNullOrWhiteSpace(newPath)) |
366 |
throw new ArgumentNullException("newPath"); |
367 |
if (!Path.IsPathRooted(newPath)) |
368 |
throw new ArgumentException("newPath must be an absolute path", "newPath"); |
369 |
Contract.EndContractBlock(); |
370 |
|
371 |
ChangeRootPath(oldPath,newPath); |
372 |
|
373 |
} |
374 |
|
375 |
|
376 |
|
377 |
private readonly string _pithosDataPath; |
378 |
private readonly ISessionFactory _factory; |
379 |
|
380 |
public FileState GetStateByFilePath(string path) |
381 |
{ |
382 |
if (String.IsNullOrWhiteSpace(path)) |
383 |
throw new ArgumentNullException("path"); |
384 |
if (!Path.IsPathRooted(path)) |
385 |
throw new ArgumentException("The path must be rooted", "path"); |
386 |
Contract.EndContractBlock(); |
387 |
|
388 |
try |
389 |
{ |
390 |
|
391 |
using(var session=_factory.OpenStatelessSession()) |
392 |
{ |
393 |
var state=session.Query<FileState>().SingleOrDefault(s => s.FilePath == path); |
394 |
if (state==null) |
395 |
return null; |
396 |
state.FilePath=state.FilePath??String.Empty; |
397 |
state.OverlayStatus = state.OverlayStatus ??FileOverlayStatus.Unversioned; |
398 |
state.FileStatus = state.FileStatus ?? FileStatus.Missing; |
399 |
state.Checksum = state.Checksum ?? String.Empty; |
400 |
state.ETag = state.ETag ?? String.Empty; |
401 |
state.SharedBy = state.SharedBy ?? String.Empty; |
402 |
return state; |
403 |
} |
404 |
|
405 |
} |
406 |
catch (Exception exc) |
407 |
{ |
408 |
Log.ErrorFormat(exc.ToString()); |
409 |
throw; |
410 |
} |
411 |
} |
412 |
|
413 |
public FileOverlayStatus GetFileOverlayStatus(string path) |
414 |
{ |
415 |
if (String.IsNullOrWhiteSpace(path)) |
416 |
throw new ArgumentNullException("path"); |
417 |
if (!Path.IsPathRooted(path)) |
418 |
throw new ArgumentException("The path must be rooted", "path"); |
419 |
Contract.EndContractBlock(); |
420 |
|
421 |
try |
422 |
{ |
423 |
|
424 |
using(var session=_factory.OpenStatelessSession()) |
425 |
{ |
426 |
return (from state in session.Query<FileState>() |
427 |
where state.FilePath == path |
428 |
select state.OverlayStatus) |
429 |
.Single() |
430 |
.GetValueOrDefault(FileOverlayStatus.Unversioned); |
431 |
} |
432 |
} |
433 |
catch (Exception exc) |
434 |
{ |
435 |
Log.ErrorFormat(exc.ToString()); |
436 |
return FileOverlayStatus.Unversioned; |
437 |
} |
438 |
} |
439 |
|
440 |
public void SetFileOverlayStatus(string path, FileOverlayStatus overlayStatus) |
441 |
{ |
442 |
if (String.IsNullOrWhiteSpace(path)) |
443 |
throw new ArgumentNullException("path"); |
444 |
if (!Path.IsPathRooted(path)) |
445 |
throw new ArgumentException("The path must be rooted","path"); |
446 |
Contract.EndContractBlock(); |
447 |
|
448 |
StoreOverlayStatus(path,overlayStatus); |
449 |
} |
450 |
|
451 |
public void SetFileState(string path, FileStatus fileStatus, FileOverlayStatus overlayStatus, string conflictReason) |
452 |
{ |
453 |
if (String.IsNullOrWhiteSpace(path)) |
454 |
throw new ArgumentNullException("path"); |
455 |
if (!Path.IsPathRooted(path)) |
456 |
throw new ArgumentException("The path must be rooted", "path"); |
457 |
Contract.EndContractBlock(); |
458 |
|
459 |
Debug.Assert(!path.Contains(FolderConstants.CacheFolder)); |
460 |
Debug.Assert(!path.EndsWith(".ignore")); |
461 |
using (ThreadContext.Stacks["StatusAgent"].Push("UpdateStatusDirect")) |
462 |
{ |
463 |
|
464 |
try |
465 |
{ |
466 |
|
467 |
using (var session = _factory.OpenSession()) |
468 |
using (var tx=session.BeginTransaction(IsolationLevel.ReadCommitted)) |
469 |
{ |
470 |
|
471 |
//var updatecmd = session.CreateSQLQuery("update FileState set OverlayStatus= :overlayStatus, FileStatus= :fileStatus,ConflictReason= :conflictReason where FilePath = :path ") |
472 |
var updatecmd = session.CreateQuery("update FileState set OverlayStatus= :overlayStatus, FileStatus= :fileStatus,ConflictReason= :conflictReason, Modified=:modified where FilePath = :path") |
473 |
.SetString("path", path) |
474 |
.SetEnum("fileStatus", fileStatus) |
475 |
.SetEnum("overlayStatus", overlayStatus) |
476 |
.SetString("conflictReason", conflictReason) |
477 |
.SetDateTime("modified",DateTime.Now); |
478 |
var affected = updatecmd.ExecuteUpdate(); |
479 |
|
480 |
if (affected == 0) |
481 |
{ |
482 |
//Can happen when downloading a new file |
483 |
var createdState = FileState.CreateFor(FileInfoExtensions.FromPath(path), StatusNotification); |
484 |
createdState.FileStatus = fileStatus; |
485 |
createdState.OverlayStatus = overlayStatus; |
486 |
createdState.ConflictReason = conflictReason; |
487 |
session.Save(createdState); |
488 |
//createdState.Create(); |
489 |
} |
490 |
session.Flush(); |
491 |
tx.Commit(); |
492 |
} |
493 |
} |
494 |
catch (Exception exc) |
495 |
{ |
496 |
Log.Error(exc.ToString()); |
497 |
throw; |
498 |
} |
499 |
} |
500 |
} |
501 |
|
502 |
|
503 |
public void StoreInfo(string path, ObjectInfo objectInfo, TreeHash treeHash) |
504 |
{ |
505 |
if (String.IsNullOrWhiteSpace(path)) |
506 |
throw new ArgumentNullException("path"); |
507 |
if (treeHash==null) |
508 |
throw new ArgumentNullException("treeHash"); |
509 |
if (!Path.IsPathRooted(path)) |
510 |
throw new ArgumentException("The path must be rooted", "path"); |
511 |
if (objectInfo == null) |
512 |
throw new ArgumentNullException("objectInfo", "objectInfo can't be empty"); |
513 |
Contract.EndContractBlock(); |
514 |
|
515 |
StoreInfoDirect(path, objectInfo, treeHash); |
516 |
|
517 |
} |
518 |
|
519 |
public void StoreInfo(string path, ObjectInfo objectInfo) |
520 |
{ |
521 |
if (String.IsNullOrWhiteSpace(path)) |
522 |
throw new ArgumentNullException("path"); |
523 |
if (!Path.IsPathRooted(path)) |
524 |
throw new ArgumentException("The path must be rooted", "path"); |
525 |
if (objectInfo == null) |
526 |
throw new ArgumentNullException("objectInfo", "objectInfo can't be empty"); |
527 |
Contract.EndContractBlock(); |
528 |
|
529 |
StoreInfoDirect(path, objectInfo, null); |
530 |
|
531 |
} |
532 |
|
533 |
private void StoreInfoDirect(string path, ObjectInfo objectInfo,TreeHash treeHash) |
534 |
{ |
535 |
try |
536 |
{ |
537 |
using (var session = _factory.OpenSession()) |
538 |
using (var tx=session.BeginTransaction(IsolationLevel.ReadCommitted)) |
539 |
{ |
540 |
|
541 |
//An entry for the new path may exist, |
542 |
IQuery deletecmd = session.CreateQuery( |
543 |
"delete from FileState where FilePath=:path and ObjectID is null") |
544 |
.SetString("path", path); |
545 |
deletecmd.ExecuteUpdate(); |
546 |
|
547 |
//string md5=treeHash.NullSafe(t=>t.MD5); |
548 |
string hashes = treeHash.NullSafe(t => t.ToJson()); |
549 |
|
550 |
var info = FileInfoExtensions.FromPath(path); |
551 |
var lastWriteTime = info.LastWriteTime; |
552 |
var isFolder = (info is DirectoryInfo); |
553 |
var lastLength = isFolder ? 0 : ((FileInfo) info).Length; |
554 |
|
555 |
var state = session.Query<FileState>().SingleOrDefault(s => s.ObjectID == (objectInfo.UUID??"?")) //Handle null UUIDs |
556 |
?? session.Query<FileState>().SingleOrDefault(s => s.FilePath == path) |
557 |
?? new FileState(); |
558 |
state.FilePath = path; |
559 |
state.IsFolder = isFolder; |
560 |
state.LastWriteDate = lastWriteTime; |
561 |
state.LastLength = lastLength; |
562 |
state.Checksum = objectInfo.X_Object_Hash; |
563 |
state.Hashes = hashes; |
564 |
state.Version = objectInfo.Version.GetValueOrDefault(); |
565 |
state.VersionTimeStamp = objectInfo.VersionTimestamp; |
566 |
state.ETag = objectInfo.ETag; |
567 |
state.FileStatus = FileStatus.Unchanged; |
568 |
state.OverlayStatus = FileOverlayStatus.Normal; |
569 |
state.ObjectID = objectInfo.UUID; |
570 |
state.Modified = DateTime.Now; |
571 |
session.SaveOrUpdate(state); |
572 |
|
573 |
|
574 |
|
575 |
session.Flush(); |
576 |
tx.Commit(); |
577 |
Log.ErrorFormat("DebugDB [{0}]:[{1}]\r\n{2}", path, objectInfo.UUID, objectInfo.X_Object_Hash); |
578 |
} |
579 |
} |
580 |
catch (Exception exc) |
581 |
{ |
582 |
Log.ErrorFormat("Failed to update [{0}]:[{1}]\r\n{2}",path,objectInfo.UUID, exc); |
583 |
throw; |
584 |
} |
585 |
} |
586 |
|
587 |
|
588 |
|
589 |
public void SetFileStatus(string path, FileStatus status) |
590 |
{ |
591 |
if (String.IsNullOrWhiteSpace(path)) |
592 |
throw new ArgumentNullException("path"); |
593 |
if (!Path.IsPathRooted(path)) |
594 |
throw new ArgumentException("The path must be rooted", "path"); |
595 |
Contract.EndContractBlock(); |
596 |
|
597 |
using (ThreadContext.Stacks["StatusAgent"].Push("UpdateStatusDirect")) |
598 |
{ |
599 |
|
600 |
try |
601 |
{ |
602 |
using (var session = _factory.OpenSession()) |
603 |
using (var tx=session.BeginTransaction(IsolationLevel.ReadCommitted)) |
604 |
{ |
605 |
|
606 |
//var updatecmd = session.CreateSQLQuery( |
607 |
var updatecmd = session.CreateQuery( |
608 |
"update FileState set FileStatus= :fileStatus, Modified=:modified where FilePath = :path ") |
609 |
.SetString("path", path) |
610 |
.SetEnum("fileStatus", status) |
611 |
.SetDateTime("modified",DateTime.Now); |
612 |
var affected = updatecmd.ExecuteUpdate(); |
613 |
|
614 |
if (affected == 0) |
615 |
{ |
616 |
var createdState = FileState.CreateFor(FileInfoExtensions.FromPath(path), StatusNotification); |
617 |
createdState.FileStatus = status; |
618 |
session.Save(createdState); |
619 |
} |
620 |
session.Flush(); |
621 |
tx.Commit(); |
622 |
} |
623 |
} |
624 |
catch (Exception exc) |
625 |
{ |
626 |
Log.Error(exc.ToString()); |
627 |
throw; |
628 |
} |
629 |
} |
630 |
} |
631 |
|
632 |
public FileStatus GetFileStatus(string path) |
633 |
{ |
634 |
if (String.IsNullOrWhiteSpace(path)) |
635 |
throw new ArgumentNullException("path"); |
636 |
if (!Path.IsPathRooted(path)) |
637 |
throw new ArgumentException("The path must be rooted", "path"); |
638 |
Contract.EndContractBlock(); |
639 |
|
640 |
|
641 |
using(var session=_factory.OpenStatelessSession()) |
642 |
return (from state in session.Query<FileState>() |
643 |
select state.FileStatus).SingleOrDefault()??FileStatus.Missing; |
644 |
} |
645 |
|
646 |
/// <summary> |
647 |
/// Deletes the status of the specified file |
648 |
/// </summary> |
649 |
/// <param name="path"></param> |
650 |
public void ClearFileStatus(string path) |
651 |
{ |
652 |
if (String.IsNullOrWhiteSpace(path)) |
653 |
throw new ArgumentNullException("path"); |
654 |
if (!Path.IsPathRooted(path)) |
655 |
throw new ArgumentException("The path must be rooted", "path"); |
656 |
Contract.EndContractBlock(); |
657 |
using(var session=_factory.OpenSession()) |
658 |
{ |
659 |
DeleteDirect(session,path); |
660 |
session.Flush(); |
661 |
} |
662 |
} |
663 |
|
664 |
/// <summary> |
665 |
/// Deletes the status of the specified folder and all its contents |
666 |
/// </summary> |
667 |
/// <param name="path"></param> |
668 |
public void ClearFolderStatus(string path) |
669 |
{ |
670 |
if (String.IsNullOrWhiteSpace(path)) |
671 |
throw new ArgumentNullException("path"); |
672 |
if (!Path.IsPathRooted(path)) |
673 |
throw new ArgumentException("The path must be rooted", "path"); |
674 |
Contract.EndContractBlock(); |
675 |
using (ThreadContext.Stacks["StatusAgent"].Push("DeleteDirect")) |
676 |
{ |
677 |
|
678 |
try |
679 |
{ |
680 |
using (var session = _factory.OpenSession()) |
681 |
{ |
682 |
DeleteDirect(session,path); |
683 |
session.Flush(); |
684 |
} |
685 |
} |
686 |
catch (Exception exc) |
687 |
{ |
688 |
Log.Error(exc.ToString()); |
689 |
throw; |
690 |
} |
691 |
} |
692 |
} |
693 |
|
694 |
public IEnumerable<FileState> GetChildren(FileState fileState) |
695 |
{ |
696 |
if (fileState == null) |
697 |
throw new ArgumentNullException("fileState"); |
698 |
Contract.EndContractBlock(); |
699 |
|
700 |
var session = _factory.GetCurrentSession(); |
701 |
var children = from state in session.Query<FileState>() |
702 |
where state.FilePath.StartsWith(fileState.FilePath + "\\") |
703 |
select state; |
704 |
return children; |
705 |
} |
706 |
|
707 |
public void EnsureFileState(string path) |
708 |
{ |
709 |
var existingState = GetStateByFilePath(path); |
710 |
if (existingState != null) |
711 |
return; |
712 |
var fileInfo = FileInfoExtensions.FromPath(path); |
713 |
|
714 |
using (var session=_factory.OpenSession()) |
715 |
{ |
716 |
var newState = FileState.CreateFor(fileInfo,StatusNotification); |
717 |
newState.FileStatus=FileStatus.Missing; |
718 |
session.SaveOrUpdate(newState); |
719 |
session.Flush(); |
720 |
//_persistenceAgent.PostAndAwait(newState.CreateAndFlush).Wait(); |
721 |
} |
722 |
|
723 |
} |
724 |
|
725 |
private void DeleteDirect(ISession session,string filePath) |
726 |
{ |
727 |
using (ThreadContext.Stacks["StatusAgent"].Push("DeleteDirect")) |
728 |
{ |
729 |
|
730 |
try |
731 |
{ |
732 |
var deletes= session.CreateQuery("delete from FileState where FilePath = :path") |
733 |
.SetParameter("path", filePath) |
734 |
.ExecuteUpdate(); |
735 |
} |
736 |
catch (Exception exc) |
737 |
{ |
738 |
Log.Error(exc.ToString()); |
739 |
throw; |
740 |
} |
741 |
} |
742 |
} |
743 |
|
744 |
|
745 |
|
746 |
|
747 |
public void CleanupOrphanStates() |
748 |
{ |
749 |
//Orphan states are those that do not correspond to an account, ie. their paths |
750 |
//do not start with the root path of any registered account |
751 |
|
752 |
var roots=(from account in Settings.Accounts |
753 |
select account.RootPath).ToList(); |
754 |
|
755 |
using (var session = _factory.OpenSession()) |
756 |
{ |
757 |
var allStates = from state in session.Query<FileState>() |
758 |
select state.FilePath; |
759 |
|
760 |
foreach (var statePath in allStates) |
761 |
{ |
762 |
if (!roots.Any(root => statePath.StartsWith(root, StringComparison.InvariantCultureIgnoreCase))) |
763 |
this.DeleteDirect(session,statePath); |
764 |
} |
765 |
session.Flush(); |
766 |
} |
767 |
} |
768 |
|
769 |
public void SaveCopy<T>(T state) where T:class |
770 |
{ |
771 |
using (var session = _factory.OpenSession()) |
772 |
{ |
773 |
session.Merge(state); |
774 |
session.Flush(); |
775 |
} |
776 |
} |
777 |
|
778 |
public void CleanupStaleStates(AccountInfo accountInfo, List<ObjectInfo> objectInfos) |
779 |
{ |
780 |
if (accountInfo == null) |
781 |
throw new ArgumentNullException("accountInfo"); |
782 |
if (objectInfos == null) |
783 |
throw new ArgumentNullException("objectInfos"); |
784 |
Contract.EndContractBlock(); |
785 |
|
786 |
|
787 |
|
788 |
//Stale states are those that have no corresponding local or server file |
789 |
|
790 |
|
791 |
var agent=FileAgent.GetFileAgent(accountInfo); |
792 |
|
793 |
var localFiles=agent.EnumerateFiles(); |
794 |
var localSet = new HashSet<string>(localFiles); |
795 |
|
796 |
//RelativeUrlToFilePath will fail for |
797 |
//infos of accounts, containers which have no Name |
798 |
|
799 |
var serverFiles = from info in objectInfos |
800 |
where info.Name != null |
801 |
select Path.Combine(accountInfo.AccountPath,info.RelativeUrlToFilePath(accountInfo.UserName)); |
802 |
var serverSet = new HashSet<string>(serverFiles); |
803 |
|
804 |
using (var session = _factory.OpenSession()) |
805 |
{ |
806 |
|
807 |
var allStates = from state in session.Query<FileState>() |
808 |
where state.FilePath.StartsWith(agent.RootPath) |
809 |
select state.FilePath; |
810 |
var stateSet = new HashSet<string>(allStates); |
811 |
stateSet.ExceptWith(serverSet); |
812 |
stateSet.ExceptWith(localSet); |
813 |
|
814 |
foreach (var remainder in stateSet) |
815 |
{ |
816 |
DeleteDirect(session,remainder); |
817 |
} |
818 |
session.Flush(); |
819 |
} |
820 |
} |
821 |
|
822 |
public static TreeHash CalculateTreeHash(FileSystemInfo fileInfo, AccountInfo accountInfo, FileState fileState, byte hashingParallelism, CancellationToken cancellationToken, IProgress<HashProgress> progress) |
823 |
{ |
824 |
fileInfo.Refresh(); |
825 |
//If the file doesn't exist, return the empty treehash |
826 |
if (!fileInfo.Exists) |
827 |
return TreeHash.Empty; |
828 |
|
829 |
//FileState may be null if there is no stored state for this file |
830 |
//if (fileState==null) |
831 |
return Signature.CalculateTreeHashAsync(fileInfo, |
832 |
accountInfo.BlockSize, |
833 |
accountInfo.BlockHash, |
834 |
hashingParallelism, |
835 |
cancellationToken, progress); |
836 |
//Can we use the stored hashes? |
837 |
//var localTreeHash = fileState.LastMD5 == Signature.CalculateMD5(fileInfo) |
838 |
// ? TreeHash.Parse(fileState.Hashes) |
839 |
// : Signature.CalculateTreeHashAsync(fileInfo, |
840 |
// accountInfo.BlockSize, |
841 |
// accountInfo.BlockHash, |
842 |
// hashingParallelism, |
843 |
// cancellationToken, progress); |
844 |
//return localTreeHash; |
845 |
} |
846 |
|
847 |
|
848 |
|
849 |
private object ExecuteWithRetry(Func<ISession, object, object> call, object state) |
850 |
{ |
851 |
int retries = 3; |
852 |
while (retries > 0) |
853 |
try |
854 |
{ |
855 |
using (var session=_factory.OpenSession()) |
856 |
{ |
857 |
var result=call(session, state); |
858 |
session.Flush(); |
859 |
return result; |
860 |
} |
861 |
} |
862 |
catch (Exception/* ActiveRecordException */) |
863 |
{ |
864 |
retries--; |
865 |
if (retries <= 0) |
866 |
throw; |
867 |
} |
868 |
return null; |
869 |
} |
870 |
|
871 |
//TODO: Must separate between UpdateChecksum and UpdateFileHashes |
872 |
|
873 |
public void ChangeRootPath(string oldPath, string newPath) |
874 |
{ |
875 |
if (String.IsNullOrWhiteSpace(oldPath)) |
876 |
throw new ArgumentNullException("oldPath"); |
877 |
if (!Path.IsPathRooted(oldPath)) |
878 |
throw new ArgumentException("oldPath must be an absolute path", "oldPath"); |
879 |
if (string.IsNullOrWhiteSpace(newPath)) |
880 |
throw new ArgumentNullException("newPath"); |
881 |
if (!Path.IsPathRooted(newPath)) |
882 |
throw new ArgumentException("newPath must be an absolute path", "newPath"); |
883 |
Contract.EndContractBlock(); |
884 |
|
885 |
//Ensure the paths end with the same character |
886 |
if (!oldPath.EndsWith("\\")) |
887 |
oldPath = oldPath + "\\"; |
888 |
if (!newPath.EndsWith("\\")) |
889 |
newPath = newPath + "\\"; |
890 |
|
891 |
ExecuteWithRetry((session, instance) => |
892 |
{ |
893 |
const string hqlUpdate = |
894 |
"update FileState set FilePath = replace(FilePath,:oldPath,:newPath), Modified=:modified where FilePath like :oldPath || '%' "; |
895 |
var renames = session.CreateQuery(hqlUpdate) |
896 |
.SetString("oldPath", oldPath) |
897 |
.SetString("newPath", newPath) |
898 |
.SetDateTime("modified",DateTime.Now) |
899 |
.ExecuteUpdate(); |
900 |
return renames; |
901 |
}, null); |
902 |
} |
903 |
|
904 |
|
905 |
/// <summary> |
906 |
/// Mark Unversioned all FileState rows from the database whose path |
907 |
/// starts with one of the removed paths |
908 |
/// </summary> |
909 |
/// <param name="removed"></param> |
910 |
public void UnversionPaths(List<string> removed) |
911 |
{ |
912 |
if (removed == null) |
913 |
return; |
914 |
if (removed.Count == 0) |
915 |
return; |
916 |
|
917 |
//Create a disjunction (list of OR statements |
918 |
var disjunction = new Disjunction(); |
919 |
foreach (var path in removed) |
920 |
{ |
921 |
//with the restriction FileState.FilePath like '@path%' |
922 |
disjunction.Add(Restrictions.On<FileState>(s => s.FilePath) |
923 |
.IsLike(path, MatchMode.Start)); |
924 |
} |
925 |
|
926 |
//Generate a query from the disjunction |
927 |
var query = QueryOver.Of<FileState>().Where(disjunction); |
928 |
|
929 |
ExecuteWithRetry((session, instance) => |
930 |
{ |
931 |
using (var tx = session.BeginTransaction()) |
932 |
{ |
933 |
var states = query.GetExecutableQueryOver(session).List(); |
934 |
foreach (var state in states) |
935 |
{ |
936 |
state.FileStatus = FileStatus.Unversioned; |
937 |
state.OverlayStatus = FileOverlayStatus.Unversioned; |
938 |
session.Update(session); |
939 |
} |
940 |
tx.Commit(); |
941 |
} |
942 |
return null; |
943 |
}, null); |
944 |
} |
945 |
|
946 |
public List<FileState> GetAllStates() |
947 |
{ |
948 |
using(var session=_factory.OpenSession()) |
949 |
{ |
950 |
return session.Query<FileState>().ToList(); |
951 |
} |
952 |
} |
953 |
|
954 |
public List<string> GetAllStatePaths() |
955 |
{ |
956 |
using (var session = _factory.OpenSession()) |
957 |
{ |
958 |
return session.Query<FileState>().Select(state => state.FilePath).ToList(); |
959 |
} |
960 |
} |
961 |
|
962 |
public List<FileState> GetConflictStates() |
963 |
{ |
964 |
using (var session = _factory.OpenSession()) |
965 |
{ |
966 |
var fileStates = from state in session.Query<FileState>() |
967 |
where state.FileStatus == FileStatus.Conflict || |
968 |
state.OverlayStatus == FileOverlayStatus.Conflict |
969 |
select state; |
970 |
return fileStates.ToList(); |
971 |
} |
972 |
} |
973 |
|
974 |
public void UpdateFileChecksum(string path, string etag, TreeHash treeHash) |
975 |
{ |
976 |
if (String.IsNullOrWhiteSpace(path)) |
977 |
throw new ArgumentNullException("path"); |
978 |
if (!Path.IsPathRooted(path)) |
979 |
throw new ArgumentException("The path must be rooted", "path"); |
980 |
Contract.EndContractBlock(); |
981 |
|
982 |
if (string.IsNullOrWhiteSpace(path)) |
983 |
throw new ArgumentNullException("absolutePath"); |
984 |
Contract.EndContractBlock(); |
985 |
|
986 |
var hashes = treeHash.ToJson(); |
987 |
var topHash = treeHash.TopHash.ToHashString(); |
988 |
|
989 |
ExecuteWithRetry((session, instance) => |
990 |
{ |
991 |
const string hqlUpdate = "update FileState set Checksum= :checksum,Hashes=:hashes,ETag=:etag, Modified=:modified where FilePath = :path "; |
992 |
var updatedEntities = session.CreateQuery(hqlUpdate) |
993 |
.SetString("path", path) |
994 |
.SetString("checksum", topHash) |
995 |
.SetString("hashes", hashes) |
996 |
.SetString("etag", etag) |
997 |
.SetDateTime("modified", DateTime.Now) |
998 |
.ExecuteUpdate(); |
999 |
return updatedEntities; |
1000 |
}, null); |
1001 |
} |
1002 |
|
1003 |
//Store only the hashes |
1004 |
public void UpdateFileHashes(string absolutePath, TreeHash treeHash) |
1005 |
{ |
1006 |
if (string.IsNullOrWhiteSpace(absolutePath)) |
1007 |
throw new ArgumentNullException("absolutePath"); |
1008 |
Contract.EndContractBlock(); |
1009 |
|
1010 |
var hashes = treeHash.ToJson(); |
1011 |
var topHash = treeHash.TopHash.ToHashString(); |
1012 |
|
1013 |
ExecuteWithRetry((session, instance) => |
1014 |
{ |
1015 |
|
1016 |
const string hqlUpdate = "update FileState set Hashes=:hashes,Modified=:modified where FilePath = :path "; |
1017 |
/* |
1018 |
const string hqlUpdate = "update FileState set Checksum= :checksum,Hashes=:hashes where FilePath = :path "; |
1019 |
*/ |
1020 |
var updatedEntities = session.CreateQuery(hqlUpdate) |
1021 |
.SetString("path", absolutePath) |
1022 |
//.SetString("checksum", topHash) |
1023 |
// .SetString("md5",treeHash.MD5) |
1024 |
.SetString("hashes", hashes) |
1025 |
.SetDateTime("modified", DateTime.Now) |
1026 |
.ExecuteUpdate(); |
1027 |
return updatedEntities; |
1028 |
}, null); |
1029 |
} |
1030 |
|
1031 |
|
1032 |
public void RenameState(string oldPath, string newPath) |
1033 |
{ |
1034 |
if (string.IsNullOrWhiteSpace(oldPath)) |
1035 |
throw new ArgumentNullException("oldPath"); |
1036 |
Contract.EndContractBlock(); |
1037 |
|
1038 |
ExecuteWithRetry((session, instance) => |
1039 |
{ |
1040 |
const string hqlUpdate = |
1041 |
"update FileState set FilePath= :newPath, Modified=:modified where FilePath = :oldPath "; |
1042 |
var updatedEntities = session.CreateQuery(hqlUpdate) |
1043 |
.SetString("oldPath", oldPath) |
1044 |
.SetString("newPath", newPath) |
1045 |
.SetDateTime("modified", DateTime.Now) |
1046 |
.ExecuteUpdate(); |
1047 |
return updatedEntities; |
1048 |
}, null); |
1049 |
|
1050 |
} |
1051 |
|
1052 |
public void StoreOverlayStatus(string absolutePath, FileOverlayStatus newStatus) |
1053 |
{ |
1054 |
if (string.IsNullOrWhiteSpace(absolutePath)) |
1055 |
throw new ArgumentNullException("absolutePath"); |
1056 |
Contract.EndContractBlock(); |
1057 |
|
1058 |
using(var session=_factory.OpenSession()) |
1059 |
{ |
1060 |
using (var tx = session.BeginTransaction()) |
1061 |
{ |
1062 |
var state = session.Query<FileState>().SingleOrDefault(s => s.FilePath == absolutePath) |
1063 |
?? new FileState{ |
1064 |
FilePath = absolutePath, |
1065 |
OverlayStatus = newStatus, |
1066 |
ETag = Signature.MERKLE_EMPTY, |
1067 |
//LastMD5=String.Empty, |
1068 |
IsFolder = Directory.Exists(absolutePath), |
1069 |
Modified=DateTime.Now |
1070 |
}; |
1071 |
state.OverlayStatus = newStatus; |
1072 |
session.SaveOrUpdate(state); |
1073 |
session.Flush(); |
1074 |
tx.Commit(); |
1075 |
} |
1076 |
}; |
1077 |
} |
1078 |
|
1079 |
} |
1080 |
|
1081 |
|
1082 |
} |