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