Revision 540b8cf8

b/trunk/Pithos.Core/Agents/BlockExtensions.cs
1
// -----------------------------------------------------------------------
2
// <copyright file="FileInfoExtensions.cs" company="GRNET">
3
// Copyright 2011 GRNET S.A. All rights reserved.
4
// 
5
// Redistribution and use in source and binary forms, with or
6
// without modification, are permitted provided that the following
7
// conditions are met:
8
// 
9
//   1. Redistributions of source code must retain the above
10
//      copyright notice, this list of conditions and the following
11
//      disclaimer.
12
// 
13
//   2. Redistributions in binary form must reproduce the above
14
//      copyright notice, this list of conditions and the following
15
//      disclaimer in the documentation and/or other materials
16
//      provided with the distribution.
17
// 
18
// THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
19
// OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
21
// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
22
// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
25
// USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
26
// AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
27
// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
28
// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29
// POSSIBILITY OF SUCH DAMAGE.
30
// 
31
// The views and conclusions contained in the software and
32
// documentation are those of the authors and should not be
33
// interpreted as representing official policies, either expressed
34
// or implied, of GRNET S.A.
35
// </copyright>
36
// -----------------------------------------------------------------------
37

  
38

  
39
using System;
40
using System.Collections.Generic;
41
using System.Diagnostics.Contracts;
42
using System.Linq;
43
using System.Text;
44
using System.IO;
45
using System.Text.RegularExpressions;
46
using System.Threading.Tasks;
47
using Pithos.Network;
48

  
49
namespace Pithos.Core.Agents
50
{
51
    static class BlockExtensions
52
    {
53
        
54

  
55
        public static int Read(this FileInfo fileInfo,byte[] buffer,int offset,int count)
56
        {            
57
            //Open the stream only long enough to read a block
58
            using (var stream = fileInfo.OpenRead())
59
            {
60
                stream.Seek(offset, SeekOrigin.Begin);
61
                return  stream.Read(buffer, 0, count);                
62
            }
63
        }
64

  
65
       public static string CalculateHash(this FileSystemInfo info,int blockSize,string algorithm)
66
        {
67
            if (info==null)
68
                throw new ArgumentNullException("info");
69
            if (String.IsNullOrWhiteSpace(info.FullName))
70
                throw new ArgumentException("info");
71
            if (blockSize<=0)
72
                throw new ArgumentOutOfRangeException("blockSize",blockSize,"blockSize must be greater than 0");
73
            if (String.IsNullOrWhiteSpace(algorithm))
74
                throw new ArgumentNullException("algorithm");
75
            Contract.EndContractBlock();
76

  
77
           //The hash for directories is an empty string
78
           if (info is DirectoryInfo)
79
                return String.Empty;
80
           
81
           var fileInfo = (FileInfo)info;
82
           if (fileInfo.Length <= blockSize)
83
                return Signature.CalculateMD5(info.FullName);
84
            else
85
                return Signature.CalculateTreeHash(info.FullName, blockSize, algorithm).TopHash.ToHashString();
86

  
87
        }
88
    }
89
}
b/trunk/Pithos.Core/Agents/FileAgent.cs
191 191
            return monitoredFiles;
192 192
        }                
193 193

  
194
        public IEnumerable<string> EnumerateFilesSystemInfosAsRelativeUrls(string searchPattern="*")
195
        {
196
            var rootDir = new DirectoryInfo(RootPath);
197
            var monitoredFiles = from file in rootDir.EnumerateFileSystemInfos(searchPattern, SearchOption.AllDirectories)
198
                                 where !Ignore(file.FullName)
199
                                 select file.AsRelativeUrlTo(RootPath);
200
            return monitoredFiles;
201
        }                
202

  
194 203

  
195 204
        
