Statistics
| Branch: | Revision:

root / trunk / Pithos.Core / Agents / StatusAgent.cs @ e4067290

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

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

    
74
        [System.ComponentModel.Composition.Import]
75
        public IPithosSettings Settings { get; set; }
76

    
77
        private Agent<Action> _persistenceAgent;
78

    
79

    
80

    
81
        public StatusAgent()
82
        {            
83
            var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
84

    
85
            _pithosDataPath = Path.Combine(appDataPath , "GRNET\\PITHOS");
86
            if (!Directory.Exists(_pithosDataPath))
87
                Directory.CreateDirectory(_pithosDataPath);
88

    
89
            var dbPath = Path.Combine(_pithosDataPath, "pithos.db");
90

    
91
            MigrateOldDb(dbPath, appDataPath);
92

    
93

    
94
            var source = GetConfiguration(_pithosDataPath);
95
            ActiveRecordStarter.Initialize(source,typeof(FileState),typeof(FileTag));
96
            
97
            ActiveRecordStarter.UpdateSchema();
98

    
99

    
100
            if (!File.Exists(dbPath))
101
                ActiveRecordStarter.CreateSchema();
102

    
103
            CreateTrigger();
104
            
105
        }
106

    
107

    
108
        private static void MigrateOldDb(string dbPath, string appDataPath)
109
        {
110
            if(String.IsNullOrWhiteSpace(dbPath))
111
                throw new ArgumentNullException("dbPath");
112
            if(String.IsNullOrWhiteSpace(appDataPath))
113
                throw new ArgumentNullException("appDataPath");
114
            Contract.EndContractBlock();
115

    
116
            var oldDbPath = Path.Combine(appDataPath, "Pithos", "pithos.db");
117
            var oldDbInfo = new FileInfo(oldDbPath);
118
            if (oldDbInfo.Exists && !File.Exists(dbPath))
119
            {
120
                Log.InfoFormat("Moving database from {0} to {1}",oldDbInfo.FullName,dbPath);
121
                var oldDirectory = oldDbInfo.Directory;
122
                oldDbInfo.MoveTo(dbPath);
123
                
124
                if (Log.IsDebugEnabled)
125
                    Log.DebugFormat("Deleting {0}",oldDirectory.FullName);
126
                
127
                oldDirectory.Delete(true);
128
            }
129
        }
130

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

    
151

    
152
        private static InPlaceConfigurationSource GetConfiguration(string pithosDbPath)
153
        {
154
            if (String.IsNullOrWhiteSpace(pithosDbPath))
155
                throw new ArgumentNullException("pithosDbPath");
156
            if (!Path.IsPathRooted(pithosDbPath))
157
                throw new ArgumentException("path must be a rooted path", "pithosDbPath");
158
            Contract.EndContractBlock();
159

    
160
            var properties = new Dictionary<string, string>
161
                                 {
162
                                     {"connection.driver_class", "NHibernate.Driver.SQLite20Driver"},
163
                                     {"dialect", "NHibernate.Dialect.SQLiteDialect"},
164
                                     {"connection.provider", "NHibernate.Connection.DriverConnectionProvider"},
165
                                     {
166
                                         "proxyfactory.factory_class",
167
                                         "NHibernate.ByteCode.Castle.ProxyFactoryFactory, NHibernate.ByteCode.Castle"
168
                                         },
169
                                 };
170

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

    
174
            var source = new InPlaceConfigurationSource();                        
175
            source.Add(typeof (ActiveRecordBase), properties);
176
            source.SetDebugFlag(false);            
177
            return source;
178
        }
179

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

    
214
       
215

    
216
        public void Stop()
217
        {
218
            _persistenceAgent.Stop();            
219
        }
220
               
221

    
222
        public void ProcessExistingFiles(IEnumerable<FileInfo> existingFiles)
