Statistics
| Branch: | Revision:

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
}