196 205

  
/dev/null
1
// -----------------------------------------------------------------------
2
// <copyright file="FileInfoExtensions.cs" company="GRNET">
3
// Copyright 2011 GRNET S.A. All rights reserved.
4
// 
5
// Redistribution and use in source and binary forms, with or
6
// without modification, are permitted provided that the following
7
// conditions are met:
8
// 
9
//   1. Redistributions of source code must retain the above
10
//      copyright notice, this list of conditions and the following
11
//      disclaimer.
12
// 
13
//   2. Redistributions in binary form must reproduce the above
14
//      copyright notice, this list of conditions and the following
15
//      disclaimer in the documentation and/or other materials
16
//      provided with the distribution.
17
// 
18
// THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
19
// OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
21
// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
22
// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
25
// USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
26
// AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
27
// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
28
// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29
// POSSIBILITY OF SUCH DAMAGE.
30
// 
31
// The views and conclusions contained in the software and
32
// documentation are those of the authors and should not be
33
// interpreted as representing official policies, either expressed
34
// or implied, of GRNET S.A.
35
// </copyright>
36
// -----------------------------------------------------------------------
37

  
38

  
39
using System;
40
using System.Collections.Generic;
41
using System.Diagnostics.Contracts;
42
using System.Linq;
43
using System.Text;
44
using System.IO;
45
using System.Text.RegularExpressions;
46
using System.Threading.Tasks;
47
using Pithos.Network;
48

  
49
namespace Pithos.Core.Agents
50
{
51
    static class FileInfoExtensions
52
    {
53
        
54

  
55
        public static int Read(this FileInfo fileInfo,byte[] buffer,int offset,int count)
56
        {            
57
            //Open the stream only long enough to read a block
58
            using (var stream = fileInfo.OpenRead())
59
            {
60
                stream.Seek(offset, SeekOrigin.Begin);
61
                return  stream.Read(buffer, 0, count);                
62
            }
63
        }
64

  
65
       public static string CalculateHash(this FileSystemInfo info,int blockSize,string algorithm)
66
        {
67
            if (info==null)
68
                throw new ArgumentNullException("info");
69
            if (String.IsNullOrWhiteSpace(info.FullName))
70
                throw new ArgumentException("info");
71
            if (blockSize<=0)
72
                throw new ArgumentOutOfRangeException("blockSize",blockSize,"blockSize must be greater than 0");
73
            if (String.IsNullOrWhiteSpace(algorithm))
74
                throw new ArgumentNullException("algorithm");
75
            Contract.EndContractBlock();
76

  
77
           //The hash for directories is an empty string
78
           if (info is DirectoryInfo)
79
                return String.Empty;
80
           
81
           var fileInfo = (FileInfo)info;
82
           if (fileInfo.Length <= blockSize)
83
                return Signature.CalculateMD5(info.FullName);
84
            else
85
                return Signature.CalculateTreeHash(info.FullName, blockSize, algorithm).TopHash.ToHashString();
86

  
87
        }
88
    }
89
}
b/trunk/Pithos.Core/Agents/NetworkAgent.cs
76 76

  
77 77
        private readonly ConcurrentBag<AccountInfo> _accounts = new ConcurrentBag<AccountInfo>();
78 78

  
79

  
80
        private bool _firstPoll = true;
79 81
        public void Start()