223
        {
224
            if(existingFiles  ==null)
225
                throw new ArgumentNullException("existingFiles");
226
            Contract.EndContractBlock();
227
            
228
            //Find new or matching files with a left join to the stored states
229
            var fileStates = FileState.Queryable;
230
            var currentFiles=from file in existingFiles
231
                      join state in fileStates on file.FullName.ToLower() equals state.FilePath.ToLower() into gs
232
                      from substate in gs.DefaultIfEmpty()
233
                               select new {File = file, State = substate};
234

    
235
            //To get the deleted files we must get the states that have no corresponding
236
            //files. 
237
            //We can't use the File.Exists method inside a query, so we get all file paths from the states
238
            var statePaths = (from state in fileStates
239
                             select new {state.Id, state.FilePath}).ToList();
240
            //and check each one
241
            var missingStates= (from path in statePaths
242
                                where !File.Exists(path.FilePath) && !Directory.Exists(path.FilePath)
243
                               select path.Id).ToList();
244
            //Finally, retrieve the states that correspond to the deleted files            
245
            var deletedFiles = from state in fileStates 
246
                        where missingStates.Contains(state.Id)
247
                        select new { File = default(FileInfo), State = state };
248

    
249
            var pairs = currentFiles.Union(deletedFiles).ToList();
250

    
251
            using (var shortHasher = HashAlgorithm.Create("sha1"))
252
            {
253
                foreach (var pair in pairs)
254
                {
255
                    var fileState = pair.State;
256
                    var file = pair.File;
257
                    if (fileState == null)
258
                    {
259
                        //This is a new file                        
260
                        var createState = FileState.CreateFor(file);
261
                        _persistenceAgent.Post(createState.Create);                        
262
                    }
263
                    else if (file == null)
264
                    {
265
                        //This file was deleted while we were down. We should mark it as deleted
266
                        //We have to go through UpdateStatus here because the state object we are using
267
                        //was created by a different ORM session.
268
                        _persistenceAgent.Post(() => UpdateStatusDirect(fileState.Id, FileStatus.Deleted));
269
                    }
270
                    else
271
                    {
272
                        //This file has a matching state. Need to check for possible changes
273
                        //To check for changes, we use the cheap (in CPU terms) SHA1 algorithm
274
                        //on the entire file.
275
                        
276
                        var hashString = file.ComputeShortHash(shortHasher);                        
277
                        //TODO: Need a way to attach the hashes to the filestate so we don't
278
                        //recalculate them each time a call to calculate has is made
279
                        //We can either store them to the filestate or add them to a 
280
                        //dictionary
281

    
282
                        //If the hashes don't match the file was changed
283
                        if (fileState.ShortHash != hashString)
284
                        {
285
                            _persistenceAgent.Post(() => UpdateStatusDirect(fileState.Id, FileStatus.Modified));
286
                        }
287
                    }
288
                }
289
            }
290
                        
291
         
292
        }
293
        
294

    
295

    
296
        private int UpdateStatusDirect(Guid id, FileStatus status)
297
        {
298
            using (log4net.ThreadContext.Stacks["StatusAgent"].Push("UpdateStatusDirect"))
299
            {
300

    
301
                try
302
                {
303
                    
304
                    using (var connection = GetConnection())
305
                    using (
306
                        var command = new SQLiteCommand("update FileState set FileStatus= :fileStatus where Id = :id  ",
307
                                                        connection))
308
                    {                                                
309
                        command.Parameters.AddWithValue("fileStatus", status);
310

    
311
                        command.Parameters.AddWithValue("id", id);
312
                        
313
                        var affected = command.ExecuteNonQuery();
314
                        
315
                        return affected;
316
                    }
317

    
318
                }
319
                catch (Exception exc)
320
                {
321
                    Log.Error(exc.ToString());
322
                    throw;
323
                }
324
            }
325
        }
326
        
327
        private int UpdateStatusDirect(string path, FileStatus status)
328
        {
329
            using (log4net.ThreadContext.Stacks["StatusAgent"].Push("UpdateStatusDirect"))
330
            {
331

    
332
                try
333
                {
334

    
335
                    
336
                    using (var connection = GetConnection())
337
                    using (
338
                        var command =
339
                            new SQLiteCommand("update FileState set FileStatus= :fileStatus where FilePath = :path COLLATE NOCASE",
340
                                              connection))
341
                    {
342

    
343

    
344
                        command.Parameters.AddWithValue("fileStatus", status);
345

    
346
                        command.Parameters.AddWithValue("path", path);
347
                        
348
                        var affected = command.ExecuteNonQuery();
349
                        if (affected == 0)
350
                        {
351
                            var createdState = FileState.CreateFor(FileInfoExtensions.FromPath(path));
352
                            createdState.FileStatus = status;
353
                            createdState.Create();
354
                        }
355
                        return affected;
356
                    }
357
                }
358
                catch (Exception exc)
359
                {
360
                    Log.Error(exc.ToString());
361
                    throw;
362
                }
363
            }
364
        }
