Revision 174bbb6e
b/trunk/Pithos.Client.WPF/Configuration/PithosSettings.cs | ||
---|---|---|
231 | 231 |
{ |
232 | 232 |
_settings.Reload(); |
233 | 233 |
} |
234 |
|
|
235 |
public void Reset() |
|
236 |
{ |
|
237 |
_settings.Reset(); |
|
238 |
_settings.Save(); |
|
239 |
} |
|
234 | 240 |
} |
235 | 241 |
} |
b/trunk/Pithos.Client.WPF/Pithos.Client.WPF.csproj | ||
---|---|---|
30 | 30 |
<IsWebBootstrapper>false</IsWebBootstrapper> |
31 | 31 |
<UseApplicationTrust>false</UseApplicationTrust> |
32 | 32 |
<BootstrapperEnabled>true</BootstrapperEnabled> |
33 |
<CodeContractsAssemblyMode>0</CodeContractsAssemblyMode> |
|
33 | 34 |
</PropertyGroup> |
34 | 35 |
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|x86' "> |
35 | 36 |
<PlatformTarget>x86</PlatformTarget> |
... | ... | |
102 | 103 |
<CodeAnalysisIgnoreBuiltInRuleSets>false</CodeAnalysisIgnoreBuiltInRuleSets> |
103 | 104 |
<CodeAnalysisRuleDirectories>;C:\Program Files (x86)\Microsoft Visual Studio 10.0\Team Tools\Static Analysis Tools\FxCop\\Rules</CodeAnalysisRuleDirectories> |
104 | 105 |
<CodeAnalysisIgnoreBuiltInRules>false</CodeAnalysisIgnoreBuiltInRules> |
106 |
<CodeContractsEnableRuntimeChecking>True</CodeContractsEnableRuntimeChecking> |
|
107 |
<CodeContractsRuntimeOnlyPublicSurface>False</CodeContractsRuntimeOnlyPublicSurface> |
|
108 |
<CodeContractsRuntimeThrowOnFailure>True</CodeContractsRuntimeThrowOnFailure> |
|
109 |
<CodeContractsRuntimeCallSiteRequires>False</CodeContractsRuntimeCallSiteRequires> |
|
110 |
<CodeContractsRuntimeSkipQuantifiers>False</CodeContractsRuntimeSkipQuantifiers> |
|
111 |
<CodeContractsRunCodeAnalysis>True</CodeContractsRunCodeAnalysis> |
|
112 |
<CodeContractsNonNullObligations>True</CodeContractsNonNullObligations> |
|
113 |
<CodeContractsBoundsObligations>True</CodeContractsBoundsObligations> |
|
114 |
<CodeContractsArithmeticObligations>True</CodeContractsArithmeticObligations> |
|
115 |
<CodeContractsEnumObligations>True</CodeContractsEnumObligations> |
|
116 |
<CodeContractsRedundantAssumptions>True</CodeContractsRedundantAssumptions> |
|
117 |
<CodeContractsRunInBackground>False</CodeContractsRunInBackground> |
|
118 |
<CodeContractsShowSquigglies>True</CodeContractsShowSquigglies> |
|
119 |
<CodeContractsUseBaseLine>False</CodeContractsUseBaseLine> |
|
120 |
<CodeContractsEmitXMLDocs>False</CodeContractsEmitXMLDocs> |
|
121 |
<CodeContractsCustomRewriterAssembly /> |
|
122 |
<CodeContractsCustomRewriterClass /> |
|
123 |
<CodeContractsLibPaths /> |
|
124 |
<CodeContractsExtraRewriteOptions /> |
|
125 |
<CodeContractsExtraAnalysisOptions /> |
|
126 |
<CodeContractsBaseLineFile /> |
|
127 |
<CodeContractsCacheAnalysisResults>True</CodeContractsCacheAnalysisResults> |
|
128 |
<CodeContractsRuntimeCheckingLevel>Full</CodeContractsRuntimeCheckingLevel> |
|
129 |
<CodeContractsReferenceAssembly>%28none%29</CodeContractsReferenceAssembly> |
|
130 |
<CodeContractsAnalysisWarningLevel>0</CodeContractsAnalysisWarningLevel> |
|
105 | 131 |
</PropertyGroup> |
106 | 132 |
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Premium Debug|AnyCPU'"> |
107 | 133 |
<DebugSymbols>true</DebugSymbols> |
b/trunk/Pithos.Client.WPF/Shell/ShellViewModel.cs | ||
---|---|---|
535 | 535 |
monitor.Pause = !monitor.Pause; |
536 | 536 |
isPaused = monitor.Pause; |
537 | 537 |
} |
538 |
|
|
538 | 539 |
|
539 | 540 |
PauseSyncCaption = isPaused ? "Resume syncing" : "Pause syncing"; |
540 | 541 |
var iconKey = isPaused? "TraySyncPaused" : "TrayInSynch"; |
... | ... | |
557 | 558 |
private readonly Dictionary<PithosStatus, StatusInfo> _iconNames = new List<StatusInfo> |
558 | 559 |
{ |
559 | 560 |
new StatusInfo(PithosStatus.InSynch, "All files up to date", "TrayInSynch"), |
560 |
new StatusInfo(PithosStatus.Syncing, "Syncing Files", "TraySynching"), |
|
561 |
new StatusInfo(PithosStatus.PollSyncing, "Polling Files", "TraySynching"), |
|
562 |
new StatusInfo(PithosStatus.LocalSyncing, "Syncing Files", "TraySynching"), |
|
561 | 563 |
new StatusInfo(PithosStatus.SyncPaused, "Sync Paused", "TraySyncPaused") |
562 | 564 |
}.ToDictionary(s => s.Status); |
563 | 565 |
|
564 | 566 |
readonly IWindowManager _windowManager; |
565 | 567 |
|
568 |
//private int _syncCount=0; |
|
569 |
|
|
570 |
|
|
571 |
private PithosStatus _pithosStatus = PithosStatus.Disconnected; |
|
572 |
|
|
573 |
public void SetPithosStatus(PithosStatus status) |
|
574 |
{ |
|
575 |
if (_pithosStatus == PithosStatus.LocalSyncing && status == PithosStatus.PollComplete) |
|
576 |
return; |
|
577 |
if (_pithosStatus == PithosStatus.PollSyncing && status == PithosStatus.LocalComplete) |
|
578 |
return; |
|
579 |
if (status == PithosStatus.LocalComplete || status == PithosStatus.PollComplete) |
|
580 |
_pithosStatus = PithosStatus.InSynch; |
|
581 |
else |
|
582 |
_pithosStatus = status; |
|
583 |
UpdateStatus(); |
|
584 |
} |
|
585 |
|
|
586 |
public void SetPithosStatus(PithosStatus status,string message) |
|
587 |
{ |
|
588 |
StatusMessage = message; |
|
589 |
SetPithosStatus(status); |
|
590 |
} |
|
591 |
|
|
592 |
|
|
566 | 593 |
|
567 | 594 |
///<summary> |
568 | 595 |
/// Updates the visual status indicators of the application depending on status changes, e.g. icon, stat |
569 | 596 |
///</summary> |
570 | 597 |
public void UpdateStatus() |
571 | 598 |
{ |
572 |
var pithosStatus = _statusChecker.GetPithosStatus(); |
|
573 | 599 |
|
574 |
if (_iconNames.ContainsKey(pithosStatus)) |
|
600 |
if (_iconNames.ContainsKey(_pithosStatus))
|
|
575 | 601 |
{ |
576 |
var info = _iconNames[pithosStatus]; |
|
602 |
var info = _iconNames[_pithosStatus];
|
|
577 | 603 |
StatusIcon = String.Format(@"../Images/{0}.ico", info.IconName); |
578 |
|
|
579 |
|
|
580 |
|
|
581 |
StatusMessage = String.Format("Pithos {0}\r\n{1}", _fileVersion.Value.FileVersion,info.StatusText); |
|
582 | 604 |
} |
583 |
|
|
584 |
//_events.Publish(new Notification { Title = "Start", Message = "Start Monitoring", Level = TraceLevel.Info}); |
|
605 |
|
|
606 |
if (_pithosStatus == PithosStatus.InSynch) |
|
607 |
StatusMessage = "All files up to date"; |
|
585 | 608 |
} |
586 | 609 |
|
587 | 610 |
|
... | ... | |
838 | 861 |
var progress = notification as ProgressNotification; |
839 | 862 |
if (progress != null) |
840 | 863 |
{ |
841 |
StatusMessage = String.Format("Pithos {0}\r\n{1} {2} of {3} - {4}", |
|
864 |
StatusMessage = String.Format("Pithos {0}\r\n{1} {2:p2} of {3} - {4}",
|
|
842 | 865 |
_fileVersion.Value.FileVersion, |
843 | 866 |
progress.Action, |
844 | 867 |
progress.Block/(double)progress.TotalBlocks, |
... | ... | |
847 | 870 |
return; |
848 | 871 |
} |
849 | 872 |
|
873 |
var info = notification as StatusNotification; |
|
874 |
if (info != null) |
|
875 |
{ |
|
876 |
StatusMessage = String.Format("Pithos {0}\r\n{1}", |
|
877 |
_fileVersion.Value.FileVersion, |
|
878 |
info.Title); |
|
879 |
return; |
|
880 |
} |
|
850 | 881 |
if (String.IsNullOrWhiteSpace(notification.Message) && String.IsNullOrWhiteSpace(notification.Title)) |
851 | 882 |
return; |
852 | 883 |
|
853 |
BalloonIcon icon; |
|
854 |
switch (notification.Level) |
|
855 |
{ |
|
856 |
case TraceLevel.Error: |
|
857 |
icon = BalloonIcon.Error; |
|
858 |
break; |
|
859 |
case TraceLevel.Info: |
|
860 |
case TraceLevel.Verbose: |
|
861 |
icon = BalloonIcon.Info; |
|
862 |
break; |
|
863 |
case TraceLevel.Warning: |
|
864 |
icon = BalloonIcon.Warning; |
|
865 |
break; |
|
866 |
default: |
|
867 |
icon = BalloonIcon.None; |
|
868 |
break; |
|
869 |
} |
|
870 |
|
|
871 |
if (Settings.ShowDesktopNotifications) |
|
872 |
{ |
|
873 |
var tv = (ShellView) GetView(); |
|
874 |
System.Action clickAction = null; |
|
875 |
if (notification is ExpirationNotification) |
|
876 |
{ |
|
877 |
clickAction = ()=>ShowPreferences("AccountTab"); |
|
878 |
} |
|
879 |
var balloon=new PithosBalloon{Title=notification.Title,Message=notification.Message,Icon=icon,ClickAction=clickAction}; |
|
880 |
tv.TaskbarView.ShowCustomBalloon(balloon,PopupAnimation.Fade,4000); |
|
881 |
// tv.TaskbarView.ShowBalloonTip(notification.Title, notification.Message, icon); |
|
882 |
} |
|
884 |
ShowBalloonFor(notification); |
|
883 | 885 |
} |
884 |
#endregion |
|
886 |
|
|
887 |
private void ShowBalloonFor(Notification notification) |
|
888 |
{ |
|
889 |
Contract.Requires(notification!=null); |
|
890 |
|
|
891 |
if (!Settings.ShowDesktopNotifications) |
|
892 |
return; |
|
893 |
|
|
894 |
BalloonIcon icon; |
|
895 |
switch (notification.Level) |
|
896 |
{ |
|
897 |
case TraceLevel.Info: |
|
898 |
case TraceLevel.Verbose: |
|
899 |
return; |
|
900 |
case TraceLevel.Error: |
|
901 |
icon = BalloonIcon.Error; |
|
902 |
break; |
|
903 |
case TraceLevel.Warning: |
|
904 |
icon = BalloonIcon.Warning; |
|
905 |
break; |
|
906 |
default: |
|
907 |
return; |
|
908 |
} |
|
909 |
|
|
910 |
var tv = (ShellView) GetView(); |
|
911 |
System.Action clickAction = null; |
|
912 |
if (notification is ExpirationNotification) |
|
913 |
{ |
|
914 |
clickAction = () => ShowPreferences("AccountTab"); |
|
915 |
} |
|
916 |
var balloon = new PithosBalloon |
|
917 |
{ |
|
918 |
Title = notification.Title, |
|
919 |
Message = notification.Message, |
|
920 |
Icon = icon, |
|
921 |
ClickAction = clickAction |
|
922 |
}; |
|
923 |
tv.TaskbarView.ShowCustomBalloon(balloon, PopupAnimation.Fade, 4000); |
|
924 |
} |
|
925 |
|
|
926 |
#endregion |
|
885 | 927 |
|
886 | 928 |
public void Handle(ShowFilePropertiesEvent message) |
887 | 929 |
{ |
b/trunk/Pithos.Client.WPF/app.config | ||
---|---|---|
176 | 176 |
<appender-ref ref="TraceAppender"/> |
177 | 177 |
</logger> |
178 | 178 |
|
179 |
<logger name="Pithos" additivity="false"> |
|
180 |
<level value="DEBUG"/> |
|
181 |
<appender-ref ref="TraceAppender"/> |
|
182 |
</logger> |
|
183 |
|
|
179 | 184 |
<root> |
180 | 185 |
<level value="DEBUG" /> |
181 | 186 |
<appender-ref ref="LossyFileAppender" /> |
b/trunk/Pithos.Core.Test/MockStatusKeeper.cs | ||
---|---|---|
6 | 6 |
using System.Linq; |
7 | 7 |
using System.Text; |
8 | 8 |
using System.Threading; |
9 |
using System.Threading.Tasks; |
|
9 | 10 |
using Pithos.Interfaces; |
10 | 11 |
using Pithos.Network; |
11 | 12 |
|
... | ... | |
146 | 147 |
throw new NotImplementedException(); |
147 | 148 |
} |
148 | 149 |
|
150 |
public void EnsureFileState(string path) |
|
151 |
{ |
|
152 |
throw new NotImplementedException(); |
|
153 |
} |
|
154 |
|
|
149 | 155 |
|
150 | 156 |
private PithosStatus _pithosStatus = PithosStatus.InSynch; |
151 | 157 |
public void SetPithosStatus(PithosStatus status) |
... | ... | |
158 | 164 |
return _pithosStatus; |
159 | 165 |
} |
160 | 166 |
|
161 |
public void SetFileOverlayStatus(string path, FileOverlayStatus overlayStatus)
|
|
167 |
public Task SetFileOverlayStatus(string path, FileOverlayStatus overlayStatus, string shortHash = null)
|
|
162 | 168 |
{ |
163 | 169 |
_overlayCache[path] = overlayStatus; |
170 |
return Task.Factory.StartNew(()=>{}); |
|
164 | 171 |
} |
165 | 172 |
|
166 | 173 |
public void RemoveFileOverlayStatus(string path) |
... | ... | |
177 | 184 |
_overlayCache.TryRemove(oldPath, out value); |
178 | 185 |
} |
179 | 186 |
|
180 |
public void UpdateFileChecksum(string path, string checksum) |
|
187 |
public void UpdateFileChecksum(string path, string shortHash, string checksum)
|
|
181 | 188 |
{ |
182 | 189 |
_checksums[path] = checksum; |
183 | 190 |
} |
b/trunk/Pithos.Core/Agents/Agent.cs | ||
---|---|---|
53 | 53 |
public class Agent<TMessage> : IDisposable |
54 | 54 |
{ |
55 | 55 |
private readonly ConcurrentQueue<TMessage> _queue; |
56 |
private readonly AsyncProducerConsumerCollection<TMessage> _messages; |
|
56 |
//private readonly AsyncCollection<TMessage> _messages; |
|
57 |
private readonly AsyncCollection<TMessage> _messages; |
|
57 | 58 |
private readonly CancellationTokenSource _cancelSource = new CancellationTokenSource(); |
58 | 59 |
public CancellationToken CancellationToken; |
59 | 60 |
|
... | ... | |
63 | 64 |
public Agent(Action<Agent<TMessage>> action) |
64 | 65 |
{ |
65 | 66 |
_queue=new ConcurrentQueue<TMessage>(); |
66 |
_messages = new AsyncProducerConsumerCollection<TMessage>(_queue);
|
|
67 |
_messages = new AsyncCollection<TMessage>(_queue); |
|
67 | 68 |
_process = action; |
68 | 69 |
CancellationToken = _cancelSource.Token; |
69 | 70 |
} |
... | ... | |
78 | 79 |
_messages.Add(message); |
79 | 80 |
} |
80 | 81 |
|
82 |
ConcurrentDictionary<TMessage,TaskCompletionSource<object>> _awaiters=new ConcurrentDictionary<TMessage,TaskCompletionSource<object>>(); |
|
83 |
|
|
84 |
public Task PostAndAwait(TMessage message) |
|
85 |
{ |
|
86 |
var tcs = new TaskCompletionSource<object>(); |
|
87 |
_awaiters[message] = tcs; |
|
88 |
Post(message); |
|
89 |
return tcs.Task; |
|
90 |
} |
|
91 |
|
|
81 | 92 |
/// <summary> |
82 | 93 |
/// Receives a message asynchronously, optionally with a timeout. Receive throws a TimeoutException if the timeout expires |
83 | 94 |
/// </summary> |
... | ... | |
87 | 98 |
return _messages.Take(); |
88 | 99 |
} |
89 | 100 |
|
101 |
public void NotifyComplete(TMessage message) |
|
102 |
{ |
|
103 |
TaskCompletionSource<object> tcs; |
|
104 |
if (_awaiters.TryRemove(message,out tcs)) |
|
105 |
tcs.SetResult(null); |
|
106 |
} |
|
107 |
|
|
90 | 108 |
|
91 | 109 |
|
92 | 110 |
/// <summary> |
... | ... | |
148 | 166 |
if (disposing) |
149 | 167 |
{ |
150 | 168 |
Stop(); |
151 |
_messages.Dispose(); |
|
152 | 169 |
_cancelSource.Dispose(); |
153 | 170 |
} |
154 | 171 |
} |
... | ... | |
209 | 226 |
onError(ex); |
210 | 227 |
} |
211 | 228 |
return default(T); |
212 |
},CancellationToken);
|
|
229 |
}); |
|
213 | 230 |
} |
214 | 231 |
} |
215 | 232 |
} |
b/trunk/Pithos.Core/Agents/AsyncCollection.cs | ||
---|---|---|
1 |
//-------------------------------------------------------------------------- |
|
2 |
// |
|
3 |
// Copyright (c) Microsoft Corporation. All rights reserved. |
|
4 |
// |
|
5 |
// File: AsyncProducerConsumerCollection.cs |
|
6 |
// |
|
7 |
//-------------------------------------------------------------------------- |
|
8 |
|
|
9 |
using System; |
|
10 |
using System.Collections.Concurrent; |
|
11 |
using System.Diagnostics; |
|
12 |
using System.Threading; |
|
13 |
using System.Threading.Tasks; |
|
14 |
|
|
15 |
namespace Pithos.Core |
|
16 |
{ |
|
17 |
/// <summary>Provides an asynchronous producer/consumer collection.</summary> |
|
18 |
[DebuggerDisplay("Count={CurrentCount}")] |
|
19 |
public sealed class AsyncCollection<T> |
|
20 |
{ |
|
21 |
/// <summary>Asynchronous semaphore used to keep track of asynchronous work.</summary> |
|
22 |
private AsyncSemaphore _semaphore = new AsyncSemaphore(0); |
|
23 |
/// <summary>The data stored in the collection.</summary> |
|
24 |
private IProducerConsumerCollection<T> _collection; |
|
25 |
|
|
26 |
/// <summary>Initializes the asynchronous producer/consumer collection to store data in a first-in-first-out (FIFO) order.</summary> |
|
27 |
public AsyncCollection() : this(new ConcurrentQueue<T>()) { } |
|
28 |
|
|
29 |
/// <summary>Initializes the asynchronous producer/consumer collection.</summary> |
|
30 |
/// <param name="collection">The underlying collection to use to store data.</param> |
|
31 |
public AsyncCollection(IProducerConsumerCollection<T> collection) |
|
32 |
{ |
|
33 |
if (collection == null) throw new ArgumentNullException("collection"); |
|
34 |
_collection = collection; |
|
35 |
} |
|
36 |
|
|
37 |
/// <summary>Adds an element to the collection.</summary> |
|
38 |
/// <param name="item">The item to be added.</param> |
|
39 |
public void Add(T item) |
|
40 |
{ |
|
41 |
if (_collection.TryAdd(item)) |
|
42 |
Task.Factory.StartNew(s => ((AsyncSemaphore)s).Release(), |
|
43 |
_semaphore, CancellationToken.None, TaskCreationOptions.PreferFairness, TaskScheduler.Default); |
|
44 |
else throw new InvalidOperationException("Invalid collection"); |
|
45 |
} |
|
46 |
|
|
47 |
/// <summary>Takes an element from the collection asynchronously.</summary> |
|
48 |
/// <returns>A Task that represents the element removed from the collection.</returns> |
|
49 |
public Task<T> Take() |
|
50 |
{ |
|
51 |
return _semaphore.WaitAsync().ContinueWith(_ => |
|
52 |
{ |
|
53 |
T result; |
|
54 |
if (!_collection.TryTake(out result)) throw new InvalidOperationException("Invalid collection"); |
|
55 |
return result; |
|
56 |
}, TaskContinuationOptions.OnlyOnRanToCompletion); |
|
57 |
} |
|
58 |
|
|
59 |
/// <summary>Gets the number of elements in the collection.</summary> |
|
60 |
public int Count { get { return _collection.Count; } } |
|
61 |
|
|
62 |
} |
|
63 |
} |
b/trunk/Pithos.Core/Agents/AsyncManualResetEvent.cs | ||
---|---|---|
87 | 87 |
while (true) |
88 | 88 |
{ |
89 | 89 |
var tcs = _tcs; |
90 |
if (!tcs.Task.IsCompleted || |
|
91 |
Interlocked.CompareExchange(ref _tcs, new TaskCompletionSource<bool>(), tcs) == tcs) |
|
90 |
if (!tcs.Task.IsCompleted) |
|
91 |
return; |
|
92 |
#pragma warning disable 420 |
|
93 |
if (Interlocked.CompareExchange(ref _tcs, new TaskCompletionSource<bool>(), tcs) == tcs) |
|
94 |
#pragma warning restore 420 |
|
92 | 95 |
return; |
93 | 96 |
} |
94 | 97 |
} |
b/trunk/Pithos.Core/Agents/AsyncSemaphore.cs | ||
---|---|---|
1 |
// ----------------------------------------------------------------------- |
|
2 |
// <copyright file="AsyncSemaphore.cs" company="Microsoft"> |
|
3 |
// TODO: Update copyright text. |
|
4 |
// </copyright> |
|
5 |
// ----------------------------------------------------------------------- |
|
6 |
|
|
7 |
using System.Threading.Tasks; |
|
8 |
|
|
9 |
namespace Pithos.Core |
|
10 |
{ |
|
11 |
using System; |
|
12 |
using System.Collections.Generic; |
|
13 |
using System.Linq; |
|
14 |
using System.Text; |
|
15 |
|
|
16 |
/// <summary> |
|
17 |
/// TODO: Update summary. |
|
18 |
/// </summary> |
|
19 |
public class AsyncSemaphore |
|
20 |
{ |
|
21 |
private readonly static Task s_completed = TaskEx.FromResult(true); |
|
22 |
private readonly Queue<TaskCompletionSource<bool>> m_waiters = new Queue<TaskCompletionSource<bool>>(); |
|
23 |
private int m_currentCount; |
|
24 |
|
|
25 |
public AsyncSemaphore(int initialCount) |
|
26 |
{ |
|
27 |
if (initialCount < 0) throw new ArgumentOutOfRangeException("initialCount"); |
|
28 |
m_currentCount = initialCount; |
|
29 |
} |
|
30 |
public Task WaitAsync() |
|
31 |
{ |
|
32 |
|
|
33 |
lock (m_waiters) |
|
34 |
{ |
|
35 |
if (m_currentCount > 0) |
|
36 |
{ |
|
37 |
--m_currentCount; |
|
38 |
return s_completed; |
|
39 |
} |
|
40 |
else |
|
41 |
{ |
|
42 |
var waiter = new TaskCompletionSource<bool>(); |
|
43 |
m_waiters.Enqueue(waiter); |
|
44 |
return waiter.Task; |
|
45 |
} |
|
46 |
} |
|
47 |
|
|
48 |
} |
|
49 |
|
|
50 |
public void Release() |
|
51 |
{ |
|
52 |
TaskCompletionSource<bool> toRelease = null; |
|
53 |
lock (m_waiters) |
|
54 |
{ |
|
55 |
if (m_waiters.Count > 0) |
|
56 |
toRelease = m_waiters.Dequeue(); |
|
57 |
else |
|
58 |
++m_currentCount; |
|
59 |
} |
|
60 |
if (toRelease != null) |
|
61 |
toRelease.SetResult(true); |
|
62 |
} |
|
63 |
} |
|
64 |
} |
b/trunk/Pithos.Core/Agents/BlockExtensions.cs | ||
---|---|---|
43 | 43 |
using System.Collections.Generic; |
44 | 44 |
using System.Diagnostics.Contracts; |
45 | 45 |
using System.Linq; |
46 |
using System.Security.Cryptography; |
|
46 | 47 |
using System.Text; |
47 | 48 |
using System.IO; |
48 | 49 |
using System.Text.RegularExpressions; |
... | ... | |
87 | 88 |
return Signature.CalculateTreeHash(info.FullName, blockSize, algorithm).TopHash.ToHashString(); |
88 | 89 |
|
89 | 90 |
} |
91 |
|
|
92 |
/// <summary> |
|
93 |
///Calculates a simple hash for an entire file |
|
94 |
/// </summary> |
|
95 |
/// <param name="info">The file to hash</param> |
|
96 |
/// <param name="hasher">The hash algorithm to use</param> |
|
97 |
/// <returns>A hash value for the entire file. An empty string if the file does not exist.</returns> |
|
98 |
public static string ComputeShortHash(this FileInfo info, HashAlgorithm hasher) |
|
99 |
{ |
|
100 |
Contract.Requires(info != null); |
|
101 |
Contract.Requires(hasher!= null); |
|
102 |
|
|
103 |
if (!info.Exists) |
|
104 |
return String.Empty; |
|
105 |
|
|
106 |
using (var stream = info.Open(FileMode.Open, FileAccess.Read, FileShare.Read)) |
|
107 |
{ |
|
108 |
var hash = hasher.ComputeHash(stream); |
|
109 |
var hashString = hash.ToHashString(); |
|
110 |
return hashString; |
|
111 |
} |
|
112 |
} |
|
113 |
|
|
114 |
public static string ComputeShortHash(this FileInfo info) |
|
115 |
{ |
|
116 |
Contract.Requires(info != null); |
|
117 |
|
|
118 |
using (var hasher=HashAlgorithm.Create("sha1")) |
|
119 |
{ |
|
120 |
return ComputeShortHash(info,hasher); |
|
121 |
} |
|
122 |
} |
|
123 |
|
|
90 | 124 |
} |
91 | 125 |
} |
b/trunk/Pithos.Core/Agents/BlockUpdater.cs | ||
---|---|---|
207 | 207 |
if (File.Exists(FilePath)) |
208 | 208 |
File.Replace(TempPath, FilePath, null, true); |
209 | 209 |
else |
210 |
{ |
|
211 |
var targetDirectory = Path.GetDirectoryName(FilePath); |
|
212 |
if (!Directory.Exists(targetDirectory)) |
|
213 |
Directory.CreateDirectory(targetDirectory); |
|
210 | 214 |
File.Move(TempPath, FilePath); |
215 |
} |
|
211 | 216 |
} |
212 | 217 |
|
213 | 218 |
private void ClearBlocks() |
b/trunk/Pithos.Core/Agents/CloudTransferAction.cs | ||
---|---|---|
71 | 71 |
public readonly DateTime Created = DateTime.Now; |
72 | 72 |
|
73 | 73 |
|
74 |
public Lazy<string> LocalHash { get; protected set; }
|
|
75 |
public Lazy<string> TopHash { get; set; } |
|
74 |
public Lazy<TreeHash> TreeHash { get; protected set; }
|
|
75 |
//public Lazy<string> TopHash { get; set; }
|
|
76 | 76 |
|
77 | 77 |
|
78 | 78 |
[ContractInvariantMethod] |
... | ... | |
102 | 102 |
LocalFile = localFile.WithProperCapitalization(); |
103 | 103 |
CloudFile = cloudFile; |
104 | 104 |
FileState = state; |
105 |
if (LocalFile != null) |
|
106 |
{ |
|
105 |
|
|
106 |
if (LocalFile == null) |
|
107 |
return; |
|
107 | 108 |
|
108 |
LocalHash = new Lazy<string>(() => LocalFile.CalculateHash(blockSize,algorithm), |
|
109 |
LazyThreadSafetyMode.ExecutionAndPublication); |
|
110 |
} |
|
109 |
TreeHash = new Lazy<TreeHash>(() => Signature.CalculateTreeHash(LocalFile, blockSize,algorithm), |
|
110 |
LazyThreadSafetyMode.ExecutionAndPublication); |
|
111 | 111 |
} |
112 | 112 |
|
113 | 113 |
//Calculate the download path for the cloud file |
... | ... | |
239 | 239 |
OldCloudFile = CreateObjectInfoFor(accountInfo, oldFile); |
240 | 240 |
|
241 | 241 |
//This is a rename operation, a hash will not be used |
242 |
LocalHash = new Lazy<string>(() => String.Empty, LazyThreadSafetyMode.ExecutionAndPublication);
|
|
242 |
TreeHash = new Lazy<TreeHash>(() => Network.TreeHash.Empty, LazyThreadSafetyMode.ExecutionAndPublication);
|
|
243 | 243 |
} |
244 | 244 |
|
245 | 245 |
public override string ToString() |
b/trunk/Pithos.Core/Agents/FileAgent.cs | ||
---|---|---|
41 | 41 |
#endregion |
42 | 42 |
using System; |
43 | 43 |
using System.Collections.Generic; |
44 |
using System.ComponentModel.Composition; |
|
45 |
using System.Diagnostics; |
|
46 | 44 |
using System.Diagnostics.Contracts; |
47 | 45 |
using System.IO; |
48 | 46 |
using System.Linq; |
49 | 47 |
using System.Reflection; |
50 |
using System.Text; |
|
51 | 48 |
using System.Threading.Tasks; |
52 | 49 |
using Pithos.Interfaces; |
53 | 50 |
using Pithos.Network; |
54 | 51 |
using log4net; |
55 |
using log4net.Core; |
|
56 | 52 |
|
57 | 53 |
namespace Pithos.Core.Agents |
58 | 54 |
{ |
... | ... | |
67 | 63 |
|
68 | 64 |
//[Import] |
69 | 65 |
public IStatusKeeper StatusKeeper { get; set; } |
66 |
|
|
67 |
public IStatusNotification StatusNotification { get; set; } |
|
70 | 68 |
//[Import] |
71 | 69 |
public IPithosWorkflow Workflow { get; set; } |
72 | 70 |
//[Import] |
... | ... | |
89 | 87 |
|
90 | 88 |
AccountInfo = accountInfo; |
91 | 89 |
RootPath = rootPath; |
92 |
_watcher = new FileSystemWatcher(rootPath) {IncludeSubdirectories = true}; |
|
90 |
_watcher = new FileSystemWatcher(rootPath) {IncludeSubdirectories = true,InternalBufferSize=8*4096};
|
|
93 | 91 |
_adapter = new FileSystemWatcherAdapter(_watcher); |
94 | 92 |
|
95 | 93 |
_adapter.Changed += OnFileEvent; |
96 | 94 |
_adapter.Created += OnFileEvent; |
97 | 95 |
_adapter.Deleted += OnFileEvent; |
98 |
_adapter.Renamed += OnRenameEvent; |
|
96 |
//_adapter.Renamed += OnRenameEvent;
|
|
99 | 97 |
_adapter.Moved += OnMoveEvent; |
100 | 98 |
_watcher.EnableRaisingEvents = true; |
101 | 99 |
|
... | ... | |
135 | 133 |
|
136 | 134 |
try |
137 | 135 |
{ |
136 |
//StatusKeeper.EnsureFileState(state.Path); |
|
137 |
|
|
138 | 138 |
UpdateFileStatus(state); |
139 | 139 |
UpdateOverlayStatus(state); |
140 | 140 |
UpdateFileChecksum(state); |
... | ... | |
193 | 193 |
{ |
194 | 194 |
if (_watcher != null) |
195 | 195 |
{ |
196 |
_watcher.Changed -= OnFileEvent; |
|
197 |
_watcher.Created -= OnFileEvent; |
|
198 |
_watcher.Deleted -= OnFileEvent; |
|
199 |
_watcher.Renamed -= OnRenameEvent; |
|
200 | 196 |
_watcher.Dispose(); |
201 | 197 |
} |
202 | 198 |
_watcher = null; |
... | ... | |
329 | 325 |
} |
330 | 326 |
|
331 | 327 |
|
328 |
/* |
|
332 | 329 |
//Post a Change message for renames containing the old and new names |
333 | 330 |
void OnRenameEvent(object sender, RenamedEventArgs e) |
334 | 331 |
{ |
... | ... | |
347 | 344 |
TriggeringChange = e.ChangeType |
348 | 345 |
}); |
349 | 346 |
} |
347 |
*/ |
|
350 | 348 |
|
351 |
//Post a Change message for renames containing the old and new names
|
|
349 |
//Post a Change message for moves containing the old and new names
|
|
352 | 350 |
void OnMoveEvent(object sender, MovedEventArgs e) |
353 | 351 |
{ |
354 | 352 |
var oldFullPath = e.OldFullPath; |
... | ... | |
415 | 413 |
switch (state.Status) |
416 | 414 |
{ |
417 | 415 |
case FileStatus.Created: |
416 |
this.StatusKeeper.SetFileOverlayStatus(state.Path, FileOverlayStatus.Modified,state.ShortHash); |
|
417 |
break; |
|
418 | 418 |
case FileStatus.Modified: |
419 |
this.StatusKeeper.SetFileOverlayStatus(state.Path, FileOverlayStatus.Modified); |
|
419 |
this.StatusKeeper.SetFileOverlayStatus(state.Path, FileOverlayStatus.Modified,state.ShortHash);
|
|
420 | 420 |
break; |
421 | 421 |
case FileStatus.Deleted: |
422 | 422 |
//this.StatusAgent.RemoveFileOverlayStatus(state.Path); |
423 | 423 |
break; |
424 | 424 |
case FileStatus.Renamed: |
425 | 425 |
this.StatusKeeper.ClearFileStatus(state.OldPath); |
426 |
this.StatusKeeper.SetFileOverlayStatus(state.Path, FileOverlayStatus.Modified); |
|
426 |
this.StatusKeeper.SetFileOverlayStatus(state.Path, FileOverlayStatus.Modified,state.ShortHash);
|
|
427 | 427 |
break; |
428 | 428 |
case FileStatus.Unchanged: |
429 |
this.StatusKeeper.SetFileOverlayStatus(state.Path, FileOverlayStatus.Normal); |
|
429 |
this.StatusKeeper.SetFileOverlayStatus(state.Path, FileOverlayStatus.Normal,state.ShortHash);
|
|
430 | 430 |
break; |
431 | 431 |
} |
432 | 432 |
|
... | ... | |
451 | 451 |
if (Directory.Exists(path)) |
452 | 452 |
return state; |
453 | 453 |
|
454 |
|
|
454 | 455 |
var info = new FileInfo(path); |
455 |
string hash = info.CalculateHash(StatusKeeper.BlockSize,StatusKeeper.BlockHash); |
|
456 |
StatusKeeper.UpdateFileChecksum(path, hash); |
|
456 |
StatusNotification.Notify(new StatusNotification(String.Format("Hashing [{0}]",info.Name))); |
|
457 |
|
|
458 |
var shortHash = info.ComputeShortHash(); |
|
459 |
|
|
460 |
string merkleHash = info.CalculateHash(StatusKeeper.BlockSize,StatusKeeper.BlockHash); |
|
461 |
StatusKeeper.UpdateFileChecksum(path,shortHash, merkleHash); |
|
457 | 462 |
|
458 |
state.Hash = hash;
|
|
463 |
state.Hash = merkleHash;
|
|
459 | 464 |
return state; |
460 | 465 |
} |
461 | 466 |
|
b/trunk/Pithos.Core/Agents/FileSystemWatcherAdapter.cs | ||
---|---|---|
41 | 41 |
#endregion |
42 | 42 |
using System.Diagnostics.Contracts; |
43 | 43 |
using System.IO; |
44 |
using System.Reflection; |
|
44 | 45 |
using System.Threading.Tasks; |
45 | 46 |
using Pithos.Interfaces; |
47 |
using log4net; |
|
46 | 48 |
|
47 | 49 |
namespace Pithos.Core.Agents |
48 | 50 |
{ |
... | ... | |
53 | 55 |
/// </summary> |
54 | 56 |
public class FileSystemWatcherAdapter |
55 | 57 |
{ |
56 |
public event FileSystemEventHandler Changed; |
|
58 |
private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); |
|
59 |
|
|
60 |
|
|
61 |
public event FileSystemEventHandler Changed; |
|
57 | 62 |
public event FileSystemEventHandler Created; |
58 | 63 |
public event FileSystemEventHandler Deleted; |
59 |
public event RenamedEventHandler Renamed; |
|
64 |
//public event RenamedEventHandler Renamed;
|
|
60 | 65 |
public event MovedEventHandler Moved; |
61 | 66 |
|
62 | 67 |
public FileSystemWatcherAdapter(FileSystemWatcher watcher) |
... | ... | |
68 | 73 |
watcher.Changed += OnChangeOrCreate; |
69 | 74 |
watcher.Created += OnChangeOrCreate; |
70 | 75 |
watcher.Deleted += OnDeleted; |
71 |
watcher.Renamed += OnRename; |
|
72 |
|
|
76 |
watcher.Renamed += OnRename; |
|
77 |
watcher.Error += OnError; |
|
78 |
} |
|
79 |
|
|
80 |
private void OnError(object sender, ErrorEventArgs e) |
|
81 |
{ |
|
82 |
var error = e.GetException(); |
|
83 |
Log.Error("FSW error",error); |
|
73 | 84 |
} |
74 | 85 |
|
75 | 86 |
private string _cachedDeletedFullPath; |
... | ... | |
86 | 97 |
Contract.Ensures(!String.IsNullOrWhiteSpace(_cachedDeletedFullPath)); |
87 | 98 |
Contract.EndContractBlock(); |
88 | 99 |
|
89 |
//Handle any previously deleted event |
|
100 |
TaskEx.Run(() => InnerOnDeleted(sender, e)); |
|
101 |
} |
|
102 |
|
|
103 |
private void InnerOnDeleted(object sender, FileSystemEventArgs e) |
|
104 |
{ |
|
105 |
//Handle any previously deleted event |
|
106 |
if (Log.IsDebugEnabled) |
|
107 |
Log.DebugFormat("[{0}] for [{1}]", Enum.GetName(typeof(WatcherChangeTypes), e.ChangeType), e.FullPath); |
|
90 | 108 |
PropagateCachedDeleted(sender); |
91 | 109 |
|
92 | 110 |
//A delete event may be an actual delete event or the first event in a move action. |
... | ... | |
99 | 117 |
// as this is actually a MOVE operation |
100 | 118 |
//Deleting by Shift+Delete results in a delete event for each file followed by the delete of the folder itself |
101 | 119 |
_cachedDeletedFullPath = e.FullPath; |
102 |
|
|
120 |
|
|
103 | 121 |
//TODO: This requires synchronization of the _cachedDeletedFullPath field |
104 | 122 |
//TODO: This creates a new task for each file even though we can cancel any existing tasks if a new event arrives |
105 | 123 |
//Maybe, use a timer instead of a task |
106 |
|
|
124 |
|
|
107 | 125 |
TaskEx.Delay(PropagateDelay).ContinueWith(t => |
108 |
{
|
|
109 |
var myPath = e.FullPath;
|
|
110 |
if (_cachedDeletedFullPath==myPath)
|
|
111 |
PropagateCachedDeleted(sender);
|
|
112 |
});
|
|
126 |
{ |
|
127 |
var myPath = e.FullPath; |
|
128 |
if (_cachedDeletedFullPath == myPath)
|
|
129 |
PropagateCachedDeleted(sender); |
|
130 |
}); |
|
113 | 131 |
} |
114 | 132 |
|
115 | 133 |
private void OnRename(object sender, RenamedEventArgs e) |
... | ... | |
119 | 137 |
Contract.Ensures(_cachedDeletedFullPath == null); |
120 | 138 |
Contract.EndContractBlock(); |
121 | 139 |
|
140 |
TaskEx.Run(() => InnerRename(sender, e)); |
|
141 |
} |
|
142 |
|
|
143 |
private void InnerRename(object sender, RenamedEventArgs e) |
|
144 |
{ |
|
122 | 145 |
try |
123 | 146 |
{ |
147 |
if (Log.IsDebugEnabled) |
|
148 |
Log.DebugFormat("[{0}] for [{1}]", Enum.GetName(typeof(WatcherChangeTypes), e.ChangeType), e.FullPath); |
|
124 | 149 |
//Propagate any previous cached delete event |
125 | 150 |
PropagateCachedDeleted(sender); |
126 |
|
|
127 |
if (Renamed!=null) |
|
128 |
Renamed(sender, e); |
|
129 |
|
|
151 |
|
|
152 |
if (Moved!= null) |
|
153 |
{ |
|
154 |
try |
|
155 |
{ |
|
156 |
|
|
157 |
Moved(sender, new MovedEventArgs(e.FullPath,e.Name,e.OldFullPath,e.OldName)); |
|
158 |
} |
|
159 |
catch (Exception exc) |
|
160 |
{ |
|
161 |
Log.Error("Rename event error", exc); |
|
162 |
throw; |
|
163 |
} |
|
164 |
|
|
165 |
var directory = new DirectoryInfo(e.FullPath); |
|
166 |
if (directory.Exists) |
|
167 |
{ |
|
168 |
var newDirectory = e.FullPath; |
|
169 |
var oldDirectory = e.OldFullPath; |
|
170 |
|
|
171 |
foreach ( |
|
172 |
var child in |
|
173 |
directory.EnumerateFileSystemInfos("*", SearchOption.AllDirectories)) |
|
174 |
{ |
|
175 |
var newChildDirectory = Path.GetDirectoryName(child.FullName); |
|
176 |
|
|
177 |
var relativePath = child.AsRelativeTo(newDirectory); |
|
178 |
var relativeFolder = Path.GetDirectoryName(relativePath); |
|
179 |
var oldChildDirectory = Path.Combine(oldDirectory, relativeFolder); |
|
180 |
Moved(sender, |
|
181 |
new MovedEventArgs(newChildDirectory, child.Name, oldChildDirectory, |
|
182 |
child.Name)); |
|
183 |
} |
|
184 |
} |
|
185 |
} |
|
130 | 186 |
} |
131 | 187 |
finally |
132 | 188 |
{ |
133 |
_cachedDeletedFullPath = null;
|
|
189 |
_cachedDeletedFullPath = null; |
|
134 | 190 |
} |
135 | 191 |
} |
136 | 192 |
|
... | ... | |
142 | 198 |
throw new ArgumentException("e"); |
143 | 199 |
Contract.Ensures(_cachedDeletedFullPath == null); |
144 | 200 |
Contract.EndContractBlock(); |
201 |
TaskEx.Run(() => InnerChangeOrCreated(sender, e)); |
|
145 | 202 |
|
203 |
} |
|
204 |
|
|
205 |
private void InnerChangeOrCreated(object sender, FileSystemEventArgs e) |
|
206 |
{ |
|
146 | 207 |
try |
147 | 208 |
{ |
209 |
if (Log.IsDebugEnabled) |
|
210 |
Log.DebugFormat("[{0}] for [{1}]",Enum.GetName(typeof(WatcherChangeTypes),e.ChangeType),e.FullPath); |
|
148 | 211 |
//A Move action results in a sequence of a Delete and a Create or Change event |
149 | 212 |
//If the actual action is a Move, raise a Move event instead of the actual event |
150 | 213 |
if (HandleMoved(sender, e)) |
... | ... | |
159 | 222 |
{ |
160 | 223 |
actualEvent(sender, e); |
161 | 224 |
//For Folders, raise Created events for all children |
162 |
RaiseCreatedForChildren(sender,e); |
|
225 |
RaiseCreatedForChildren(sender, e);
|
|
163 | 226 |
} |
164 | 227 |
} |
165 | 228 |
finally |
... | ... | |
167 | 230 |
//Finally, make sure the cached path is cleared |
168 | 231 |
_cachedDeletedFullPath = null; |
169 | 232 |
} |
170 |
|
|
171 | 233 |
} |
172 | 234 |
|
173 | 235 |
private void RaiseCreatedForChildren(object sender, FileSystemEventArgs e) |
... | ... | |
181 | 243 |
//Skip if this is not a folder |
182 | 244 |
if (!dir.Exists) |
183 | 245 |
return; |
184 |
foreach (var info in dir.EnumerateFileSystemInfos("*",SearchOption.AllDirectories))
|
|
246 |
try
|
|
185 | 247 |
{ |
186 |
var path = Path.GetDirectoryName(info.FullName); |
|
187 |
Created(sender,new FileSystemEventArgs(WatcherChangeTypes.Created,path,info.Name)); |
|
188 |
} |
|
248 |
foreach (var info in dir.EnumerateFileSystemInfos("*",SearchOption.AllDirectories)) |
|
249 |
{ |
|
250 |
var path = Path.GetDirectoryName(info.FullName); |
|
251 |
Created(sender,new FileSystemEventArgs(WatcherChangeTypes.Created,path,info.Name)); |
|
252 |
} |
|
253 |
} |
|
254 |
catch (IOException exc) |
|
255 |
{ |
|
256 |
TaskEx.Delay(1000) |
|
257 |
.ContinueWith(_=>RaiseCreatedForChildren(sender,e)); |
|
258 |
|
|
259 |
} |
|
189 | 260 |
} |
190 | 261 |
|
191 | 262 |
private bool HandleMoved(object sender, FileSystemEventArgs e) |
... | ... | |
212 | 283 |
|
213 | 284 |
try |
214 | 285 |
{ |
286 |
if (Log.IsDebugEnabled) |
|
287 |
Log.DebugFormat("Moved for [{0}]", e.FullPath); |
|
288 |
|
|
215 | 289 |
//If the actual action is a Move, raise a Move event instead of the actual event |
216 | 290 |
var newDirectory = Path.GetDirectoryName(e.FullPath); |
217 | 291 |
var oldDirectory = Path.GetDirectoryName(_cachedDeletedFullPath); |
... | ... | |
261 | 335 |
var deletedFileName = Path.GetFileName(_cachedDeletedFullPath); |
262 | 336 |
var deletedFileDirectory = Path.GetDirectoryName(_cachedDeletedFullPath); |
263 | 337 |
|
338 |
if (Log.IsDebugEnabled) |
|
339 |
Log.DebugFormat("Propagating delete for [{0}]", _cachedDeletedFullPath); |
|
340 |
|
|
264 | 341 |
//Only a single file Delete event is raised when moving a file to the Recycle Bin, as this is actually a MOVE operation |
265 | 342 |
//In this case we need to raise the proper events for all child objects of the deleted directory. |
266 | 343 |
//UNFORTUNATELY, this can't be detected here, eg. by retrieving the child objects, because they are already deleted |
b/trunk/Pithos.Core/Agents/NetworkAgent.cs | ||
---|---|---|
89 | 89 |
|
90 | 90 |
public void Start() |
91 | 91 |
{ |
92 |
if (_agent != null) |
|
93 |
return; |
|
94 |
|
|
95 |
if (Log.IsDebugEnabled) |
|
96 |
Log.Debug("Starting Network Agent"); |
|
97 |
|
|
92 | 98 |
_agent = Agent<CloudAction>.Start(inbox => |
93 | 99 |
{ |
94 | 100 |
Action loop = null; |
... | ... | |
123 | 129 |
|
124 | 130 |
try |
125 | 131 |
{ |
132 |
StatusNotification.SetPithosStatus(PithosStatus.LocalSyncing,"Processing"); |
|
126 | 133 |
_proceedEvent.Reset(); |
127 | 134 |
//UpdateStatus(PithosStatus.Syncing); |
128 | 135 |
var accountInfo = action.AccountInfo; |
... | ... | |
204 | 211 |
{ |
205 | 212 |
if (_agent.IsEmpty) |
206 | 213 |
_proceedEvent.Set(); |
207 |
UpdateStatus(PithosStatus.InSynch);
|
|
214 |
UpdateStatus(PithosStatus.LocalComplete);
|
|
208 | 215 |
} |
209 | 216 |
} |
210 | 217 |
} |
... | ... | |
212 | 219 |
|
213 | 220 |
private void UpdateStatus(PithosStatus status) |
214 | 221 |
{ |
215 |
StatusKeeper.SetPithosStatus(status);
|
|
216 |
StatusNotification.Notify(new Notification()); |
|
222 |
StatusNotification.SetPithosStatus(status);
|
|
223 |
//StatusNotification.Notify(new Notification());
|
|
217 | 224 |
} |
218 | 225 |
|
219 | 226 |
private void RenameLocalFile(AccountInfo accountInfo, CloudAction action) |
... | ... | |
297 | 304 |
|
298 | 305 |
var cloudHash = cloudFile.Hash.ToLower(); |
299 | 306 |
var previousCloudHash = cloudFile.PreviousHash.ToLower(); |
300 |
var localHash = action.LocalHash.Value.ToLower(); |
|
301 |
var topHash = action.TopHash.Value.ToLower(); |
|
307 |
var localHash = action.TreeHash.Value.TopHash.ToHashString();// LocalHash.Value.ToLower();
|
|
308 |
//var topHash = action.TopHash.Value.ToLower();
|
|
302 | 309 |
|
303 | 310 |
//At this point we know that an object has changed on the server and that a local |
304 | 311 |
//file already exists. We need to decide whether the file has only changed on |
305 | 312 |
//the server or there is a conflicting change on the client. |
306 | 313 |
// |
307 | 314 |
|
308 |
//Not enough to compare only the local hashes (MD5), also have to compare the tophashes |
|
309 |
//If any of the hashes match, we are done |
|
310 |
if ((cloudHash == localHash || cloudHash == topHash)) |
|
315 |
//If the hashes match, we are done |
|
316 |
if (cloudHash == localHash) |
|
311 | 317 |
{ |
312 | 318 |
Log.InfoFormat("Skipping {0}, hashes match", downloadPath); |
313 | 319 |
return; |
... | ... | |
351 | 357 |
Contract.EndContractBlock(); |
352 | 358 |
|
353 | 359 |
_deleteAgent.ProceedEvent.Wait(); |
360 |
/* |
|
354 | 361 |
|
355 | 362 |
//If the action targets a local file, add a treehash calculation |
356 | 363 |
if (!(cloudAction is CloudDeleteAction) && cloudAction.LocalFile as FileInfo != null) |
357 | 364 |
{ |
358 | 365 |
var accountInfo = cloudAction.AccountInfo; |
359 | 366 |
var localFile = (FileInfo) cloudAction.LocalFile; |
367 |
|
|
360 | 368 |
if (localFile.Length > accountInfo.BlockSize) |
361 | 369 |
cloudAction.TopHash = |
362 | 370 |
new Lazy<string>(() => Signature.CalculateTreeHashAsync(localFile, |
... | ... | |
367 | 375 |
{ |
368 | 376 |
cloudAction.TopHash = new Lazy<string>(() => cloudAction.LocalHash.Value); |
369 | 377 |
} |
378 |
|
|
370 | 379 |
} |
371 | 380 |
else |
372 | 381 |
{ |
373 | 382 |
//The hash for a directory is the empty string |
374 | 383 |
cloudAction.TopHash = new Lazy<string>(() => String.Empty); |
375 | 384 |
} |
385 |
*/ |
|
376 | 386 |
|
377 | 387 |
if (cloudAction is CloudDeleteAction) |
378 | 388 |
_deleteAgent.Post((CloudDeleteAction)cloudAction); |
... | ... | |
497 | 507 |
} |
498 | 508 |
else |
499 | 509 |
{ |
500 |
//Retrieve the hashmap from the server |
|
501 |
var serverHash = await client.GetHashMap(account, container, url); |
|
502 |
//If it's a small file |
|
503 |
if (serverHash.Hashes.Count == 1) |
|
504 |
//Download it in one go |
|
505 |
await |
|
506 |
DownloadEntireFileAsync(accountInfo, client, cloudFile, relativeUrl, localPath, |
|
507 |
serverHash); |
|
508 |
//Otherwise download it block by block |
|
509 |
else |
|
510 |
await DownloadWithBlocks(accountInfo, client, cloudFile, relativeUrl, localPath, serverHash); |
|
511 |
|
|
512 |
if (cloudFile.AllowedTo == "read") |
|
510 |
var isChanged = IsObjectChanged(cloudFile, localPath); |
|
511 |
if (isChanged) |
|
513 | 512 |
{ |
514 |
var attributes = File.GetAttributes(localPath); |
|
515 |
File.SetAttributes(localPath, attributes | FileAttributes.ReadOnly); |
|
513 |
//Retrieve the hashmap from the server |
|
514 |
var serverHash = await client.GetHashMap(account, container, url); |
|
515 |
//If it's a small file |
|
516 |
if (serverHash.Hashes.Count == 1) |
|
517 |
//Download it in one go |
|
518 |
await |
|
519 |
DownloadEntireFileAsync(accountInfo, client, cloudFile, relativeUrl, localPath, |
|
520 |
serverHash); |
|
521 |
//Otherwise download it block by block |
|
522 |
else |
|
523 |
await |
|
524 |
DownloadWithBlocks(accountInfo, client, cloudFile, relativeUrl, localPath, |
|
525 |
serverHash); |
|
526 |
|
|
527 |
if (cloudFile.AllowedTo == "read") |
|
528 |
{ |
|
529 |
var attributes = File.GetAttributes(localPath); |
|
530 |
File.SetAttributes(localPath, attributes | FileAttributes.ReadOnly); |
|
531 |
} |
|
516 | 532 |
} |
517 | 533 |
} |
518 | 534 |
|
... | ... | |
523 | 539 |
} |
524 | 540 |
} |
525 | 541 |
|
542 |
private bool IsObjectChanged(ObjectInfo cloudFile, string localPath) |
|
543 |
{ |
|
544 |
//If the target is a directory, there are no changes to download |
|
545 |
if (Directory.Exists(localPath)) |
|
546 |
return false; |
|
547 |
//If the file doesn't exist, we have a chagne |
|
548 |
if (!File.Exists(localPath)) |
|
549 |
return true; |
|
550 |
//If there is no stored state, we have a change |
|
551 |
var localState = StatusKeeper.GetStateByFilePath(localPath); |
|
552 |
if (localState == null) |
|
553 |
return true; |
|
554 |
|
|
555 |
var info = new FileInfo(localPath); |
|
556 |
var shortHash = info.ComputeShortHash(); |
|
557 |
//If the file is different from the stored state, we have a change |
|
558 |
if (localState.ShortHash != shortHash) |
|
559 |
return true; |
|
560 |
//If the top hashes differ, we have a change |
|
561 |
return (localState.Checksum != cloudFile.Hash); |
|
562 |
} |
|
563 |
|
|
526 | 564 |
//Download a small file with a single GET operation |
527 | 565 |
private async Task DownloadEntireFileAsync(AccountInfo accountInfo, CloudFilesClient client, ObjectInfo cloudFile, Uri relativeUrl, string filePath,TreeHash serverHash) |
528 | 566 |
{ |
... | ... | |
539 | 577 |
Contract.EndContractBlock(); |
540 | 578 |
|
541 | 579 |
var localPath = Pithos.Interfaces.FileInfoExtensions.GetProperFilePathCapitalization(filePath); |
542 |
//If the file already exists |
|
543 |
if (File.Exists(localPath)) |
|
544 |
{ |
|
545 |
//First check with MD5 as this is a small file |
|
546 |
var localMD5 = Signature.CalculateMD5(localPath); |
|
547 |
var cloudHash=serverHash.TopHash.ToHashString(); |
|
548 |
if (localMD5==cloudHash) |
|
549 |
return; |
|
550 |
//Then check with a treehash |
|
551 |
var localTreeHash = Signature.CalculateTreeHash(localPath, serverHash.BlockSize, serverHash.BlockHash); |
|
552 |
var localHash = localTreeHash.TopHash.ToHashString(); |
|
553 |
if (localHash==cloudHash) |
|
554 |
return; |
|
555 |
} |
|
580 |
StatusNotification.SetPithosStatus(PithosStatus.LocalSyncing,String.Format("Downloading {0}",Path.GetFileName(localPath))); |
|
556 | 581 |
StatusNotification.Notify(new CloudNotification { Data = cloudFile }); |
557 | 582 |
|
558 | 583 |
var fileAgent = GetFileAgent(accountInfo); |
... | ... | |
608 | 633 |
var blockUpdater = new BlockUpdater(fileAgent.CachePath, localPath, relativePath, serverHash); |
609 | 634 |
|
610 | 635 |
|
611 |
|
|
636 |
StatusNotification.SetPithosStatus(PithosStatus.LocalSyncing,String.Format("Calculating hashmap for {0} before download",Path.GetFileName(localPath))); |
|
612 | 637 |
//Calculate the file's treehash |
613 | 638 |
var treeHash = await Signature.CalculateTreeHashAsync(localPath, serverHash.BlockSize, serverHash.BlockHash, 2); |
614 | 639 |
|
... | ... | |
676 | 701 |
|
677 | 702 |
if (fileInfo.Extension.Equals("ignore", StringComparison.InvariantCultureIgnoreCase)) |
678 | 703 |
return; |
704 |
|
|
705 |
// |
|
706 |
if (action.FileState == null) |
|
707 |
action.FileState = StatusKeeper.GetStateByFilePath(fileInfo.FullName); |
|
679 | 708 |
//Do not upload files in conflict |
680 | 709 |
if (action.FileState.FileStatus == FileStatus.Conflict ) |
681 | 710 |
{ |
... | ... | |
724 | 753 |
|
725 | 754 |
var client = new CloudFilesClient(accountInfo); |
726 | 755 |
//Even if GetObjectInfo times out, we can proceed with the upload |
727 |
var info = client.GetObjectInfo(account, cloudFile.Container, cloudFile.Name);
|
|
756 |
var cloudInfo = client.GetObjectInfo(account, cloudFile.Container, cloudFile.Name);
|
|
728 | 757 |
|
729 | 758 |
//If this is a read-only file, do not upload changes |
730 |
if (info.AllowedTo == "read")
|
|
759 |
if (cloudInfo.AllowedTo == "read")
|
|
731 | 760 |
return; |
732 | 761 |
|
733 | 762 |
//TODO: Check how a directory hash is calculated -> All dirs seem to have the same hash |
734 | 763 |
if (fileInfo is DirectoryInfo) |
735 | 764 |
{ |
736 | 765 |
//If the directory doesn't exist the Hash property will be empty |
737 |
if (String.IsNullOrWhiteSpace(info.Hash))
|
|
766 |
if (String.IsNullOrWhiteSpace(cloudInfo.Hash))
|
|
738 | 767 |
//Go on and create the directory |
739 | 768 |
await |
740 | 769 |
client.PutObject(account, cloudFile.Container, cloudFile.Name, fullFileName, |
... | ... | |
743 | 772 |
else |
744 | 773 |
{ |
745 | 774 |
|
746 |
var cloudHash = info.Hash.ToLower(); |
|
775 |
var cloudHash = cloudInfo.Hash.ToLower(); |
|
776 |
|
|
777 |
StatusNotification.Notify(new StatusNotification(String.Format("Hashing {0} for Upload",fileInfo.Name))); |
|
778 |
|
|
779 |
//TODO: This is the same as the calculation for the Local Hash! |
|
780 |
//First, calculate the tree hash |
|
781 |
/* |
|
782 |
var treeHash = await Signature.CalculateTreeHashAsync(fullFileName, accountInfo.BlockSize, |
|
783 |
accountInfo.BlockHash, 2); |
|
784 |
*/ |
|
747 | 785 |
|
748 |
var hash = action.LocalHash.Value; |
|
749 |
var topHash = action.TopHash.Value; |
|
786 |
|
|
787 |
var treeHash = action.TreeHash.Value; |
|
788 |
var topHash = treeHash.TopHash.ToHashString(); |
|
789 |
|
|
790 |
//var topHash = action.TopHash.Value; |
|
750 | 791 |
|
751 | 792 |
//If the file hashes match, abort the upload |
752 |
if (hash == cloudHash || topHash == cloudHash)
|
|
793 |
if (topHash == cloudHash /*|| topHash == cloudHash*/)
|
|
753 | 794 |
{ |
754 | 795 |
//but store any metadata changes |
755 |
StatusKeeper.StoreInfo(fullFileName, info);
|
|
796 |
StatusKeeper.StoreInfo(fullFileName, cloudInfo);
|
|
756 | 797 |
Log.InfoFormat("Skip upload of {0}, hashes match", fullFileName); |
757 | 798 |
return; |
758 | 799 |
} |
... | ... | |
765 | 806 |
//Upload even small files using the Hashmap. The server may already contain |
766 | 807 |
//the relevant block |
767 | 808 |
|
768 |
//First, calculate the tree hash |
|
769 |
var treeHash = await Signature.CalculateTreeHashAsync(fullFileName, accountInfo.BlockSize, |
|
770 |
accountInfo.BlockHash, 2); |
|
771 |
|
|
772 | 809 |
//TODO: If the upload fails with a 403, abort it and mark conflict |
773 | 810 |
|
774 | 811 |
await |
... | ... | |
847 | 884 |
throw new ArgumentException("Invalid container","cloudFile"); |
848 | 885 |
Contract.EndContractBlock(); |
849 | 886 |
|
887 |
StatusNotification.Notify(new StatusNotification(String.Format("Uploading {0} for Upload", fileInfo.Name))); |
|
888 |
|
|
850 | 889 |
var fullFileName = fileInfo.GetProperCapitalization(); |
851 | 890 |
|
852 | 891 |
var account = cloudFile.Account ?? accountInfo.UserName; |
... | ... | |
886 | 925 |
//Repeat until there are no more missing hashes |
887 | 926 |
missingHashes = await client.PutHashMap(account, container, url, treeHash); |
888 | 927 |
} |
928 |
|
|
889 | 929 |
ReportUploadProgress(fileInfo.Name, missingHashes.Count, missingHashes.Count, fileInfo.Length); |
890 | 930 |
} |
891 | 931 |
|
892 | 932 |
private void ReportUploadProgress(string fileName,int block, int totalBlocks, long fileSize) |
893 | 933 |
{ |
894 |
StatusNotification.Notify(new ProgressNotification(fileName,"Uploading",block,totalBlocks,fileSize) ); |
|
934 |
StatusNotification.Notify(totalBlocks == 0 |
|
935 |
? new ProgressNotification(fileName, "Uploading", 1, 1, fileSize) |
|
936 |
: new ProgressNotification(fileName, "Uploading", block, totalBlocks, fileSize)); |
|
895 | 937 |
} |
938 |
|
|
896 | 939 |
private void ReportDownloadProgress(string fileName,int block, int totalBlocks, long fileSize) |
897 | 940 |
{ |
898 |
StatusNotification.Notify(new ProgressNotification(fileName,"Downloading",block,totalBlocks,fileSize) ); |
|
941 |
StatusNotification.Notify(totalBlocks == 0 |
|
942 |
? new ProgressNotification(fileName, "Downloading", 1, 1, fileSize) |
|
943 |
: new ProgressNotification(fileName, "Downloading", block, totalBlocks, fileSize)); |
|
899 | 944 |
} |
900 | 945 |
} |
901 | 946 |
|
b/trunk/Pithos.Core/Agents/PollAgent.cs | ||
---|---|---|
105 | 105 |
/// <returns></returns> |
106 | 106 |
public async Task PollRemoteFiles(DateTime? since = null) |
107 | 107 |
{ |
108 |
Debug.Assert(Thread.CurrentThread.IsBackground, "Polling Ended up in the main thread!"); |
|
108 |
if (Log.IsDebugEnabled) |
|
109 |
Log.DebugFormat("Polling changes after [{0}]",since); |
|
109 | 110 |
|
110 |
UpdateStatus(PithosStatus.Syncing);
|
|
111 |
StatusNotification.Notify(new PollNotification()); |
|
111 |
Debug.Assert(Thread.CurrentThread.IsBackground, "Polling Ended up in the main thread!");
|
|
112 |
|
|
112 | 113 |
|
113 | 114 |
using (ThreadContext.Stacks["Retrieve Remote"].Push("All accounts")) |
114 | 115 |
{ |
... | ... | |
116 | 117 |
var nextSince = since; |
117 | 118 |
try |
118 | 119 |
{ |
120 |
UpdateStatus(PithosStatus.PollSyncing); |
|
121 |
|
|
119 | 122 |
//Next time we will check for all changes since the current check minus 1 second |
120 | 123 |
//This is done to ensure there are no discrepancies due to clock differences |
121 | 124 |
var current = DateTime.Now.AddSeconds(-1); |
... | ... | |
135 | 138 |
//In case of failure retry with the same "since" value |
136 | 139 |
} |
137 | 140 |
|
138 |
UpdateStatus(PithosStatus.InSynch);
|
|
141 |
UpdateStatus(PithosStatus.PollComplete);
|
|
139 | 142 |
//The multiple try blocks are required because we can't have an await call |
140 | 143 |
//inside a finally block |
141 | 144 |
//TODO: Find a more elegant solution for reschedulling in the event of an exception |
... | ... | |
512 | 515 |
{ |
513 | 516 |
try |
514 | 517 |
{ |
515 |
StatusKeeper.SetPithosStatus(status);
|
|
516 |
StatusNotification.Notify(new Notification()); |
|
518 |
StatusNotification.SetPithosStatus(status);
|
|
519 |
//StatusNotification.Notify(new Notification());
|
|
517 | 520 |
} |
518 | 521 |
catch (Exception exc) |
519 | 522 |
{ |
b/trunk/Pithos.Core/Agents/StatusAgent.cs | ||
---|---|---|
48 | 48 |
using System.IO; |
49 | 49 |
using System.Linq; |
50 | 50 |
using System.Reflection; |
51 |
using System.Security.Cryptography; |
|
51 | 52 |
using System.Text; |
52 | 53 |
using System.Threading; |
53 | 54 |
using System.Threading.Tasks; |
54 | 55 |
using Castle.ActiveRecord; |
56 |
using Castle.ActiveRecord.Framework; |
|
55 | 57 |
using Castle.ActiveRecord.Framework.Config; |
56 | 58 |
using Pithos.Interfaces; |
57 | 59 |
using Pithos.Network; |
... | ... | |
185 | 187 |
{ |
186 | 188 |
Log.ErrorFormat("[ERROR] STATE \n{0}", ex); |
187 | 189 |
} |
190 |
queue.NotifyComplete(action); |
|
188 | 191 |
// ReSharper disable AccessToModifiedClosure |
189 | 192 |
queue.DoAsync(loop); |
190 | 193 |
// ReSharper restore AccessToModifiedClosure |
... | ... | |
201 | 204 |
{ |
202 | 205 |
_persistenceAgent.Stop(); |
203 | 206 |
} |
204 |
|
|
207 |
|
|
205 | 208 |
|
206 | 209 |
public void ProcessExistingFiles(IEnumerable<FileInfo> existingFiles) |
207 | 210 |
{ |
... | ... | |
230 | 233 |
where missingStates.Contains(state.Id) |
231 | 234 |
select new { File = default(FileInfo), State = state }; |
232 | 235 |
|
233 |
var pairs = currentFiles.Union(deletedFiles); |
|
236 |
var pairs = currentFiles.Union(deletedFiles).ToList();
|
|
234 | 237 |
|
235 |
foreach(var pair in pairs)
|
|
238 |
using (var shortHasher = HashAlgorithm.Create("sha1"))
|
|
236 | 239 |
{ |
237 |
var fileState = pair.State; |
|
238 |
var file = pair.File; |
|
239 |
if (fileState == null) |
|
240 |
{ |
|
241 |
//This is a new file |
|
242 |
var fullPath = pair.File.FullName; |
|
243 |
var createState = FileState.CreateForAsync(fullPath, BlockSize, BlockHash); |
|
244 |
createState.ContinueWith(state => _persistenceAgent.Post(state.Result.Create)); |
|
245 |
} |
|
246 |
else if (file == null) |
|
240 |
foreach (var pair in pairs) |
|
247 | 241 |
{ |
248 |
//This file was deleted while we were down. We should mark it as deleted |
|
249 |
//We have to go through UpdateStatus here because the state object we are using |
|
250 |
//was created by a different ORM session. |
|
251 |
_persistenceAgent.Post(()=> UpdateStatusDirect(fileState.Id, FileStatus.Deleted)); |
|
252 |
} |
|
253 |
else |
|
254 |
{ |
|
255 |
//This file has a matching state. Need to check for possible changes |
|
256 |
var hashString = file.CalculateHash(BlockSize,BlockHash); |
|
257 |
//TODO: Need a way to attach the hashes to the filestate so we don't |
|
258 |
//recalculate them each time a call to calculate has is made |
|
259 |
//We can either store them to the filestate or add them to a |
|
260 |
//dictionary |
|
261 |
|
|
262 |
//If the hashes don't match the file was changed |
|
263 |
if (fileState.Checksum != hashString) |
|
242 |
var fileState = pair.State; |
|
243 |
var file = pair.File; |
|
244 |
if (fileState == null) |
|
264 | 245 |
{ |
265 |
_persistenceAgent.Post(() => UpdateStatusDirect(fileState.Id, FileStatus.Modified)); |
|
266 |
} |
|
246 |
//This is a new file |
|
247 |
var createState = FileState.CreateFor(file); |
|
248 |
_persistenceAgent.Post(createState.Create); |
|
249 |
} |
|
250 |
else if (file == null) |
|
251 |
{ |
|
252 |
//This file was deleted while we were down. We should mark it as deleted |
|
253 |
//We have to go through UpdateStatus here because the state object we are using |
|
254 |
//was created by a different ORM session. |
|
255 |
_persistenceAgent.Post(() => UpdateStatusDirect(fileState.Id, FileStatus.Deleted)); |
|
256 |
} |
|
257 |
else |
|
258 |
{ |
|
259 |
//This file has a matching state. Need to check for possible changes |
|
260 |
//To check for changes, we use the cheap (in CPU terms) SHA1 algorithm |
|
261 |
//on the entire file. |
|
262 |
|
|
263 |
var hashString = file.ComputeShortHash(shortHasher); |
|
264 |
//TODO: Need a way to attach the hashes to the filestate so we don't |
|
265 |
//recalculate them each time a call to calculate has is made |
|
266 |
//We can either store them to the filestate or add them to a |
|
267 |
//dictionary |
|
268 |
|
|
269 |
//If the hashes don't match the file was changed |
|
270 |
if (fileState.ShortHash != hashString) |
|
271 |
{ |
|
272 |
_persistenceAgent.Post(() => UpdateStatusDirect(fileState.Id, FileStatus.Modified)); |
|
273 |
} |
|
274 |
} |
|
267 | 275 |
} |
268 |
}; |
|
276 |
} |
|
277 |
|
|
269 | 278 |
|
270 | 279 |
} |
280 |
|
|
281 |
|
|
271 | 282 |
|
272 | 283 |
private int UpdateStatusDirect(Guid id, FileStatus status) |
273 | 284 |
{ |
... | ... | |
387 | 398 |
|
388 | 399 |
} |
389 | 400 |
|
390 |
private PithosStatus _pithosStatus=PithosStatus.InSynch; |
|
391 |
|
|
392 |
public void SetPithosStatus(PithosStatus status) |
|
393 |
{ |
|
394 |
_pithosStatus = status; |
|
395 |
} |
|
396 |
|
|
397 |
public PithosStatus GetPithosStatus() |
|
398 |
{ |
|
399 |
return _pithosStatus; |
|
400 |
} |
Also available in: Unified diff