80 82
        {
81

  
83
            _firstPoll = true;
82 84
            _agent = Agent<CloudAction>.Start(inbox =>
83 85
            {
84 86
                Action loop = null;
......
447 449

  
448 450
                    await TaskEx.WhenAll(tasks.ToList());
449 451

  
452
                    _firstPoll = false;
450 453
                    ProcessRemoteFiles(nextSince);
451 454
                }
452 455
                catch (Exception ex)
......
480 483
                try
481 484
                {
482 485
                    _pauseAgent.Wait();
486
                    //Get the poll time now. We may miss some deletions but it's better to keep a file that was deleted
487
                    //than delete a file that was created while we were executing the poll
488
                    var pollTime = DateTime.Now;
489
                    
483 490
                    //Get the list of server objects changed since the last check
484 491
                    //The name of the container is passed as state in order to create a dictionary of tasks in a subsequent step
485 492
                    var listObjects = from container in containers
......
499 506
                                            from obj in objectList.Result
500 507
                                            select obj;
501 508

  
509
                        //TODO: Change the way deleted objects are detected.
510
                        //The list operation returns all existing objects so we could detect deleted remote objects
511
                        //by detecting objects that exist only locally. There are several cases where this is NOT the case:
512
                        //1.    The first time the application runs, as there may be files that were added while 
513
                        //      the application was down.
514
                        //2.    An object that is currently being uploaded will not appear in the remote list
515
                        //      until the upload finishes.
516
                        //      SOLUTION 1: Check the upload/download queue for the file
517
                        //      SOLUTION 2: Check the SQLite states for the file's entry. If it is being uploaded, 
518
                        //          or its last modification was after the current poll, don't delete it. This way we don't
519
                        //          delete objects whose upload finished too late to be included in the list.
520
                        //We need to detect and protect against such situations
521
                        //TODO: Does FileState have a LastModification field?
522
                        //TODO: How do we update the LastModification field? Do we need to add SQLite triggers?
523
                        //      Do we need to use a proper SQLite schema?
524
                        //      We can create a trigger with 
525
                        // CREATE TRIGGER IF NOT EXISTS update_last_modified UPDATE ON FileState FOR EACH ROW
526
                        //  BEGIN
527
                        //      UPDATE FileState SET LastModification=datetime('now')  WHERE Id=old.Id;
528
                        //  END;
529
                        //
530
                        //NOTE: Some files may have been deleted remotely while the application was down. 
531
                        //  We DO have to delete those files. Checking the trash makes it easy to detect them,
532
                        //  Otherwise, we can't be really sure whether we need to upload or delete 
533
                        //  the local-only files.
534
                        //  SOLUTION 1: Ask the user when such a local-only file is detected during the first poll.
535
                        //  SOLUTION 2: Mark conflict and ask the user as in #1
536

  
502 537
                        var trashObjects = dict["trash"].Result;
503 538
                        //var sharedObjects = ((Task<IList<ObjectInfo>>) task.Result[2]).Result;
504 539

  
......
509 544
                                            !remoteObjects.Any(
510 545
                                                info => info.Name == trash.Name && info.Hash == trash.Hash)
511 546
                                        select trash;
512
                        ProcessDeletedFiles(accountInfo, realTrash);
547
                        ProcessTrashedFiles(accountInfo, realTrash);
513 548

  
514 549

  
515
                        var remote = from info in remoteObjects
550
                        var cleanRemotes = from info in remoteObjects
516 551
                                     //.Union(sharedObjects)
517 552
                                     let name = info.Name
518 553
                                     where !name.EndsWith(".ignore", StringComparison.InvariantCultureIgnoreCase) &&
......
520 555
                                                            StringComparison.InvariantCultureIgnoreCase)
521 556
                                     select info;
522 557

  
558

  
559
                        
560

  
561

  
523 562
                        //Create a list of actions from the remote files
524
                        var allActions = ObjectsToActions(accountInfo, remote);
563
                        var allActions = ObjectsToActions(accountInfo, cleanRemotes);
564

  
565
                        ProcessDeletedFiles(accountInfo, cleanRemotes, pollTime);
566
                        //var relativePath = objectInfo.RelativeUrlToFilePath(accountInfo.UserName);
525 567

  
526 568
                        //And remove those that are already being processed by the agent
527 569
                        var distinctActions = allActions
......
548 590
            }
549 591
        }
550 592

  
593
        /// <summary>
594
        /// Deletes local files that are not found in the list of cloud files
595
        /// </summary>
596
        /// <param name="accountInfo"></param>
597
        /// <param name="cloudFiles"></param>
598
        /// <param name="pollTime"></param>
599
        private void ProcessDeletedFiles(AccountInfo accountInfo, IEnumerable<ObjectInfo> cloudFiles, DateTime pollTime)
600
        {
601
            if (accountInfo == null)
602
                throw new ArgumentNullException("accountInfo");
603
            if (String.IsNullOrWhiteSpace(accountInfo.AccountPath))
604
                throw new ArgumentException("The AccountInfo.AccountPath is empty", "accountInfo");
605
            if (cloudFiles == null)
606
                throw new ArgumentNullException("cloudFiles");
607
            Contract.EndContractBlock();
608

  
609
            if (_firstPoll) return;
610

  
611
            var deleteCandidates = from state in FileState.Queryable
612
                                   let stateUrl = FileInfoExtensions.FromPath(state.FilePath)
613
                                       .AsRelativeUrlTo(accountInfo.AccountPath)
614
                                   where state.Modified <= pollTime &&
615
                                         !cloudFiles.Any(r => r.Name == stateUrl)
616
                                   select state;
617

  
618
            foreach (var deleteCandidate in deleteCandidates)
619
            {
620
                File.Delete(deleteCandidate.FilePath);
621
                StatusKeeper.ClearFileStatus(deleteCandidate.FilePath);
622
            }
623
        }
624

  
551 625
        private static void CreateContainerFolders(AccountInfo accountInfo, IEnumerable<ContainerInfo> containers)
552 626
        {
553 627
            var containerPaths = from container in containers
......
613 687
            return AgentLocator<FileAgent>.Get(accountInfo.AccountPath);
614 688
        }
615 689

  
616
        private void ProcessDeletedFiles(AccountInfo accountInfo,IEnumerable<ObjectInfo> trashObjects)
690
        private void ProcessTrashedFiles(AccountInfo accountInfo,IEnumerable<ObjectInfo> trashObjects)