365

    
366
        private int UpdateStatusDirect(string absolutePath, FileStatus fileStatus, FileOverlayStatus overlayStatus, string conflictReason)
367
        {
368
            using (log4net.ThreadContext.Stacks["StatusAgent"].Push("UpdateStatusDirect"))
369
            {
370

    
371
                try
372
                {
373

    
374
                    
375
                    using (var connection = GetConnection())
376
                    using (
377
                        var command =
378
                            new SQLiteCommand(
379
                                "update FileState set OverlayStatus= :overlayStatus, FileStatus= :fileStatus,ConflictReason= :conflictReason where FilePath = :path COLLATE NOCASE ",
380
                                connection))
381
                    {
382

    
383
                        command.Parameters.AddWithValue("path", absolutePath);
384
                        command.Parameters.AddWithValue("fileStatus", fileStatus);
385
                        command.Parameters.AddWithValue("overlayStatus", overlayStatus);
386
                        command.Parameters.AddWithValue("conflictReason", conflictReason);
387
                        
388
                        var affected = command.ExecuteNonQuery();
389
                        if (affected == 0)
390
                        {
391
                            var createdState=FileState.CreateFor(FileInfoExtensions.FromPath(absolutePath));
392
                            createdState.FileStatus = fileStatus;
393
                            createdState.OverlayStatus = overlayStatus;
394
                            createdState.ConflictReason = conflictReason;
395
                            createdState.Create();  
396
                        }
397
                        return affected;
398
                    }
399
                }
400
                catch (Exception exc)
401
                {
402
                    Log.Error(exc.ToString());
403
                    throw;
404
                }
405
            }
406
        }
407
        
408

    
409

    
410
        public string BlockHash { get; set; }
411

    
412
        public int BlockSize { get; set; }
413
        public void ChangeRoots(string oldPath, string newPath)
414
        {
415
            if (String.IsNullOrWhiteSpace(oldPath))
416
                throw new ArgumentNullException("oldPath");
417
            if (!Path.IsPathRooted(oldPath))
418
                throw new ArgumentException("oldPath must be an absolute path", "oldPath");
419
            if (string.IsNullOrWhiteSpace(newPath))
420
                throw new ArgumentNullException("newPath");
421
            if (!Path.IsPathRooted(newPath))
422
                throw new ArgumentException("newPath must be an absolute path", "newPath");
423
            Contract.EndContractBlock();
424

    
425
            FileState.ChangeRootPath(oldPath,newPath);
426

    
427
        }
428

    
429

    
430

    
431
        private readonly string _pithosDataPath;
432

    
433

    
434
        public FileState GetStateByFilePath(string path)
435
        {
436
            if (String.IsNullOrWhiteSpace(path))
437
                throw new ArgumentNullException("path");
438
            if (!Path.IsPathRooted(path))
439
                throw new ArgumentException("The path must be rooted", "path");
440
            Contract.EndContractBlock();
441

    
442
            try
443
            {
444
                
445
                using (var connection = GetConnection())
446
                using (var command = new SQLiteCommand("select Id, FilePath, OverlayStatus,FileStatus ,Checksum ,ShortHash,Version    ,VersionTimeStamp,IsShared   ,SharedBy   ,ShareWrite  from FileState where FilePath=:path COLLATE NOCASE", connection))
447
                {
448
                    
449
                    command.Parameters.AddWithValue("path", path);
450
                    
451
                    using (var reader = command.ExecuteReader())
452
                    {
453
                        if (reader.Read())
454
                        {
455
                            //var values = new object[reader.FieldCount];
456
                            //reader.GetValues(values);
457
                            var state = new FileState
458
                                            {
459
                                                Id = reader.GetGuid(0),
460
                                                FilePath = reader.IsDBNull(1)?"":reader.GetString(1),
461
                                                OverlayStatus =reader.IsDBNull(2)?FileOverlayStatus.Unversioned: (FileOverlayStatus) reader.GetInt64(2),
462
                                                FileStatus = reader.IsDBNull(3)?FileStatus.Missing:(FileStatus) reader.GetInt64(3),
463
                                                Checksum = reader.IsDBNull(4)?"":reader.GetString(4),
464
                                                ShortHash= reader.IsDBNull(5)?"":reader.GetString(5),
465
                                                Version = reader.IsDBNull(6)?default(long):reader.GetInt64(6),
466
                                                VersionTimeStamp = reader.IsDBNull(7)?default(DateTime):reader.GetDateTime(7),
467
                                                IsShared = !reader.IsDBNull(8) && reader.GetBoolean(8),
468
                                                SharedBy = reader.IsDBNull(9)?"":reader.GetString(9),
469
                                                ShareWrite = !reader.IsDBNull(10) && reader.GetBoolean(10)
470
                                            };
471
/*
472
                            var state = new FileState
473
                                            {
474
                                                Id = (Guid) values[0],
475
                                                FilePath = (string) values[1],
476
                                                OverlayStatus = (FileOverlayStatus) (long)values[2],
477
                                                FileStatus = (FileStatus) (long)values[3],
478
                                                Checksum = (string) values[4],
479
                                                Version = (long?) values[5],
480
                                                VersionTimeStamp = (DateTime?) values[6],
481
                                                IsShared = (long)values[7] == 1,
482
                                                SharedBy = (string) values[8],
483
                                                ShareWrite = (long)values[9] == 1
484
                                            };
485
*/
486
                            return state;
487
                        }
488
                        else
489
                        {
490
                            return null;
491
                        }
492

    
493
                    }                    
494
                }
495
            }
496
            catch (Exception exc)
497
            {
498
                Log.ErrorFormat(exc.ToString());
499
                throw;
500
            }            
501
        }
