Statistics
| Branch: | Revision:

root / trunk / Pithos.Core / Agents / StatusAgent.cs @ 0397f6ec

History | View | Annotate | Download (39.1 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.SQLite;
47
using System.Diagnostics;
48
using System.Diagnostics.Contracts;
49
using System.IO;
50
using System.Linq;
51
using System.Reflection;
52
using System.Security.Cryptography;
53
using System.Text;
54
using System.Threading;
55
using System.Threading.Tasks;
56
using Castle.ActiveRecord;
57
using Castle.ActiveRecord.Framework;
58
using Castle.ActiveRecord.Framework.Config;
59
using Castle.ActiveRecord.Queries;
60
using NHibernate;
61
using NHibernate.ByteCode.Castle;
62
using NHibernate.Cfg;
63
using NHibernate.Cfg.Loquacious;
64
using NHibernate.Dialect;
65
using NHibernate.Exceptions;
66
using Pithos.Interfaces;
67
using Pithos.Network;
68
using log4net;
69
using Environment = System.Environment;
70

    
71
namespace Pithos.Core.Agents
72
{
73
    [Export(typeof(IStatusChecker)),Export(typeof(IStatusKeeper))]
74
    public class StatusAgent:IStatusChecker,IStatusKeeper
75
    {
76
        private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
77

    
78
        [System.ComponentModel.Composition.Import]
79
        public IPithosSettings Settings { get; set; }
80

    
81
        [System.ComponentModel.Composition.Import]
82
        public IStatusNotification StatusNotification { get; set; }
83

    
84
        private Agent<Action> _persistenceAgent;
85

    
86

    
87

    
88
        public StatusAgent()
89
        {            
90
            var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
91

    
92
            _pithosDataPath = Path.Combine(appDataPath , "GRNET\\PITHOS");
93
            if (!Directory.Exists(_pithosDataPath))
94
                Directory.CreateDirectory(_pithosDataPath);
95

    
96
            var dbPath = Path.Combine(_pithosDataPath, "pithos.db");
97

    
98
            MigrateOldDb(dbPath, appDataPath);
99

    
100

    
101
            var source = GetConfiguration(_pithosDataPath);
102
            ActiveRecordStarter.Initialize(source,typeof(FileState),typeof(FileTag));
103
            
104
            ActiveRecordStarter.UpdateSchema();
105

    
106

    
107
            if (!File.Exists(dbPath))
108
                ActiveRecordStarter.CreateSchema();
109

    
110
            CreateTrigger();
111
            
112
        }
113

    
114

    
115
        private static void MigrateOldDb(string dbPath, string appDataPath)
116
        {
117
            if(String.IsNullOrWhiteSpace(dbPath))
118
                throw new ArgumentNullException("dbPath");
119
            if(String.IsNullOrWhiteSpace(appDataPath))
120
                throw new ArgumentNullException("appDataPath");
121
            Contract.EndContractBlock();
122

    
123
            var oldDbPath = Path.Combine(appDataPath, "Pithos", "pithos.db");
124
            var oldDbInfo = new FileInfo(oldDbPath);
125
            if (oldDbInfo.Exists && !File.Exists(dbPath))
126
            {
127
                Log.InfoFormat("Moving database from {0} to {1}",oldDbInfo.FullName,dbPath);
128
                var oldDirectory = oldDbInfo.Directory;
129
                oldDbInfo.MoveTo(dbPath);
130
                
131
                if (Log.IsDebugEnabled)
132
                    Log.DebugFormat("Deleting {0}",oldDirectory.FullName);
133
                
134
                oldDirectory.Delete(true);
135
            }
136
        }
137

    
138
        private void CreateTrigger()
139
        {
140
            using (var connection = GetConnection())
141
            using (var triggerCommand = connection.CreateCommand())
142
            {
143
                var cmdText = new StringBuilder()
144
                    .AppendLine("CREATE TRIGGER IF NOT EXISTS update_last_modified UPDATE ON FileState FOR EACH ROW")
145
                    .AppendLine("BEGIN")
146
                    .AppendLine("UPDATE FileState SET Modified=datetime('now')  WHERE Id=old.Id;")
147
                    .AppendLine("END;")
148
                    .AppendLine("CREATE TRIGGER IF NOT EXISTS insert_last_modified INSERT ON FileState FOR EACH ROW")
149
                    .AppendLine("BEGIN")
150
                    .AppendLine("UPDATE FileState SET Modified=datetime('now')  WHERE Id=new.Id;")
151
                    .AppendLine("END;")
152
                    .ToString();
153
                triggerCommand.CommandText = cmdText;                
154
                triggerCommand.ExecuteNonQuery();
155
            }
156
        }
157

    
158

    
159
        private static InPlaceConfigurationSource GetConfiguration(string pithosDbPath)
160
        {
161
            if (String.IsNullOrWhiteSpace(pithosDbPath))
162
                throw new ArgumentNullException("pithosDbPath");
163
            if (!Path.IsPathRooted(pithosDbPath))
164
                throw new ArgumentException("path must be a rooted path", "pithosDbPath");
165
            Contract.EndContractBlock();
166

    
167
            var properties = new Dictionary<string, string>
168
                                 {
169
                                     {"connection.driver_class", "NHibernate.Driver.SQLite20Driver"},
170
                                     {"dialect", "NHibernate.Dialect.SQLiteDialect"},
171
                                     {"connection.provider", "NHibernate.Connection.DriverConnectionProvider"},
172
                                     {
173
                                         "proxyfactory.factory_class",
174
                                         "NHibernate.ByteCode.Castle.ProxyFactoryFactory, NHibernate.ByteCode.Castle"
175
                                         },
176
                                 };
177

    
178
            var connectionString = String.Format(@"Data Source={0}\pithos.db;Version=3;Enlist=N", pithosDbPath);
179
            properties.Add("connection.connection_string", connectionString);
180

    
181
            var source = new InPlaceConfigurationSource();                        
182
            source.Add(typeof (ActiveRecordBase), properties);
183
            source.SetDebugFlag(false);            
184
            return source;
185
        }
186

    
187
        public void StartProcessing(CancellationToken token)
188
        {
189
            _persistenceAgent = Agent<Action>.Start(queue =>
190
            {
191
                Action loop = null;
192
                loop = () =>
193
                {
194
                    var job = queue.Receive();
195
                    job.ContinueWith(t =>
196
                    {
197
                        var action = job.Result;
198
                        try
199
                        {
200
                            action();
201
                        }
202
                        catch (SQLiteException ex)
203
                        {
204
                            Log.ErrorFormat("[ERROR] SQL \n{0}", ex);
205
                        }
206
                        catch (Exception ex)
207
                        {
208
                            Log.ErrorFormat("[ERROR] STATE \n{0}", ex);
209
                        }
210
                        queue.NotifyComplete(action);
211
// ReSharper disable AccessToModifiedClosure
212
                        queue.DoAsync(loop);
213
// ReSharper restore AccessToModifiedClosure
214
                    });
215
                };
216
                loop();
217
            });
218
            
219
        }
220

    
221
       
222

    
223
        public void Stop()
224
        {
225
            _persistenceAgent.Stop();            
226
        }
227

    
228

    
229
        public void ProcessExistingFiles(IEnumerable<FileInfo> existingFiles)
230
        {
231
            if (existingFiles == null)
232
                throw new ArgumentNullException("existingFiles");
233
            Contract.EndContractBlock();
234

    
235
            //Find new or matching files with a left join to the stored states
236
            var fileStates = FileState.Queryable.ToList();
237
            var currentFiles = from file in existingFiles
238
                               join state in fileStates on file.FullName.ToLower() equals state.FilePath.ToLower() into
239
                                   gs
240
                               from substate in gs.DefaultIfEmpty()
241
                               select Tuple.Create(file, substate);
242

    
243
            //To get the deleted files we must get the states that have no corresponding
244
            //files. 
245
            //We can't use the File.Exists method inside a query, so we get all file paths from the states
246
            var statePaths = (from state in fileStates
247
                              select new {state.Id, state.FilePath}).ToList();
248
            //and check each one
249
            var missingStates = (from path in statePaths
250
                                 where !File.Exists(path.FilePath) && !Directory.Exists(path.FilePath)
251
                                 select path.Id).ToList();
252
            //Finally, retrieve the states that correspond to the deleted files            
253
            var deletedFiles = from state in fileStates
254
                               where missingStates.Contains(state.Id)
255
                               select Tuple.Create(default(FileInfo), state);
256

    
257
            var pairs = currentFiles.Union(deletedFiles).ToList();
258

    
259
            i = 1;
260
            var total = pairs.Count;
261
            foreach (var pair in pairs)
262
            {
263
                ProcessFile(total, pair);
264
            }
265
        }
266

    
267
        int i = 1;
268

    
269
        private void ProcessFile(int total, Tuple<FileInfo,FileState> pair)
270
        {
271
            var idx = Interlocked.Increment(ref i);
272
            using (StatusNotification.GetNotifier("Indexing file {0} of {1}", "Indexed file {0} of {1} ", idx, total))
273
            {
274
                var fileState = pair.Item2;
275
                var file = pair.Item1;
276
                if (fileState == null)
277
                {
278
                    //This is a new file                        
279
                    var createState = FileState.CreateFor(file,StatusNotification);
280
                    _persistenceAgent.Post(createState.Create);
281
                }
282
                else if (file == null)
283
                {
284
                    //This file was deleted while we were down. We should mark it as deleted
285
                    //We have to go through UpdateStatus here because the state object we are using
286
                    //was created by a different ORM session.
287
                    _persistenceAgent.Post(() => UpdateStatusDirect((Guid) fileState.Id, FileStatus.Deleted));
288
                }
289
                else
290
                {
291
                    //This file has a matching state. Need to check for possible changes
292
                    //To check for changes, we use the cheap (in CPU terms) MD5 algorithm
293
                    //on the entire file.
294

    
295
                    var hashString = file.ComputeShortHash(StatusNotification);
296
                    Debug.Assert(hashString.Length==32);
297

    
298

    
299
                    //TODO: Need a way to attach the hashes to the filestate so we don't
300
                    //recalculate them each time a call to calculate has is made
301
                    //We can either store them to the filestate or add them to a 
302
                    //dictionary
303

    
304
                    //If the hashes don't match the file was changed
305
                    if (fileState.ETag != hashString)
306
                    {
307
                        _persistenceAgent.Post(() => UpdateStatusDirect((Guid) fileState.Id, FileStatus.Modified));
308
                    }
309
                }
310
            }
311
        }
312

    
313

    
314
        private int UpdateStatusDirect(Guid id, FileStatus status)
315
        {
316
            using (log4net.ThreadContext.Stacks["StatusAgent"].Push("UpdateStatusDirect"))
317
            {
318

    
319
                try
320
                {
321
                    using (var session = ActiveRecordMediator.GetSessionFactoryHolder().CreateSession(typeof(FileState)))
322
                    {
323
                        var walquery = session.CreateSQLQuery("PRAGMA journal_mode=WAL");
324
                        walquery.List();
325

    
326
                          //var updatecmd = session.CreateSQLQuery(
327
                        var updatecmd = session.CreateQuery(
328
                            "update FileState set FileStatus= :fileStatus where Id = :id  ")
329
                            .SetGuid("id", id)
330
                            .SetEnum("fileStatus", status);
331
                        var affected = updatecmd.ExecuteUpdate();
332
                        
333
                        return affected;
334
                    }
335

    
336
                }
337
                catch (Exception exc)
338
                {
339
                    Log.Error(exc.ToString());
340
                    throw;
341
                }
342
            }
343
        }
344
        
345
        private int UpdateStatusDirect(string path, FileStatus status)
346
        {
347
            using (log4net.ThreadContext.Stacks["StatusAgent"].Push("UpdateStatusDirect"))
348
            {
349

    
350
                try
351
                {                    
352
                    using (var session = ActiveRecordMediator.GetSessionFactoryHolder().CreateSession(typeof(FileState)))
353
                    using (var tx=session.BeginTransaction(IsolationLevel.ReadCommitted))
354
                    {
355

    
356
                        //var walquery = session.CreateSQLQuery("PRAGMA journal_mode=WAL");
357
                        var walquery = session.CreateQuery("PRAGMA journal_mode=WAL");
358
                        walquery.List();
359

    
360
                        //var updatecmd = session.CreateSQLQuery(
361
                        var updatecmd = session.CreateQuery(
362
                            "update FileState set FileStatus= :fileStatus where FilePath = :path COLLATE NOCASE")
363
                            .SetString("path", path)
364
                            .SetEnum("fileStatus", status);
365
                        var affected = updatecmd.ExecuteUpdate();
366

    
367
                        if (affected == 0)
368
                        {                            
369
                            var createdState = FileState.CreateFor(FileInfoExtensions.FromPath(path), StatusNotification);
370
                            createdState.FileStatus = status;
371
                            session.Save(createdState);
372
                        }
373
                        tx.Commit();
374
                        return affected;
375
                    }
376
                }
377
                catch (Exception exc)
378
                {
379
                    Log.Error(exc.ToString());
380
                    throw;
381
                }
382
            }
383
        }
384

    
385
        private int UpdateStatusDirect(string absolutePath, FileStatus fileStatus, FileOverlayStatus overlayStatus, string conflictReason)
386
        {
387
            using (log4net.ThreadContext.Stacks["StatusAgent"].Push("UpdateStatusDirect"))
388
            {
389

    
390
                try
391
                {
392

    
393
                    using (var session = ActiveRecordMediator.GetSessionFactoryHolder().CreateSession(typeof(FileState)))
394
                    using (var tx=session.BeginTransaction(IsolationLevel.ReadCommitted))
395
                    {
396

    
397
                        //var walquery = session.CreateSQLQuery("PRAGMA journal_mode=WAL");
398
                        var walquery = session.CreateSQLQuery("PRAGMA journal_mode=WAL");
399
                        walquery.List();
400

    
401

    
402
                        //var updatecmd = session.CreateSQLQuery("update FileState set OverlayStatus= :overlayStatus, FileStatus= :fileStatus,ConflictReason= :conflictReason where FilePath = :path COLLATE NOCASE")
403
                        var updatecmd = session.CreateQuery("update FileState set OverlayStatus= :overlayStatus, FileStatus= :fileStatus,ConflictReason= :conflictReason where FilePath = :path")
404
                                                .SetString("path", absolutePath)
405
                                                .SetEnum("fileStatus", fileStatus)
406
                                                .SetEnum("overlayStatus", overlayStatus)
407
                                                .SetString("conflictReason", conflictReason);
408
                        var affected = updatecmd.ExecuteUpdate();
409

    
410
                        if (affected == 0)
411
                        {
412
                            var createdState = FileState.CreateFor(FileInfoExtensions.FromPath(absolutePath), StatusNotification);
413
                            createdState.FileStatus = fileStatus;
414
                            createdState.OverlayStatus = overlayStatus;
415
                            createdState.ConflictReason = conflictReason;
416
                            createdState.LastMD5 = String.Empty;
417
                            session.Save(createdState);
418
                            //createdState.Create();
419
                        }
420
                        tx.Commit();
421
                        return affected;
422
                    }
423
                }
424
                catch (Exception exc)
425
                {
426
                    Log.Error(exc.ToString());
427
                    throw;
428
                }
429
            }
430
        }
431
        
432

    
433

    
434
        public string BlockHash { get; set; }
435

    
436
        public int BlockSize { get; set; }
437
        public void ChangeRoots(string oldPath, string newPath)
438
        {
439
            if (String.IsNullOrWhiteSpace(oldPath))
440
                throw new ArgumentNullException("oldPath");
441
            if (!Path.IsPathRooted(oldPath))
442
                throw new ArgumentException("oldPath must be an absolute path", "oldPath");
443
            if (string.IsNullOrWhiteSpace(newPath))
444
                throw new ArgumentNullException("newPath");
445
            if (!Path.IsPathRooted(newPath))
446
                throw new ArgumentException("newPath must be an absolute path", "newPath");
447
            Contract.EndContractBlock();
448

    
449
            FileState.ChangeRootPath(oldPath,newPath);
450

    
451
        }
452

    
453

    
454

    
455
        private readonly string _pithosDataPath;
456

    
457

    
458
        public FileState GetStateByFilePath(string path)
459
        {
460
            if (String.IsNullOrWhiteSpace(path))
461
                throw new ArgumentNullException("path");
462
            if (!Path.IsPathRooted(path))
463
                throw new ArgumentException("The path must be rooted", "path");
464
            Contract.EndContractBlock();
465

    
466
            try
467
            {
468
                
469
                using (var connection = GetConnection())
470
                using (var command = new SQLiteCommand("select Id, FilePath, OverlayStatus,FileStatus ,Checksum ,ETag,Version    ,VersionTimeStamp,IsShared   ,SharedBy   ,ShareWrite, LastMD5,LastLength,LastWriteDate  from FileState where FilePath=:path COLLATE NOCASE", connection))
471
                {
472
                    
473
                    command.Parameters.AddWithValue("path", path);
474
                    
475
                    using (var reader = command.ExecuteReader())
476
                    {
477
                        if (reader.Read())
478
                        {
479
                            //var values = new object[reader.FieldCount];
480
                            //reader.GetValues(values);
481
                            var state = new FileState
482
                                            {
483
                                                Id = reader.GetGuid(0),
484
                                                FilePath = reader.IsDBNull(1)?"":reader.GetString(1),
485
                                                OverlayStatus =reader.IsDBNull(2)?FileOverlayStatus.Unversioned: (FileOverlayStatus) reader.GetInt64(2),
486
                                                FileStatus = reader.IsDBNull(3)?FileStatus.Missing:(FileStatus) reader.GetInt64(3),
487
                                                Checksum = reader.IsDBNull(4)?"":reader.GetString(4),
488
                                                ETag= reader.IsDBNull(5)?"":reader.GetString(5),
489
                                                Version = reader.IsDBNull(6)?default(long):reader.GetInt64(6),
490
                                                VersionTimeStamp = reader.IsDBNull(7)?default(DateTime):reader.GetDateTime(7),
491
                                                IsShared = !reader.IsDBNull(8) && reader.GetBoolean(8),
492
                                                SharedBy = reader.IsDBNull(9)?"":reader.GetString(9),
493
                                                ShareWrite = !reader.IsDBNull(10) && reader.GetBoolean(10),
494
                                                LastMD5=reader.GetString(11),
495
                                                LastLength=reader.IsDBNull(12)? default(long):reader.GetInt64(12),
496
                                                LastWriteDate=reader.IsDBNull(13)?default(DateTime):reader.GetDateTime(13)
497
                                            };
498

    
499
                            return state;
500
                        }
501
                        else
502
                        {
503
                            return null;
504
                        }
505

    
506
                    }                    
507
                }
508
            }
509
            catch (Exception exc)
510
            {
511
                Log.ErrorFormat(exc.ToString());
512
                throw;
513
            }            
514
        }
515

    
516
        public FileOverlayStatus GetFileOverlayStatus(string path)
517
        {
518
            if (String.IsNullOrWhiteSpace(path))
519
                throw new ArgumentNullException("path");
520
            if (!Path.IsPathRooted(path))
521
                throw new ArgumentException("The path must be rooted", "path");
522
            Contract.EndContractBlock();
523

    
524
            try
525
            {
526
                
527
                using (var connection = GetConnection())
528
                using (var command = new SQLiteCommand("select OverlayStatus from FileState where FilePath=:path  COLLATE NOCASE", connection))
529
                {
530
                    
531
                    command.Parameters.AddWithValue("path", path);
532
                    
533
                    var s = command.ExecuteScalar();
534
                    return (FileOverlayStatus) Convert.ToInt32(s);
535
                }
536
            }
537
            catch (Exception exc)
538
            {
539
                Log.ErrorFormat(exc.ToString());
540
                return FileOverlayStatus.Unversioned;
541
            }
542
        }
543

    
544
        private string GetConnectionString()
545
        {
546
            var connectionString = String.Format(@"Data Source={0}\pithos.db;Version=3;Enlist=N;Pooling=True", _pithosDataPath);
547
            return connectionString;
548
        }
549

    
550
        private SQLiteConnection GetConnection()
551
        {
552
            var connectionString = GetConnectionString();
553
            var connection = new SQLiteConnection(connectionString);
554
            connection.Open();
555
            using(var cmd =connection.CreateCommand())
556
            {
557
                cmd.CommandText = "PRAGMA journal_mode=WAL";
558
                cmd.ExecuteNonQuery();
559
            }
560
            return connection;
561
        }
562

    
563
       /* public void SetFileOverlayStatus(string path, FileOverlayStatus overlayStatus)
564
        {
565
            if (String.IsNullOrWhiteSpace(path))
566
                throw new ArgumentNullException("path");
567
            if (!Path.IsPathRooted(path))
568
                throw new ArgumentException("The path must be rooted","path");
569
            Contract.EndContractBlock();
570

    
571
            _persistenceAgent.Post(() => FileState.StoreOverlayStatus(path,overlayStatus));
572
        }*/
573

    
574
        public Task SetFileOverlayStatus(string path, FileOverlayStatus overlayStatus, string etag = null)
575
        {
576
            if (String.IsNullOrWhiteSpace(path))
577
                throw new ArgumentNullException("path");
578
            if (!Path.IsPathRooted(path))
579
                throw new ArgumentException("The path must be rooted","path");
580
            Contract.EndContractBlock();
581

    
582
            return _persistenceAgent.PostAndAwait(() => FileState.StoreOverlayStatus(path,overlayStatus,etag));
583
        }
584

    
585
       /* public void RenameFileOverlayStatus(string oldPath, string newPath)
586
        {
587
            if (String.IsNullOrWhiteSpace(oldPath))
588
                throw new ArgumentNullException("oldPath");
589
            if (!Path.IsPathRooted(oldPath))
590
                throw new ArgumentException("The oldPath must be rooted", "oldPath");
591
            if (String.IsNullOrWhiteSpace(newPath))
592
                throw new ArgumentNullException("newPath");
593
            if (!Path.IsPathRooted(newPath))
594
                throw new ArgumentException("The newPath must be rooted", "newPath");
595
            Contract.EndContractBlock();
596

    
597
            _persistenceAgent.Post(() =>FileState.RenameState(oldPath, newPath));
598
        }*/
599

    
600
        public void SetFileState(string path, FileStatus fileStatus, FileOverlayStatus overlayStatus, string conflictReason)
601
        {
602
            if (String.IsNullOrWhiteSpace(path))
603
                throw new ArgumentNullException("path");
604
            if (!Path.IsPathRooted(path))
605
                throw new ArgumentException("The path must be rooted", "path");
606
            Contract.EndContractBlock();
607

    
608
            Debug.Assert(!path.Contains(FolderConstants.CacheFolder));
609
            Debug.Assert(!path.EndsWith(".ignore"));
610

    
611
            _persistenceAgent.Post(() => UpdateStatusDirect(path, fileStatus, overlayStatus, conflictReason));
612
        }
613

    
614
        
615
        public void StoreInfo(string path, ObjectInfo objectInfo)
616
        {
617
            if (String.IsNullOrWhiteSpace(path))
618
                throw new ArgumentNullException("path");
619
            if (!Path.IsPathRooted(path))
620
                throw new ArgumentException("The path must be rooted", "path");
621
            if (objectInfo == null)
622
                throw new ArgumentNullException("objectInfo", "objectInfo can't be empty");
623
            Contract.EndContractBlock();
624

    
625
            _persistenceAgent.Post(() => StoreInfoDirect(path, objectInfo));
626

    
627
        }
628

    
629
        private void StoreInfoDirect(string path, ObjectInfo objectInfo)
630
        {
631
            try
632
            {
633
                    using (var session = ActiveRecordMediator.GetSessionFactoryHolder().CreateSession(typeof(FileState)))
634
                    using (var tx=session.BeginTransaction(IsolationLevel.ReadCommitted))
635
                    {
636

    
637
                        //var walquery = session.CreateSQLQuery("PRAGMA journal_mode=WAL");
638
                        var walquery = session.CreateSQLQuery("PRAGMA journal_mode=WAL");
639
                        walquery.List();
640

    
641
                        Func<IQuery, IQuery> setCriteria = q => q
642
                                                    .SetString("path", path)
643
                                                    .SetString("checksum",objectInfo.X_Object_Hash)
644
                                                    .SetString("etag", objectInfo.ETag)
645
                                                    .SetInt64("version", objectInfo.Version.GetValueOrDefault())
646
                                                    .SetDateTime("versionTimeStamp",objectInfo.VersionTimestamp.GetValueOrDefault())
647
                                                    .SetEnum("fileStatus", FileStatus.Unchanged)
648
                                                    .SetEnum("overlayStatus",FileOverlayStatus.Normal)
649
                                                    .SetString("objectID", objectInfo.UUID);
650
                        //IQuery updatecmd = session.CreateSQLQuery(
651
                        IQuery updatecmd = session.CreateQuery(
652
                            "update FileState set FilePath=:path,FileStatus= :fileStatus,OverlayStatus= :overlayStatus, Checksum=:checksum, ETag=:etag,LastMD5=:etag,Version=:version,VersionTimeStamp=:versionTimeStamp where ObjectID = :objectID  ");
653
                        updatecmd = setCriteria(updatecmd);                           
654
                        var affected = updatecmd.ExecuteUpdate();                        
655
                        
656
                    //If the ID exists, update the status
657
                    if (affected == 0)
658
                    {
659
                        //If the ID doesn't exist, try to update using the path, and store the ID as well.
660
                        //updatecmd = session.CreateSQLQuery(
661
                        updatecmd = session.CreateQuery(
662
                        //    "update FileState set FileStatus= :fileStatus,OverlayStatus= :overlayStatus, ObjectID=:objectID, Checksum=:checksum, ETag=:etag,LastMD5=:etag,Version=:version,VersionTimeStamp=:versionTimeStamp where FilePath = :path  COLLATE NOCASE ");
663
                            "update FileState set FileStatus= :fileStatus,OverlayStatus= :overlayStatus, ObjectID=:objectID, Checksum=:checksum, ETag=:etag,LastMD5=:etag,Version=:version,VersionTimeStamp=:versionTimeStamp where FilePath = :path");
664
                        updatecmd=setCriteria(updatecmd);
665
                        affected = updatecmd.ExecuteUpdate();
666
                    }
667
                    if (affected==0)
668
                    {
669
                        //IQuery insertCmd=session.CreateSQLQuery(
670
                        IQuery insertCmd = session.CreateSQLQuery(
671
                            "INSERT INTO FileState (Id,FilePath,Checksum,Version,VersionTimeStamp,ETag,LastMD5,FileStatus,OverlayStatus,ObjectID) VALUES (:id,:path,:checksum,:version,:versionTimeStamp,:etag,:etag,:fileStatus,:overlayStatus,:objectID)");
672
                        insertCmd=setCriteria(insertCmd).SetGuid("id", Guid.NewGuid());
673
                        affected = insertCmd.ExecuteUpdate();
674
                    }
675
                    tx.Commit();
676
                }
677
            }
678
            catch (Exception exc)
679
            {
680
                Log.ErrorFormat("Failed to update [{0}]:[{1}]\r\n{2}",path,objectInfo.UUID, exc);
681
                throw;
682
            }
683
        }
684

    
685
        private bool StateExists(string filePath,SQLiteConnection connection)
686
        {
687
            using (var command = new SQLiteCommand("Select count(*) from FileState where FilePath=:path  COLLATE NOCASE", connection))
688
            {
689
                command.Parameters.AddWithValue("path", filePath);
690
                var result = command.ExecuteScalar();
691
                return ((long)result >= 1);
692
            }
693

    
694
        }
695

    
696
        private bool StateExistsByID(string objectId,SQLiteConnection connection)
697
        {
698
            using (var command = new SQLiteCommand("Select count(*) from FileState where ObjectId=:id", connection))
699
            {
700
                command.Parameters.AddWithValue("id", objectId);
701
                var result = command.ExecuteScalar();
702
                return ((long)result >= 1);
703
            }
704

    
705
        }
706

    
707
        public void SetFileStatus(string path, FileStatus status)
708
        {
709
            if (String.IsNullOrWhiteSpace(path))
710
                throw new ArgumentNullException("path");
711
            if (!Path.IsPathRooted(path))
712
                throw new ArgumentException("The path must be rooted", "path");
713
            Contract.EndContractBlock();
714
            
715
            _persistenceAgent.Post(() => UpdateStatusDirect(path, status));
716
        }
717

    
718
        public FileStatus GetFileStatus(string path)
719
        {
720
            if (String.IsNullOrWhiteSpace(path))
721
                throw new ArgumentNullException("path");
722
            if (!Path.IsPathRooted(path))
723
                throw new ArgumentException("The path must be rooted", "path");
724
            Contract.EndContractBlock();
725

    
726
            
727
            using (var connection = GetConnection())
728
            {
729
                var command = new SQLiteCommand("select FileStatus from FileState where FilePath=:path  COLLATE NOCASE", connection);
730
                command.Parameters.AddWithValue("path", path);
731
                
732
                var statusValue = command.ExecuteScalar();
733
                if (statusValue==null)
734
                    return FileStatus.Missing;
735
                return (FileStatus)Convert.ToInt32(statusValue);
736
            }
737
        }
738

    
739
        /// <summary>
740
        /// Deletes the status of the specified file
741
        /// </summary>
742
        /// <param name="path"></param>
743
        public void ClearFileStatus(string path)
744
        {
745
            if (String.IsNullOrWhiteSpace(path))
746
                throw new ArgumentNullException("path");
747
            if (!Path.IsPathRooted(path))
748
                throw new ArgumentException("The path must be rooted", "path");
749
            Contract.EndContractBlock();
750

    
751
            _persistenceAgent.Post(() => DeleteDirect(path));   
752
        }
753

    
754
        /// <summary>
755
        /// Deletes the status of the specified folder and all its contents
756
        /// </summary>
757
        /// <param name="path"></param>
758
        public void ClearFolderStatus(string path)
759
        {
760
            if (String.IsNullOrWhiteSpace(path))
761
                throw new ArgumentNullException("path");
762
            if (!Path.IsPathRooted(path))
763
                throw new ArgumentException("The path must be rooted", "path");
764
            Contract.EndContractBlock();
765
            //TODO: May throw if the agent is cleared for some reason. Should never happen
766
            _persistenceAgent.Post(() => DeleteFolderDirect(path));   
767
        }
768

    
769
        public IEnumerable<FileState> GetChildren(FileState fileState)
770
        {
771
            if (fileState == null)
772
                throw new ArgumentNullException("fileState");
773
            Contract.EndContractBlock();
774

    
775
            var children = from state in FileState.Queryable
776
                           where state.FilePath.StartsWith(fileState.FilePath + "\\")
777
                           select state;
778
            return children;
779
        }
780

    
781
        public void EnsureFileState(string path)
782
        {
783
            var existingState = GetStateByFilePath(path);
784
            if (existingState != null)
785
                return;
786
            var fileInfo = FileInfoExtensions.FromPath(path);
787
            using (new SessionScope())
788
            {
789
                var newState = FileState.CreateFor(fileInfo,StatusNotification);
790
                newState.FileStatus=FileStatus.Missing;
791
                _persistenceAgent.PostAndAwait(newState.CreateAndFlush).Wait();
792
            }
793

    
794
        }
795

    
796
        private int DeleteDirect(string filePath)
797
        {
798
            using (log4net.ThreadContext.Stacks["StatusAgent"].Push("DeleteDirect"))
799
            {
800

    
801
                try
802
                {
803

    
804
                    
805
                    using (var connection = GetConnection())
806
                    {
807
                        var command = new SQLiteCommand("delete from FileState where FilePath = :path  COLLATE NOCASE",
808
                                                        connection);
809

    
810
                        command.Parameters.AddWithValue("path", filePath);
811
                        
812
                        var affected = command.ExecuteNonQuery();
813
                        return affected;
814
                    }
815
                }
816
                catch (Exception exc)
817
                {
818
                    Log.Error(exc.ToString());
819
                    throw;
820
                }
821
            }
822
        }
823

    
824
        private int DeleteFolderDirect(string filePath)
825
        {
826
            using (log4net.ThreadContext.Stacks["StatusAgent"].Push("DeleteDirect"))
827
            {
828

    
829
                try
830
                {
831

    
832
                    
833
                    using (var connection = GetConnection())
834
                    {
835
                        var command = new SQLiteCommand(@"delete from FileState where FilePath = :path or FilePath like :path || '\%'  COLLATE NOCASE",
836
                                                        connection);
837

    
838
                        command.Parameters.AddWithValue("path", filePath);
839
                        
840
                        var affected = command.ExecuteNonQuery();
841
                        return affected;
842
                    }
843
                }
844
                catch (Exception exc)
845
                {
846
                    Log.Error(exc.ToString());
847
                    throw;
848
                }
849
            }
850
        }
851

    
852
        public void UpdateFileChecksum(string path, string etag, string checksum)
853
        {
854
            if (String.IsNullOrWhiteSpace(path))
855
                throw new ArgumentNullException("path");
856
            if (!Path.IsPathRooted(path))
857
                throw new ArgumentException("The path must be rooted", "path");            
858
            Contract.EndContractBlock();
859

    
860
            _persistenceAgent.Post(() => FileState.UpdateChecksum(path, etag,checksum));
861
        }
862

    
863
        public void UpdateLastMD5(FileInfo file, string etag)
864
        {
865
            if (file==null)
866
                throw new ArgumentNullException("file");
867
            if (String.IsNullOrWhiteSpace(etag))
868
                throw new ArgumentNullException("etag");
869
            Contract.EndContractBlock();
870

    
871
            _persistenceAgent.Post(() => FileState.UpdateLastMD5(file, etag));
872
        }
873

    
874

    
875
        public void CleanupOrphanStates()
876
        {
877
            //Orphan states are those that do not correspond to an account, ie. their paths
878
            //do not start with the root path of any registered account
879

    
880
            var roots=(from account in Settings.Accounts
881
                      select account.RootPath).ToList();
882
            
883
            var allStates = from state in FileState.Queryable
884
                select state.FilePath;
885

    
886
            foreach (var statePath in allStates)
887
            {
888
                if (!roots.Any(root=>statePath.StartsWith(root,StringComparison.InvariantCultureIgnoreCase)))
889
                    this.DeleteDirect(statePath);
890
            }
891
        }
892

    
893
        public void CleanupStaleStates(AccountInfo accountInfo, List<ObjectInfo> objectInfos)
894
        {
895
            if (accountInfo == null)
896
                throw new ArgumentNullException("accountInfo");
897
            if (objectInfos == null)
898
                throw new ArgumentNullException("objectInfos");
899
            Contract.EndContractBlock();
900
            
901

    
902

    
903
            //Stale states are those that have no corresponding local or server file
904
            
905

    
906
            var agent=FileAgent.GetFileAgent(accountInfo);
907

    
908
            var localFiles=agent.EnumerateFiles();
909
            var localSet = new HashSet<string>(localFiles);
910

    
911
            //RelativeUrlToFilePath will fail for
912
            //infos of accounts, containers which have no Name
913

    
914
            var serverFiles = from info in objectInfos
915
                              where info.Name != null
916
                              select Path.Combine(accountInfo.AccountPath,info.RelativeUrlToFilePath(accountInfo.UserName));
917
            var serverSet = new HashSet<string>(serverFiles);
918

    
919
            var allStates = from state in FileState.Queryable
920
                            where state.FilePath.StartsWith(agent.RootPath)
921
                            select state.FilePath;
922
            var stateSet = new HashSet<string>(allStates);
923
            stateSet.ExceptWith(serverSet);
924
            stateSet.ExceptWith(localSet);
925

    
926
            foreach (var remainder in stateSet)
927
            {
928
                DeleteDirect(remainder);
929
            }
930

    
931
            
932
        }
933
    }
934

    
935
   
936
}