617 691
        {
618 692
            var fileAgent = GetFileAgent(accountInfo);
619 693
            foreach (var trashObject in trashObjects)
b/trunk/Pithos.Core/Agents/StatusAgent.cs
6 6
using System.Diagnostics.Contracts;
7 7
using System.IO;
8 8
using System.Linq;
9
using System.Text;
9 10
using System.Threading;
10 11
using System.Threading.Tasks;
11 12
using Castle.ActiveRecord;
......
40 41

  
41 42
            if (!File.Exists(Path.Combine(_pithosDataPath ,"pithos.db")))
42 43
                ActiveRecordStarter.CreateSchema();
43
        }        
44 44

  
45
            CreateTrigger();
46
        }
47

  
48
        private void CreateTrigger()
49
        {
50
            using (var connection = GetConnection())
51
            using (var triggerCommand = connection.CreateCommand())
52
            {
53
                var cmdText = new StringBuilder()
54
                    .AppendLine("CREATE TRIGGER IF NOT EXISTS update_last_modified UPDATE ON FileState FOR EACH ROW")
55
                    .AppendLine("BEGIN")
56
                    .AppendLine("UPDATE FileState SET Modified=datetime('now')  WHERE Id=old.Id;")
57
                    .AppendLine("END;")
58
                    .AppendLine("CREATE TRIGGER IF NOT EXISTS insert_last_modified INSERT ON FileState FOR EACH ROW")
59
                    .AppendLine("BEGIN")
60
                    .AppendLine("UPDATE FileState SET Modified=datetime('now')  WHERE Id=new.Id;")
61
                    .AppendLine("END;")
62
                    .ToString();
63
                triggerCommand.CommandText = cmdText;                
64
                triggerCommand.ExecuteNonQuery();
65
            }
66
        }
45 67

  
46 68

  
47 69
        private static InPlaceConfigurationSource GetConfiguration(string pithosDbPath)
b/trunk/Pithos.Core/FileState.cs
43 43
using Castle.ActiveRecord.Framework;
44 44
using Pithos.Core.Agents;
45 45
using Pithos.Interfaces;
46
using Pithos.Network;
46 47
using log4net;
47 48

  
48 49
namespace Pithos.Core
......
104 105
            set { _tags = value; }
105 106
        }
106 107

  
108
        [Property]
109
        public DateTime Modified { get; set; }
110

  
111

  
112
        public FileSystemInfo GetFileSystemInfo()
113
        {
114
            if (String.IsNullOrWhiteSpace(FilePath))
115
                throw new InvalidOperationException();
116
            Contract.EndContractBlock();
107 117

  
118
            return Directory.Exists(FilePath) ?
119
                (FileSystemInfo)new DirectoryInfo(FilePath)
120
                : new FileInfo(FilePath);
121
        }
122

  
123
        public string GetRelativeUrl(AccountInfo accountInfo)
124
        {
125
            if (accountInfo==null)
126
                throw new ArgumentNullException("accountInfo");
127
            Contract.EndContractBlock();
128

  
129
            var fsi=GetFileSystemInfo();
130
            return fsi.AsRelativeUrlTo(accountInfo.AccountPath);
131
        }
108 132
        /*public static FileState FindByFilePath(string absolutePath)
109 133
        {
110 134
            if (string.IsNullOrWhiteSpace(absolutePath))
b/trunk/Pithos.Core/Pithos.Core.csproj
389 389
    <Compile Include="Agents\CloudTransferAction.cs" />
390 390
    <Compile Include="Agents\CollectionExtensions.cs" />
391 391
    <Compile Include="Agents\FileAgent.cs" />
392
    <Compile Include="Agents\FileInfoExtensions.cs" />
392
    <Compile Include="Agents\BlockExtensions.cs" />
393 393
    <Compile Include="Agents\FileSystemWatcherAdapter.cs" />
394 394
    <Compile Include="Agents\MovedEventArgs.cs" />
395 395
    <Compile Include="Agents\NetworkAgent.cs" />
b/trunk/Pithos.Interfaces/FileInfoExtensions.cs
182 182

  
183 183
            throw new NotSupportedException("Unexpected parameter type");
184 184
        }
185

  
186
        public static FileSystemInfo FromPath(string filePath)
187
        {
188
            if (String.IsNullOrWhiteSpace(filePath))
189
                throw new ArgumentNullException("filePath");
190
            Contract.EndContractBlock();
191

  
192
            return Directory.Exists(filePath) ? 
193
                (FileSystemInfo) new DirectoryInfo(filePath) 
194
                : new FileInfo(filePath);
195
        }
185 196
    }
186 197
}

Also available in: Unified diff