502

    
503
        public FileOverlayStatus GetFileOverlayStatus(string path)
504
        {
505
            if (String.IsNullOrWhiteSpace(path))
506
                throw new ArgumentNullException("path");
507
            if (!Path.IsPathRooted(path))
508
                throw new ArgumentException("The path must be rooted", "path");
509
            Contract.EndContractBlock();
510

    
511
            try
512
            {
513
                
514
                using (var connection = GetConnection())
515
                using (var command = new SQLiteCommand("select OverlayStatus from FileState where FilePath=:path  COLLATE NOCASE", connection))
516
                {
517
                    
518
                    command.Parameters.AddWithValue("path", path);
519
                    
520
                    var s = command.ExecuteScalar();
521
                    return (FileOverlayStatus) Convert.ToInt32(s);
522
                }
523
            }
524
            catch (Exception exc)
525
            {
526
                Log.ErrorFormat(exc.ToString());
527
                return FileOverlayStatus.Unversioned;
528
            }
529
        }
530

    
531
        private string GetConnectionString()
532
        {
533
            var connectionString = String.Format(@"Data Source={0}\pithos.db;Version=3;Enlist=N;Pooling=True", _pithosDataPath);
534
            return connectionString;
535
        }
536

    
537
        private SQLiteConnection GetConnection()
538
        {
539
            var connectionString = GetConnectionString();
540
            var connection = new SQLiteConnection(connectionString);
541
            connection.Open();
542
            using(var cmd =connection.CreateCommand())
543
            {
544
                cmd.CommandText = "PRAGMA journal_mode=WAL";
545
                cmd.ExecuteNonQuery();
546
            }
547
            return connection;
548
        }
549

    
550
       /* public void SetFileOverlayStatus(string path, FileOverlayStatus overlayStatus)
551
        {
552
            if (String.IsNullOrWhiteSpace(path))
553
                throw new ArgumentNullException("path");
554
            if (!Path.IsPathRooted(path))
555
                throw new ArgumentException("The path must be rooted","path");
556
            Contract.EndContractBlock();
557

    
558
            _persistenceAgent.Post(() => FileState.StoreOverlayStatus(path,overlayStatus));
559
        }*/
560

    
561
        public Task SetFileOverlayStatus(string path, FileOverlayStatus overlayStatus, string shortHash = null)
562
        {
563
            if (String.IsNullOrWhiteSpace(path))
564
                throw new ArgumentNullException("path");
565
            if (!Path.IsPathRooted(path))
566
                throw new ArgumentException("The path must be rooted","path");
567
            Contract.EndContractBlock();
568

    
569
            return _persistenceAgent.PostAndAwait(() => FileState.StoreOverlayStatus(path,overlayStatus,shortHash));
570
        }
571

    
572
       /* public void RenameFileOverlayStatus(string oldPath, string newPath)
573
        {
574
            if (String.IsNullOrWhiteSpace(oldPath))
575
                throw new ArgumentNullException("oldPath");
576
            if (!Path.IsPathRooted(oldPath))
577
                throw new ArgumentException("The oldPath must be rooted", "oldPath");
578
            if (String.IsNullOrWhiteSpace(newPath))
579
                throw new ArgumentNullException("newPath");
580
            if (!Path.IsPathRooted(newPath))
581
                throw new ArgumentException("The newPath must be rooted", "newPath");
582
            Contract.EndContractBlock();
583

    
584
            _persistenceAgent.Post(() =>FileState.RenameState(oldPath, newPath));
585
        }*/
586

    
587
        public void SetFileState(string path, FileStatus fileStatus, FileOverlayStatus overlayStatus, string conflictReason)
588
        {
589
            if (String.IsNullOrWhiteSpace(path))
590
                throw new ArgumentNullException("path");
591
            if (!Path.IsPathRooted(path))
592
                throw new ArgumentException("The path must be rooted", "path");
593
            Contract.EndContractBlock();
594

    
595
            Debug.Assert(!path.Contains(FolderConstants.CacheFolder));
596
            Debug.Assert(!path.EndsWith(".ignore"));
597

    
598
            _persistenceAgent.Post(() => UpdateStatusDirect(path, fileStatus, overlayStatus, conflictReason));
599
        }
600

    
601
/*
602
        public void StoreInfo(string path,ObjectInfo objectInfo)
603
        {
604
            if (String.IsNullOrWhiteSpace(path))
605
                throw new ArgumentNullException("path");
606
            if (!Path.IsPathRooted(path))
607
                throw new ArgumentException("The path must be rooted", "path");            
608
            if (objectInfo == null)
609
                throw new ArgumentNullException("objectInfo", "objectInfo can't be empty");
610
            Contract.EndContractBlock();
611

    
612
            _persistenceAgent.Post(() =>
613
            {
614
                var filePath = path.ToLower();
615
                //Load the existing files state and set its properties in one session            
616
                using (new SessionScope())
617
                {
618
                    //Forgetting to use a sessionscope results in two sessions being created, one by 
619
                    //FirstOrDefault and one by Save()
620
                    var state =FileState.FindByFilePath(filePath);
621
                    
622
                    //Create a new empty state object if this is a new file
623
                    state = state ?? new FileState();
624

    
625
                    state.FilePath = filePath;
626
                    state.Checksum = objectInfo.Hash;
627
                    state.Version = objectInfo.Version;
628
                    state.VersionTimeStamp = objectInfo.VersionTimestamp;
629

    
630
                    state.FileStatus = FileStatus.Unchanged;
631
                    state.OverlayStatus = FileOverlayStatus.Normal;
632
                    
633
                  
634
                    //Do the save
635
                    state.Save();
636
                }
637
            });
638

    
639
        }
640
*/
641
        
642
        public void StoreInfo(string path, ObjectInfo objectInfo)
643
        {
644
            if (String.IsNullOrWhiteSpace(path))
645
                throw new ArgumentNullException("path");
646
            if (!Path.IsPathRooted(path))
647
                throw new ArgumentException("The path must be rooted", "path");
648
            if (objectInfo == null)
649
                throw new ArgumentNullException("objectInfo", "objectInfo can't be empty");
650
            Contract.EndContractBlock();
651

    
652
            _persistenceAgent.Post(() => StoreInfoDirect(path, objectInfo));
653

    
654
        }
655

    
656
        private void StoreInfoDirect(string path, ObjectInfo objectInfo)
657
        {
658
            try
659
            {
660
                
661
                using (var connection = GetConnection())
662
                using (var command = new SQLiteCommand(connection))
663
                {
664
                    //If the ID exists, update the status
665
                    if (StateExistsByID(objectInfo.UUID,connection))
666
                        command.CommandText =
667
                            "update FileState set FilePath=:path,FileStatus= :fileStatus, Checksum=:checksum, ShortHash=:shortHash,Version=:version,VersionTimeStamp=:versionTimeStamp where ObjectID = :objectID  ";                        
668
                    else if (StateExists(path, connection))
669
                        //If the ID doesn't exist, try to update using the path, and store the ID as well.
670
                        command.CommandText =
671
                            "update FileState set FileStatus= :fileStatus, ObjectID=:objectID, Checksum=:checksum, ShortHash=:shortHash,Version=:version,VersionTimeStamp=:versionTimeStamp where FilePath = :path  COLLATE NOCASE ";
672
                    else
673
                    {
674
                        command.CommandText =
675
                            "INSERT INTO FileState (Id,FilePath,Checksum,Version,VersionTimeStamp,ShortHash,FileStatus,OverlayStatus,ObjectID) VALUES (:id,:path,:checksum,:version,:versionTimeStamp,:shortHash,:fileStatus,:overlayStatus,:objectID)";
676
                        command.Parameters.AddWithValue("id", Guid.NewGuid());
677
                    }
678

    
679
                    command.Parameters.AddWithValue("path", path);
680
                    command.Parameters.AddWithValue("checksum", objectInfo.X_Object_Hash);
681
                    command.Parameters.AddWithValue("shortHash", objectInfo.ETag);
682
                    command.Parameters.AddWithValue("version", objectInfo.Version);
683
                    command.Parameters.AddWithValue("versionTimeStamp", objectInfo.VersionTimestamp);
684
                    command.Parameters.AddWithValue("fileStatus", FileStatus.Unchanged);
685
                    command.Parameters.AddWithValue("overlayStatus", FileOverlayStatus.Normal);
686
                    command.Parameters.AddWithValue("objectID",objectInfo.UUID);
687

    
688
                    var affected = command.ExecuteNonQuery();
689
                    return;
690
                }
691
            }
692
            catch (Exception exc)
693
            {
694
                Log.Error(exc.ToString());
695
                throw;
696
            }
697
        }
698

    
699
        private bool StateExists(string filePath,SQLiteConnection connection)
700
        {
701
            using (var command = new SQLiteCommand("Select count(*) from FileState where FilePath=:path  COLLATE NOCASE", connection))
702
            {
703
                command.Parameters.AddWithValue("path", filePath);
704
                var result = command.ExecuteScalar();
705
                return ((long)result >= 1);
706
            }
707

    
708
        }
709

    
710
        private bool StateExistsByID(string objectId,SQLiteConnection connection)
711
        {
712
            using (var command = new SQLiteCommand("Select count(*) from FileState where ObjectId=:id", connection))
713
            {
714
                command.Parameters.AddWithValue("id", objectId);
715
                var result = command.ExecuteScalar();
716
                return ((long)result >= 1);
717
            }
718

    
719
        }
720

    
721
        public void SetFileStatus(string path, FileStatus status)
722
        {
723
            if (String.IsNullOrWhiteSpace(path))
724
                throw new ArgumentNullException("path");
725
            if (!Path.IsPathRooted(path))
726
                throw new ArgumentException("The path must be rooted", "path");
727
            Contract.EndContractBlock();
728
            
729
            _persistenceAgent.Post(() => UpdateStatusDirect(path, status));
730
        }
731

    
732
        public FileStatus GetFileStatus(string path)
733
        {
734
            if (String.IsNullOrWhiteSpace(path))
735
                throw new ArgumentNullException("path");
736
            if (!Path.IsPathRooted(path))
737
                throw new ArgumentException("The path must be rooted", "path");
738
            Contract.EndContractBlock();
739

    
740
            
741
            using (var connection = GetConnection())
742
            {
743
                var command = new SQLiteCommand("select FileStatus from FileState where FilePath=:path  COLLATE NOCASE", connection);
744
                command.Parameters.AddWithValue("path", path);
745
                
746
                var statusValue = command.ExecuteScalar();
747
                if (statusValue==null)
748
                    return FileStatus.Missing;
749
                return (FileStatus)Convert.ToInt32(statusValue);
750
            }
751
        }
752

    
753
        /// <summary>
754
        /// Deletes the status of the specified file
755
        /// </summary>
756
        /// <param name="path"></param>
757
        public void ClearFileStatus(string path)
758
        {
759
            if (String.IsNullOrWhiteSpace(path))
760
                throw new ArgumentNullException("path");
761
            if (!Path.IsPathRooted(path))
762
                throw new ArgumentException("The path must be rooted", "path");
763
            Contract.EndContractBlock();
764

    
765
            _persistenceAgent.Post(() => DeleteDirect(path));   
766
        }
767

    
768
        /// <summary>
769
        /// Deletes the status of the specified folder and all its contents
770
        /// </summary>
771
        /// <param name="path"></param>
772
        public void ClearFolderStatus(string path)
773
        {
774
            if (String.IsNullOrWhiteSpace(path))
775
                throw new ArgumentNullException("path");
776
            if (!Path.IsPathRooted(path))
777
                throw new ArgumentException("The path must be rooted", "path");
778
            Contract.EndContractBlock();
779
            //TODO: May throw if the agent is cleared for some reason. Should never happen
780
            _persistenceAgent.Post(() => DeleteFolderDirect(path));   
781
        }
782

    
783
        public IEnumerable<FileState> GetChildren(FileState fileState)
784
        {
785
            if (fileState == null)
786
                throw new ArgumentNullException("fileState");
787
            Contract.EndContractBlock();
788

    
789
            var children = from state in FileState.Queryable
790
                           where state.FilePath.StartsWith(fileState.FilePath + "\\")
791
                           select state;
792
            return children;
793
        }
794

    
795
        public void EnsureFileState(string path)
796
        {
797
            var existingState = GetStateByFilePath(path);
798
            if (existingState != null)
799
                return;
800
            var fileInfo = FileInfoExtensions.FromPath(path);
801
            using (new SessionScope())
802
            {
803
                var newState = FileState.CreateFor(fileInfo);
804
                newState.FileStatus=FileStatus.Missing;
805
                _persistenceAgent.PostAndAwait(newState.CreateAndFlush).Wait();
806
            }
807

    
808
        }
809

    
810
        private int DeleteDirect(string filePath)
811
        {
812
            using (log4net.ThreadContext.Stacks["StatusAgent"].Push("DeleteDirect"))
813
            {
814

    
815
                try
816
                {
817

    
818
                    
819
                    using (var connection = GetConnection())
820
                    {
821
                        var command = new SQLiteCommand("delete from FileState where FilePath = :path  COLLATE NOCASE",
822
                                                        connection);
823

    
824
                        command.Parameters.AddWithValue("path", filePath);
825
                        
826
                        var affected = command.ExecuteNonQuery();
827
                        return affected;
828
                    }
829
                }
830
                catch (Exception exc)
831
                {
832
                    Log.Error(exc.ToString());
833
                    throw;
834
                }
835
            }
836
        }
837

    
838
        private int DeleteFolderDirect(string filePath)
839
        {
840
            using (log4net.ThreadContext.Stacks["StatusAgent"].Push("DeleteDirect"))
841
            {
842

    
843
                try
844
                {
845

    
846
                    
847
                    using (var connection = GetConnection())
848
                    {
849
                        var command = new SQLiteCommand(@"delete from FileState where FilePath = :path or FilePath like :path || '\%'  COLLATE NOCASE",
850
                                                        connection);
851

    
852
                        command.Parameters.AddWithValue("path", filePath);
853
                        
854
                        var affected = command.ExecuteNonQuery();
855
                        return affected;
856
                    }
857
                }
858
                catch (Exception exc)
859
                {
860
                    Log.Error(exc.ToString());
861
                    throw;
862
                }
863
            }
864
        }
865

    
866
        public void UpdateFileChecksum(string path, string shortHash, string checksum)
867
        {
868
            if (String.IsNullOrWhiteSpace(path))
869
                throw new ArgumentNullException("path");
870
            if (!Path.IsPathRooted(path))
871
                throw new ArgumentException("The path must be rooted", "path");            
872
            Contract.EndContractBlock();
873

    
874
            _persistenceAgent.Post(() => FileState.UpdateChecksum(path, shortHash,checksum));
875
        }
876

    
877

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

    
883
            var roots=(from account in Settings.Accounts
884
                      select account.RootPath).ToList();
885
            
886
            var allStates = from state in FileState.Queryable
887
                select state.FilePath;
888

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

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

    
905

    
906
            //Stale states are those that have no corresponding local or server file
907
            
908

    
909
            var agent=FileAgent.GetFileAgent(accountInfo);
910

    
911
            var localFiles=agent.EnumerateFiles();
912
            var localSet = new HashSet<string>(localFiles);
913

    
914
            //RelativeUrlToFilePath will fail for
915
            //infos of accounts, containers which have no Name
916

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

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

    
929
            foreach (var remainder in stateSet)
930
            {
931
                DeleteDirect(remainder);
932
            }
933

    
934
            
935
        }
936
    }
937

    
938
   
939
}